postspec 0.1.1
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.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +6 -0
- data/Gemfile +5 -0
- data/README.md +36 -0
- data/Rakefile +6 -0
- data/TODO +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/postspec +40 -0
- data/lib/postspec/config.rb +38 -0
- data/lib/postspec/environment.rb +491 -0
- data/lib/postspec/frame.rb +147 -0
- data/lib/postspec/render.rb +178 -0
- data/lib/postspec/state.rb +112 -0
- data/lib/postspec/version.rb +3 -0
- data/lib/postspec.rb +367 -0
- data/lib/postspec_helper.rb +128 -0
- data/lib/share/postspec_schema.sql +137 -0
- data/performance/performance_spec.rb +114 -0
- data/postspec.gemspec +41 -0
- data/snippets/sequences.sql +19 -0
- data/snippets/serials.sql +13 -0
- data/snippets/triggers.sql +14 -0
- metadata +237 -0
data/lib/postspec.rb
ADDED
@@ -0,0 +1,367 @@
|
|
1
|
+
require "developer_exceptions"
|
2
|
+
|
3
|
+
require "fixture_fox"
|
4
|
+
|
5
|
+
require "postspec/version.rb"
|
6
|
+
require "postspec/frame.rb"
|
7
|
+
require "postspec/render.rb"
|
8
|
+
require "postspec/state.rb"
|
9
|
+
require "postspec/environment.rb"
|
10
|
+
require "postspec/config.rb"
|
11
|
+
|
12
|
+
include DeveloperExceptions
|
13
|
+
|
14
|
+
# before everything
|
15
|
+
# truncate or setup seed
|
16
|
+
#
|
17
|
+
# after everything
|
18
|
+
# drop seed methods
|
19
|
+
|
20
|
+
module Postspec
|
21
|
+
class Postspec
|
22
|
+
class Error < RuntimeError; end
|
23
|
+
|
24
|
+
SHARE_DIR = File.expand_path(File.dirname(__FILE__)) + "/share"
|
25
|
+
|
26
|
+
# Database connection. A PgConn object
|
27
|
+
attr_reader :conn
|
28
|
+
|
29
|
+
# Meta object. A PgMeta object
|
30
|
+
attr_reader :meta
|
31
|
+
|
32
|
+
# Type of the database. A PgGraph::Type object
|
33
|
+
attr_reader :type
|
34
|
+
|
35
|
+
# List of table types in the database except tables from hidden schemas (eg. postspec)
|
36
|
+
attr_reader :tables
|
37
|
+
|
38
|
+
# If true and a test case fails, postspec will commit all changes and
|
39
|
+
# ignore any further tests. If rspec is called with the --fail-fast option
|
40
|
+
# the test run will terminate immediately. Default true
|
41
|
+
attr_reader :fail
|
42
|
+
|
43
|
+
# State
|
44
|
+
attr_reader :state
|
45
|
+
|
46
|
+
# Map from UID to record ID of inserted, updated, and deleted records
|
47
|
+
forward_to :state, :inserted, :updated, :deleted
|
48
|
+
|
49
|
+
# Current mode. Can be one of :seed, :empty, ...
|
50
|
+
forward_to :state, :mode
|
51
|
+
|
52
|
+
# Transaction frame stack
|
53
|
+
attr_reader :frames
|
54
|
+
|
55
|
+
# Current frame
|
56
|
+
def frame() @frames.top end
|
57
|
+
forward_to :frame, :ids, :anchors
|
58
|
+
|
59
|
+
# Prick anchors (FIXME)
|
60
|
+
attr_reader :prick_anchors
|
61
|
+
|
62
|
+
# Render object
|
63
|
+
attr_reader :render
|
64
|
+
|
65
|
+
# TODO: PgMeta object
|
66
|
+
#
|
67
|
+
# +mode+ can be one of :seed, :empty (TODO :reseed, :keep)
|
68
|
+
def initialize(conn, reflector: nil, mode: :empty, anchors: [], fail: true)
|
69
|
+
# puts "Postspec#initialize"
|
70
|
+
constrain conn, PgConn
|
71
|
+
constrain reflector, NilClass, String, PgGraph::Reflector
|
72
|
+
constrain mode, lambda { |m| [:empty, :seed].include?(m) }
|
73
|
+
constrain anchors, [Hash], NilClass
|
74
|
+
constrain fail, TrueClass, FalseClass
|
75
|
+
|
76
|
+
@conn = conn
|
77
|
+
@meta = PgMeta.new(@conn)
|
78
|
+
|
79
|
+
# Make sure the postspec schema is not included in the type model. TODO:
|
80
|
+
# Consolidate this into the :ignore option of PgGraph::Type.new as is
|
81
|
+
# done with the prick schema below
|
82
|
+
has_postspec = @meta.schemas.key?("postspec")
|
83
|
+
!has_postspec or (@meta.schemas["postspec"].hidden = true)
|
84
|
+
|
85
|
+
@type = PgGraph::Type.new(@meta, reflector, ignore: ["prick"])
|
86
|
+
@render = Render.new(self)
|
87
|
+
@tables = type.schemas.map(&:tables).flatten
|
88
|
+
@fail = fail
|
89
|
+
@failed = false
|
90
|
+
@success = true
|
91
|
+
|
92
|
+
# Compile-time state variable with the current search_path. Frames are
|
93
|
+
# initialized with this value when they're declared (#use, #statement,
|
94
|
+
# etc.)
|
95
|
+
@search_path = %w(public)
|
96
|
+
|
97
|
+
# Ensure postgres environment (the postspec schema). TODO: Move into Environment#initialize
|
98
|
+
@environment = Environment.new(self)
|
99
|
+
if @environment.exist?
|
100
|
+
if last_state = State.read(conn)
|
101
|
+
if last_state.ready
|
102
|
+
if !last_state.clean
|
103
|
+
# Last run didn't cleanup after itself
|
104
|
+
@environment.clean
|
105
|
+
# elsif ... HERE
|
106
|
+
end
|
107
|
+
|
108
|
+
if last_state.mode != mode
|
109
|
+
# We have changed mode
|
110
|
+
@environment.teardown(last_state.mode)
|
111
|
+
@environment.setup(mode)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
else
|
115
|
+
# First run after deep-cleaning
|
116
|
+
end
|
117
|
+
else
|
118
|
+
@environment.create
|
119
|
+
@environment.setup(mode)
|
120
|
+
end
|
121
|
+
|
122
|
+
# State of current run. Note that @state needs to be initialized after
|
123
|
+
# the previous state has been read into last_state
|
124
|
+
@state = State.create(conn, mode)
|
125
|
+
|
126
|
+
# Compare seed ids with table_sequence_ids to tell if a table has been
|
127
|
+
# tampered with
|
128
|
+
#
|
129
|
+
# FIXME Implement
|
130
|
+
# %(
|
131
|
+
# select table_uid
|
132
|
+
# record_id
|
133
|
+
# from table_sequence_ids() i
|
134
|
+
# left join postspec.seeds s on s.table_uid = i.table_uid and s.record_id < i.record_id
|
135
|
+
# )
|
136
|
+
|
137
|
+
# Frames. TODO: Move into FrameStack#initialize
|
138
|
+
@frames = FrameStack.new(self)
|
139
|
+
if mode == :seed
|
140
|
+
@frames.push SeedFrame.new(self, @environment.uids, FixtureFox::Anchors.new(@type, anchors))
|
141
|
+
else
|
142
|
+
@frames.push EmptyFrame.new(self)
|
143
|
+
end
|
144
|
+
@conn.execute(render.execution_unit(tables.map(&:uid), frames.top.push_sql))
|
145
|
+
|
146
|
+
@foxes_stack = [] # stack of stack of Fox objects. TODO: Why a stack?
|
147
|
+
@datas_stack = [] # stack of stack of Data objects
|
148
|
+
|
149
|
+
@state.ready = true
|
150
|
+
State.write(conn, @state)
|
151
|
+
end
|
152
|
+
|
153
|
+
def terminate
|
154
|
+
# puts "Postspec#terminate"
|
155
|
+
@state.status = success?
|
156
|
+
@state.clean = !failed?
|
157
|
+
State.write(conn, @state)
|
158
|
+
if frames.top
|
159
|
+
@conn.execute(frames.top.pop_sql)
|
160
|
+
@frames.pop
|
161
|
+
end
|
162
|
+
@conn.terminate
|
163
|
+
end
|
164
|
+
|
165
|
+
# Current fox object. This can be nil (TODO alternatively: Create a dummy fox in the root frame)
|
166
|
+
def fox() @frames.top.is_a?(FixtureFox::Fox) ? @frames.top.fox : nil end
|
167
|
+
|
168
|
+
# True if a transaction is in progress
|
169
|
+
def transaction?() @conn.transaction? end
|
170
|
+
|
171
|
+
# True if this is the primary transaction. A primary transaction is a
|
172
|
+
# Postgres transaction while secondary transactions are savepoints
|
173
|
+
def primary_transaction?() @frames.size == 1 end
|
174
|
+
|
175
|
+
# Transactionn timestamp
|
176
|
+
def timestamp() @conn.timestamp end
|
177
|
+
|
178
|
+
# True if no tests failed. Default true
|
179
|
+
def success?() @success end
|
180
|
+
|
181
|
+
# True if Postspec is in failed state. In failed state no new commands can
|
182
|
+
# be issued. Postspec enters a failed state when it encounters an error and
|
183
|
+
# #fail is true. Note that #failed? can be false while #success? is also
|
184
|
+
# false. That happens when rspec is called without the --fail-fast option
|
185
|
+
def failed?() @failed end
|
186
|
+
|
187
|
+
# Set failed state but only if #fail is true
|
188
|
+
def fail!
|
189
|
+
@success = false
|
190
|
+
@failed = true if @fail
|
191
|
+
end
|
192
|
+
|
193
|
+
def search_path() @search_path end
|
194
|
+
def search_path=(*paths)
|
195
|
+
@search_path = Array(paths).flatten.compact
|
196
|
+
@search_path = %w(public) if @search_path.empty?
|
197
|
+
end
|
198
|
+
|
199
|
+
# Only called from RSpec::Core::ExampleGroup#set_search_path. FIXME: Why not search_path=
|
200
|
+
def set_search_path(rspec, *paths)
|
201
|
+
constrain paths, String, [String]
|
202
|
+
self.search_path = paths
|
203
|
+
|
204
|
+
# Closure variables
|
205
|
+
this = self
|
206
|
+
search_path = Array(paths)
|
207
|
+
|
208
|
+
rspec.before(:all) {
|
209
|
+
frame = this.frames.push NopFrame.new(this.frames.top, search_path)
|
210
|
+
this.conn.execute(frame.push_sql)
|
211
|
+
}
|
212
|
+
|
213
|
+
rspec.after(:all) {
|
214
|
+
frame = this.frames.pop
|
215
|
+
this.conn.execute(frame.pop_sql)
|
216
|
+
}
|
217
|
+
end
|
218
|
+
|
219
|
+
def use(rspec, *files)
|
220
|
+
|
221
|
+
# Closure variables
|
222
|
+
this = self
|
223
|
+
search_path = self.search_path
|
224
|
+
|
225
|
+
rspec.before(:all) {
|
226
|
+
frame = this.frames.push FoxFrame.new(this.frames.top, search_path, this.type, files)
|
227
|
+
|
228
|
+
this.conn.push_transaction if frame.transaction?
|
229
|
+
begin
|
230
|
+
this.conn.execute(frame.push_sql)
|
231
|
+
this.conn.commit if !frame.transaction?
|
232
|
+
rescue PG::Error
|
233
|
+
this.fail!
|
234
|
+
this.conn.cancel_transaction
|
235
|
+
exit(1)
|
236
|
+
end
|
237
|
+
}
|
238
|
+
|
239
|
+
rspec.after(:all) {
|
240
|
+
# fail!(clear: !fail) FIXME ????
|
241
|
+
frame = this.frames.pop
|
242
|
+
this.conn.execute(frame.pop_sql) if !this.failed?
|
243
|
+
this.conn.pop_transaction(commit: this.failed?) if frame.transaction?
|
244
|
+
this.conn.commit if !frame.transaction?
|
245
|
+
}
|
246
|
+
end
|
247
|
+
|
248
|
+
# Can only be used in group context. Encloses subsequent commands in a transaction
|
249
|
+
def statement(rspec, sql, fail: true, &block)
|
250
|
+
this = self
|
251
|
+
search_path = self.search_path
|
252
|
+
|
253
|
+
rspec.before(:all) {
|
254
|
+
frame = this.frames.push NopFrame.new(this.frames.top, search_path)
|
255
|
+
this.conn.push_transaction if frame.transaction?
|
256
|
+
begin
|
257
|
+
this.conn.execute(frame.push_sql)
|
258
|
+
this.conn.execute(sql)
|
259
|
+
this.conn.commit if !frame.transaction?
|
260
|
+
rescue PG::Error
|
261
|
+
this.fail!
|
262
|
+
this.conn.cancel_transaction
|
263
|
+
exit(1)
|
264
|
+
end
|
265
|
+
}
|
266
|
+
|
267
|
+
rspec.after(:all) {
|
268
|
+
frame = this.frames.pop
|
269
|
+
if this.conn.transaction?
|
270
|
+
this.conn.pop_transaction(commit: this.failed?) if frame.transaction?
|
271
|
+
this.conn.commit if !frame.transaction?
|
272
|
+
else
|
273
|
+
this.conn.execute(frame.pop_sql) if !this.failed?
|
274
|
+
end
|
275
|
+
}
|
276
|
+
end
|
277
|
+
|
278
|
+
# Can only be used in example context. Doesn't manipulate transactions
|
279
|
+
def exec(sql)
|
280
|
+
@datas[-1] = @fixture = nil
|
281
|
+
@conn.execute(sql)
|
282
|
+
end
|
283
|
+
|
284
|
+
# Can only be used in example context. Doesn't manipulate transactions
|
285
|
+
def execute(sql)
|
286
|
+
@datas[-1] = @fixture = nil
|
287
|
+
@conn.execute(sql)
|
288
|
+
end
|
289
|
+
|
290
|
+
# Execute procedure. Note that the current transaction is committed before
|
291
|
+
# the procedure is run. The transaction stack is reestablished after the
|
292
|
+
# test is done. TODO: Why can't it be renamed #call? TODO: Implement args
|
293
|
+
def procedure(rspec, stored_procedure, *args)
|
294
|
+
this = self
|
295
|
+
rspec.before(:all) {
|
296
|
+
this.freeze_transaction
|
297
|
+
this.conn.execute("call #{stored_procedure}()")
|
298
|
+
}
|
299
|
+
rspec.around(:each) { |example|
|
300
|
+
example.run
|
301
|
+
}
|
302
|
+
rspec.after(:all) {
|
303
|
+
this.thaw_transaction
|
304
|
+
}
|
305
|
+
end
|
306
|
+
|
307
|
+
# The connection object
|
308
|
+
def db() @conn end
|
309
|
+
|
310
|
+
# The content of the database as a PgGraph::Data object. Note that this
|
311
|
+
# loads the entire database (TODO: Lazy loading)
|
312
|
+
def data() @datas[-1] ||= type.instantiate(db, fox.anchors) end
|
313
|
+
|
314
|
+
# The content of the fixture as a PgGraph::Data object
|
315
|
+
def fixture()
|
316
|
+
@fixture ||= begin
|
317
|
+
if fox.ast
|
318
|
+
fox.data
|
319
|
+
elsif @foxes.size >= 2
|
320
|
+
@foxes[-2].data
|
321
|
+
else
|
322
|
+
type.instantiate
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
def transaction(&block)
|
328
|
+
push_transaction
|
329
|
+
yield
|
330
|
+
pop_transaction
|
331
|
+
end
|
332
|
+
|
333
|
+
def push_transaction
|
334
|
+
@foxes.push (transaction? ? @foxes.last.dup : FixtureFox::Fox.new(type, schema: search_path.first))
|
335
|
+
@datas.push nil
|
336
|
+
@conn.push_transaction
|
337
|
+
end
|
338
|
+
|
339
|
+
def pop_transaction
|
340
|
+
@foxes.pop
|
341
|
+
@datas.pop
|
342
|
+
fail!(clear: !fail)
|
343
|
+
@conn.pop_transaction(commit: failed?)
|
344
|
+
end
|
345
|
+
|
346
|
+
# Starts a transaction if not already in a transaction. Does nothing otherwise
|
347
|
+
def ensure_transaction
|
348
|
+
push_transaction if !transaction?
|
349
|
+
end
|
350
|
+
|
351
|
+
def freeze_transaction()
|
352
|
+
@foxes_stack.push @foxes
|
353
|
+
@datas_stack.push @datas
|
354
|
+
@foxes.each { @conn.pop_transaction(commit: true) }
|
355
|
+
end
|
356
|
+
|
357
|
+
def thaw_transaction()
|
358
|
+
!@foxes_stack.empty? or raise Error, "Stack underrun"
|
359
|
+
@foxes = @foxes_stack.pop
|
360
|
+
@datas = @datas_stack.pop
|
361
|
+
@foxes.each { |fox|
|
362
|
+
@conn.push_transaction
|
363
|
+
fox.data.write(@conn)
|
364
|
+
}
|
365
|
+
end
|
366
|
+
end
|
367
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
|
2
|
+
def postspec() Postspec.postspec end
|
3
|
+
def use(*args) postspec.use(*args) end
|
4
|
+
def statement(*args, **opts, &block) postspec.statement(*args, **opts, &block) end
|
5
|
+
def exec(*args) postspec.conn.execute(*args) end
|
6
|
+
def execute(*args, **opts, &block) postspec.execute(*args, **opts, &block) end
|
7
|
+
def fox() postspec.fox end
|
8
|
+
def db() postspec.db end
|
9
|
+
def timestamp() postspec.timestamp end
|
10
|
+
def data() postspec.data end
|
11
|
+
def inserted_records() postspec.inserted end
|
12
|
+
def updated_records() postspec.updated end
|
13
|
+
def deleted_records() postspec.deleted end
|
14
|
+
def anchors() postspec.anchors end
|
15
|
+
def fixture() postspec.fixture end
|
16
|
+
def transaction(&block) postspec.transaction(&block) end
|
17
|
+
def push_transaction() postspec.push_transaction end
|
18
|
+
def pop_transaction() postspec.pop_transaction end
|
19
|
+
|
20
|
+
RSpec.configure do |config|
|
21
|
+
config.before(:suite) do
|
22
|
+
# puts "postspec_helper before suite"
|
23
|
+
$postspec_failed = false
|
24
|
+
Postspec.configure { |postspec_config|
|
25
|
+
postspec_config.fail_fast ||= (config.fail_fast == 1)
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
config.after(:suite) do
|
30
|
+
# puts "postspec_helper after suite"
|
31
|
+
postspec.terminate if !postspec.failed?
|
32
|
+
end
|
33
|
+
|
34
|
+
config.around(:example) do |example|
|
35
|
+
# puts "postspec_helper around example"
|
36
|
+
if postspec.failed?
|
37
|
+
example.skip
|
38
|
+
else
|
39
|
+
postspec.conn.push_transaction
|
40
|
+
begin
|
41
|
+
# puts ">> TRYING"
|
42
|
+
result = example.run
|
43
|
+
# puts ">> RESULT: #{result.inspect} (#{result.class})"
|
44
|
+
# puts ">> EXAMPLE: #{example.exception.inspect} (#{example.exception.class})"
|
45
|
+
# puts ">> EXECUTION_RESULT: #{example.execution_result}"
|
46
|
+
if result.is_a?(PG::Error)
|
47
|
+
postspec.fail!
|
48
|
+
postspec.conn.cancel_transaction
|
49
|
+
elsif result.is_a?(Exception)
|
50
|
+
postspec.fail!
|
51
|
+
postspec.conn.pop_transaction(commit: postspec.failed?)
|
52
|
+
else
|
53
|
+
postspec.conn.pop_transaction(commit: postspec.failed?)
|
54
|
+
end
|
55
|
+
rescue
|
56
|
+
puts ">> OTHER EXCEPTIONS"
|
57
|
+
puts ">> OTHER EXCEPTIONS"
|
58
|
+
puts ">> OTHER EXCEPTIONS"
|
59
|
+
postspec.conn.cancel_transaction
|
60
|
+
postspec.fail!
|
61
|
+
raise
|
62
|
+
end
|
63
|
+
result
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class RSpec::Core::ExampleGroup
|
69
|
+
def self.set_search_path(*args)
|
70
|
+
postspec.set_search_path(self, *args)
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.use(*args)
|
74
|
+
postspec.use(self, *args)
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.statement(*args)
|
78
|
+
postspec.statement(self, *args)
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.procedure(*args)
|
82
|
+
postspec.procedure(self, *args)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Monkey patch RSpec to interpret a nil argument to describe/context as "no
|
87
|
+
# output and no indentation". It is used in the implementation of #group below
|
88
|
+
#
|
89
|
+
module RSpec
|
90
|
+
module Core
|
91
|
+
module Formatters
|
92
|
+
# @private
|
93
|
+
class DocumentationFormatter
|
94
|
+
def example_group_started(notification)
|
95
|
+
description = notification.group.description.strip
|
96
|
+
if description != ""
|
97
|
+
output.puts if @group_level == 0
|
98
|
+
output.puts "#{current_indentation}#{notification.group.description.strip}"
|
99
|
+
@group_level += 1
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def example_group_finished(notification)
|
104
|
+
description = notification.group.description.strip
|
105
|
+
if description != ""
|
106
|
+
@group_level -= 1 if @group_level > 0
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Extend RSpec with a "group" declaration that provides a scope like
|
115
|
+
# describe/context but is silent
|
116
|
+
#
|
117
|
+
module RSpec
|
118
|
+
module Core
|
119
|
+
class ExampleGroup
|
120
|
+
class << self
|
121
|
+
def group(&block)
|
122
|
+
describe(nil, &block)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
@@ -0,0 +1,137 @@
|
|
1
|
+
|
2
|
+
drop schema if exists "postspec" cascade;
|
3
|
+
create schema postspec;
|
4
|
+
|
5
|
+
set search_path to postspec;
|
6
|
+
|
7
|
+
-- Return a map from table UID to last value of sequence. All user tables are
|
8
|
+
-- included but tables without sequences (subtables) have last value set to
|
9
|
+
-- null
|
10
|
+
create or replace function table_sequence_ids() returns table(table_uid text, record_id bigint) as $$
|
11
|
+
declare
|
12
|
+
name varchar(255);
|
13
|
+
tuple record;
|
14
|
+
begin
|
15
|
+
for tuple in
|
16
|
+
select tc.relnamespace::regnamespace::text || '.' || tc.relname as table_uid,
|
17
|
+
s.relnamespace::regnamespace::text || '.' || s.relname as sequence_uid
|
18
|
+
from pg_class tc
|
19
|
+
left join (
|
20
|
+
select d.refobjid,
|
21
|
+
sc.relname,
|
22
|
+
sc.relkind,
|
23
|
+
sc.relnamespace::regnamespace::text as relnamespace
|
24
|
+
from pg_depend d
|
25
|
+
join pg_class sc on sc.oid = d.objid
|
26
|
+
where coalesce(sc.relkind = 'S', true)
|
27
|
+
and coalesce(sc.relnamespace::regnamespace::text != 'postspec', true)
|
28
|
+
) s on s.refobjid = tc.oid
|
29
|
+
where tc.relkind = 'r'
|
30
|
+
and tc.relnamespace::regnamespace::text not like 'pg_%'
|
31
|
+
and tc.relnamespace::regnamespace::text not in ('information_schema', 'postspec', 'prick')
|
32
|
+
loop
|
33
|
+
if tuple.sequence_uid is null then
|
34
|
+
table_uid := tuple.table_uid;
|
35
|
+
record_id := null;
|
36
|
+
return next;
|
37
|
+
else
|
38
|
+
return query execute
|
39
|
+
'select ' || quote_literal(tuple.table_uid) || '::text as table_uid, ' ||
|
40
|
+
'case is_called when true then last_value else last_value - 1 end as last_value ' ||
|
41
|
+
'from ' || tuple.sequence_uid;
|
42
|
+
end if;
|
43
|
+
end loop;
|
44
|
+
return;
|
45
|
+
end
|
46
|
+
$$ language plpgsql;
|
47
|
+
|
48
|
+
create or replace function readonly_failure() returns trigger as $$
|
49
|
+
begin
|
50
|
+
raise 'Postspec: Can''t modify seed data in %', TG_TABLE_NAME::regclass::text;
|
51
|
+
return null;
|
52
|
+
end;
|
53
|
+
$$ language plpgsql immutable leakproof;
|
54
|
+
|
55
|
+
-- :call-seq:
|
56
|
+
-- register_insert(record_id)
|
57
|
+
--
|
58
|
+
create or replace function register_insert() returns trigger as $$
|
59
|
+
begin
|
60
|
+
insert into postspec.inserts (table_uid, record_id) values
|
61
|
+
(TG_TABLE_SCHEMA || '.' || TG_TABLE_NAME, new.id);
|
62
|
+
return new;
|
63
|
+
end;
|
64
|
+
$$ language plpgsql;
|
65
|
+
|
66
|
+
-- :call-seq:
|
67
|
+
-- register_update(record_id)
|
68
|
+
--
|
69
|
+
create or replace function register_update() returns trigger as $$
|
70
|
+
begin
|
71
|
+
insert into postspec.updates (table_uid, record_id) values
|
72
|
+
(TG_TABLE_SCHEMA || '.' || TG_TABLE_NAME, old.id);
|
73
|
+
return new;
|
74
|
+
end;
|
75
|
+
$$ language plpgsql;
|
76
|
+
|
77
|
+
-- :call-seq:
|
78
|
+
-- register_delete(record_id)
|
79
|
+
--
|
80
|
+
create or replace function register_delete() returns trigger as $$
|
81
|
+
begin
|
82
|
+
insert into postspec.deletes (table_uid, record_id) values
|
83
|
+
(TG_TABLE_SCHEMA || '.' || TG_TABLE_NAME, old.id);
|
84
|
+
-- ('postspec.' || TG_TABLE_NAME, old.id);
|
85
|
+
return old;
|
86
|
+
end;
|
87
|
+
$$ language plpgsql;
|
88
|
+
|
89
|
+
create table runs (
|
90
|
+
id integer generated by default as identity primary key,
|
91
|
+
mode text not null,
|
92
|
+
ready boolean not null default false,
|
93
|
+
clean boolean not null default false,
|
94
|
+
status boolean,
|
95
|
+
duration numeric generated always as (
|
96
|
+
round(extract(epoch from updated_at - created_at)::numeric, 3)
|
97
|
+
) stored,
|
98
|
+
created_at timestamp without time zone not null default (now() at time zone 'UTC'),
|
99
|
+
updated_at timestamp without time zone
|
100
|
+
);
|
101
|
+
|
102
|
+
create table seeds (
|
103
|
+
id integer generated by default as identity primary key,
|
104
|
+
table_uid text,
|
105
|
+
record_id integer
|
106
|
+
);
|
107
|
+
|
108
|
+
create table inserts (
|
109
|
+
id integer generated by default as identity primary key,
|
110
|
+
table_uid text,
|
111
|
+
record_id integer
|
112
|
+
);
|
113
|
+
|
114
|
+
create table updates (
|
115
|
+
id integer generated by default as identity primary key,
|
116
|
+
table_uid text,
|
117
|
+
record_id integer
|
118
|
+
);
|
119
|
+
|
120
|
+
create table deletes (
|
121
|
+
id integer generated by default as identity primary key,
|
122
|
+
table_uid text,
|
123
|
+
record_id integer
|
124
|
+
);
|
125
|
+
|
126
|
+
set search_path to public;
|
127
|
+
|
128
|
+
grant all on schema postspec to public;
|
129
|
+
grant all on postspec.runs to public;
|
130
|
+
grant all on postspec.seeds to public;
|
131
|
+
grant all on postspec.inserts to public;
|
132
|
+
grant all on postspec.updates to public;
|
133
|
+
grant all on postspec.deletes to public;
|
134
|
+
|
135
|
+
|
136
|
+
|
137
|
+
|