A production grade Go CLI tool that parses NEM12 formatted meter data files and generates SQL INSERT statements for the meter_readings table.
Designed for large-file streaming with concurrent processing via goroutines.
# Build
make build
# Run with sample data (output to stdout)
make run
# Run with output to file
./bin/nem12reader -input testdata/sample.csv -output output.sql
# Run tests
make testUsage: nem12reader -input <path> [-output <path>] [-workers <N>]
Flags:
-input string Path to the NEM12 input file (required)
-output string Path to output SQL file (default: stdout)
-workers int Number of parallel workers (default: NumCPU)
The application processes NEM12 files, which contain hierarchical data: a 200 record defines a meter (NMI), followed by multiple 300 records containing the actual interval consumption data based on that meter's configuration. To handle very large files efficiently without exhausting memory, the application implements a concurrent Map-Reduce pattern:
- Split (1 goroutine): Reads the NEM12 file stream line by line using. It parses the hierarchy, grouping a
200record and all its subsequent300records into a singleNMIBlockmemory structure. This ensures constant memory usage regardless of the total file size. It then sends these blocks through a channel to the workers. - Map (N goroutines): Multiple worker goroutines listen on the channel. Each worker receives an
NMIBlock, parses the dates and interval values from the300records, and generates the corresponding SQLINSERTstatements. Because each block is an independent unit of work, parsing and SQL generation are fully parallelized, drastically reducing processing time. - Reduce (1 goroutine): A single writer goroutine collects the generated SQL strings from all map workers through a results channel and writes them sequentially to the output file (or stdout) using buffered I/O, preventing write contentions.
bin/ # Compiled binary output
cmd/nem12reader/main.go # CLI entrypoint
internal/
model/model.go # Domain types (MeterReading)
parser/parser.go # Streaming NEM12 parser
generator/generator.go # SQL INSERT statement generator
pipeline/pipeline.go # Concurrent processing pipeline
testdata/sample.csv # Sample NEM12 file
CREATE TABLE meter_readings (
id uuid DEFAULT gen_random_uuid() NOT NULL,
nmi varchar(10) NOT NULL,
timestamp timestamp NOT NULL,
consumption numeric NOT NULL,
CONSTRAINT meter_readings_pk PRIMARY KEY (id),
CONSTRAINT meter_readings_unique_consumption UNIQUE (nmi, timestamp)
);Each reading produces an idempotent INSERT with upsert semantics:
INSERT INTO meter_readings (nmi, timestamp, consumption) VALUES ('NEM1201009', '2005-03-01 06:00:00', 0.461)
ON CONFLICT ON CONSTRAINT meter_readings_unique_consumption DO UPDATE SET consumption = EXCLUDED.consumption;- Go was chosen specifically because of its lightweight concurrency primitives (goroutines and channels), which map naturally to a streaming parallelization pipeline. Unlike traditional OS threads, goroutines consume incredibly little overhead, allowing us to spin up many workers matching CPU cores. This ensures that large files are parsed much faster than sequence-based languages, satisfying the requirement to handle heavy workloads quickly.
- Single Process/Binary — The entire processing flow is contained within a single executable application, making deployment trivial with zero setup or external services required.
- Standard library only — The problem of streaming text files and parsing CSV-like fields is well-scoped and inherently fast using Go's
bufioandstringsmodules. We avoid heavyweight libraries or ORMs (as permitted by the problem statement) to minimize memory usage. This allows a continuous stream from disk to processing to disk with constant memory regardless of file length. - PostgreSQL-compatible SQL output — Matches the provided
CREATE TABLEschema. The output statements use Upsert syntax (ON CONFLICT... DO UPDATE) to provide idempotent handling of re-run records.
- Direct database insertion.
--batch-sizeflag to wrap groups of INSERTs in explicitBEGIN/COMMITtransactions for better DB performance.- Structured logging.
- Map-Reduce pattern: the NEM12 format naturally partitions into independent NMI blocks (each 200 record + its 300 records). Map-Reduce use this: the splitter groups lines into blocks, and N map workers process blocks fully in parallel (both parsing and SQL generation). This eliminates the single-threaded parser bottleneck of a simple fan-out pipeline.
- Streaming splitter : reads line-by-line with constant memory usage. Blocks are emitted as soon as they are complete, so the file is never fully loaded into memory. This is critical for handling very large files.
- Channel-based stages: cleanly decouples Split, Map, and Reduce. Each stage runs concurrently and communicates via typed Go channels, following idiomatic patterns.
- Stateless Map function:
ParseBlock()is a pure function with no shared state; it can be called safely from any number of goroutines without synchronisation. ON CONFLICT DO UPDATE: makes the generated SQL idempotent. Re-running the output against the database updates existing rows rather than failing on duplicates.- Context propagation: every stage respects
context.Contextfor cancellation, enabling graceful shutdown onSIGINT/SIGTERM. - Single binary: the entire tool compiles to one executable with no runtime dependencies, simplifying distribution and execution.