prick 0.18.0 → 0.20.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +10 -4
  3. data/README.md +7 -7
  4. data/Rakefile +3 -1
  5. data/TODO +13 -11
  6. data/bin/console +2 -1
  7. data/doc/build-yml.txt +14 -0
  8. data/exe/prick +264 -28
  9. data/lib/builder/batch.rb +147 -0
  10. data/lib/builder/builder.rb +122 -0
  11. data/lib/builder/node.rb +189 -0
  12. data/lib/builder/node_pool.rb +105 -0
  13. data/lib/builder/parser.rb +120 -0
  14. data/lib/local/command.rb +193 -0
  15. data/lib/{prick → local}/git.rb +148 -22
  16. data/lib/local/timer.rb +98 -0
  17. data/lib/prick/constants.rb +54 -66
  18. data/lib/prick/diff.rb +28 -18
  19. data/lib/prick/prick_version.rb +161 -0
  20. data/lib/prick/state.rb +80 -165
  21. data/lib/prick/version.rb +2 -163
  22. data/lib/prick.rb +43 -27
  23. data/lib/share/init/.gitignore +10 -0
  24. data/lib/share/init/.prick-context +2 -0
  25. data/lib/share/init/.rspec +3 -0
  26. data/{share/schema/schema/public → lib/share/init/migration}/.keep +0 -0
  27. data/lib/share/init/prick.yml +6 -0
  28. data/lib/share/init/schema/.keep +0 -0
  29. data/lib/share/init/schema/build.yml +2 -0
  30. data/lib/share/init/schema/prick/.keep +0 -0
  31. data/lib/share/init/schema/prick/build.yml +5 -0
  32. data/lib/share/init/schema/prick/data.sql +6 -0
  33. data/{share/schema → lib/share/init}/schema/prick/tables.sql +2 -3
  34. data/lib/share/init/schema/public/.keep +0 -0
  35. data/lib/share/init/spec/prick_helper.rb +1 -0
  36. data/lib/share/init/spec/prick_spec.rb +6 -0
  37. data/lib/share/init/spec/spec_helper.rb +50 -0
  38. data/lib/share/migrate/migration/build.yml +4 -0
  39. data/lib/share/migrate/migration/diff.after-tables.sql +0 -0
  40. data/lib/share/migrate/migration/diff.before-tables.sql +0 -0
  41. data/lib/share/migrate/migration/diff.tables.sql +0 -0
  42. data/lib/subcommand/prick-build.rb +55 -0
  43. data/lib/subcommand/prick-create.rb +78 -0
  44. data/lib/subcommand/prick-drop.rb +25 -0
  45. data/lib/subcommand/prick-fox.rb +62 -0
  46. data/lib/subcommand/prick-init.rb +46 -0
  47. data/lib/subcommand/prick-make.rb +202 -0
  48. data/lib/subcommand/prick-migrate.rb +37 -0
  49. data/lib/subcommand/prick-release.rb +23 -0
  50. data/lib/subcommand/prick-setup.rb +20 -0
  51. data/lib/subcommand/prick-teardown.rb +18 -0
  52. data/prick.gemspec +43 -16
  53. metadata +161 -72
  54. data/.gitignore +0 -29
  55. data/.travis.yml +0 -7
  56. data/doc/create_release.txt +0 -17
  57. data/doc/flow.txt +0 -98
  58. data/doc/migra +0 -1
  59. data/doc/migrations.txt +0 -172
  60. data/doc/notes.txt +0 -116
  61. data/doc/prick.txt +0 -114
  62. data/doc/sh.prick +0 -316
  63. data/lib/ext/algorithm.rb +0 -14
  64. data/lib/ext/fileutils.rb +0 -26
  65. data/lib/ext/forward_method.rb +0 -18
  66. data/lib/ext/pg.rb +0 -18
  67. data/lib/ext/shortest_path.rb +0 -44
  68. data/lib/prick/archive.rb +0 -124
  69. data/lib/prick/branch.rb +0 -254
  70. data/lib/prick/builder.rb +0 -205
  71. data/lib/prick/cache.rb +0 -34
  72. data/lib/prick/command.rb +0 -102
  73. data/lib/prick/database.rb +0 -82
  74. data/lib/prick/dsort.rb +0 -151
  75. data/lib/prick/ensure.rb +0 -119
  76. data/lib/prick/exceptions.rb +0 -25
  77. data/lib/prick/head.rb +0 -183
  78. data/lib/prick/migration.rb +0 -70
  79. data/lib/prick/program.rb +0 -506
  80. data/lib/prick/project.rb +0 -626
  81. data/lib/prick/rdbms.rb +0 -137
  82. data/lib/prick/schema.rb +0 -27
  83. data/lib/prick/share.rb +0 -64
  84. data/libexec/strip-comments +0 -33
  85. data/make_releases +0 -72
  86. data/make_schema +0 -10
  87. data/share/diff/diff.after-tables.sql +0 -4
  88. data/share/diff/diff.before-tables.sql +0 -4
  89. data/share/diff/diff.tables.sql +0 -8
  90. data/share/features/diff.sql +0 -2
  91. data/share/features/feature/diff.sql +0 -2
  92. data/share/features/feature/migrate.sql +0 -2
  93. data/share/features/features.sql +0 -2
  94. data/share/features/features.yml +0 -2
  95. data/share/features/migrations.sql +0 -4
  96. data/share/gitignore +0 -2
  97. data/share/migration/diff.tables.sql +0 -8
  98. data/share/migration/features.yml +0 -6
  99. data/share/migration/migrate.sql +0 -3
  100. data/share/migration/migrate.yml +0 -8
  101. data/share/migration/tables.sql +0 -3
  102. data/share/schema/build.yml +0 -14
  103. data/share/schema/schema/build.yml +0 -3
  104. data/share/schema/schema/prick/build.yml +0 -14
  105. data/share/schema/schema/prick/data.sql +0 -7
  106. data/share/schema/schema/prick/schema.sql +0 -3
  107. data/share/schema/schema/public/build.yml +0 -13
  108. data/share/schema/schema.sql +0 -3
  109. data/test_assorted +0 -192
  110. data/test_feature +0 -112
  111. data/test_refactor +0 -34
  112. data/test_single_dev +0 -83
