mirroring 0.1.1 → 0.1.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +10 -0
- data/app/models/access_file.rb +6 -41
- data/app/models/csv_file.rb +47 -0
- data/lib/mirroring/engine.rb +2 -2
- data/lib/mirroring/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 551d83b55c483bde0db966432149621b6ef18109fc83728801a20249aee59144
|
|
4
|
+
data.tar.gz: 65b72f3109b2fd308da7f8d23ad28cae434b4d1da8542647c06a11511baf858a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 97e282e4c09569ed750c527c7d7bb4ff977a83cee1d46884b75b2daca51b1df4dcbaf14a5cfb2a46c3e0b37e2aa932bd82d6624d3505547b167242364085d460
|
|
7
|
+
data.tar.gz: 6a1ddb6696341e928adad4c74f340c4c2f98aa5d1e9bdd0d0c1eb73a1fecea3de149dfd9518712df3eecc4d504b4bbe6bb12e13cbd14541c118adfe26d872e7d
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
For more information about changelogs, check [Keep a Changelog](http://keepachangelog.com).
|
|
6
6
|
|
|
7
|
+
## 0.1.2 - 2026-10-25
|
|
8
|
+
|
|
9
|
+
* [FEATURE] `CsvFile#stage`, which stages a CSV file whose first line names its fields
|
|
10
|
+
|
|
11
|
+
`CsvFile.new('aids-2025.csv').stage('aids_2025', connection)` stages the rows as
|
|
12
|
+
`AccessFile#stage` does a table's, "Report Number" as report_number, ready for `Model.mirror`.
|
|
13
|
+
`AccessFile` is now a `CsvFile` that reads each table through mdb-export; what it stages is the
|
|
14
|
+
same.
|
|
15
|
+
|
|
7
16
|
## 0.1.1 - 2026-10-23
|
|
8
17
|
|
|
9
18
|
* [ENHANCEMENT] `AccessFile#stage` reads the table into one string, a megabyte at a time
|
data/README.md
CHANGED
|
@@ -8,6 +8,7 @@ source did and counting them.
|
|
|
8
8
|
- **`Counted`**: a table written a record at a time, such as a document fetched by URL.
|
|
9
9
|
- **`Run`**: one refresh, in one transaction, the only one running, with how many rows it
|
|
10
10
|
inserted, updated and deleted.
|
|
11
|
+
- **`CsvFile`**: stages a CSV file whose first line names its fields.
|
|
11
12
|
- **`AccessFile`**: stages a table of a Microsoft Access file, read with `mdb-export`.
|
|
12
13
|
|
|
13
14
|
It needs PostgreSQL.
|
|
@@ -59,6 +60,15 @@ Run.record 'engines' do
|
|
|
59
60
|
end
|
|
60
61
|
```
|
|
61
62
|
|
|
63
|
+
A CSV file stages the same way, its rows named by the second argument:
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
Run.record 'incidents' do
|
|
67
|
+
staged = CsvFile.new('aids-2025.csv').stage('aids_2025', Incident.connection)
|
|
68
|
+
Incident.mirror staged, source: 'aids-2025'
|
|
69
|
+
end
|
|
70
|
+
```
|
|
71
|
+
|
|
62
72
|
A table written a record at a time counts each save and delete in the run at hand:
|
|
63
73
|
|
|
64
74
|
```ruby
|
data/app/models/access_file.rb
CHANGED
|
@@ -1,51 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class AccessFile
|
|
1
|
+
# A Microsoft Access database, as DRS and the NTSB publish theirs, each table read as CSV with
|
|
2
|
+
# mdb-export (mdbtools): a table's own id is access_id.
|
|
3
|
+
class AccessFile < CsvFile
|
|
5
4
|
# The mdb-export command, writing dates with their century, which it leaves out by default.
|
|
6
5
|
EXPORT = ['mdb-export', '-D', '%Y-%m-%d', '-T', '%Y-%m-%d %H:%M:%S'].freeze
|
|
7
6
|
|
|
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
7
|
private
|
|
32
8
|
|
|
9
|
+
def read(table, &) = IO.popen([*EXPORT, @path, table], 'rb', &)
|
|
10
|
+
|
|
33
11
|
def column(name)
|
|
34
|
-
column =
|
|
12
|
+
column = super
|
|
35
13
|
# An Access table's own counter, named apart from the id Rails gives every row.
|
|
36
14
|
column == 'id' ? 'access_id' : column
|
|
37
15
|
end
|
|
38
|
-
|
|
39
|
-
def copy(io, staged, raw)
|
|
40
|
-
# One string for every megabyte: a new one each, and its copy without null bytes, left garbage
|
|
41
|
-
# no GC collected before a table ended, some 100 MB over the NTSB's tables.
|
|
42
|
-
chunk = String.new(capacity: 1 << 20)
|
|
43
|
-
raw.copy_data "COPY #{staged} FROM STDIN WITH (FORMAT csv)" do
|
|
44
|
-
while io.read(1 << 20, chunk)
|
|
45
|
-
# Some narratives carry null bytes, which PostgreSQL cannot keep in text.
|
|
46
|
-
chunk.delete! "\x00"
|
|
47
|
-
raw.put_copy_data chunk
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
16
|
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require 'csv'
|
|
2
|
+
|
|
3
|
+
# A CSV file whose first line names its fields, as a source publishes one.
|
|
4
|
+
class CsvFile
|
|
5
|
+
# @param path [String] .csv file, or for an AccessFile the .accdb or .mdb file.
|
|
6
|
+
def initialize(path)
|
|
7
|
+
@path = path
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Copies a table into a temporary one of text columns, dropped when the transaction commits:
|
|
11
|
+
# "Report Number" is report_number.
|
|
12
|
+
# @param table [String] what is staged: a name for the CSV file's rows, such as aids_2025, or a
|
|
13
|
+
# table of an Access file, such as AD_DATA or events.
|
|
14
|
+
# @param connection [ActiveRecord::ConnectionAdapters::PostgreSQLAdapter] inside a transaction.
|
|
15
|
+
# @return [String] temporary table.
|
|
16
|
+
def stage(table, connection)
|
|
17
|
+
staged = "staged_#{column table}"
|
|
18
|
+
read table do |io|
|
|
19
|
+
columns = CSV.parse_line(io.gets.force_encoding(Encoding::UTF_8)).map { column it }
|
|
20
|
+
connection.execute "DROP TABLE IF EXISTS #{staged}"
|
|
21
|
+
connection.execute <<~SQL.squish
|
|
22
|
+
CREATE TEMP TABLE #{staged} (#{columns.map { %("#{it}" text) }.join ', '}) ON COMMIT DROP
|
|
23
|
+
SQL
|
|
24
|
+
copy io, staged, connection.raw_connection
|
|
25
|
+
end
|
|
26
|
+
staged
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def read(_table, &) = File.open(@path, 'rb', &)
|
|
32
|
+
|
|
33
|
+
def column(name) = name.strip.downcase.gsub(/[^a-z0-9]+/, '_').delete_prefix('_').delete_suffix('_')
|
|
34
|
+
|
|
35
|
+
def copy(io, staged, raw)
|
|
36
|
+
# One string for every megabyte: a new one each, and its copy without null bytes, left garbage
|
|
37
|
+
# no GC collected before a table ended, some 100 MB over the NTSB's tables.
|
|
38
|
+
chunk = String.new(capacity: 1 << 20)
|
|
39
|
+
raw.copy_data "COPY #{staged} FROM STDIN WITH (FORMAT csv)" do
|
|
40
|
+
while io.read(1 << 20, chunk)
|
|
41
|
+
# Some narratives carry null bytes, which PostgreSQL cannot keep in text.
|
|
42
|
+
chunk.delete! "\x00"
|
|
43
|
+
raw.put_copy_data chunk
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/mirroring/engine.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module Mirroring
|
|
2
|
-
# Lends the host its models (Run, Mirrored, Counted, AccessFile), `mirroring key:` on
|
|
3
|
-
# and the migration of runs (`bin/rails mirroring:install:migrations`).
|
|
2
|
+
# Lends the host its models (Run, Mirrored, Counted, CsvFile, AccessFile), `mirroring key:` on
|
|
3
|
+
# every model, and the migration of runs (`bin/rails mirroring:install:migrations`).
|
|
4
4
|
class Engine < ::Rails::Engine
|
|
5
5
|
engine_name 'mirroring'
|
|
6
6
|
|
data/lib/mirroring/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mirroring
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- claudiob
|
|
@@ -81,6 +81,7 @@ files:
|
|
|
81
81
|
- app/models/concerns/counted.rb
|
|
82
82
|
- app/models/concerns/mirrored.rb
|
|
83
83
|
- app/models/concerns/mirrored/incoming.rb
|
|
84
|
+
- app/models/csv_file.rb
|
|
84
85
|
- app/models/mirroring/current.rb
|
|
85
86
|
- app/models/run.rb
|
|
86
87
|
- db/migrate/20261016100000_create_runs.rb
|