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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 46c5d89777324cedb5b8468b0c33806b45dd908b6dd7ce839bd9d3290ad9d4a0
4
+ data.tar.gz: 927fb7974ae6a9f7063e0b9a90e94dab313860c2e367d1b20f73e6afef645193
5
+ SHA512:
6
+ metadata.gz: 7fb4a38a74f0af6c527605789d127243b3624cf7f7073981a69bef007087809093c0bb273f4859f80b95aa3228da061b23674ca0d012920607b1bcf7b9e8159f
7
+ data.tar.gz: 23df58f3ba1eb8a48af8bed88ae3fedf8e151c5cf453fa5b4c24f211faaec732480c66c5560ec3e95832baeef379a7c4621bec6636033dc93091152b70e7bcf2
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+
13
+ # Ignore auto-generated main file
14
+ /main
15
+
16
+ # Ignore Gemfile.lock. See https://stackoverflow.com/questions/4151495/should-gemfile-lock-be-included-in-gitignore
17
+ /Gemfile.lock
18
+
19
+ # Put your personal ignore files in /home/clr/.config/git/ignore
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-2.7.1
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ ---
2
+ language: ruby
3
+ cache: bundler
4
+ rvm:
5
+ - 2.7.1
6
+ before_install: gem install bundler -v 2.1.4
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in postspec.gemspec
4
+ gemspec
5
+
data/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # Postspec
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/postspec`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'postspec'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle install
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install postspec
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/postspec.
36
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/TODO ADDED
@@ -0,0 +1,6 @@
1
+ o Also use prick anchors
2
+ o Modify all table to be unlogged for better performance: alter table table_name set unlogged
3
+ o Doesn't detect changes to the database by the developer between runs of
4
+ postspec). Can be fixed by inspecting postspec.[change-tables]
5
+ o Doesn't remove change-triggers after runs (this may be a feature)
6
+ o Needs a clean-postspec option that removes the postspec schema and triggers
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "postspec"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/exe/postspec ADDED
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'pg_graph'
4
+ require 'shellopts'
5
+
6
+ SPEC = "
7
+ @ Demo
8
+
9
+ Demonstration of reading and writing a database. Has actually nothing to do
10
+ with postspec
11
+
12
+ -r,reflections=EFILE @ Reflections file
13
+
14
+ -- DATABASE"
15
+
16
+ def timeit(label = nil, &block)
17
+ t0 = Time.now
18
+ r = yield
19
+ dt = Time.now - t0
20
+ if dt < 1
21
+ puts "#{label}: #{(1000.0 * dt).round(1)}ms"
22
+ else
23
+ puts "#{label}: #{dt.round(1)}s"
24
+ end
25
+ r
26
+ end
27
+
28
+ opts, args = ShellOpts.process(SPEC, ARGV)
29
+
30
+ database = args.expect(1)
31
+
32
+ puts "Reading and writing the given database"
33
+ indent {
34
+ conn = PgConn.new(database)
35
+ meta = timeit("Meta") { PgMeta.new(conn) }
36
+ type = timeit("Type") { PgGraph::Type.new(meta, opts.reflections) }
37
+ data = timeit("Read data") { PgGraph::Data.new(type, conn) }
38
+ load_data = timeit("Write data") { data.write(conn) }
39
+ }
40
+
@@ -0,0 +1,38 @@
1
+ # Used to initialize postspec from spec/spec_helper.rb or spec/postspec_helper.rb
2
+ #
3
+ module Postspec
4
+ Config = Struct.new(:database, :mode, :reflections, :anchors, :fail_fast)
5
+
6
+ @database = nil
7
+ @mode = nil
8
+ @reflections = nil
9
+ @anchors = nil
10
+ @fail_fast = nil
11
+
12
+ DEFAULT_MODE = :empty
13
+ DEFAULT_FAIL_FAST = true
14
+
15
+ def self.database() @database end
16
+ def self.mode() @mode end
17
+ def self.reflections() @reflections end
18
+ def self.anchors() @anchors end
19
+ def self.fail_fast() @fail_fast end
20
+
21
+ def self.configure(&block)
22
+ config = Config.new(@database, DEFAULT_MODE, nil, nil, DEFAULT_FAIL_FAST)
23
+ yield(config)
24
+ @database = config.database
25
+ @mode = config.mode || DEFAULT_MODE
26
+ @reflections = config.reflections
27
+ @anchors = config.anchors
28
+ @fail_fast = config.fail_fast || DEFAULT_FAIL_FAST
29
+ self
30
+ end
31
+
32
+ def self.postspec()
33
+ !@database.nil? or raise Error, "Database unspecified"
34
+ @postspec ||= Postspec.new(
35
+ PgConn.new(@database), mode: mode, reflector: @reflections, anchors: @anchors, fail: @fail_fast)
36
+ end
37
+ end
38
+
@@ -0,0 +1,491 @@
1
+
2
+ module Postspec
3
+ class Environment
4
+ attr_reader :postspec
5
+ forward_to :postspec, :conn, :render
6
+
7
+ def uids() table_sequence_ids end # FIXME: What is this? And why not using @uids?
8
+
9
+ def initialize(postspec)
10
+ @postspec = postspec
11
+ end
12
+
13
+ def table_sequence_ids(all: false)
14
+ conn.map \
15
+ "select table_uid, record_id from postspec.table_sequence_ids()" +
16
+ (all ? "" : " where record_id is not null")
17
+ end
18
+
19
+ def table_seed_ids
20
+ conn.map "select table_uid, record_id from postspec.seeds"
21
+ end
22
+
23
+ def table_max_ids
24
+ result = table_sequence_ids(all: true)
25
+ postspec.type.tables.select(&:subtable?).each { |table|
26
+ result[table.uid] = result[table.supertable.uid] or raise "Oops"
27
+ }
28
+ result
29
+ end
30
+
31
+ # Returns map from table UID to record ID of the last record to be kept so
32
+ # that 'delete from table_uid where id > record_id' does the right thing.
33
+ # Note: This assumes that the change triggers has been installed beforehand
34
+ def dirty_tables
35
+ seed_ids = table_seed_ids
36
+ table_max_ids.map { |table_uid, record_id|
37
+ (seed_id = seed_ids[table_uid]) ? [table_uid, seed_id] : [table_uid, record_id]
38
+ }.compact.to_h
39
+ end
40
+
41
+ def create()
42
+ # puts "Environment#create"
43
+ conn.execute IO.read(Postspec::SHARE_DIR + "/postspec_schema.sql")
44
+ conn.execute render.change_triggers(:create)
45
+ end
46
+
47
+ def drop()
48
+ # puts "Environment#drop"
49
+ conn.execute "drop schema if exists postspec cascade"
50
+ end
51
+
52
+ def exist?()
53
+ conn.exist? "select 1 from pg_namespace where nspname = 'postspec'"
54
+ end
55
+
56
+ # Removes excess data and meta data, and resets sequences. It doens't change any triggers
57
+ def clean
58
+ # puts "Environment#clean"
59
+ @uids = {}
60
+ user_tables = dirty_tables
61
+ postspec_tables = CHANGE_TABLE_UIDS
62
+ sql = render.delete_tables(user_tables) +
63
+ render.delete_tables(postspec_tables)
64
+ conn.execute render.execution_unit(user_tables.keys, sql)
65
+ end
66
+
67
+ def setup(mode)
68
+ # puts "Environment#setup"
69
+ sql =
70
+ case mode
71
+ when :seed
72
+ @uids = table_max_ids
73
+ render.seed_triggers(:create, @uids)
74
+ when :empty
75
+ @uids = {}
76
+ []
77
+ else
78
+ raise ArgumentError
79
+ end
80
+ conn.execute sql
81
+ end
82
+
83
+ def teardown(mode)
84
+ # puts "Environment#teardown"
85
+ @uids = nil
86
+ tables, reset_sql = reset_data
87
+ sql =
88
+ case mode
89
+ when :seed; render.seed_triggers(:drop) + render.delete_tables(SEED_TABLE_UIDS)
90
+ when :empty; []
91
+ else
92
+ raise ArgumentError
93
+ end + reset_sql
94
+ conn.execute render.execution_unit(tables, sql)
95
+ end
96
+
97
+ def reset()
98
+ # puts "Environment#reset"
99
+ conn.execute render.execution_unit(*reset_data)
100
+ end
101
+
102
+ private
103
+ # Generate SQL to delete data using the postspec.inserts table
104
+ def reset_data
105
+ uids = conn.map "select table_uid, min(record_id) from postspec.inserts group by table_uid"
106
+ sql = render.delete_tables(uids) + render.delete_tables(CHANGE_TABLE_UIDS)
107
+ [uids.keys, sql]
108
+ end
109
+
110
+ CHANGE_TABLE_UIDS = %w(postspec.inserts postspec.updates postspec.deletes)
111
+ SEED_TABLE_UIDS = %w(postspec.seeds)
112
+ RUN_TABLE_UIDS = %w(postspec.runs)
113
+ TABLE_UIDS = CHANGE_TABLE_UIDS + SEED_TABLE_UIDS + RUN_TABLE_UIDS
114
+ end
115
+ end
116
+
117
+ __END__
118
+
119
+
120
+
121
+
122
+ class Environment
123
+ attr_reader :postspec
124
+ forward_to :postspec, :conn, :render
125
+
126
+ attr_reader :uids # UID to ID map. Only used by the seed environment
127
+
128
+ def user_tables() @tables ||= postspec.tables.map(&:uid) end
129
+
130
+ CHANGE_TABLE_UIDS = %w(postspec.inserts postspec.updates postspec.deletes)
131
+ SEED_TABLE_UIDS = %w(postspec.seeds)
132
+ RUN_TABLE_UIDS = %w(postspec.runs)
133
+ TABLE_UIDS = CHANGE_TABLE_UIDS + SEED_TABLE_UIDS + RUN_TABLE_UIDS
134
+
135
+ def initialize(postspec)
136
+ @postspec = postspec
137
+ @uids = nil
138
+ setup_schema if !exist_schema?
139
+ end
140
+
141
+
142
+
143
+ # setup(mode); teardown(mode) is a NOP
144
+
145
+ def setup(mode) self.send :"setup_#{mode}_environment" end
146
+ def teardown(mode) self.send :"teardown_#{mode}_environment" end
147
+ def reset(mode, *type) self.send :"reset_#{mode}_environment", *type.compact end
148
+
149
+ def setup_schema
150
+ conn.execute(IO.read(SHARE_DIR + "/postspec_schema.sql"))
151
+ end
152
+
153
+ def teardown_schema
154
+ conn.execute "drop schema if exists postspec cascade"
155
+ end
156
+
157
+ def reset_schema
158
+ teardown_seed_environment
159
+ teardown_empty_environment
160
+ end
161
+
162
+ def exist_schema?
163
+ conn.exist? "select 1 from pg_namespace where nspname = 'postspec'"
164
+ end
165
+
166
+ def setup_change_environment
167
+ conn.exec render.change_triggers(:create)
168
+ end
169
+
170
+ def teardown_change_environment
171
+ sql =
172
+ render.change_triggers(:drop) +
173
+ render.delete_tables(CHANGE_TABLE_UIDS)
174
+ conn.exec render.execution_unit(CHANGE_TABLE_UIDS, sql)
175
+ end
176
+
177
+ # Not used - should be delegated to seed or empty environment
178
+ # def reset_change_environment
179
+ # end
180
+
181
+
182
+
183
+ def setup_seed_environment()
184
+ @uids = conn.map("select name, id from postspec.sequence_ids()")
185
+ sql = render.seed_triggers(:create, uids)
186
+ conn.exec sql
187
+ end
188
+
189
+ def teardown_seed_environment()
190
+ @uids = nil
191
+ uids = conn.map "select table_uid, min(record_id) from postspec.inserts"
192
+ sql =
193
+ render.seed_triggers(:drop) +
194
+ render.delete_tables(uids) +
195
+ render.delete_tables(CHANGE_TABLE_UIDS) +
196
+ render.delete_tables(SEED_TABLE_UIDS)
197
+ conn.exec render.execution_unit(uids.keys, sql)
198
+ end
199
+
200
+ def reset_seed_environment
201
+ uids = conn.map "select table_uid, min(record_id) from postspec.inserts"
202
+ sql =
203
+ render.delete_tables(uids) +
204
+ render.delete_tables(CHANGE_TABLE_UIDS)
205
+ conn.exec render.execution_unit(uids.keys, sql)
206
+ end
207
+
208
+
209
+ def setup_empty_environment
210
+ @uids = {}
211
+ sql = render.delete_tables(user_tables + CHANGE_TABLE_UIDS)
212
+ conn.exec render.execution_unit(tables, sql)
213
+ end
214
+
215
+ def teardown_empty_environment
216
+ @uids = nil
217
+ end
218
+
219
+ def reset_empty_environment
220
+ # Because we know the initial environment was empty we only need to
221
+ # inspect the postspect.inserts tables to find records to delete
222
+ changed_tables = conn.values "select distinct table_uid from inserts"
223
+ sql = render.delete_tables(changed_tables + CHANGE_TABLE_UIDS)
224
+ conn.exec render.execution_unit(tables, sql)
225
+ end
226
+ end
227
+ end
228
+
229
+ __END__
230
+
231
+ end
232
+
233
+ def drop
234
+
235
+ class PostspecEnvironment
236
+ attr_reader :postspec
237
+ forward_to :postspec, :conn, :render
238
+
239
+ def initialize(postspec)
240
+ @postspec = postspec
241
+ self.ensure
242
+ end
243
+
244
+ def self.instance(postspec)
245
+ @instance ||= PostspecEnvironment.new(postspec)
246
+ end
247
+
248
+ def create
249
+ end
250
+
251
+ def drop
252
+ end
253
+
254
+ def reset
255
+ State.reset(postspec)
256
+ end
257
+
258
+ def ensure
259
+ if !postspec.meta.schemas.key?("postspec")
260
+ create
261
+ else
262
+ # meta knows about the postspec schema so we hide it
263
+ postspec.meta.schemas["postspec"].hidden = true
264
+ reset
265
+ end
266
+ end
267
+
268
+ private
269
+ @instance = nil
270
+ end
271
+
272
+ class Environment
273
+ attr_reader :postspec_environment
274
+ attr_reader :state
275
+
276
+ forward_to :postspec_environment, :postspec, :conn, :render
277
+
278
+ # :call-seq:
279
+ # ::new(postspec, state)
280
+ # ::new(postspec, mode)
281
+ def self.new(postspec, arg)
282
+ klass =
283
+ case mode
284
+ when :seed; SeedEnvironment
285
+ when :empty; EmptyEnvironment
286
+ else
287
+ KeepEnvironment
288
+ end
289
+ object = klass.allocate
290
+ state = arg.is_state? ? arg : State.create(postspec, arg)
291
+ object.send(:initialize, PostspecEnvironment.instance(postspec), state)
292
+ object
293
+ end
294
+
295
+ def terminate
296
+ state.write
297
+ end
298
+
299
+ def create() raise NotThis end
300
+ def drop() raise NotThis end
301
+ def reset() postspec_environment.reset end
302
+
303
+ protected
304
+ def initialize(postspec_environment, state)
305
+ @postspec_environment = postspec_environment
306
+ @state = state
307
+ end
308
+ end
309
+
310
+ class SeedEnvironment < Environment
311
+ def create
312
+ uids = @conn.map("select name, id from postspec.sequence_ids()", :name)
313
+ conn.exec render.seed_triggers(:create, uids)
314
+ conn.exec "delete from postspec.seeds"
315
+ end
316
+
317
+ def drop
318
+ conn.exec render.seed_triggers(:drop)
319
+ conn.exec "delete from postspec.seeds"
320
+ end
321
+
322
+ def reset # TODO: Merge with PgGraph::Data::SqlRender
323
+ end
324
+ end
325
+
326
+ class EmptyEnvironment < Environment
327
+ def create() end
328
+ def drop() end
329
+
330
+ def reset
331
+ super
332
+ conn.exec state.inserts.keys.map { |uid| "delete from #{uid}" }
333
+ end
334
+ end
335
+
336
+ class KeepEnvironment < Environment
337
+ end
338
+ end
339
+
340
+ __END__
341
+
342
+ class Environment
343
+ attr_reader :postspec
344
+ attr_reader :id
345
+ attr_reader :mode
346
+ attr_accessor :status
347
+ def duration() @duraction ||= Time.now - created_at end
348
+ attr_reader :created_at
349
+
350
+ forward_to :postspec, :conn
351
+
352
+ def initialize(postspec, id, mode, status, duration, created_at)
353
+ @postspec = postspec
354
+ @id = id
355
+ @mode = mode
356
+ @status = status
357
+ @duration = duration
358
+ @created_at = created_at
359
+ end
360
+
361
+ def update
362
+ sql = %(
363
+ update postspec.runs
364
+ set status = #{status},
365
+ duration = #{duration}
366
+ where id = #{id}
367
+ )
368
+ conn.exec(sql)
369
+ end
370
+
371
+ def self.create(conn, mode)
372
+ sql = %(
373
+ insert into postspec.runs (mode) values ('#{mode}') returning id, created_at
374
+ )
375
+ id, created_at = @conn.tuple(sql)
376
+ State.new(conn, id, mode, nil, nil, created_at)
377
+ end
378
+
379
+ def self.read_last(conn)
380
+ sql = %(
381
+ select id, mode, status, duration, created_at
382
+ from postspec.runs
383
+ order by desc id
384
+ limit 1
385
+ )
386
+ tuple = @conn.tuples(sql).first
387
+ tuple && State.new(conn, *tuple)
388
+ end
389
+ end
390
+
391
+ class SeedEnvironment < Environment
392
+ def initialize(postspec, id, mode, status, duration, created_at)
393
+ end
394
+ def create
395
+ super
396
+ conn.execute(render.readonly_triggers(:create, ids)
397
+ end
398
+
399
+ def update
400
+ end
401
+
402
+ def drop
403
+ conn.execute(render.readonly_triggers(:drop, ids)
404
+ end
405
+ end
406
+
407
+ class EmptyEnvironment < Environment
408
+ def create
409
+ super
410
+ conn.execute(render.delete_tables(postspec.tables.map(&:uid))
411
+ end
412
+
413
+ def update
414
+ end
415
+
416
+ def drop
417
+ end
418
+ end
419
+
420
+ class KeepEnvironment < Environment
421
+ end
422
+
423
+
424
+
425
+
426
+
427
+ class Environment
428
+ attr_reader :postspec
429
+ attr_reader :state
430
+ forward_to :postspec, :conn, :meta, :render
431
+
432
+ def initialize(postspec, mode)
433
+ @postspec = postspec
434
+ @state = State.create(postspec.conn, mode)
435
+
436
+ last_state
437
+
438
+ end
439
+
440
+ def create()
441
+ if meta.schemas.key?("postspec")
442
+ # meta knows about the postspec schema so we hide it
443
+ meta.schemas["postspec"].hidden = true
444
+ # @conn.execute render.delete_postspec_tables
445
+ else
446
+ # meta doesn't know about the postspec schema
447
+ conn.execute(IO.read(SHARE_DIR + "/postspec_schema.sql"))
448
+ end
449
+ conn.execute(render.change_triggers(:create))
450
+ end
451
+
452
+ def reset()
453
+ conn.execute(render.reset_postspec_tables)
454
+ end
455
+
456
+ def drop()
457
+ conn.execute(render.change_triggers(:drop))
458
+ conn.execute("drop schema postspec cascade")
459
+ end
460
+ end
461
+
462
+ class SeedEnvironment < Environment
463
+ def create
464
+ super
465
+ conn.execute(render.readonly_triggers(:create, ids)
466
+ end
467
+
468
+ def update
469
+ end
470
+
471
+ def drop
472
+ conn.execute(render.readonly_triggers(:drop, ids)
473
+ end
474
+ end
475
+
476
+ class EmptyEnvironment < Environment
477
+ def create
478
+ super
479
+ conn.execute(render.delete_tables(postspec.tables.map(&:uid))
480
+ end
481
+
482
+ def update
483
+ end
484
+
485
+ def drop
486
+ end
487
+ end
488
+
489
+ class KeepEnvironment < Environment
490
+ end
491
+ end