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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 26eca3d7349f81fc083e5a9f764944932777d067defe4712c31f83ef76d1663f
4
- data.tar.gz: 32e2e458c549b1068e975c9f54a4a6b6a0c9d345d6cf4a9405d1c260a1bda74e
3
+ metadata.gz: 551d83b55c483bde0db966432149621b6ef18109fc83728801a20249aee59144
4
+ data.tar.gz: 65b72f3109b2fd308da7f8d23ad28cae434b4d1da8542647c06a11511baf858a
5
5
  SHA512:
6
- metadata.gz: c7d9eea05b26411cbfd2804cb22ae439e74f629dc77c3c5a80f114d8c1b28094d18c666a23e6e3123c6c791ff2c78f28a1a54f1665c2794425ccc2399918f08d
7
- data.tar.gz: c08a094f66251000cacc007da45cf53f4c79ed8c7edf9632fa3b8e818f49558aabb9912a055025b616775ef9fb5d1efc6eaef397824ecead6838c800ef849a2b
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
@@ -1,51 +1,16 @@
1
- require 'csv'
2
-
3
- # A Microsoft Access database, as DRS and the NTSB publish theirs, read with mdb-export (mdbtools).
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 = name.strip.downcase.gsub(/[^a-z0-9]+/, '_').delete_prefix('_').delete_suffix('_')
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
@@ -1,6 +1,6 @@
1
1
  module Mirroring
2
- # Lends the host its models (Run, Mirrored, Counted, AccessFile), `mirroring key:` on every model,
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
 
@@ -1,4 +1,4 @@
1
1
  module Mirroring
2
2
  # The released version, as RubyGems lists it.
3
- VERSION = '0.1.1'
3
+ VERSION = '0.1.2'
4
4
  end
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.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