drive 0.6.0 → 0.7.0
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 +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +5 -58
- data/lib/drive/engine.rb +2 -3
- data/lib/drive/version.rb +1 -1
- data/lib/drive.rb +2 -3
- metadata +4 -41
- data/app/models/access_file.rb +0 -47
- data/app/models/concerns/counted.rb +0 -17
- data/app/models/concerns/mirrored.rb +0 -90
- data/app/models/drive/current.rb +0 -7
- data/app/models/run.rb +0 -33
- data/db/migrate/20261005100000_create_runs_and_changes.rb +0 -28
- data/db/migrate/20261006100000_drop_changes.rb +0 -16
- data/lib/drive/test_helper.rb +0 -20
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ba84684bcb7ceb627421453571e0976616e77b7756ef5677ae79f7837b23945b
|
|
4
|
+
data.tar.gz: 2068762d2e4c2ec3c94eee70b35cc14b6270d74504c55d02d4be4bc2ccf7b280
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 15717e1c2263a8335350fe3d6bcba8d9807df283f6c72180b4e273d6912ef39a1f82172798e7265f189ae92c39b5689ba6cbdf3fd25e08c0be09b1a0aa4b21c0
|
|
7
|
+
data.tar.gz: 881c0fdbefb2d8466f1443d6d867c1965964b52f68801fd1000cf4117e5c300086f05b1a8cef048558ced13eae2398c4ed38dcb84e4f0cb5663d6e671d5f2541
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,16 @@ For more information about changelogs, check [Keep a Changelog](http://keepachan
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## 0.7.0 - 2026-10-16
|
|
10
|
+
|
|
11
|
+
* [BREAKING] `Mirrored`, `Counted`, `Run`, `AccessFile` and `Drive::TestHelper` are the `mirroring` gem's
|
|
12
|
+
|
|
13
|
+
An app that mirrors adds `gem 'mirroring'` and declares each mirrored model with
|
|
14
|
+
`mirroring key: :code` (`key: false` where the source names none) in place of `include Mirrored`
|
|
15
|
+
and `KEY`, and includes `Mirroring::TestHelper` in its tests. `Run.record`, `mirror`,
|
|
16
|
+
`include Counted` and `AccessFile` keep their names. Drive no longer needs PostgreSQL, nor ships
|
|
17
|
+
the migrations of runs: an app keeps the `runs` table it has.
|
|
18
|
+
|
|
9
19
|
## 0.6.0 - 2026-10-09
|
|
10
20
|
|
|
11
21
|
* [BREAKING] `include RateLimited` sets no limit by itself: a controller calls `rate_limited`
|
data/README.md
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
# Drive
|
|
2
2
|
|
|
3
|
-
A Rails engine for the easyFAA apps. Each
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
A Rails engine for the easyFAA apps. Each sits behind Cloudflare and shows the same error pages.
|
|
4
|
+
A copy of the tables a source publishes is the [`mirroring`](https://github.com/easyfaa/mirroring)
|
|
5
|
+
gem's.
|
|
6
6
|
|
|
7
|
-
- **`Mirrored`**: a table copying one of the source's tables field for field, as text. A refresh
|
|
8
|
-
stages the source's table, then inserts, updates and deletes only the rows the source did.
|
|
9
|
-
- **`Counted`**: a table written a record at a time, such as a document fetched by URL.
|
|
10
|
-
- **`Run`**: one refresh, in one transaction, the only one running, with how many rows it
|
|
11
|
-
inserted, updated and deleted.
|
|
12
|
-
- **`AccessFile`**: stages a table of a Microsoft Access file, read with `mdb-export`.
|
|
13
7
|
- **`RateLimited`**: `rate_limited` holds an address to 20 pages a minute and 200 a day, or the
|
|
14
8
|
app's own limits, and tells it so in words. A search engine's own crawler has no limit.
|
|
15
9
|
- **`Crawler`**: whether a request comes from a search engine's crawler: Cloudflare vouches for
|
|
@@ -21,8 +15,6 @@ shows the same error pages.
|
|
|
21
15
|
- **`NNumber`**, **`Designation`**, **`Serial`**: an N-number, a designation's key and a serial
|
|
22
16
|
number, as every app reads them.
|
|
23
17
|
|
|
24
|
-
It needs PostgreSQL.
|
|
25
|
-
|
|
26
18
|
## How to install
|
|
27
19
|
|
|
28
20
|
```bash
|
|
@@ -31,55 +23,10 @@ gem install drive
|
|
|
31
23
|
|
|
32
24
|
```ruby
|
|
33
25
|
# Gemfile
|
|
34
|
-
gem 'drive', '~> 0.
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
`~> 0.6.0` takes every 0.6.x and stops short of 0.7, which may break what 0.6 does.
|
|
38
|
-
|
|
39
|
-
Then copy its migrations, of runs, and run them:
|
|
40
|
-
|
|
41
|
-
```bash
|
|
42
|
-
bin/rails drive:install:migrations db:migrate
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
## How to use
|
|
46
|
-
|
|
47
|
-
A mirrored table has the source's fields as text columns, plus `source`, `row_key` and
|
|
48
|
-
`row_hash`, unique on `source` and `row_key`. Its model says which fields tell a row apart:
|
|
49
|
-
|
|
50
|
-
```ruby
|
|
51
|
-
class Engine < ApplicationRecord
|
|
52
|
-
include Mirrored
|
|
53
|
-
|
|
54
|
-
KEY = %w[code].freeze # [] where the source names none: a row is then told apart by all it says
|
|
55
|
-
end
|
|
26
|
+
gem 'drive', '~> 0.7.0'
|
|
56
27
|
```
|
|
57
28
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
```ruby
|
|
61
|
-
Run.record 'engines' do
|
|
62
|
-
staged = AccessFile.new('ENGINE.accdb').stage('ENGINE', Engine.connection)
|
|
63
|
-
Engine.mirror staged, source: 'ENGINE'
|
|
64
|
-
end
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
A table written a record at a time counts each save and delete in the run at hand:
|
|
68
|
-
|
|
69
|
-
```ruby
|
|
70
|
-
class Publication < ApplicationRecord
|
|
71
|
-
include Counted
|
|
72
|
-
end
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
Runs commit one at a time, so another app reading the tables directly sees either the last
|
|
76
|
-
run's rows or the one before's, never a mix.
|
|
77
|
-
|
|
78
|
-
In tests, `Drive::TestHelper#stage` stages rows as a source's file would:
|
|
79
|
-
|
|
80
|
-
```ruby
|
|
81
|
-
ActiveSupport.on_load(:active_support_test_case) { include Drive::TestHelper }
|
|
82
|
-
```
|
|
29
|
+
`~> 0.7.0` takes every 0.7.x and stops short of 0.8, which may break what 0.7 does.
|
|
83
30
|
|
|
84
31
|
## Development
|
|
85
32
|
|
data/lib/drive/engine.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
module Drive
|
|
2
|
-
# Lends the host its models (
|
|
3
|
-
#
|
|
4
|
-
# (`bin/rails drive:install:migrations`).
|
|
2
|
+
# Lends the host its models (Crawler, NNumber, Designation, Serial), RateLimited and the error
|
|
3
|
+
# pages.
|
|
5
4
|
class Engine < ::Rails::Engine
|
|
6
5
|
engine_name 'drive'
|
|
7
6
|
|
data/lib/drive/version.rb
CHANGED
data/lib/drive.rb
CHANGED
|
@@ -3,8 +3,7 @@ require 'drive/inflections'
|
|
|
3
3
|
require 'drive/cloudflare'
|
|
4
4
|
require 'drive/engine'
|
|
5
5
|
|
|
6
|
-
# What
|
|
7
|
-
#
|
|
8
|
-
# alike (Drive::Cloudflare, Crawler, RateLimited, the error pages, NNumber, Designation, Serial).
|
|
6
|
+
# What every easyFAA app in front of Cloudflare needs alike: Drive::Cloudflare, Crawler,
|
|
7
|
+
# RateLimited, the error pages, NNumber, Designation and Serial.
|
|
9
8
|
module Drive
|
|
10
9
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: drive
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- claudiob
|
|
@@ -9,34 +9,6 @@ bindir: bin
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
-
- !ruby/object:Gem::Dependency
|
|
13
|
-
name: csv
|
|
14
|
-
requirement: !ruby/object:Gem::Requirement
|
|
15
|
-
requirements:
|
|
16
|
-
- - ">="
|
|
17
|
-
- !ruby/object:Gem::Version
|
|
18
|
-
version: '0'
|
|
19
|
-
type: :runtime
|
|
20
|
-
prerelease: false
|
|
21
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
-
requirements:
|
|
23
|
-
- - ">="
|
|
24
|
-
- !ruby/object:Gem::Version
|
|
25
|
-
version: '0'
|
|
26
|
-
- !ruby/object:Gem::Dependency
|
|
27
|
-
name: pg
|
|
28
|
-
requirement: !ruby/object:Gem::Requirement
|
|
29
|
-
requirements:
|
|
30
|
-
- - ">="
|
|
31
|
-
- !ruby/object:Gem::Version
|
|
32
|
-
version: '0'
|
|
33
|
-
type: :runtime
|
|
34
|
-
prerelease: false
|
|
35
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
-
requirements:
|
|
37
|
-
- - ">="
|
|
38
|
-
- !ruby/object:Gem::Version
|
|
39
|
-
version: '0'
|
|
40
12
|
- !ruby/object:Gem::Dependency
|
|
41
13
|
name: rails
|
|
42
14
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -51,8 +23,7 @@ dependencies:
|
|
|
51
23
|
- - ">="
|
|
52
24
|
- !ruby/object:Gem::Version
|
|
53
25
|
version: '8.1'
|
|
54
|
-
description: 'A Rails engine for the easyFAA apps: each
|
|
55
|
-
FAA, the NTSB) table for table, changing only the rows the source did, and each
|
|
26
|
+
description: 'A Rails engine for the easyFAA apps: each sits behind Cloudflare and
|
|
56
27
|
shows the same error pages, limits, crawlers and N-numbers.'
|
|
57
28
|
email:
|
|
58
29
|
- claudiob@users.noreply.github.com
|
|
@@ -65,24 +36,16 @@ files:
|
|
|
65
36
|
- README.md
|
|
66
37
|
- app/controllers/concerns/rate_limited.rb
|
|
67
38
|
- app/controllers/drive/errors_controller.rb
|
|
68
|
-
- app/models/access_file.rb
|
|
69
|
-
- app/models/concerns/counted.rb
|
|
70
|
-
- app/models/concerns/mirrored.rb
|
|
71
39
|
- app/models/crawler.rb
|
|
72
40
|
- app/models/designation.rb
|
|
73
|
-
- app/models/drive/current.rb
|
|
74
41
|
- app/models/n_number.rb
|
|
75
|
-
- app/models/run.rb
|
|
76
42
|
- app/models/serial.rb
|
|
77
43
|
- app/views/drive/errors/show.html.erb
|
|
78
44
|
- app/views/layouts/drive/errors.html.erb
|
|
79
|
-
- db/migrate/20261005100000_create_runs_and_changes.rb
|
|
80
|
-
- db/migrate/20261006100000_drop_changes.rb
|
|
81
45
|
- lib/drive.rb
|
|
82
46
|
- lib/drive/cloudflare.rb
|
|
83
47
|
- lib/drive/engine.rb
|
|
84
48
|
- lib/drive/inflections.rb
|
|
85
|
-
- lib/drive/test_helper.rb
|
|
86
49
|
- lib/drive/version.rb
|
|
87
50
|
homepage: https://github.com/easyfaa/drive
|
|
88
51
|
licenses:
|
|
@@ -105,7 +68,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
105
68
|
- !ruby/object:Gem::Version
|
|
106
69
|
version: '0'
|
|
107
70
|
requirements: []
|
|
108
|
-
rubygems_version: 4.0.
|
|
71
|
+
rubygems_version: 4.0.20
|
|
109
72
|
specification_version: 4
|
|
110
|
-
summary: 'What the easyFAA apps share:
|
|
73
|
+
summary: 'What the easyFAA apps share: error pages, Cloudflare, limits and N-numbers.'
|
|
111
74
|
test_files: []
|
data/app/models/access_file.rb
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
require 'csv'
|
|
2
|
-
|
|
3
|
-
# A Microsoft Access database, as DRS and the NTSB publish theirs, read with mdb-export (mdbtools).
|
|
4
|
-
class AccessFile
|
|
5
|
-
# The mdb-export command, writing dates with their century, which it leaves out by default.
|
|
6
|
-
EXPORT = ['mdb-export', '-D', '%Y-%m-%d', '-T', '%Y-%m-%d %H:%M:%S'].freeze
|
|
7
|
-
|
|
8
|
-
# @param path [String] .accdb or .mdb file.
|
|
9
|
-
def initialize(path)
|
|
10
|
-
@path = path
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
# Copies a table into a temporary one of text columns, dropped when the transaction commits:
|
|
14
|
-
# "AD Number" is ad_number, and the table's own id is access_id.
|
|
15
|
-
# @param table [String] table name in the Access file, such as AD_DATA or events.
|
|
16
|
-
# @param connection [ActiveRecord::ConnectionAdapters::PostgreSQLAdapter] inside a transaction.
|
|
17
|
-
# @return [String] temporary table.
|
|
18
|
-
def stage(table, connection)
|
|
19
|
-
staged = "staged_#{column table}"
|
|
20
|
-
IO.popen [*EXPORT, @path, table], 'rb' do |io|
|
|
21
|
-
columns = CSV.parse_line(io.gets.force_encoding(Encoding::UTF_8)).map { column it }
|
|
22
|
-
connection.execute "DROP TABLE IF EXISTS #{staged}"
|
|
23
|
-
connection.execute <<~SQL.squish
|
|
24
|
-
CREATE TEMP TABLE #{staged} (#{columns.map { %("#{it}" text) }.join ', '}) ON COMMIT DROP
|
|
25
|
-
SQL
|
|
26
|
-
copy io, staged, connection.raw_connection
|
|
27
|
-
end
|
|
28
|
-
staged
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
private
|
|
32
|
-
|
|
33
|
-
def column(name)
|
|
34
|
-
column = name.strip.downcase.gsub(/[^a-z0-9]+/, '_').delete_prefix('_').delete_suffix('_')
|
|
35
|
-
# An Access table's own counter, named apart from the id Rails gives every row.
|
|
36
|
-
column == 'id' ? 'access_id' : column
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def copy(io, staged, raw)
|
|
40
|
-
raw.copy_data "COPY #{staged} FROM STDIN WITH (FORMAT csv)" do
|
|
41
|
-
while (chunk = io.read(1 << 20))
|
|
42
|
-
# Some narratives carry null bytes, which PostgreSQL cannot keep in text.
|
|
43
|
-
raw.put_copy_data chunk.delete("\x00")
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
# A table written a record at a time, each save and delete counted in the current run.
|
|
2
|
-
module Counted
|
|
3
|
-
extend ActiveSupport::Concern
|
|
4
|
-
|
|
5
|
-
included do
|
|
6
|
-
after_save :count_upsert, if: :saved_changes?
|
|
7
|
-
after_destroy :count_delete
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
private
|
|
11
|
-
|
|
12
|
-
def count_upsert
|
|
13
|
-
Run.current.tally(**(previously_new_record? ? { inserted: 1 } : { updated: 1 }))
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def count_delete = Run.current.tally(deleted: 1)
|
|
17
|
-
end
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
# A table copying a source's table field for field as text, changing only the rows the source did.
|
|
2
|
-
module Mirrored
|
|
3
|
-
extend ActiveSupport::Concern
|
|
4
|
-
|
|
5
|
-
# The columns this app adds to what the source published.
|
|
6
|
-
OWN = %w[id source row_key row_hash created_at updated_at].freeze
|
|
7
|
-
|
|
8
|
-
included do
|
|
9
|
-
# A source's own `type` column is data, not Rails' single-table inheritance.
|
|
10
|
-
self.inheritance_column = nil
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
class_methods do
|
|
14
|
-
# Brings the rows from one source in line with a table staged from it, in the current run.
|
|
15
|
-
# @param staged [String] temporary table holding what the source publishes.
|
|
16
|
-
# @param source [String] file or document type the rows come from.
|
|
17
|
-
# @return [Hash{Symbol => Integer}] how many rows were inserted, updated and deleted.
|
|
18
|
-
def mirror(staged, source:)
|
|
19
|
-
stage_incoming staged
|
|
20
|
-
counts = { inserted: inserted(source), updated: updated(source), deleted: deleted(source) }
|
|
21
|
-
apply source
|
|
22
|
-
counts.tap { Run.current.tally(**it) }
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
private
|
|
26
|
-
|
|
27
|
-
def fields = column_names - OWN
|
|
28
|
-
|
|
29
|
-
def stage_incoming(staged)
|
|
30
|
-
present = connection.columns(staged).map(&:name)
|
|
31
|
-
picked = fields.map { |field| present.include?(field) ? %(s."#{field}") : 'NULL::text' }
|
|
32
|
-
hash = "md5(ROW(#{picked.join ', '})::text)"
|
|
33
|
-
keys = self::KEY.map { %(s."#{it}") }
|
|
34
|
-
connection.execute 'DROP TABLE IF EXISTS mirror_incoming'
|
|
35
|
-
connection.execute <<~SQL.squish
|
|
36
|
-
CREATE TEMP TABLE mirror_incoming ON COMMIT DROP AS
|
|
37
|
-
SELECT DISTINCT ON (row_key) * FROM (
|
|
38
|
-
SELECT #{picked.zip(fields).map { |value, field| %(#{value} AS "#{field}") }.join ', '},
|
|
39
|
-
#{keys.any? ? "concat_ws('|', #{keys.join ', '})" : hash} AS row_key,
|
|
40
|
-
#{hash} AS row_hash
|
|
41
|
-
FROM #{staged} s
|
|
42
|
-
) rows ORDER BY row_key, row_hash
|
|
43
|
-
SQL
|
|
44
|
-
connection.execute 'CREATE INDEX ON mirror_incoming (row_key)'
|
|
45
|
-
connection.execute 'ANALYZE mirror_incoming'
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def inserted(source)
|
|
49
|
-
rows <<~SQL.squish
|
|
50
|
-
FROM mirror_incoming i
|
|
51
|
-
WHERE NOT EXISTS (SELECT 1 FROM #{quoted_table_name} t
|
|
52
|
-
WHERE t.source = #{connection.quote source} AND t.row_key = i.row_key)
|
|
53
|
-
SQL
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def updated(source)
|
|
57
|
-
rows <<~SQL.squish
|
|
58
|
-
FROM mirror_incoming i JOIN #{quoted_table_name} t USING (row_key)
|
|
59
|
-
WHERE t.source = #{connection.quote source} AND t.row_hash <> i.row_hash
|
|
60
|
-
SQL
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def deleted(source)
|
|
64
|
-
rows <<~SQL.squish
|
|
65
|
-
FROM #{quoted_table_name} i
|
|
66
|
-
WHERE i.source = #{connection.quote source}
|
|
67
|
-
AND NOT EXISTS (SELECT 1 FROM mirror_incoming n WHERE n.row_key = i.row_key)
|
|
68
|
-
SQL
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def rows(from) = connection.select_value("SELECT count(*) #{from}").to_i
|
|
72
|
-
|
|
73
|
-
def apply(source)
|
|
74
|
-
quoted = fields.map { connection.quote_column_name it }
|
|
75
|
-
connection.delete <<~SQL.squish
|
|
76
|
-
DELETE FROM #{quoted_table_name} t WHERE t.source = #{connection.quote source}
|
|
77
|
-
AND NOT EXISTS (SELECT 1 FROM mirror_incoming n WHERE n.row_key = t.row_key)
|
|
78
|
-
SQL
|
|
79
|
-
connection.execute <<~SQL.squish
|
|
80
|
-
INSERT INTO #{quoted_table_name} AS t
|
|
81
|
-
(source, row_key, row_hash, #{quoted.join ', '}, created_at, updated_at)
|
|
82
|
-
SELECT #{connection.quote source}, row_key, row_hash, #{quoted.join ', '}, now(), now()
|
|
83
|
-
FROM mirror_incoming
|
|
84
|
-
ON CONFLICT (source, row_key) DO UPDATE SET row_hash = excluded.row_hash,
|
|
85
|
-
#{quoted.map { "#{it} = excluded.#{it}" }.join ', '}, updated_at = now()
|
|
86
|
-
WHERE t.row_hash <> excluded.row_hash
|
|
87
|
-
SQL
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
end
|
data/app/models/drive/current.rb
DELETED
data/app/models/run.rb
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
# One refresh of a source: when it started and finished, and how many rows it changed.
|
|
2
|
-
class Run < ApplicationRecord
|
|
3
|
-
# The lock every refresh takes, so runs commit one at a time.
|
|
4
|
-
LOCK = 1_729_001
|
|
5
|
-
|
|
6
|
-
# Refreshes a source inside one transaction, the only one running, recording what it changed.
|
|
7
|
-
# @param source [String] what is refreshed, such as "registry".
|
|
8
|
-
# @return [Run] finished run.
|
|
9
|
-
def self.record(source)
|
|
10
|
-
transaction do
|
|
11
|
-
connection.execute "SELECT pg_advisory_xact_lock(#{LOCK})"
|
|
12
|
-
run = create! source:, started_at: Time.current
|
|
13
|
-
Drive::Current.set(run:) { yield run }
|
|
14
|
-
run.tap { it.update! finished_at: Time.current }
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
# @return [Run] run of the refresh at hand.
|
|
19
|
-
def self.current = Drive::Current.run
|
|
20
|
-
|
|
21
|
-
# Adds what a table's refresh did to this run's counts.
|
|
22
|
-
# @param inserted [Integer] rows inserted.
|
|
23
|
-
# @param updated [Integer] rows updated.
|
|
24
|
-
# @param deleted [Integer] rows deleted.
|
|
25
|
-
def tally(inserted: 0, updated: 0, deleted: 0)
|
|
26
|
-
self.inserted += inserted
|
|
27
|
-
self.updated += updated
|
|
28
|
-
self.deleted += deleted
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# @return [String] source and start, as the runs' table names a run.
|
|
32
|
-
def to_s = "#{source} #{started_at.to_fs :db}"
|
|
33
|
-
end
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
# Each refresh of a source (a run), and every row it inserted, changed or deleted (a change),
|
|
2
|
-
# which is the feed another app follows by the change's id.
|
|
3
|
-
class CreateRunsAndChanges < ActiveRecord::Migration[8.1]
|
|
4
|
-
def change
|
|
5
|
-
create_enum :operation, %w[ upsert delete ]
|
|
6
|
-
|
|
7
|
-
create_table :runs do |t|
|
|
8
|
-
t.text :source, null: false
|
|
9
|
-
t.integer :inserted, default: 0, null: false
|
|
10
|
-
t.integer :updated, default: 0, null: false
|
|
11
|
-
t.integer :deleted, default: 0, null: false
|
|
12
|
-
t.datetime :started_at, null: false
|
|
13
|
-
t.datetime :finished_at
|
|
14
|
-
t.virtual :name, type: :text, as: "source || ' #' || id", stored: true
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
create_table :changes do |t|
|
|
18
|
-
t.enum :operation, enum_type: :operation, null: false
|
|
19
|
-
t.text :kind, null: false
|
|
20
|
-
t.text :source, null: false
|
|
21
|
-
t.text :row_key, null: false
|
|
22
|
-
t.references :run, null: false, foreign_key: true
|
|
23
|
-
t.jsonb :record
|
|
24
|
-
t.datetime :created_at, null: false
|
|
25
|
-
t.index %i[ kind row_key ]
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
class DropChanges < ActiveRecord::Migration[8.1]
|
|
2
|
-
def change
|
|
3
|
-
drop_table :changes do |t|
|
|
4
|
-
t.enum :operation, enum_type: :operation, null: false
|
|
5
|
-
t.text :kind, null: false
|
|
6
|
-
t.text :source, null: false
|
|
7
|
-
t.text :row_key, null: false
|
|
8
|
-
t.references :run, null: false, foreign_key: true
|
|
9
|
-
t.jsonb :record
|
|
10
|
-
t.datetime :created_at, null: false
|
|
11
|
-
t.index %i[kind row_key]
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
drop_enum :operation, %w[upsert delete]
|
|
15
|
-
end
|
|
16
|
-
end
|
data/lib/drive/test_helper.rb
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
module Drive
|
|
2
|
-
# What a host's tests need to mirror rows without a source's file.
|
|
3
|
-
module TestHelper
|
|
4
|
-
# Stages rows as a source's file would, a text column per field, for a model to mirror.
|
|
5
|
-
# @param table [String] temporary table to create.
|
|
6
|
-
# @param rows [Array<Hash{String => String}>] the rows, each by field.
|
|
7
|
-
# @return [String] the table.
|
|
8
|
-
def stage(table, rows)
|
|
9
|
-
columns = rows.flat_map(&:keys).uniq
|
|
10
|
-
connection = ActiveRecord::Base.connection
|
|
11
|
-
connection.execute "DROP TABLE IF EXISTS #{table}"
|
|
12
|
-
connection.execute "CREATE TEMP TABLE #{table} (#{columns.map { %("#{it}" text) }.join ', '})"
|
|
13
|
-
rows.each do |row|
|
|
14
|
-
values = columns.map { connection.quote row[it] }
|
|
15
|
-
connection.execute "INSERT INTO #{table} VALUES (#{values.join ', '})"
|
|
16
|
-
end
|
|
17
|
-
table
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|