prick 0.30.0 → 0.32.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: 7302ba57c3bed137c90862b29e2a2b14938395963a3521aa10a2b096d516696f
4
- data.tar.gz: f7d7e31e0f24c4346bffff6d03d104c7f8e9e9f2605bde0a6f67f48a16736499
3
+ metadata.gz: fed99dbfdf6e89642b4e8fdddf976efa58b1c8b75d9169fa460bd8190805cc63
4
+ data.tar.gz: 97fec32bbd130043b2c782f133ab5af30e9bcc5c42d4c83a26311f486786d671
5
5
  SHA512:
6
- metadata.gz: 3e1fb9a5b8a9befdd1367b6653dd46dc0df0ca17ce94a15153f4d590390b1341c5930ffbc2a5608cdf906b0d22e56aaf1782c96c01ea3ffdbbbf85dc5c097f98
7
- data.tar.gz: c79a6ecd5bd77169517c6b70dca2363da933fc3f73550e390297c991a2aed4c8ac0d6887855b93112fcac2d32818f3ee5e93609d1dfedae913e53f8dc6d8d187
6
+ metadata.gz: 35859b1f387d1bf8ccb04a37a853d509e8038102f9521a7b4b35884db5d2682e5a06bd3dda9f4aff704a105603d9df17a43d2d1bbfa66f39ba751584b73b7cb9
7
+ data.tar.gz: cc7dfb26f5c10b1489d82f5be5c824e7d7d118de29d9ceae6b424e26506701650da57ed800107ce8ad10ea0bda3e1d29934000170702b285ae3d7a3a8123426f
data/exe/prick CHANGED
@@ -51,7 +51,8 @@ SPEC = %(
51
51
  teardown! -- [DATABASE...] @ Remove database and owner
52
52
  Drop database, database users, and database owner if possible and not the
53
53
  current user. DATABASE defaults to the current database in which case the
54
- state file is also removed
54
+ state file is also removed. If the --force option is present, the
55
+ database is dropped even if it is not a Prick database
55
56
 
56
57
  clean! -- [DATABASE] @ Empty database and remove users
57
58
  Empty the database and drop related users except the database owner and
@@ -96,33 +97,34 @@ SPEC = %(
96
97
  create.all!
97
98
  TODO
98
99
 
99
- drop.schema! -- [SCHEMA...]
100
- @ Drop schemas
101
-
102
- Drops the given schemas or all schemas if called without arguments
103
-
104
- drop.users!
100
+ drop.users! -- [DATABASE]
105
101
  @ Drop database users
106
102
 
107
103
  Drops all database users except the database owner
108
104
 
105
+ drop.owner! -- [DATABASE]
106
+ Drop database owner
107
+
108
+ drop.database! -- [DATABASE]
109
+ Drop database
110
+
111
+ drop.schema! -- [DATABASE] [SCHEMA...]
112
+ @ Drop schemas
113
+
114
+ Drops the given schemas or all schemas if called without arguments. The
115
+ 'prick' schema is only deleted if named explicity
116
+
109
117
  drop.data!
110
118
  @ Drop data
111
119
 
112
120
  TODO
113
121
 
114
- drop.owner!
115
- Drop database owner
116
-
117
- drop.database!
118
- Drop database
119
-
120
122
  build! -f,force -t,time --dump=KIND? -- [SCHEMA]
121
123
  Build the project. If SCHEMA is defined, later schemas are excluded.
122
- KIND can be 'nodes', 'allnodes' or 'batches' (the default)
124
+ KIND can be 'nodes', 'allnodes' or 'batches' (the default).
123
125
 
124
- Usually, schemas marked with 'no refresh' is not built, use --force to
125
- rebuild all schemas
126
+ Schemas marked with 'no refresh' is not built unless the --force is
127
+ present
126
128
 
127
129
  make! -t,time --dump=KIND? -- [SCHEMA]
128
130
  @ Only rebuild changed files
@@ -131,6 +133,10 @@ SPEC = %(
131
133
  rebuild affected parts of the project. KIND can be 'nodes', 'allnodes' or
132
134
  'batches' (the default)
133
135
 
136
+ bash! --main
137
+ Emit a bash script to build the database. The script is constructed from
138
+ the build attributes in the environment file
139
+
134
140
  fox! -- FILE...
135
141
  Load fox file data. Data are reset to their initial state after build
136
142
  before the fox data are loaded. This makes it possible to experiment with
@@ -182,6 +188,8 @@ SPEC = %(
182
188
  read name title <<< $(prick dump value PRICK_NAME PRICK_TITLE)
183
189
 
184
190
  dump.node!
191
+ Dump build nodes
192
+
185
193
  dump.build!
186
194
  dump.data! @ TODO
187
195
  dump.schema! @ TODO
@@ -194,7 +202,7 @@ def require_db(database = Prick.state&.database, exist: true)
194
202
  dba = State.connection
195
203
  if exist
196
204
  dba.rdbms.exist?(database) or Prick.error "Can't find database '#{database}'"
197
- if Prick.state&.conn&.name == database
205
+ if Prick.state&.connection&.name == database
198
206
  close_conn = false
199
207
  conn = Prick.state.conn
200
208
  else
@@ -353,29 +361,32 @@ begin
353
361
  dump = cmd.dump? ? cmd.dump("batches")&.to_sym || :batches : nil
354
362
  Prick::SubCommand.make(database, username, args.expect(0..1), timer: cmd.time?, dump: dump)
355
363
 
364
+ when :bash!
365
+ Prick::SubCommand.bash(main: cmd.main?)
366
+
356
367
  when :fox!
357
368
  require_db
358
369
  Prick::SubCommand.fox(database, username, args)
359
370
 
360
371
  when :drop!
361
- require_db if cmd.subcommand != :owner!
362
- if cmd.subcommand == :schema!
363
- schemas = args.to_a
364
- else
365
- args.expect(0)
366
- end
367
372
  case cmd.subcommand
368
- when :all!
369
- Prick::SubCommand.drop_all(database, username)
370
373
  when :users!
374
+ # Should set state.username to owner of database
375
+ database = state.database = args.expect(0..1) || database
376
+ username = state.username = State.connection.rdbms.owner(database)
377
+ require_db(database)
371
378
  Prick::SubCommand.drop_users(database)
372
379
  when :owner!
373
- Prick::SubCommand.drop_owner(username)
380
+ owner = args.expect(0..1) || username
381
+ Prick::SubCommand.drop_owner(owner)
374
382
  when :database!
383
+ database = args.expect(0..1) || database
375
384
  Prick::SubCommand.drop_database(database)
376
385
  when :data!
377
386
  raise NotImplementedError
378
387
  when :schema!
388
+ require_db
389
+ schemas = args.to_a
379
390
  Prick::SubCommand.drop_schema(database, schemas)
380
391
  else
381
392
  Prick.error "Unknown subject: #{subject}"
@@ -393,7 +404,7 @@ begin
393
404
  Prick::SubCommand.migrate(database, username, file: cmd.file)
394
405
 
395
406
  when :list!
396
- format = (cmd.subcommand!.long? ? :long : :short) if cmd.subcommand != :users!
407
+ format = (cmd.subcommand!.long? ? :long : :short) if cmd.subcommand && cmd.subcommand != :users!
397
408
  case cmd.subcommand
398
409
  when :environments!; Prick::SubCommand.list_environments(format: format)
399
410
  when :databases!; Prick::SubCommand.list_databases(format: format)
@@ -403,7 +414,7 @@ begin
403
414
  when :users!; Prick::SubCommand.list_users
404
415
  when :owners!; Prick::SubCommand.list_owners(format: format)
405
416
  else
406
- Prick.error "Unknown subcommand: #{cmd.subcommand}"
417
+ Prick.error "Unknown subcommand '#{cmd.subcommand || args.first }'"
407
418
  end
408
419
 
409
420
  when :dump!
@@ -439,15 +450,14 @@ begin
439
450
  Prick.error "Conflicting options - --export and --local"
440
451
  end
441
452
  puts state.bash_source(args.first ? args : nil, scope: scope)
442
- exit
443
453
  when :value!
444
454
  puts args.expect(1..).map { |var| Array(state.bash_environment[var]).join(' ') }
445
455
  else
446
456
  object = args.extract(1) # Fails if object is absent
447
457
  Prick.error "Unknown dump object: #{object}"
448
458
  end
449
- else
450
- Prick.failure "Internal error: Unhandled command - #{opts.subcommand.inspect}"
459
+ else
460
+ Prick.failure "Internal error: Unhandled command - #{opts.subcommand.inspect}"
451
461
  end
452
462
 
453
463
  rescue Prick::Build::PostgresError => ex
data/idea.txt ADDED
@@ -0,0 +1,38 @@
1
+
2
+ Build spec
3
+ Custom variables
4
+ variables VAR...
5
+
6
+ Variables
7
+ graph # old 'standard'
8
+ schema
9
+ rebuild
10
+ refresh
11
+ require
12
+
13
+ Code
14
+ build
15
+ rebuild
16
+ refresh
17
+ import
18
+ export
19
+
20
+ Special code functions
21
+ super
22
+ build file-or-directory # makes sure not to load a file twice
23
+ sql "sql statement"
24
+ use sql-file # makes sure not to load a file twice
25
+ load dump-files # makes sure not to load a file twice
26
+
27
+ build_ENVIRONMENT # eg. 'build_app_registry_backend'
28
+ rebuild_ENVIRONMENT
29
+ refresh_ENVIRONMENT
30
+
31
+ Environment variables
32
+ PRICK_ENVIRONMENTS # all environments included in the current environment
33
+
34
+ Helper functions
35
+ # Consults PRICK_ENVIRONMENTS to check if the given environment is include
36
+ # in the current environment
37
+ has_environment ENVIRONMENT
38
+
@@ -50,12 +50,13 @@ module Prick
50
50
  # The hierarchy of environments is defined in the PRICK_ENVIRONMENT_FILE
51
51
  #
52
52
  def expand_filename(dir, filename)
53
- environment = Prick.state.environment
53
+ envs = Prick.state.environments
54
+ env = envs[Prick.state.environment]
54
55
  bash_vars = Prick.state.bash_environment
55
56
 
56
57
  last = nil
57
- for env in [environment] + Environment[environment].ancestors.reverse
58
- bash_vars["ENVIRONMENT"] = env
58
+ for env in [env] + env.ancestors.reverse
59
+ bash_vars["ENVIRONMENT"] = env.name
59
60
  file = expand_variables(filename, bash_vars)
60
61
  last ||= (file != last and file) or return nil # return if no ENVIRONMENT substitution
61
62
  path = (file.start_with?("/") ? file : File.join(dir, file))
@@ -1,145 +1,366 @@
1
+ require 'set'
1
2
  require 'yaml'
2
3
  require 'dsort'
3
4
 
4
- # TODO:
5
- # o Check :parent vs. :inherit (or explain the difference). Also check
6
- # handling of :comment
7
-
5
+ # TODO
6
+ # o Add a CODE type
7
+ # o super
8
8
  module Prick
9
- # An environment as defined in the prick.environment file. The environment is
10
- # constant across all instances of State so we ensure that it is loaded only
11
- # once
12
9
  class Environment
13
- # Environments name
10
+ # The enclosing Environments object. Used in #types
11
+ attr_reader :environments
12
+
13
+ # Environment name (String)
14
14
  attr_reader :name
15
15
 
16
- # Environment comment. If not nil, this environment is considered a user-environment
17
- attr_accessor :comment
16
+ # List of names of inherited environments
17
+ def inherit = assignments[:inherit]
18
+
19
+ # List of directly inherited environment objects. Assigned by the
20
+ # analyzer
21
+ attr_accessor :parents
22
+
23
+ # Ancestors (array of Environment objects) sorted in dependency order
24
+ attr_accessor :ancestors
25
+
26
+ # The ancestor that defines the build attribute or nil if ambigous.
27
+ # Assigned by the analyzer
28
+ attr_accessor :super_environment
18
29
 
19
- # Names of parent environments
20
- def parents = @values[:parents]
30
+ def has_super?() = !super_environment.nil?
21
31
 
22
- # Ancestors sorted in dependency order
23
- def ancestors
24
- @ancestors ||= effective_values[:parents].sort_by { |name| @@SORTED_INDEXES[name] }
32
+ # Map from variable identifier to the environment that defines the value.
33
+ # This assumes there are no ambigous assignments
34
+ def assigners
35
+ @assigners ||= begin
36
+ effective_variables.keys.map { |ident|
37
+ if assignments.key?(ident)
38
+ [ident, self]
39
+ else
40
+ [ident, ancestors.reverse.find { |env| env.assignments.key?(ident) }]
41
+ end
42
+ }.to_h
43
+ end
25
44
  end
26
45
 
27
- # List of variables including :parents
28
- def variables = @@VARIABLES
46
+ # Environment comment
47
+ def comment = assignments[:comment]
29
48
 
30
- # List of user defined variables
31
- def user_variables = @@VARIABLES - [:parents, :comment]
49
+ # Map from variable identifier (Symbol) to type. Type is one of the strings
50
+ # listed in Environments::TYPES
51
+ def types() = @environments.types
32
52
 
33
- # Hash from variable to value
34
- attr_accessor :values
53
+ # Map from variable identifier (Symbol) to value. Inherited variables are not
54
+ # included in #assignments
55
+ attr_reader :assignments
35
56
 
36
- # Hash from variable name to effective value. The effective value is the
37
- # sum of this environment's value and its parents' values
38
- attr_accessor :effective_values
57
+ # Return true if the environment assigns an attribute
58
+ def assign?(ident) = @assignments.key?(ident)
39
59
 
40
- def self.[](name) @@ENVIRONMENTS[name] end
41
- def self.environment?(name) @@ENVIRONMENTS.key?(name) end
42
- def self.environments() @@SORTED_ENVIRONMENTS.map { |key| @@ENVIRONMENTS[key] } end
43
- def self.variables() @@VARIABLES end
60
+ # Map from variable identifier (Symbol) to effective value. Initially equal
61
+ # to #assignments but are later augmented or merged with the corresponding
62
+ # assignments from inherited environments
63
+ attr_accessor :effective_variables
44
64
 
45
- def initialize(name)
65
+ def initialize(environments, name, assignments)
66
+ constrain environments, Environments
67
+ constrain name, String
68
+ constrain assignments, { Symbol => Object }
69
+ @environments = environments
46
70
  @name = name
47
- @comment = nil
48
- @values = variables.map { |key| [key, []] }.to_h
49
- @effective_values = variables.map { |key| [key, []] }.to_h
50
- @@ENVIRONMENTS[name] = self
71
+ @assignments = assignments
72
+ @assignments[:inherit] ||= []
73
+ @effective_variables = assignments.transform_values { |v| v.dup } # Deep-dup
51
74
  end
52
75
 
53
- def self.load_environments(yaml) # hash maps from environment name to object
54
- if hash
55
- parse(yaml)
56
- analyze
57
- end
58
- end
76
+ forward_to :effective_variables, :key?, :[], :[]=, :to_h, :empty?, :size, :each, :map
59
77
 
60
- # does not include the name of the environment
61
- def bash_env
62
- user_variables.map { |variable|
63
- ["PRICK_ENVIRONMENT_#{variable.upcase}", effective_values[variable]]
78
+ def bash() = "build_#{name}"
79
+ def super_bash() = (has_super? ? "build_#{name}_super" : nil)
80
+ def bash_environment
81
+ effective_variables.map { |ident, value|
82
+ ["PRICK_ENVIRONMENT_#{ident.upcase}", value]
64
83
  }.to_h
65
84
  end
66
85
 
86
+ def inspect = "#<#{self.class} @name=#{@name}>"
87
+
67
88
  def dump
68
- puts name
89
+ puts "#{name}:"
69
90
  indent {
70
- values.each { |var, val|
71
- next if val.empty?
72
- puts "#{var}:"
73
- indent { puts val }
74
- }
75
- effective_values.each { |var, val|
76
- next if val.empty?
77
- puts "effective_#{var}:"
78
- indent { puts val }
91
+ self.each { |ident,val|
92
+ if ident == :build
93
+ puts "super: #{super_environment&.name || 'false'}"
94
+ end
95
+ assigner = assigners[ident]&.name || name
96
+ assigner_str = (assigner != name ? " (#{assigner})" : "")
97
+ if types[ident] == "TEXT"
98
+ puts "#{ident}#{assigner_str}:"
99
+ indent { puts val }
100
+ else
101
+ puts "#{ident}#{assigner_str}: #{val.inspect}"
102
+ end
103
+ if ident == :inherit
104
+ puts "ancestors: #{ancestors.map(&:name).inspect}"
105
+ end
79
106
  }
80
107
  }
81
108
  end
109
+ end
110
+
111
+ class Environments
112
+ # Map from environment name to Environment object
113
+ attr_reader :environments
114
+
115
+ # Environments acts like a hash from name to Environment object
116
+ forward_to :@environments, :[], :[]=, :key?, :keys, :values, :each, :map
117
+
118
+ # Maps from variable name (Symbol) to type (String)
119
+ attr_reader :types
120
+
121
+ # List of all variables (Symbol). Same as 'types.keys'
122
+ def variables = types.keys
123
+
124
+ # List of variable identifiers defined by prick
125
+ def prick_variables = @@PRICK_VARIABLES
126
+
127
+ # List of user defined variables
128
+ def user_variables = variables - prick_variables
129
+
130
+ def initialize(hash)
131
+ @types = @@TYPES.dup
132
+ @environments = {}
133
+ parse_variables(hash)
134
+ parse_environments(hash)
135
+ analyze
136
+ end
82
137
 
83
- def self.dump
84
- environments.each(&:dump)
138
+ def undent(s)
139
+ s = s[1..] if s[0] == "\n"
140
+ s =~ /^(\s*)\S$/m
141
+ indent = $1
142
+ s.gsub(/^#{indent}/m, "")
85
143
  end
86
144
 
87
- private
88
- @@VARIABLES = []
89
- @@ENVIRONMENTS = {}
90
- @@SORTED_ENVIRONMENTS = []
145
+ def bash_environment
91
146
 
92
- def self.yaml_to_array(value)
93
- !value.nil? && Array(value).flatten.compact.map(&:to_s).map(&:split).flatten
94
147
  end
95
148
 
96
- def self.parse(hash)
97
- hash = hash.dup
98
- @@VARIABLES = [:parents, :comment] + (yaml_to_array(hash.delete("variables") || []).map(&:to_sym) || [])
99
-
100
- for environment, settings in hash
101
- env = Environment.new(environment)
102
- settings ||= {}
103
- if settings.is_a? Hash
104
- for variable, value in settings
105
- if variable == "comment"
106
- env.comment = value
107
- else
108
- variable = (variable == "inherit" ? :parents : variable.to_sym)
109
- value = yaml_to_array(value)
110
- variables.include?(variable) or raise ArgumentError, "Illegal variable: '#{variable}'"
111
- env.values[variable] = value
149
+ def bash_command(environment = nil)
150
+ constrain environment, String, nil
151
+ if environment
152
+ env = self[environment] or raise "Unknown environment: '#{environment}'"
153
+ envs = [env]
154
+ else
155
+ envs = environments
156
+ end
157
+
158
+ puts "### SCRIPT by #{File.basename(__FILE__)}"
159
+
160
+ puts undent %(
161
+
162
+ ## STACK METHODS - ChatGPT
163
+
164
+ # global variable
165
+ super_stack=()
166
+
167
+ function push_super_stack() {
168
+ local method=$1
169
+ super_stack+=($method)
170
+ }
171
+
172
+ function pop_super_stack() {
173
+ super_stack=("${super_stack[@]::${#super_stack[@]}-1}")
174
+ }
175
+
176
+ function super() {
177
+ eval ${super_stack[-1]}
178
+ }
179
+ )
180
+
181
+ puts "## ENVIRONMENT METHODS"
182
+ puts
183
+ for name, env in environments
184
+ puts "function build_#{name}() {"
185
+ assigner = env.assigners[:build]
186
+ indent {
187
+ if assigner == env
188
+ if env.has_super?
189
+ puts "push_super_stack #{env.super_bash}"
190
+ end
191
+ puts env[:build]
192
+ if env.has_super?
193
+ puts "pop_super_stack"
112
194
  end
195
+ else
196
+ puts "build_#{assigner.name} # default super"
113
197
  end
114
- else
115
- raise ArgumentError, "Illegal value for '#{environment}': #{settings.inspect}"
198
+ }
199
+ puts "}"
200
+ puts
201
+ if env.has_super? && assigner == env
202
+ puts "build_#{name}_super() { build_#{env.super_environment.name}; }"
203
+ puts
116
204
  end
117
205
  end
206
+
207
+ if environment
208
+ puts "## DEFAULT BUILD METHOD"
209
+ puts
210
+ puts "function build() { build_#{environment}; }"
211
+ puts
212
+ end
213
+ end
214
+
215
+ def dump
216
+ puts "Types"
217
+ indent { types.each { |k,v| puts "#{k}: #{v}" } }
218
+ puts
219
+ puts "Environments"
220
+ indent { environments.values.each { |env| env.dump } }
221
+ end
222
+
223
+ private
224
+ TYPES = %w(BOOLEAN STRING LIST TEXT)
225
+ @@TYPES = { comment: "STRING", inherit: "LIST", build: "TEXT" }
226
+ @@PRICK_VARIABLES = @@TYPES.keys
227
+
228
+ def parse_variables(yaml)
229
+ decls = yaml.delete("variables") || ""
230
+ decls.split.each { |decl|
231
+ decl =~ /^(.*):(.*)$/ or ShellOpts.error "Illegal declaration of '#{decl}' in variable list"
232
+ name, type = $1.to_sym, $2
233
+ type = type.upcase
234
+ TYPES.include?(type) or ShellOpts.error "Illegal type '#{type}'"
235
+ @types[name] = type
236
+ }
118
237
  end
119
238
 
120
- def self.analyze
121
- # Sort environments in dependency order
122
- deps = @@ENVIRONMENTS.map { |name, env| [name, env.parents] }
123
- @@SORTED_ENVIRONMENTS = DSort.dsort(deps)
124
- @@SORTED_INDEXES = @@SORTED_ENVIRONMENTS.map.with_index { |env, idx| [env, idx] }.to_h
239
+ def parse_environments(yaml)
240
+ yaml.each { |environment, variables|
241
+ assignments = variables.map { |ident, value|
242
+ ident = ident.to_sym
243
+ case types[ident]
244
+ when "BOOLEAN"
245
+ [TrueClass, FalseClass].include?(value.class) or raise "Illegal value for #{ident}: #{value}"
246
+ ;
247
+ when "STRING"
248
+ ; # nop
249
+ when "LIST"
250
+ value = value.split
251
+ when "TEXT"
252
+ value = value.chomp
253
+ when nil
254
+ ShellOpts.error "Unknown variable '#{ident}'"
255
+ else
256
+ raise ArgumentError
257
+ end
258
+ [ident, value]
259
+ }.to_h
260
+ @environments[environment] = Environment.new(self, environment, assignments)
261
+ }
262
+ end
125
263
 
126
- # Check for undeclared inherited environments
127
- @@SORTED_ENVIRONMENTS.each { |environment|
128
- @@ENVIRONMENTS.key?(environment) or raise ArgumentError, "Can't find '#{inherited}' environment"
264
+ def analyze
265
+ # Assign Environment#parent
266
+ values.each { |env|
267
+ env.parents = env.inherit.map { |name|
268
+ self[name] or raise ArgumentError, "Can't find '#{name}' environment referred from '#{name}'"
269
+ }
129
270
  }
130
271
 
272
+ # Sort environment names in dependency order
273
+ deps = self.map { |name, env| [name, env.inherit] }
274
+ sorted_environments = DSort.dsort(deps).map { |name| self[name] }
275
+
131
276
  # Compute effective attribute values by processing environments in
132
- # dependency order so that all parents' environments are computed before
277
+ # dependency order so that all inherited environments are computed before
133
278
  # the current environment
134
- for name in @@SORTED_ENVIRONMENTS
135
- env = Environment[name]
136
- env.effective_values = env.values.transform_values { |v| v.dup } # Deep-dup
137
- for parent in env.parents.dup
138
- for var in variables
139
- env.effective_values[var].unshift *Environment[parent].effective_values[var]
279
+ for env in sorted_environments
280
+ for inherited in env.parents
281
+ for ident, type in types
282
+ next if ident == :comment # Comments are not inherited
283
+ next if !inherited.key?(ident)
284
+ value = inherited[ident]
285
+ case type
286
+ when "BOOLEAN"; env[ident] = value if !env.key?(ident)
287
+ when "STRING"; env[ident] ||= value
288
+ when "LIST";
289
+ next if ident == :inherit # Does not accumulate
290
+ # FIXME !inherited.key? should prevent env[ident].nil?
291
+ env[ident] = (value + (env[ident] || [])).uniq
292
+ when "TEXT"; env[ident] = env[ident] || value
293
+ else
294
+ raise ArgumentError
295
+ end
296
+ end
297
+ end
298
+ end
299
+
300
+ # Assign #ancestors
301
+ sorted_indexes = sorted_environments.map.with_index { |env, idx| [env.name, idx] }.to_h
302
+ for env in sorted_environments
303
+ env.ancestors =
304
+ (env.parents + env.parents.map(&:ancestors))
305
+ .flatten
306
+ .uniq
307
+ .sort_by { |env| sorted_indexes[env.name] }
308
+ end
309
+
310
+ # Check ambigous string, text, and boolean definitions (:initial is never
311
+ # ambigous and :comment is special). TODO: Find a faster algorithm
312
+ for env in sorted_environments
313
+ for ident, value in env.effective_variables
314
+ type = types[ident]
315
+
316
+ # Ignore mergeable types (LIST)
317
+ next if !%w(STRING TEXT BOOLEAN).include?(type)
318
+
319
+ # Ignore :initial and :comment
320
+ next if [:initial, :comment].include?(ident)
321
+
322
+ # Ignore if variable is assigned in the current environment and not the
323
+ # special :build attribute
324
+ next if env.assignments.key?(ident) && ident != :build
325
+
326
+ # Pool of ancestors. When an environment assigns a value, all ancestors
327
+ # of the environment are removed from the pool. If any remaining environment
328
+ # also assigns the variable, the definition is ambigous. 'pool' and
329
+ # 'queue' works together to provide a set of environments with
330
+ # queue-like properties
331
+ pool = Set.new(env.ancestors)
332
+
333
+ # Queue of ancestors. It is used to iterate the pool environments in
334
+ # reversed dependency order
335
+ queue = env.ancestors.reverse
336
+
337
+ # Look for the first environment that assigns the variable
338
+ while ancestor = queue.shift
339
+ next if !pool.include?(ancestor)
340
+ if ancestor.assignments.key?(ident)
341
+ # Remove all ancestors that are inherited by the assigning environment
342
+ pool -= ancestor.ancestors
343
+ if ident == :build
344
+ env.super_environment = ancestor #if ident == :build
345
+ end
346
+ break
347
+ end
348
+ end
349
+
350
+ # Check that remaining environments do not also assign the variable.
351
+ # The :block attribute is checked even if defined in the current
352
+ # environment to be able to tell if 'super' is ambigous
353
+ while ancestor = queue.shift
354
+ next if !pool.include?(ancestor)
355
+ if ancestor.assign?(ident)
356
+ if ident == :build && env.assign?(:build)
357
+ env.super_environment = nil
358
+ else
359
+ raise ArgumentError, "Ambigious definition of '#{ident}' in #{ancestor.name} environment"
360
+ end
361
+ end
140
362
  end
141
363
  end
142
- env.effective_values.transform_values! { |v| v.uniq }
143
364
  end
144
365
  end
145
366
  end
@@ -1,9 +1,5 @@
1
1
  require 'fcntl'
2
2
 
3
- require 'forward_to'
4
-
5
- include ForwardTo
6
-
7
3
  module Command
8
4
  class Error < RuntimeError
9
5
  attr_reader :cmd
@@ -5,52 +5,95 @@ module Fmt
5
5
  widths = headers.map(&:size)
6
6
  signs = []
7
7
  types = []
8
+ indexes = [] # absolute position of column. Zero based
8
9
  for i in (0...headers.size)
9
- widths[i] =
10
- (
11
- [widths[i]] +
12
- table.map { |row|
13
- case value = row[i]
14
- when TrueClass, FalseClass
15
- types[i] = 's'
16
- signs[i] = '-'
17
- 5
18
- when NilClass
19
- types[i] = 's'
20
- signs[i] = '-'
21
- 4
22
- when String
23
- types[i] = 's'
24
- signs[i] = '-'
25
- value.size
26
- when Integer
27
- types[i] = 'i'
28
- signs[i] = ''
29
- Math.log10(value) + 1
30
- else
31
- raise ArgumentError, value
32
- end
33
- }
34
- ).max
10
+ value_width =
11
+ table.map { |row|
12
+ case value = row[i]
13
+ when TrueClass, FalseClass
14
+ types[i] = 's'
15
+ signs[i] = '-'
16
+ 5
17
+ when NilClass
18
+ types[i] = 's'
19
+ signs[i] = '-'
20
+ 4
21
+ when String
22
+ types[i] = 's'
23
+ signs[i] = '-'
24
+ value.split("\n").map(&:size).max || 0
25
+ when Integer
26
+ types[i] = 'i'
27
+ signs[i] = ''
28
+ Math.log10(value) + 1
29
+ when Array
30
+ types[i] = 's'
31
+ signs[i] = '-'
32
+ all_size = value.join(' ').size
33
+ if all_size < 60
34
+ all_size
35
+ else
36
+ value.map(&:size).max
37
+ end
38
+ when PrickVersion
39
+ types[i] = 's'
40
+ signs[i] = '-'
41
+ value.to_s.size
42
+ else
43
+ raise ArgumentError, "Illegal value: '#{value}' (#{value.class})"
44
+ end
45
+ }.max || 0
46
+ widths[i] = [widths[i], value_width].max
47
+ indexes[i] = (i == 0 ? 0 : indexes[i-1] + widths[i-1] + 1)
35
48
  end
36
49
 
37
- widths = widths.map { _1 }
38
-
39
50
  for width, value in widths.zip(headers)
40
51
  printf "%-#{width}s ", value
41
52
  end
42
53
  puts
54
+
43
55
  widths.each.with_index { |width, index|
44
56
  char = (headers[index] =~ /^\s*$/ ? " " : "-")
45
57
  printf "%#{width}s ", char * width
46
58
  }
47
59
  puts
60
+
48
61
  for row in table
49
- for sign, width, type, value in signs.zip(widths, types, row)
50
- value = (value.nil? ? "-" : value)
51
- printf "%#{sign}#{width}#{type} ", value
62
+ # for index, sign, width, type, value in signs.zip(indexes, signs, widths, types, row) # FIXME doesn't work?
63
+ for i in (0...headers.size)
64
+ index, sign, width, type, value = indexes[i], signs[i], widths[i], types[i], row[i]
65
+
66
+ if value && value =~ /\n/m
67
+ value = value.split("\n")
68
+ print value.first
69
+ for line in value[1..]
70
+ puts
71
+ print "#{' ' * index}#{line}"
72
+ end
73
+ elsif value.is_a?(Array)
74
+ values = value.dup
75
+ lines = []
76
+ while !values.empty?
77
+ rest = width
78
+ line = []
79
+ while !values.empty? && values.first.size <= rest
80
+ rest -= values.first.size
81
+ line << values.shift
82
+ end
83
+ lines << line.join(" ")
84
+ end
85
+ print lines.first
86
+ for line in lines[1..]
87
+ puts
88
+ print "#{' ' * index}#{line}"
89
+ end
90
+ else
91
+ value = (value.nil? ? "-" : value)
92
+ printf "%#{sign}#{width}#{type} ", "#{value}"
93
+ end
52
94
  end
53
95
  puts
54
96
  end
55
97
  end
56
98
  end
99
+
data/lib/prick/state.rb CHANGED
@@ -52,14 +52,17 @@ module Prick
52
52
  # database is absent
53
53
  attr_accessor :username
54
54
 
55
- # Environment name. If not set in the state file, the enviroment is read
56
- # from the database when the first connection is established by the
57
- # #connection method. Use 'Environment[environment]' to get the
55
+ # Map from environment name to environment object
56
+ attr_reader :environments
57
+
58
+ # Name of current environment. If not set in the state file, the enviroment
59
+ # is read from the database when the first connection is established by the
60
+ # #connection method. Use '#environments[environment]' to get the
58
61
  # corresponding Environment object
59
62
  def environment() @environment end
60
63
  def environment=(env)
61
64
  constrain env, String, nil
62
- env.nil? || Environment.environment?(env) or raise "Illegal environment: '#{env}'"
65
+ env.nil? || environments.key?(env) or raise "Illegal environment: '#{env}'"
63
66
  @environment = env
64
67
  end
65
68
 
@@ -106,13 +109,13 @@ module Prick
106
109
  # Project user (owner) connection. Memoized to connect only once.
107
110
  # TODO Rename. Also rename self.connection
108
111
  def connection(database: nil, username: nil, environment: nil, &block)
109
- database ||= self.database
110
- username ||= self.username
111
- environment ||= self.environment
112
- !database.nil? or Prick.error "Can't connect to Postgres - no database specified"
113
- # exist_database_environment? or Prick.error "Database '#{database}' is not initialized"
114
-
115
112
  if @connection.nil?
113
+ database ||= self.database
114
+ username ||= self.username
115
+ environment ||= self.environment
116
+ !database.nil? or Prick.error "Can't connect to Postgres - no database specified"
117
+ # exist_database_environment? or Prick.error "Database '#{database}' is not initialized"
118
+
116
119
  @connection = PgConn.new(database, username)
117
120
 
118
121
  # Set database_version/environment/prick members
@@ -122,7 +125,7 @@ module Prick
122
125
  self.environment =
123
126
  environment ||
124
127
  Prick.state.environment ||
125
- Environment.environment?(database_environment) && database_environment ||
128
+ environments.key?(database_environment) && database_environment ||
126
129
  DEFAULT_ENVIRONMENT
127
130
  end
128
131
  if block_given?
@@ -193,13 +196,18 @@ module Prick
193
196
  })
194
197
 
195
198
  # PRICK_ENVIRONMENT_* variables
196
- hash.merge! (Prick.state.environment.nil? ? {} : Environment[Prick.state.environment].bash_env)
199
+ if !Prick.state.environment.nil?
200
+ hash.merge! environments[environment].bash_environment
201
+ end
197
202
  end
198
203
  end
199
204
 
200
205
  # @scope can be :global (variables are exported), :local (variables are
201
206
  # declared local), or nil (variables are global but not exported)
207
+ #
208
+ # Only non-text variables are emitted
202
209
  def bash_source(vars = nil, scope: nil)
210
+ exclude = Array(exclude || []).flatten.map { _1.to_s }
203
211
  case scope
204
212
  when :global; prefix="export "
205
213
  when :local; prefix="local "
@@ -208,15 +216,26 @@ module Prick
208
216
  raise ArgumentError, "Illegal value for scope: #{scope.inspect}"
209
217
  end
210
218
 
211
- (vars || bash_environment.keys).map { |var|
219
+ vars ||= bash_environment&.keys || []
220
+ assignments = []
221
+ vars.each { |var|
212
222
  val = bash_environment[var]
213
- "#{prefix}#{var}=\"#{Array(val).join(' ')}\"\n"
214
- }.join
223
+ # next if val =~ /['"\n]/m # We don't quote
224
+ if val.is_a?(Array)
225
+ if val.first.is_a?(Array)
226
+ val = val.map { |v| v.join("\n") }.join("\n")
227
+ else
228
+ val = val.join(" ")
229
+ end
230
+ end
231
+ assignments << "#{prefix}#{var}='#{val}'\n"
232
+ }
233
+ assignments.join
215
234
  end
216
235
 
217
236
  # It is an error if the project file exists.
218
- def save_project
219
- !File.exists?(project_file) or Prick.error "Won't overwrite '#{project_file}'"
237
+ def save_project(overwrite: false)
238
+ overwrite || !File.exists?(project_file) or Prick.error "Won't overwrite '#{project_file}'"
220
239
  hash = { name: name, title: title, version: version.to_s, prick: prick_version.to_s }
221
240
  save_yaml(project_file, hash)
222
241
  end
@@ -243,7 +262,7 @@ module Prick
243
262
  "prick.builds",
244
263
  name: name,
245
264
  version: version.to_s, prick: prick_version,
246
- branch: branch, rev: rev(:short), clean: clean?,
265
+ branch: branch, rev: rev(kind: :short), clean: clean?,
247
266
  environment: environment)
248
267
  end
249
268
  end
@@ -257,7 +276,7 @@ module Prick
257
276
  "prick.builds",
258
277
  name: name,
259
278
  version: version.to_s, prick: prick_version,
260
- branch: branch, rev: rev(:short), clean: clean?,
279
+ branch: branch, rev: rev(kind: :short), clean: clean?,
261
280
  environment: environment,
262
281
  success: success, duration: duration, prick_duration: dt)
263
282
  end
@@ -272,7 +291,7 @@ module Prick
272
291
  puts "#{method}: #{self.send method}"
273
292
  end
274
293
  puts "environments:"
275
- indent { Environment.dump }
294
+ indent { environments.dump }
276
295
  }
277
296
  end
278
297
 
@@ -281,7 +300,7 @@ module Prick
281
300
 
282
301
  def read_yaml(file)
283
302
  begin
284
- YAML.load(File.read file)
303
+ YAML.load File.read(file).sub(/^__END__\n.*/m, "")
285
304
  rescue Errno::ENOENT
286
305
  Prick.error "Can't read #{file}"
287
306
  end
@@ -290,7 +309,7 @@ module Prick
290
309
  def load_yaml(file, mandatory_keys, optional_keys = [])
291
310
  mandatory_keys = mandatory_keys.map(&:to_s)
292
311
  optional_keys = optional_keys.map(&:to_s)
293
- hash = read_yaml(file)
312
+ hash = read_yaml(file) or Prick.error "Not a valid YAML file - #{file}"
294
313
  for key in mandatory_keys
295
314
  !hash[key].to_s.empty? or Prick.error "Can't find '#{key}' in #{file}"
296
315
  end
@@ -347,7 +366,7 @@ module Prick
347
366
 
348
367
  def load_environment_file
349
368
  hash = environment_file ? read_yaml(environment_file) : {}
350
- Environment.load_environments(hash)
369
+ @environments = Environments.new(hash)
351
370
  @environment_loaded = true
352
371
  end
353
372
 
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ class IO
4
+ def self.capture(&block) # ChatGPT
5
+ block_given? or raise ArgumentError
6
+ stdout = $stdout
7
+ begin
8
+ $stdout = StringIO.new
9
+ yield
10
+ $stdout.string
11
+ ensure
12
+ $stdout = stdout
13
+ end
14
+ end
15
+ end
16
+
17
+ module Prick::SubCommand
18
+ def self.bash(main: false)
19
+ # IO.capture {
20
+ env = Prick.state.environment # Shorthands
21
+ envs = Prick.state.environments
22
+
23
+ puts "#!/usr/bin/bash"
24
+ puts
25
+ puts "# This file is auto-generated by prick. Please don't touch"
26
+ puts
27
+ puts ". bash.include"
28
+ puts
29
+
30
+ # Emit environment. PRICK_ENVIRONMENT_BUILD is excluded because its value
31
+ # is bash source that is hard to escape and there is no use for this
32
+ # variable anyway
33
+ puts "### ENVIRONMENT by state.rb"
34
+ puts
35
+ puts Prick.state.bash_source(scope: :global)
36
+ puts
37
+
38
+ # Emit script
39
+ if env
40
+ envs.bash_command env
41
+ else
42
+ envs.bash_command
43
+ end
44
+
45
+ if main
46
+ puts "### MAIN by prick-load.rb"
47
+ puts
48
+ puts "build"
49
+ end
50
+ # }
51
+ end
52
+ end
53
+
@@ -18,7 +18,7 @@ module Prick::SubCommand
18
18
  conn = nil
19
19
  builder = nil
20
20
 
21
- constrain super_conn.rdbms.exist?, true
21
+ constrain super_conn.rdbms.exist?(database), true # FIXME Same problem as below
22
22
 
23
23
  Timer.time "Load build object" do
24
24
  if super_conn.rdbms.exist? database # FIXME Why create database? Setup should have done this
@@ -88,7 +88,7 @@ module Prick
88
88
  }
89
89
  !diff.same? or Prick.failure "No changes"
90
90
  ensure
91
- drop_all(from_db)
91
+ drop_all(from_db) # FIXME This will fail, maybe use teardown instead?
92
92
  drop_all(to_db)
93
93
  end
94
94
  end
@@ -29,6 +29,8 @@ module Prick::SubCommand
29
29
  end
30
30
  else
31
31
  # We don't terminate sessions because we assume one-database-users
32
+ # FIXME: users is undefined
33
+ raise NotImpementedError
32
34
  conn.role.drop(users, cascade: true) # Fails if the users owns objects in other databases
33
35
  end
34
36
  end
@@ -44,30 +46,12 @@ module Prick::SubCommand
44
46
  constrain database, String
45
47
  constrain schemas, [String]
46
48
  if schemas.empty?
47
- State.connection { |conn| conn.rdbms.empty!(database) }
49
+ State.connection { |conn| conn.rdbms.empty!(database, exclude: "prick") }
48
50
  else
49
51
  Prick.state.connection { |conn|
50
52
  schemas.each { |schema| conn.schema.drop(schema, cascade: true) }
51
53
  }
52
54
  end
53
55
  end
54
-
55
- # Drop database and its users including the owner if possible and not the
56
- # current user
57
- def self.drop_all(database, username = nil)
58
- constrain database, String
59
- State.connection { |conn|
60
- if username.nil? && conn.rdbms.exist?(database)
61
- owner = State.connection.rdbms.owner(database)
62
- else
63
- owner = username || database
64
- end
65
- if conn.rdbms.exist? database
66
- drop_users(database)
67
- drop_database(database)
68
- end
69
- drop_owner(owner) if owner != ENV['USER']
70
- }
71
- end
72
56
  end
73
57
 
@@ -1,6 +1,6 @@
1
1
  module Prick::SubCommand
2
2
  def self.list_environments(format: :long)
3
- environments = Environment.environments.select { |env| env.comment }
3
+ environments = Prick.state.environments.values.select { |env| env.comment }
4
4
  if format == :short
5
5
  puts environments.map(&:name)
6
6
  else
@@ -12,11 +12,24 @@ module Prick::SubCommand
12
12
 
13
13
  def self.list_variables(format: :long, all: false)
14
14
  if format == :short
15
- puts Prick.state.bash_environment.keys
15
+ puts Prick.state.bash_environment(all: all).keys
16
16
  else
17
17
  headers = %w(variable value)
18
18
  vars = Prick.state.bash_environment(all: all).reject { |k,v| k == "PATH" }
19
- rows = vars.map { |k,v| [k, Array(v).flatten.join(" ")] }
19
+ rows = vars.map
20
+ # rows = vars.map { |k,v|
21
+ # if v.is_a?(Array)
22
+ # if v.first&.is_a?(Array)
23
+ # v = v.first
24
+ # else
25
+ # v = v.join(" ")
26
+ # end
27
+ # else
28
+ # v = v.to_s
29
+ # end
30
+ #
31
+ # [k, v]
32
+ # }
20
33
  Fmt.puts_table(headers, rows)
21
34
  end
22
35
  end
@@ -8,7 +8,7 @@ module Prick
8
8
  Git.synchronized? or raise "Won't release: Repository is not synchronized with origin"
9
9
 
10
10
  version = Prick.state.version.increment!(kind).to_s
11
- Prick.state.save_project
11
+ Prick.state.save_project(overwrite: true)
12
12
 
13
13
  Git.add(Prick.state.project_file)
14
14
  Git.add(Prick.state.schema_file)
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.30.0"
4
+ VERSION = "0.32.0"
5
5
  end
data/lib/prick.rb CHANGED
@@ -2,9 +2,15 @@ require 'prick/version.rb'
2
2
 
3
3
  require 'pg_conn'
4
4
 
5
- require 'fixture_fox'
6
5
  require 'indented_io'
6
+
7
7
  require 'constrain'
8
+ include Constrain
9
+
10
+ require 'forward_to'
11
+ include ForwardTo
12
+
13
+ require 'fixture_fox'
8
14
 
9
15
  module Prick
10
16
  class Error < RuntimeError; end
@@ -33,17 +39,18 @@ module Prick
33
39
  end
34
40
 
35
41
  require_relative 'prick/subcommand/subcommand.rb'
42
+ require_relative 'prick/subcommand/prick-bash.rb'
36
43
  require_relative 'prick/subcommand/prick-build.rb'
37
44
  require_relative 'prick/subcommand/prick-clean.rb'
38
45
  require_relative 'prick/subcommand/prick-create.rb'
39
46
  require_relative 'prick/subcommand/prick-drop.rb'
40
- require_relative 'prick/subcommand/prick-set.rb'
41
- require_relative 'prick/subcommand/prick-list.rb'
42
47
  require_relative 'prick/subcommand/prick-fox.rb'
43
48
  require_relative 'prick/subcommand/prick-init.rb'
49
+ require_relative 'prick/subcommand/prick-list.rb'
44
50
  require_relative 'prick/subcommand/prick-make.rb'
45
51
  require_relative 'prick/subcommand/prick-migrate.rb'
46
52
  require_relative 'prick/subcommand/prick-release.rb'
53
+ require_relative 'prick/subcommand/prick-set.rb'
47
54
  require_relative 'prick/subcommand/prick-setup.rb'
48
55
  require_relative 'prick/subcommand/prick-teardown.rb'
49
56
 
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.30.0
4
+ version: 0.32.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-03-23 00:00:00.000000000 Z
11
+ date: 2024-04-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: semantic
@@ -182,6 +182,7 @@ files:
182
182
  - bin/setup
183
183
  - doc/build-yml.txt
184
184
  - exe/prick
185
+ - idea.txt
185
186
  - lib/prick.rb
186
187
  - lib/prick/builder/batch.rb
187
188
  - lib/prick/builder/builder.rb
@@ -218,6 +219,7 @@ files:
218
219
  - lib/prick/share/migrate/migration/diff.before-tables.sql
219
220
  - lib/prick/share/migrate/migration/diff.tables.sql
220
221
  - lib/prick/state.rb
222
+ - lib/prick/subcommand/prick-bash.rb
221
223
  - lib/prick/subcommand/prick-build.rb
222
224
  - lib/prick/subcommand/prick-clean.rb
223
225
  - lib/prick/subcommand/prick-create.rb
@@ -252,7 +254,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
252
254
  - !ruby/object:Gem::Version
253
255
  version: '0'
254
256
  requirements: []
255
- rubygems_version: 3.3.18
257
+ rubygems_version: 3.3.7
256
258
  signing_key:
257
259
  specification_version: 4
258
260
  summary: A release control and management system for postgresql