prick 0.28.1 → 0.29.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 10bf0db9a65cd8441d8f17ea9ad055f177d49508f2445ee733722fcc0fb5c6dc
4
- data.tar.gz: 693a602c5f57b6bdd97cdefa00c69a0c517cd54e4b32e2d10657fa1c8cfb8949
3
+ metadata.gz: ec055b6544046998736dddf822f2ff0563c6e16deeae039f586ec96651ce5b65
4
+ data.tar.gz: 2687ec86827fc0c808a916462b34f7b71ab993fe0d7d5ef5444d4f5775ce91e2
5
5
  SHA512:
6
- metadata.gz: 1ec3e792b312d572d9eb65feb3c595d94ec944b9854f12c9f16a805dfce24a45d0be6fe923c02fc1bf57c9a3ce0d6b221c28eaeaea5a1ab1a0f044e228a07b5e
7
- data.tar.gz: a0cc6eb39133fbfc7eef170a173378bd685a3f43315ac633748dc1fafa02589dae100d1fe8f30ab4687a175e3d8a13d0fca793245fbb10fb2297fed5d8e4c659
6
+ metadata.gz: c278a8a151aefeb23c40dc2468fd271e14b45a031f73942453157b3e8789209fa55094bde9d92276fa85c530c204795e8474bd688ccfca139b7945501f373e73
7
+ data.tar.gz: 4d2df4c44b4c10e13d89ee584e5c1839b963d5c7bff42f5618c20b66173f540383cf1bc3059670a5af4922c4ad594a5ceaddc617cdbf2f54a60f2c8a96ce4653
data/exe/prick CHANGED
@@ -34,10 +34,19 @@ SPEC = %(
34
34
  Change to directory DIR before doing anything else
35
35
 
36
36
  -d,database=DATABASE
37
- Override database name from prick.yml
37
+ Override database name. Default is read from .prick.context
38
38
 
39
39
  -U,username=USERNAME
40
- Override username from from prick.yml
40
+ Override username. Default is read from .prick.context
41
+
42
+ -e,environment=ENVIRONMENT
43
+ Override environment. Default is read from from .prick.context
44
+
45
+ -p,project=PRICK-FILE
46
+ Override default environment file ('prick.yml')
47
+
48
+ -c,context=CONTEXT-FILE
49
+ Override default context file ('.prick.context')
41
50
 
42
51
  version!
43
52
  Print project version
@@ -58,6 +67,9 @@ SPEC = %(
58
67
  teardown!
59
68
  Drop the database and the database user. TODO: Also run teardown scripts
60
69
 
70
+ clean!
71
+ Empties the database and drops related users except the database user
72
+
61
73
  create.data!
62
74
  create.schema!
63
75
  create.database!
@@ -75,8 +87,9 @@ SPEC = %(
75
87
  drop! -- [KIND]
76
88
  @ Drop objects
77
89
 
78
- Kind can be 'users', 'data', 'schema', 'database' (the default), or 'all'. It is
79
- not an error if the object doesn't exist. TODO Only 'users' is currently defined
90
+ Kind can be 'users', 'data', 'schema', 'user', 'database' (the default),
91
+ or 'all'. It is not an error if the object doesn't exist. TODO 'data' and
92
+ 'schema' is not implemented
80
93
 
81
94
  build! -f,force -t,time --dump=KIND? -- [SCHEMA]
82
95
  Build the project. If SCHEMA is defined, later schemas are excluded.
@@ -94,7 +107,10 @@ SPEC = %(
94
107
 
95
108
  fox! -- FILE...
96
109
  Load fox file data. Data are reset to their initial state after build
97
- before the fox data are loaded
110
+ before the fox data are loaded. This makes it possible to experiment with
111
+ different data sets
112
+
113
+ # TODO: A --clean option that resets data
98
114
 
99
115
  release! -- KIND
100
116
  Create a release of the given kind. KIND can be 'major', 'minor', or
@@ -108,6 +124,7 @@ SPEC = %(
108
124
  dump.data!
109
125
  dump.schema!
110
126
  dump.database!
127
+ dump.environment!
111
128
  TODO
112
129
 
113
130
  dump.migration! --force -- VERSION
@@ -150,13 +167,18 @@ begin
150
167
  # Check state
151
168
  File.exist?(PRICK_PROJECT_FILE) or raise Prick::Error, "Not in a prick project directory"
152
169
 
170
+ # Handle -p and -c
171
+ project_file = opts.project || PRICK_PROJECT_PATH
172
+ context_file = opts.context || PRICK_CONTEXT_PATH
173
+
153
174
  # Load state
154
- Prick.state = State.load
175
+ state = Prick.state = State.load(project_file, context_file)
176
+
177
+ # Set database, username and environment
178
+ database = state.database = opts.database || state.database
179
+ username = state.username = opts.username || state.username
180
+ environment = state.environment = opts.environment || state.environment
155
181
 
156
- # Handle -d and -U options
157
- database = opts.database || Prick.state.database
158
- username = opts.username || Prick.state.username
159
-
160
182
  # Expect a sub-command
161
183
  cmd = opts.subcommand! or raise Prick::Error, "Subcomand expected"
162
184
 
@@ -176,6 +198,9 @@ begin
176
198
  when :teardown!
177
199
  Prick::SubCommand.teardown(database, username)
178
200
 
201
+ when :clean!
202
+ Prick::SubCommand.clean(database)
203
+
179
204
  when :create!
180
205
  create_command = opts.create!
181
206
  case create_command.subcommand
@@ -186,14 +211,15 @@ begin
186
211
  username, version,
187
212
  force: create_command.subcommand!.force?,
188
213
  file: create_command.subcommand!.file)
189
- else
190
- raise NotImplementedError
214
+ else
215
+ raise NotImplementedError
191
216
  end
192
217
 
193
218
  when :build!
194
219
  dump = cmd.dump? ? cmd.dump("batches")&.to_sym || :batches : nil
220
+ # exclude = cmd.exclude? ? cmd.exclude.split(",") : []
195
221
  Prick::SubCommand.build(
196
- database, username, args.expect(0..1), force: cmd.force?, timer: cmd.time?, dump: dump)
222
+ database, username, args.expect(0..1), force: cmd.force?, timer: cmd.time?, dump: dump)
197
223
 
198
224
  when :make!
199
225
  dump = cmd.dump? ? cmd.dump("batches")&.to_sym || :batches : nil
@@ -208,6 +234,8 @@ begin
208
234
  Prick::SubCommand.drop_all(database)
209
235
  when :users
210
236
  Prick::SubCommand.drop_users(database)
237
+ when :user
238
+ Prick::SubCommand.drop_user(username)
211
239
  when :database
212
240
  Prick::SubCommand.drop_database(database)
213
241
  when :data, :schema
@@ -229,20 +257,44 @@ begin
229
257
  when :dump!
230
258
  subject = cmd.subcommand!
231
259
  case cmd.subcommand
232
- when :migration
260
+ when :migration!
233
261
  arg = args.expect(1)
234
262
  version = PrickVersion.try(arg) or raise "Illegal version number: #{arg}"
235
263
  Prick::SubCommand.create_migration(username, version, force: subject.force?, file: "/dev/stdout")
236
- when :data, :schema, :database
264
+ when :type!
265
+ conn = PgConn.new(database, username)
266
+ builder = Prick::Build::Builder.new(conn, Prick::SCHEMA_DIR)
267
+ meta = PgMeta.new(conn, exclude_schemas: builder.pg_graph_ignore_schemas)
268
+ graph = PgGraph::Type.new(meta, builder.reflections_file)
269
+ graph.dump
270
+ when :data!, :schema!, :database!
237
271
  raise NotImplementedError
272
+ when :environment!
273
+ puts state.bash_source
238
274
  else
239
- raise Prick::Error, "Unknown subject: #{subject.inspect}"
275
+ raise Prick::Error, "Unknown dump object: #{cmd.subcommand!.__name__}"
240
276
  end
241
277
 
278
+
242
279
  else
243
280
  raise Prick::Fail, "Internal error: Unhandled command - #{opts.subcommand.inspect}"
244
281
  end
245
282
 
283
+ rescue Prick::Build::PostgresError => ex
284
+ $stderr.puts "Error: #{ex.message}"
285
+ if STDERR.tty?
286
+ file, lineno, charno = ex.err
287
+ if file && lineno
288
+ lines = IO.readlines(file).map(&:chomp)
289
+ i = lineno - 1
290
+ from = [i - 10, 0].max
291
+ to = [i + 10, lines.size].min
292
+ $stderr.puts
293
+ $stderr.puts lines[from...i], lines[i].bold, lines[i+1...to]
294
+ end
295
+ end
296
+ exit 1
297
+
246
298
  rescue RuntimeError, IOError, ShellOpts::Failure, Prick::Fail, Prick::Build::PostgresError => ex
247
299
  ShellOpts.failure(ex.message)
248
300
 
data/lib/builder/batch.rb CHANGED
@@ -14,10 +14,10 @@ module Prick
14
14
  @nodes = []
15
15
  end
16
16
 
17
- def execute(&block)
17
+ def execute(**opts, &block)
18
18
  name = self.class.to_s.sub(/.*::/, "").split(/(?=[A-Z])/).map(&:downcase).join(" ")
19
19
  time "Execute #{name} (nodes: #{nodes.size})" do
20
- yield
20
+ yield(**opts)
21
21
  end
22
22
  end
23
23
 
@@ -28,23 +28,38 @@ module Prick
28
28
  end
29
29
 
30
30
  class SqlBatch < BuildBatch
31
- def execute
31
+ def execute(step: true) # This is marginally faster!
32
32
  super {
33
33
  begin
34
34
  sql = []
35
+ file = nil
36
+ node = nil
37
+
35
38
  # A SQL batch allows the first node to be an evaluated or executed node
36
39
  if nodes.first.is_a?(CommandNode)
37
- time "Execute script" do
38
- sql = [nodes.first.source]
40
+ node = nodes.shift
41
+ sql = [node.source]
42
+ end
43
+
44
+ if step
45
+ conn.execute sql, silent: true if !sql.empty?
46
+ for node in nodes
47
+ conn.execute node.source, silent: true
39
48
  end
49
+ else
50
+ node = nil
51
+ conn.execute sql + nodes[sql.size..-1].map(&:source), silent: true
40
52
  end
41
- conn.execute sql + nodes[sql.size..-1].map(&:source)
42
53
 
43
- rescue PG::SyntaxError, PG::Error => ex
44
- error, line, pos = parse_pg_message(ex.message)
45
- if line
54
+ rescue PG::Error => ex
55
+ error, line, char = conn.err
56
+ file = nil
57
+ if step
58
+ ;
59
+ elsif line
60
+ # Locate error node and make line number relative
46
61
  if line < nodes.first.lines
47
- node = nodes.first;
62
+ node = nodes.first
48
63
  else
49
64
  line -= nodes.first.lines
50
65
  node = nodes[1..-1].find { |node|
@@ -58,26 +73,20 @@ module Prick
58
73
  end
59
74
  }
60
75
  end
76
+ # file = node.path
77
+ end
61
78
 
62
- if node.is_a?(SqlNode)
63
- message = ["prick: #{error} in #{node.path}", line, pos].compact.join(":")
64
- else
65
- message = "prick: #{error} from #{node.path}"
66
- end
79
+ if node.is_a?(SqlNode)
80
+ message = ["#{error} in #{node.path}", line, char].compact.join(":")
81
+ elsif node
82
+ message = "#{error} from #{node.path}"
67
83
  else
68
- raise
84
+ message = conn.errmsg + " in SQL batch"
69
85
  end
70
- raise PostgresError.new(message)
86
+ raise PostgresError.new(message, file, line, char)
71
87
  end
72
88
  }
73
89
  end
74
-
75
- private
76
- # Returns [error, line, pos] tuple
77
- def parse_pg_message(message)
78
- message =~ /ERROR:\s*(.*?)\n(LINE (\d+): ).*?\n(\s*\^)/m or return nil
79
- [$1, $3.to_i, $4.size - $2.size]
80
- end
81
90
  end
82
91
 
83
92
  class ModuleBatch < BuildBatch
@@ -139,7 +148,7 @@ module Prick
139
148
  # FIXME: Why only in FoxBatch - should be set higher up in the system
140
149
  conn.execute "update prick.versions set built_at = now() at time zone 'UTC'"
141
150
  rescue PG::SyntaxError, PG::Error => ex
142
- raise PostgresError, "prick: #{ex.message}"
151
+ raise PostgresError.new(ex.message)
143
152
  end
144
153
  }
145
154
  t_type.emit
@@ -10,7 +10,18 @@ include Constrain
10
10
  module Prick
11
11
  module Build
12
12
  class Error < StandardError; end
13
- class PostgresError < Error; end
13
+ class PostgresError < Error
14
+ attr_reader :file
15
+ attr_reader :lineno
16
+ attr_reader :charno
17
+
18
+ def initialize(message, file = nil, lineno = nil, charno = nil)
19
+ super(message)
20
+ @file, @lineno, @charno = file, lineno, charno
21
+ end
22
+
23
+ def err = [@file, @lineno, @charno]
24
+ end
14
25
 
15
26
  class Builder
16
27
  # PgConn object
@@ -34,7 +34,7 @@ module Prick
34
34
  end
35
35
  end
36
36
 
37
- # Search for an executable in path
37
+ # Search for an executable in path. Return nil if not found
38
38
  def find_executable(filename) # ChatGPT
39
39
  Prick.state.executable_search_path.split(File::PATH_SEPARATOR).each do |directory|
40
40
  path = File.join(directory, filename)
@@ -43,80 +43,37 @@ module Prick
43
43
  nil
44
44
  end
45
45
 
46
- # Expand $ENVIRONMENT variable in file names. The function implements a
47
- # hierarchy of environments:
46
+ # Expand environments variables file name. Iterates through inherited
47
+ # environments if the file name contain the $ENVIRONMENT variable. Return
48
+ # nil if not found
48
49
  #
49
- # production
50
- # (online)
51
- # development
52
- # online
53
- # offline
54
- # backend
55
- # frontend
56
- # test
57
- #
58
- # If an environment doesn't exist in #dir, then the higher level
59
- # environments are tried in turn. Eg. if the current environment is
60
- # 'test', the algorithm expands '$ENVIRONMENT' to 'test', 'offline', and
61
- # 'development' until an existing file is found. If no file is found the
62
- # the unexpanded value is returned
63
- #
64
- # The production environment acts as a online environment but not a
65
- # development environment. This means it will first search for
66
- # 'production', then 'online', and then stop without searching for
67
- # 'development'
68
- #
69
- # NOTE: This is a hardcoded feature for an internal project. It is
70
- # configurable in the next major version
71
- #
72
- # TODO: Refactor: dir is not handled well when the filename includes a
73
- # $PWD variblea
50
+ # The hierarchy of environments is defined in the PRICK_ENVIRONMENT_FILE
74
51
  #
75
52
  def expand_filename(dir, filename)
76
- # Expand variables
77
- path = expand_variables(filename, Prick.state.bash_environment)
53
+ environment = Prick.state.environment
54
+ bash_vars = Prick.state.bash_environment
78
55
 
79
- # Use only filename as template if expansion is an absolute path
80
- if path.start_with? "/"
81
- template = filename
82
- else
83
- template = "#{dir}/#{filename}"
84
- path = "#{dir}/#{path}"
85
- end
86
- return path if File.exist?(path)
56
+ last = nil
57
+ for env in [environment] + Environment[environment].ancestors.reverse
58
+ bash_vars["ENVIRONMENT"] = env
59
+ file = expand_variables(filename, bash_vars)
60
+ last ||= (file != last and file) or return nil # return if no ENVIRONMENT substitution
61
+ path = (file.start_with?("/") ? file : File.join(dir, file))
87
62
 
88
- # Iterate through environments
89
- env = Prick.state.environment.to_s
90
- while true
63
+ # Check for file (may be executable)
91
64
  return path if File.exist?(path)
92
- case env
93
- when "production"
94
- file = expand_variables(path, ENVIRONMENT: "online") or return nil
95
- return file if File.exist? file
96
- break
97
- when "development"; break
98
- when "online"; env = "development"
99
- when "offline"; env = "development"
100
- when "backend"; env = "offline"
101
- when "frontend"; env = "offline"
102
- when "test"; env = "offline"
103
- else
104
- raise Error, "Illegal env: '#{env}'"
105
- end
106
- path = expand_variables(template, Prick.state.bash_environment.merge(ENVIRONMENT: env))
107
- end
108
65
 
109
- # Look for executable in PATH
110
- if filename !~ /\// && (path = find_executable(filename))
111
- return path
112
- else
113
- return nil
66
+ # Check for executable in search path
67
+ path = find_executable(file) and return path if file !~ /\//
114
68
  end
69
+
70
+ # Return nil if not found
71
+ return nil
115
72
  end
116
73
 
117
74
  # Expand $ENVIRONMENT variable
118
75
  def expand_string(string)
119
- expand_variables(string, ENVIRONMENT: Prick.state.environment.to_s, PWD: Dir.getwd)
76
+ expand_variables(string, Prick.state.bash_environment)
120
77
  end
121
78
 
122
79
  def parse_directory(parent, dir)
@@ -170,8 +127,9 @@ module Prick
170
127
  if entry =~ /^(\S+?)(\?)?(?:\s+(.+))?\s*$/
171
128
  command = $1
172
129
  optional = !$2.nil?
173
- args = expand_string($3 || '').split
174
- path = expand_filename(dir, command)
130
+ rest = $3
131
+ args = expand_string(rest || '').split
132
+ path = expand_filename(dir, command)
175
133
  path || optional or raise Error, "Can't find '#{entry}' in #{dir}/ from #{unit}"
176
134
  !path.nil? or return nil
177
135
  else
@@ -215,11 +173,12 @@ module Prick
215
173
  SqlNode.new(unit, phase, path)
216
174
  when /\.fox$/
217
175
  FoxNode.new(unit, :seed, path)
176
+ when /build-.*\.yml$/
177
+ parse_build_file(unit, dir, path)
218
178
  else
219
179
  raise Error, "Expected executable, fox, or sql file: #{File.basename(path)} in #{dir}"
220
180
  end
221
181
  else
222
- path =
223
182
  raise Error, "Can't find '#{entry}' in #{dir}/ from #{unit}"
224
183
  end
225
184
  end
@@ -6,7 +6,7 @@
6
6
  # backslash. All occurrences of a variable in the string are replaced
7
7
  #
8
8
  # The str argument is the template string and the variables argument is a hash
9
- # from variable name (Symbol) to variable value
9
+ # from variable name (String) to variable value
10
10
  #
11
11
  # Note that the characters '\x00' and '\x01' are used internally and may not be
12
12
  # present in the template string
@@ -17,11 +17,24 @@ def expand_variables(str, variables) # ChatGPT
17
17
 
18
18
  # Expand variables
19
19
  str.gsub!(/\$(\w+)\b|\$\{(\w+)\}/) do |match| # Strange that '\b' is necessary
20
- key = ($1 || $2).to_sym
21
- variables[key] || match
20
+ key = $1 || $2
21
+ # variables[key] || match
22
+ variables[key] || ""
22
23
  end
23
24
 
24
25
  # Restore escaped characters
25
26
  str.gsub("\x00", '\\').gsub("\x01", '$')
26
27
  end
27
28
 
29
+ # Return true if any of the variables will be expanded
30
+ def expand_variables?(str, variables)
31
+ # Replace escaped bashslashes and dollar signs
32
+ str = str.gsub('\\\\', "\x00").gsub('\\$', "\x01")
33
+
34
+ # Look for expansion
35
+ str.gsub!(/\$(\w+)\b|\$\{(\w+)\}/) do |match| # Strange that '\b' is necessary
36
+ return true if variables.include?($1 || $2)
37
+ end
38
+
39
+ return false
40
+ end
data/lib/local/ansi.rb ADDED
@@ -0,0 +1,6 @@
1
+
2
+ class String
3
+ ANSI_BOLD_START = "\e[1m"
4
+ ANSI_BOLD_STOP = "\e[22m"
5
+ def bold = "#{ANSI_BOLD_START}#{self.to_s}#{ANSI_BOLD_STOP}"
6
+ end
@@ -36,6 +36,10 @@ module Prick
36
36
  PRICK_PROJECT_FILE = "prick.yml"
37
37
  PRICK_PROJECT_PATH = PRICK_PROJECT_FILE
38
38
 
39
+ # # Environment specification file
40
+ # PRICK_ENVIRONMENT_FILE = "prick.environment.yml"
41
+ # PRICK_ENVIRONMENTS_PATH = PRICK_ENVIRONMENT_FILE
42
+
39
43
  # Context file
40
44
  PRICK_CONTEXT_FILE = ".prick-context"
41
45
  PRICK_CONTEXT_PATH = PRICK_CONTEXT_FILE
@@ -0,0 +1,134 @@
1
+ require 'yaml'
2
+ require 'dsort'
3
+
4
+ module Prick
5
+ class Environment
6
+ # Environments name
7
+ attr_reader :name
8
+
9
+ # Names of parent environments
10
+ def parents = @values[:parents]
11
+
12
+ # Ancestors sorted in dependency order
13
+ def ancestors
14
+ @ancestors ||= effective_values[:parents].sort_by { |name| @@SORTED_INDEXES[name] }
15
+ end
16
+
17
+ # List of variables including :parents
18
+ def variables = @@VARIABLES
19
+
20
+ # List of user defined variables
21
+ def user_variables = @@VARIABLES - [:parent]
22
+
23
+ # Hash from variable to value
24
+ attr_accessor :values
25
+
26
+ # Hash from variable name to effective value. The effective value is the
27
+ # sum of this environment's value and its parents' values
28
+ attr_accessor :effective_values
29
+
30
+ def self.[](name) @@ENVIRONMENTS[name] end
31
+ def self.environment?(name) @@ENVIRONMENTS.key?(name) end
32
+ def self.environments() @@SORTED_ENVIRONMENTS.map { |key| @@ENVIRONMENTS[key] } end
33
+ def self.variables() @@VARIABLES end
34
+
35
+ def initialize(name)
36
+ @name = name
37
+ @values = variables.map { |key| [key, []] }.to_h
38
+ @effective_values = variables.map { |key| [key, []] }.to_h
39
+ @@ENVIRONMENTS[name] = self
40
+ end
41
+
42
+ def self.load_environments(hash) # hash can be nil
43
+ if hash
44
+ parse(hash)
45
+ analyze
46
+ end
47
+ end
48
+
49
+ # does not include the name of the environment
50
+ def bash_env
51
+ user_variables.map { |variable|
52
+ ["PRICK_ENVIRONMENT_#{variable.upcase}", effective_values[variable]]
53
+ }.to_h
54
+ end
55
+
56
+ def dump
57
+ puts name
58
+ indent {
59
+ values.each { |var, val|
60
+ next if val.empty?
61
+ puts "#{var}:"
62
+ indent { puts val }
63
+ }
64
+ effective_values.each { |var, val|
65
+ next if val.empty?
66
+ puts "effective_#{var}:"
67
+ indent { puts val }
68
+ }
69
+ }
70
+ end
71
+
72
+ def self.dump
73
+ environments.each(&:dump)
74
+ end
75
+
76
+ private
77
+ @@VARIABLES = []
78
+ @@ENVIRONMENTS = {}
79
+ @@SORTED_ENVIRONMENTS = []
80
+
81
+ def self.yaml_to_array(value)
82
+ !value.nil? && Array(value).flatten.compact.map(&:to_s).map(&:split).flatten
83
+ end
84
+
85
+ def self.parse(hash)
86
+ hash = hash.dup
87
+ @@VARIABLES = [:parents] + (yaml_to_array(hash.delete("variables")).map(&:to_sym) || [])
88
+
89
+ for environment, settings in hash
90
+ env = Environment.new(environment)
91
+ if settings.is_a? Hash
92
+ for variable, value in settings
93
+ variable = (variable == "inherit" ? :parents : variable.to_sym)
94
+ value = yaml_to_array(value)
95
+ if variables.include?(variable)
96
+ env.values[variable] = value
97
+ else
98
+ raise ArgumentError, "Illegal variable: '#{variable}'"
99
+ end
100
+ end
101
+ else
102
+ raise ArgumentError, "Illegal value for '#{environment}': #{settings.inspect}"
103
+ end
104
+ end
105
+ end
106
+
107
+ def self.analyze
108
+ # Sort environments in dependency order
109
+ deps = @@ENVIRONMENTS.map { |name, env| [name, env.parents] }
110
+ @@SORTED_ENVIRONMENTS = DSort.dsort(deps)
111
+ @@SORTED_INDEXES = @@SORTED_ENVIRONMENTS.map.with_index { |env, idx| [env, idx] }.to_h
112
+
113
+ # Check for undeclared inherited environments
114
+ @@SORTED_ENVIRONMENTS.each { |environment|
115
+ @@ENVIRONMENTS.key?(environment) or raise ArgumentError, "Can't find '#{inherited}' environment"
116
+ }
117
+
118
+ # Compute effective attribute values by processing environments in
119
+ # dependency order so that all parents' environments are computed before
120
+ # the current environment
121
+ for name in @@SORTED_ENVIRONMENTS
122
+ env = Environment[name]
123
+ env.effective_values = env.values.transform_values { |v| v.dup } # Deep-dup
124
+ for parent in env.parents.dup
125
+ for var in variables
126
+ env.effective_values[var].unshift *Environment[parent].effective_values[var]
127
+ end
128
+ end
129
+ env.effective_values.transform_values! { |v| v.uniq }
130
+ end
131
+ end
132
+ end
133
+ end
134
+
data/lib/prick/state.rb CHANGED
@@ -27,16 +27,22 @@ module Prick
27
27
  # Version of prick(1)
28
28
  attr_accessor :prick_version
29
29
 
30
- # Run-time environment. Can be :production, :development, or :test
31
- attr_accessor :environment
32
-
33
30
  # Name of database
34
31
  attr_accessor :database
35
32
 
36
33
  # Name of database user
37
34
  attr_accessor :username
38
35
 
39
- # Prick project dir. This is not a constant because exe/prick can change directory
36
+ # Run-time environment name
37
+ def environment() @environment end
38
+ def environment=(env)
39
+ constrain env, String
40
+ Environment.environment?(env) or raise "Illegal environment: '#{env}'"
41
+ @environment = env
42
+ end
43
+
44
+ # Prick project dir. This is not a constant because exe/prick can change
45
+ # directory (FIXME it can?)
40
46
  def prick_dir
41
47
  @prick_dir ||= Dir.getwd
42
48
  end
@@ -46,52 +52,54 @@ module Prick
46
52
  @executable_search_path ||= "#{ENV['PATH']}:#{prick_dir}/#{BIN_DIR}:#{prick_dir}/#{LIBEXEC_DIR}"
47
53
  end
48
54
 
49
- def bash_environment
50
- ENV.to_h.merge({
51
- DATABASE: Prick.state.database,
52
- USERNAME: Prick.state.username,
53
- ENVIRONMENT: Prick.state.environment.to_s,
54
- PRICK_DIR: Prick.state.prick_dir,
55
- PRICK_SCHEMADIR: File.join(Prick.state.prick_dir, SCHEMA_DIR),
56
- PRICK_BINDIR: File.join(Prick.state.prick_dir, BIN_DIR),
57
- PRICK_LIBEXECDIR: File.join(Prick.state.prick_dir, LIBEXEC_DIR),
58
- PRICK_VARDIR: File.join(Prick.state.prick_dir, VAR_DIR),
59
- PRICK_CACHEDIR: File.join(Prick.state.prick_dir, CACHE_DIR),
60
- PRICK_SPOOLDIR: File.join(Prick.state.prick_dir, SPOOL_DIR),
61
- PRICK_TMPDIR: File.join(Prick.state.prick_dir, TMP_DIR),
62
- PRICK_CLONEDIR: File.join(Prick.state.prick_dir, CLONE_DIR),
63
- PRICK_SPECDIR: File.join(Prick.state.prick_dir, BIN_DIR),
64
- PATH: Prick.state.executable_search_path
65
- })
66
- end
67
-
68
- def self.load
69
- begin
70
- h = YAML.load(File.read PRICK_PROJECT_PATH)
71
- rescue Errno::ENOENT
72
- raise Prick::Error, "Can't open project file: #{PRICK_PROJECT_PATH}"
73
- end
74
- state = State.new
75
- state.name = h["name"]
76
- state.title = h["title"]
77
- state.version = h["version"] && PrickVersion.new(h["version"])
78
- state.prick_version = h["prick"] && PrickVersion.new(h["prick"])
79
-
80
- begin
81
- h = YAML.load(File.read PRICK_CONTEXT_PATH)
82
- rescue Errno::ENOENT
83
- raise Prick::Error, "Can't open environment file: #{PRICK_CONTEXT_PATH}"
55
+ # Create a bash(1) environment (Hash). It is used for in-prick expansion of
56
+ # variables and also injected into the enviroment of subprocesses
57
+ def bash_environment(scope = :global)
58
+ hash = {
59
+ "DATABASE" => Prick.state.database, # FIXME: Yt
60
+ "USERNAME" => Prick.state.username, # FIXME: Yt
61
+ "ENVIRONMENT" => Prick.state.environment.to_s, # FIXME: Yt
62
+ "PRICK_NAME" => Prick.state.name,
63
+ "PRICK_TITLE" => Prick.state.title,
64
+ "PRICK_VERSION" => Prick.state.version,
65
+ "PRICK_DATABASE" => Prick.state.database,
66
+ "PRICK_USERNAME" => Prick.state.username,
67
+ "PRICK_ENVIRONMENT" => Prick.state.environment.to_s,
68
+ }
69
+ case scope
70
+ when :local; # nop
71
+ hash.merge Environment[Prick.state.environment].bash_env
72
+ when :global;
73
+ ENV.to_h.merge(hash).merge({
74
+ "PATH" => Prick.state.executable_search_path,
75
+ "PRICK_SCHEMADIR" => File.join(Prick.state.prick_dir, SCHEMA_DIR),
76
+ "PRICK_BINDIR" => File.join(Prick.state.prick_dir, BIN_DIR),
77
+ "PRICK_LIBEXECDIR" => File.join(Prick.state.prick_dir, LIBEXEC_DIR),
78
+ "PRICK_VARDIR" => File.join(Prick.state.prick_dir, VAR_DIR),
79
+ "PRICK_CACHEDIR" => File.join(Prick.state.prick_dir, CACHE_DIR),
80
+ "PRICK_SPOOLDIR" => File.join(Prick.state.prick_dir, SPOOL_DIR),
81
+ "PRICK_TMPDIR" => File.join(Prick.state.prick_dir, TMP_DIR),
82
+ "PRICK_CLONEDIR" => File.join(Prick.state.prick_dir, CLONE_DIR),
83
+ "PRICK_SPECDIR" => File.join(Prick.state.prick_dir, BIN_DIR)
84
+ }).merge(Environment[Prick.state.environment].bash_env)
85
+ else
86
+ raise ArgumentError
84
87
  end
85
- state.environment = h["environment"]&.to_sym
86
- state.database = h["database"]
87
- state.username = h["username"]
88
+ end
88
89
 
89
- # TODO Load schema version
90
+ def bash_source
91
+ bash_environment(:local).map { |var,val| "export #{var}=\"#{Array(val).join(' ')}\"\n" }.join
92
+ end
90
93
 
94
+ def self.load(project_file, context_file)
95
+ state = State.new
96
+ state.send(:parse_project_file, project_file || PRICK_PROJECT_PATH)
97
+ state.send(:parse_context_file, context_file || PRICK_CONTEXT_PATH)
91
98
  state
92
99
  end
93
100
 
94
101
  def save
102
+ raise NotImplementedError
95
103
  h = {
96
104
  "name" => name,
97
105
  "title" => title,
@@ -126,11 +134,45 @@ module Prick
126
134
  indent {
127
135
  for method in [
128
136
  :name, :title, :prick_version, :project_version, :schema_version,
129
- :database_version, :environment, :database, :username]
137
+ :database_version, :database, :username]
130
138
  puts "#{method}: #{self.send method}"
131
139
  end
140
+ puts "environments:"
141
+ indent { Environment.dump }
132
142
  }
133
143
  end
144
+
145
+ private
146
+ def load_yaml(file, expected_keys, optional_keys)
147
+ begin
148
+ hash = YAML.load(File.read file)
149
+ rescue Errno::ENOENT
150
+ raise Prick::Error, "Can't read #{file}"
151
+ end
152
+ for key in expected_keys
153
+ !hash[key].to_s.empty? or raise Prick::Error, "Can't find '#{key}' in #{file}"
154
+ end
155
+ (unknown = (hash.keys - expected_keys - optional_keys).first) and
156
+ Prick::Error "Illegal key '#{unknown}' in #{file}"
157
+ hash
158
+ end
159
+
160
+ def parse_context_file(context_file)
161
+ hash = load_yaml(context_file, %w(environment database username), [])
162
+ @database = hash["database"]
163
+ @username = hash["username"]
164
+ @environment = hash["environment"]
165
+ end
166
+
167
+ def parse_project_file(prick_file)
168
+ hash = load_yaml(prick_file, %w(name title), %w(version prick environments))
169
+ @name = hash["name"]
170
+ @title = hash["title"]
171
+ @version = hash["version"] && PrickVersion.new(hash["version"])
172
+ @prick_version = hash["prick"] && PrickVersion.new(hash["prick"])
173
+ Environment.load_environments(hash["environments"] || {})
174
+ end
134
175
  end
135
176
  end
136
177
 
178
+
data/lib/prick/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Prick
4
- VERSION = "0.28.1"
4
+ VERSION = "0.29.0"
5
5
  end
data/lib/prick.rb CHANGED
@@ -17,7 +17,9 @@ require 'ext/expand_variables.rb'
17
17
  require 'local/command.rb'
18
18
  require 'local/git.rb'
19
19
  require 'local/timer.rb'
20
+ require 'local/ansi.rb'
20
21
 
22
+ require 'prick/environment.rb'
21
23
  require 'prick/state.rb'
22
24
  require 'prick/prick_version.rb'
23
25
  require 'prick/diff.rb'
@@ -33,6 +35,7 @@ end
33
35
  require 'subcommand/prick-init.rb'
34
36
  require 'subcommand/prick-setup.rb'
35
37
  require 'subcommand/prick-teardown.rb'
38
+ require 'subcommand/prick-clean.rb'
36
39
  require 'subcommand/prick-create.rb'
37
40
  require 'subcommand/prick-build.rb'
38
41
  require 'subcommand/prick-make.rb'
@@ -3,7 +3,10 @@
3
3
  require 'builder/builder.rb'
4
4
 
5
5
  module Prick::SubCommand
6
- def self.build(database, username, schema, builddir: "schema", force: false, timer: nil, dump: nil)
6
+ def self.build(
7
+ database, username, schema,
8
+ builddir: "schema", force: false, timer: nil, dump: nil)
9
+
7
10
  Timer.on! if timer
8
11
  time "Prick::Command#build" do
9
12
  begin
@@ -15,21 +18,19 @@ module Prick::SubCommand
15
18
  builder = nil
16
19
  time "Load build object" do
17
20
  if super_conn.rdbms.exist? database
18
- conn = PgConn.new(database, username)
19
21
  exist = true
20
22
  else
21
23
  super_conn.rdbms.create database, owner: username
22
- conn = PgConn.new(database, username)
23
24
  exist = false
24
25
  end
26
+ conn = PgConn.new(database, username)
25
27
 
26
28
  builder = Prick::Build::Builder.new(conn, builddir)
27
29
 
28
30
  if exist
29
31
  if force
30
32
  # Drop all schemas but re-creates the public schema
31
- super_conn.rdbms.empty!(database)
32
- conn.schema.drop("public") # drop it again FIXME
33
+ super_conn.rdbms.empty!(database, public: false)
33
34
 
34
35
  else
35
36
  # Find schemas to refresh. This includes all schemas in the
@@ -69,9 +70,9 @@ module Prick::SubCommand
69
70
  builder.execute conn
70
71
  end
71
72
 
72
- rescue Prick::Error => ex
73
- $stderr.puts ex.message
74
- exit 1
73
+ # rescue Prick::Error => ex
74
+ # $stderr.puts ex.message
75
+ # exit 1
75
76
 
76
77
  rescue ::Command::Error => ex
77
78
  $stderr.puts ex.message
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'builder/builder.rb'
4
+
5
+ module Prick::SubCommand
6
+ # Drop users and empty database. This has the same as effect as
7
+ # teardown+setup but without recreating the user and the database that
8
+ # annoyingly terminates all user sessions
9
+ def self.clean(database)
10
+ drop_users(database)
11
+ PgConn.new("postgres") { |conn|
12
+ conn.rdbms.empty!(database) if conn.rdbms.exist?(database)
13
+ }
14
+ end
15
+ end
16
+
17
+
@@ -3,8 +3,15 @@
3
3
  require 'builder/builder.rb'
4
4
 
5
5
  module Prick::SubCommand
6
- def self.drop_users(database)
6
+ def self.drop_user(username) # No database so does not cascade
7
7
  PgConn.new("postgres") { |conn|
8
+ conn.role.drop([username])
9
+ }
10
+ end
11
+
12
+ # Drop all users associated with the given database except the owner
13
+ def self.drop_users(database)
14
+ PgConn.new(database) { |conn|
8
15
  users = conn.role.list(database: database)
9
16
  conn.role.drop(users, cascade: true)
10
17
  }
@@ -15,11 +22,9 @@ module Prick::SubCommand
15
22
  end
16
23
 
17
24
  def self.drop_all(database)
18
- PgConn.new("postgres") { |conn|
19
- users = conn.role.list(database: database)
20
- PgConn.new(database) { |db| db.role.drop(users, cascade: true) }
21
- conn.rdbms.drop database
22
- }
25
+ drop_users(database)
26
+ drop_database(database)
27
+ drop_user(username)
23
28
  end
24
29
  end
25
30
 
@@ -0,0 +1,14 @@
1
+ online:
2
+ offline:
3
+ production: online
4
+ development: offline
5
+ frontend: development
6
+ app_registry_frontend: frontend
7
+ app_portal_frontend: frontend
8
+ backend: development
9
+ app_registry_backend: backend
10
+ app_portal_backend: backend
11
+ fdw_import: online
12
+ sagsys_import: offline
13
+ app_portal_import: offline
14
+ test: offline
data/prick.gemspec CHANGED
@@ -38,6 +38,7 @@ Gem::Specification.new do |spec|
38
38
  spec.add_dependency "postspec"
39
39
  spec.add_dependency "pg_graph"
40
40
  spec.add_dependency "shellopts"
41
+ spec.add_dependency "dsort"
41
42
 
42
43
  spec.add_development_dependency "ruby-prof"
43
44
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: prick
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.28.1
4
+ version: 0.29.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Claus Rasmussen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-01-11 00:00:00.000000000 Z
11
+ date: 2024-01-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: semantic
@@ -136,6 +136,20 @@ dependencies:
136
136
  - - ">="
137
137
  - !ruby/object:Gem::Version
138
138
  version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: dsort
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
139
153
  - !ruby/object:Gem::Dependency
140
154
  name: ruby-prof
141
155
  requirement: !ruby/object:Gem::Requirement
@@ -174,12 +188,14 @@ files:
174
188
  - lib/builder/node_pool.rb
175
189
  - lib/builder/parser.rb
176
190
  - lib/ext/expand_variables.rb
191
+ - lib/local/ansi.rb
177
192
  - lib/local/command.rb
178
193
  - lib/local/git.rb
179
194
  - lib/local/timer.rb
180
195
  - lib/prick.rb
181
196
  - lib/prick/constants.rb
182
197
  - lib/prick/diff.rb
198
+ - lib/prick/environment.rb
183
199
  - lib/prick/prick_version.rb
184
200
  - lib/prick/state.rb
185
201
  - lib/prick/version.rb
@@ -203,6 +219,7 @@ files:
203
219
  - lib/share/migrate/migration/diff.before-tables.sql
204
220
  - lib/share/migrate/migration/diff.tables.sql
205
221
  - lib/subcommand/prick-build.rb
222
+ - lib/subcommand/prick-clean.rb
206
223
  - lib/subcommand/prick-create.rb
207
224
  - lib/subcommand/prick-drop.rb
208
225
  - lib/subcommand/prick-fox.rb
@@ -212,6 +229,7 @@ files:
212
229
  - lib/subcommand/prick-release.rb
213
230
  - lib/subcommand/prick-setup.rb
214
231
  - lib/subcommand/prick-teardown.rb
232
+ - prick.environments.yml
215
233
  - prick.gemspec
216
234
  homepage: http://www.nowhere.com/
217
235
  licenses: []