@@ -0,0 +1,122 @@
1
+ require 'local/command'
2
+
3
+ require 'builder/node.rb'
4
+ require 'builder/node_pool.rb'
5
+ require 'builder/batch.rb'
6
+ require 'builder/parser.rb'
7
+
8
+ include Constrain
9
+
10
+ module Prick
11
+ module Build
12
+ class Error < StandardError; end
13
+ class PostgresError < Error; end
14
+
15
+ class Builder
16
+ # PgConn object
17
+ attr_reader :conn
18
+
19
+ # Root schema directory
20
+ attr_reader :dir
21
+
22
+ # Reflections YAML file
23
+ attr_reader :reflections_file
24
+
25
+ # True if database is initially clean - ie. all tables are empty
26
+ attr_accessor :clean
27
+
28
+ # Root build node
29
+ attr_reader :root
30
+
31
+ # Pool of nodes. Initialized by #load_pool
32
+ attr_reader :pool
33
+
34
+ forward_to :pool, :nodes, :decl_nodes, :init_nodes, :term_nodes,
35
+ :seed_nodes, :fox_seed_nodes, :sql_seed_nodes,
36
+ :schemas
37
+
38
+ def batches() @batches ||= group end
39
+
40
+ def initialize(conn, dir, clean = true, touched: false)
41
+ @conn = conn
42
+ @dir = dir
43
+ @reflections_file = REFLECTIONS_PATH
44
+ @clean = clean
45
+ @pool = NodePool.new
46
+ @root = Parser.parse(conn, dir)
47
+ load_pool(@root) # Collect nodes into pool
48
+ @batches = nil # Initialized by #group
49
+ end
50
+
51
+ # Group sources into batches
52
+ def group
53
+ @batches = []
54
+ kind = nil
55
+ batch = nil
56
+
57
+ for node in [init_nodes, decl_nodes, fox_seed_nodes, sql_seed_nodes, term_nodes].flatten
58
+ case node.kind
59
+ when :module
60
+ if batch&.kind != :module
61
+ @batches << batch if batch
62
+ batch = ModuleBatch.new(self)
63
+ end
64
+ when :exe # Exe sources always create a new batch
65
+ @batches << batch if batch
66
+ batch = SqlBatch.new(self)
67
+ when batch&.kind
68
+ ;
69
+ when :sql || node.kind == :inline
70
+ if batch&.kind != :exe
71
+ @batches << batch if batch
72
+ batch = SqlBatch.new(self)
73
+ end
74
+ when :inline
75
+ @batches << batch if batch
76
+ batch = SqlBatch.new(self)
77
+ when :fox
78
+ @batches << batch if batch
79
+ batch = FoxBatch.new(self)
80
+ when :yml
81
+ next
82
+ else
83
+ raise Error, "Unexpected node kind: #{node.kind}"
84
+ end
85
+ batch.nodes << node
86
+ end
87
+ @batches << batch if batch
88
+ end
89
+
90
+ def execute(conn, create_schemas: schemas)
91
+ group if batches.nil?
92
+ conn.exec create_schemas.grep_v("public").map { |schema| "create schema #{schema}" }
93
+ for batch in batches
94
+ batch.execute
95
+ end
96
+ end
97
+
98
+ # def setup
99
+ # end
100
+
101
+ # def teardown
102
+ # end
103
+
104
+ def dump
105
+ batches ? batches.each(&:dump) : pool.dump
106
+ end
107
+
108
+ private
109
+ def load_pool(build_node)
110
+ pool.add(build_node.init_nodes)
111
+ build_node.decl_nodes.each { |node|
112
+ pool.add node
113
+ load_pool(node) if node.kind == :yml
114
+ }
115
+ pool.add(build_node.seed_nodes)
116
+ pool.add(build_node.term_nodes)
117
+ end
118
+ end
119
+ end
120
+ end
121
+
122
+
@@ -0,0 +1,189 @@
1
+ module Prick
2
+ module Build
3
+ class Node
4
+ attr_reader :parent
5
+ forward_to :parent, :conn
6
+ attr_reader :phase # :init, :decl, :seed, :term or nil (for BuildNode)
7
+ attr_reader :kind # :sql, :exe, :fox, :yml, :inline, :module
8
+ attr_reader :path
9
+ attr_reader :args # only defined for :exe (String)
10
+
11
+ def name() @name = File.basename(path) end
12
+
13
+ def schema() @schema ||= parent&.schema || "public" end
14
+ def schema=(s) @schema = s end
15
+
16
+ attr_reader :source
17
+
18
+ def source
19
+ @source ||= read_source
20
+ end
21
+
22
+ def prefix_lines() 0 end
23
+
24
+ def source_lines()
25
+ return @source_lines if @source_lines
26
+ source
27
+ @source_lines
28
+ end
29
+
30
+ def lines() prefix_lines + source_lines end
31
+
32
+ def initialize(parent, phase, kind, path, args = nil)
33
+ constrain parent, BuildNode, NilClass
34
+ constrain phase, :init, :decl, :seed, :term, nil
35
+ constrain kind, :sql, :exe, :fox, :yml, :inline, :module
36
+ constrain path, String, NilClass
37
+ @parent, @phase, @kind, @path = parent, phase, kind, path
38
+ @args = args&.empty? ? nil : args
39
+ @schema = nil
40
+ @source_lines = nil
41
+ end
42
+
43
+ def to_s() [path, args].compact.join(" ") end
44
+ def inspect() to_s end
45
+ def dump() puts "#{inspect} (#{schema})" end
46
+
47
+ protected
48
+ def read_source
49
+ @source_lines = 0
50
+ @source = []
51
+ end
52
+ end
53
+
54
+ class SqlNode < Node
55
+ def initialize(parent, phase, path)
56
+ super(parent, phase, :sql, path)
57
+ end
58
+
59
+ protected
60
+ def read_source
61
+ file = File.read(path)
62
+ @source_lines = 1 + 1 + file.count("\n")
63
+ ["set search_path to #{schema};\n", file]
64
+ end
65
+ end
66
+
67
+ class FoxNode < Node
68
+ def initialize(parent, phase, path)
69
+ super(parent, phase, :fox, path)
70
+ end
71
+ end
72
+
73
+ # Note that #path refers to the build file
74
+ class InlineNode < Node
75
+ attr_reader :stmts
76
+
77
+ def initialize(parent, phase, path, *stmts)
78
+ super(parent, phase, :inline, path)
79
+ @stmts = Array(stmts).flatten
80
+ end
81
+
82
+ def inspect() "#@path \"#{@stmts.join(";")}\"" end
83
+
84
+ protected
85
+ def read_source
86
+ @source_lines = @stmts.size
87
+ @stmts
88
+ end
89
+ end
90
+
91
+ class ModuleNode < Node
92
+ attr_reader :klass
93
+ attr_reader :command
94
+ def object() self.class.objects[klass] end
95
+
96
+ def initialize(parent, phase, path, klass, command, args = nil)
97
+ constrain klass, Symbol, String
98
+ constrain command, Symbol, String
99
+
100
+ super(parent, phase, :module, path, args)
101
+
102
+ @klass = klass.to_sym
103
+ @command = command.to_sym
104
+
105
+ if !object
106
+ Kernel.class_eval File.read(path)
107
+ self.class.objects[@klass] = eval(klass.to_s).new(conn)
108
+ end
109
+ end
110
+
111
+ def call()
112
+ object.send(@command, *args)
113
+ end
114
+
115
+ def inspect() "#{path} #{klass}##{command}" end
116
+
117
+ private
118
+ # Map from klass name (a Symbol) to object
119
+ @@objects = {}
120
+ def self.objects() @@objects end
121
+ end
122
+
123
+ class ExeNode < Node
124
+ # Using a pipe instead of just executing the command shaves off some
125
+ # deciseconds spent starting up bash. It expects the process to read
126
+ # database/username from standard input
127
+ attr_reader :pipe
128
+
129
+ def initialize(parent, phase, path, args = nil)
130
+ super(parent, phase, :exe, path, args)
131
+ @pipe = Command::Pipe.new(to_s, stderr: nil)
132
+ end
133
+
134
+ def inspect() "#{path}(#{args.join(", ")})" end
135
+
136
+ protected
137
+ def read_source
138
+ pipe.puts [conn.name, conn.user]
139
+ sql = pipe.wait
140
+ @source_lines = 1 + 1 + sql.count("\n")
141
+ ["set search_path to #{schema};\n", sql]
142
+ end
143
+ end
144
+
145
+ class BuildNode < Node
146
+ def nodes() @nodes ||= init_nodes + decl_nodes + seed_nodes + term_nodes end
147
+
148
+ attr_reader :decl_nodes
149
+ attr_reader :init_nodes
150
+ attr_reader :term_nodes
151
+ attr_reader :seed_nodes
152
+
153
+ def initialize(parent, path)
154
+ super(parent, nil, :yml, path)
155
+ @decl_nodes = []
156
+ @init_nodes = []
157
+ @term_nodes = []
158
+ @seed_nodes = []
159
+ end
160
+
161
+ def inspect() to_s end
162
+
163
+ def dump
164
+ puts "BuildNode #{path}"
165
+ indent {
166
+ puts "schema: #{schema}" if schema
167
+ decl_nodes.each(&:dump)
168
+ for kind in [:init, :term, :seed]
169
+ kind_nodes = self.send("#{kind}_nodes".to_sym)
170
+ if !kind_nodes.empty?
171
+ puts "#{kind.upcase}:"
172
+ indent { kind_nodes.each(&:dump) }
173
+ end
174
+ end
175
+ }
176
+ end
177
+ end
178
+
179
+ class RootBuildNode < BuildNode
180
+ attr_reader :conn
181
+
182
+ def initialize(conn, path)
183
+ @conn = conn
184
+ super(nil, path)
185
+ end
186
+ end
187
+ end
188
+ end
189
+
@@ -0,0 +1,105 @@
1
+ module Prick
2
+ module Build
3
+ class NodePool
4
+ def schemas() @schemas.keys end
5
+ def before_schema(s) schemas.take_while { |schema| schema != s } end
6
+ def after_schema(s) schemas.reverse.take_while { |schema| schema != s }.reverse end
7
+
8
+ attr_reader :nodes
9
+
10
+ attr_reader :init_nodes
11
+ attr_reader :decl_nodes
12
+ attr_reader :seed_nodes
13
+ attr_reader :term_nodes
14
+
15
+ # attr_reader :setup_nodes
16
+ # attr_reader :teardown_nodes
17
+
18
+ def fox_seed_nodes() seed_nodes.select { |node| node.kind == :fox } end
19
+ def sql_seed_nodes() seed_nodes.select { |node| node.kind == :sql } end
20
+
21
+ def initialize()
22
+ self.clear
23
+ end
24
+
25
+ def add(*nodes)
26
+ nodes = Array(nodes).flatten
27
+ @nodes.concat(nodes)
28
+ nodes.each { |node|
29
+ @schemas[node.schema] += 1
30
+ @kind_nodes[node.phase]&.append(node)
31
+ }
32
+ self
33
+ end
34
+
35
+ def delete(*nodes)
36
+ # puts "#delete(*nodes)"
37
+ nodes = Array(nodes).flatten
38
+ nodes.each { |node|
39
+ delete_node(node)
40
+ kind_nodes = @kind_nodes[node.phase] and kind_nodes.delete_at(kind_nodes.index(node))
41
+ }
42
+ nodes.last
43
+ end
44
+
45
+ def delete_if(phase = nil, &block)
46
+ candidates = @kind_nodes[phase] || @nodes
47
+ delete(candidates.select { |node| yield(node) })
48
+ end
49
+
50
+ def delete_schema(*schemas, exclude: [])
51
+ schemas = Array(schemas).flatten
52
+ delete_if { |node|
53
+ schemas.include?(node.schema) && !exclude.include?(node.phase) && !exclude.include?(node.kind)
54
+ }
55
+ end
56
+
57
+ def clear(*phases)
58
+ phases = Array(phases).flatten
59
+ if !phases.empty?
60
+ for phase in phases
61
+ nodes = @kind_nodes[phase]
62
+ nodes.each { |node| delete_node(node) }
63
+ @kind_nodes[phase].clear
64
+ end
65
+ else
66
+ @schemas = Hash.new(0) # map from schema name to number of nodes
67
+ @nodes = []
68
+ @init_nodes = []
69
+ @decl_nodes = []
70
+ @seed_nodes = []
71
+ @term_nodes = []
72
+ @kind_nodes = {
73
+ decl: @decl_nodes,
74
+ init: @init_nodes,
75
+ seed: @seed_nodes,
76
+ term: @term_nodes,
77
+ yml: nil
78
+ }
79
+ end
80
+ end
81
+
82
+ def dump
83
+ puts "NodePool, #{nodes.size} nodes"
84
+ indent {
85
+ puts "init_nodes:"
86
+ indent { @init_nodes.each &:dump }
87
+ puts "decl_nodes:"
88
+ indent { @decl_nodes.each &:dump }
89
+ puts "seed_nodes:"
90
+ indent { @seed_nodes.each &:dump }
91
+ puts "term_nodes:"
92
+ indent { @term_nodes.each &:dump }
93
+ }
94
+ end
95
+
96
+ private
97
+ def delete_node(node)
98
+ # puts "#delete_node"
99
+ @nodes.delete_at(@nodes.index(node))
100
+ @schemas[node.schema] -= 1
101
+ @schemas.delete(node.schema) if @schemas[node.schema] == 0
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,120 @@
1
+ module Prick
2
+ module Build
3
+ class Parser
4
+ def self.parse(conn, dir)
5
+ Parser.new(conn).parse(dir).unit
6
+ # [parser.unit, parser.schemas]
7
+ end
8
+
9
+ attr_reader :conn
10
+ attr_reader :dir
11
+ attr_reader :unit
12
+ # attr_reader :schemas
13
+
14
+ def initialize(conn)
15
+ @conn = conn
16
+ end
17
+
18
+ def parse(dir)
19
+ @dir = dir
20
+ # @schemas = {}
21
+ parse_directory(nil, dir)
22
+ self
23
+ end
24
+
25
+ private
26
+
27
+ # First build unit is a RootBuildNode, the rest are regular BuildNode objects
28
+ def make_build_unit(parent, path)
29
+ if @unit
30
+ BuildNode.new(parent, path)
31
+ else
32
+ @unit = RootBuildNode.new(conn, path)
33
+ end
34
+ end
35
+
36
+ def parse_directory(parent, dir)
37
+ build_file = "#{dir}/build.yml".sub(/\/\//, "/")
38
+ if File.exist? build_file
39
+ parse_build_file(parent, dir, build_file)
40
+ else
41
+ raise Error, "Can't find build.yml in #{dir}"
42
+ end
43
+ end
44
+
45
+ def parse_build_file(parent, dir, path)
46
+ unit = make_build_unit(parent, path)
47
+ entries = YAML.load(File.read(path)) || []
48
+ entries.each { |entry|
49
+ if entry.is_a?(Hash)
50
+ entry.each { |key, value|
51
+ if key == "schema"
52
+ unit.schema = value
53
+ # @schemas[unit.schema = value] = true
54
+ else
55
+ case key
56
+ when "init"; unit.init_nodes
57
+ when "term"; unit.term_nodes
58
+ when "seed"; unit.seed_nodes
59
+ else
60
+ raise Error, "Illegal key in #{unit.path}: #{key}"
61
+ end.concat(Array(value).map { |value| parse_entry(unit, key.to_sym, dir, value) })
62
+ end
63
+ }
64
+ else
65
+ node = parse_entry(unit, :decl, dir, entry)
66
+ if node.kind == :fox
67
+ unit.seed_nodes << node
68
+ else
69
+ unit.decl_nodes << node
70
+ end
71
+ end
72
+ }
73
+ unit
74
+ end
75
+
76
+ def parse_entry(unit, phase, dir, entry)
77
+ if entry.is_a?(Hash)
78
+ entry.size == 1 or raise Error, "sql and module are single-line values"
79
+ key, value = entry.first
80
+ case key
81
+ when "sql"; InlineNode.new(unit, phase, unit.path, value)
82
+ when "call";
83
+ args = value.split(/\s+/)
84
+ args.size >= 3 or raise "Illegal number of arguments: #{value}"
85
+ file, klass, command, args = *args
86
+ ModuleNode.new(unit, phase, "#{dir}/#{file}", klass, command, args)
87
+ else
88
+ raise Error, "Illegal key: #{key}"
89
+ end
90
+ else
91
+ name = entry
92
+ name.sub!(/\/$/, "")
93
+ if name =~ /^(\S+)\s+(.*)$/ # exe
94
+ file = $1
95
+ args = $2
96
+ else
97
+ file = name
98
+ end
99
+ path = "#{dir}/#{file}"
100
+ if File.directory? path
101
+ parse_directory(unit, path)
102
+ elsif File.executable? path
103
+ ExeNode.new(unit, phase, path, args)
104
+ elsif File.file? path
105
+ case path
106
+ when /\.sql$/
107
+ SqlNode.new(unit, phase, path)
108
+ when /\.fox$/
109
+ FoxNode.new(unit, :seed, path)
110
+ else
111
+ raise Error, "Unrecognized file type #{File.basename(path)} in #{unit.dir}"
112
+ end
113
+ else
114
+ raise Error, "Can't find #{name} in #{dir} from #{unit}"
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end