Recipe 02 / combine

Bring files together without a database.

Use a join when rows describe related things. Use concat when files describe the same thing over time. Both keep the result as an ordinary SimpleCsv you can continue transforming.

Suppose customers.csv has id and name, while orders.csv has customer_id and amount. Join them without renaming either key.

package main

import (
    "log"

    "github.com/osvik/simplecsv"
)

func main() {
    customers, err := simplecsv.ReadCsvFileE("customers.csv")
    if err != nil {
        log.Fatal(err)
    }

    orders, err := simplecsv.ReadCsvFileE("orders.csv")
    if err != nil {
        log.Fatal(err)
    }

    enriched, err := customers.JoinE(orders, "id", "customer_id")
    if err != nil {
        log.Fatal(err)
    }

    if err := enriched.WriteCsvFileE("orders-with-customers.csv"); err != nil {
        log.Fatal(err)
    }
}

The result keeps all columns from the left CSV, then adds the right CSV columns except its join key. Header collisions are rejected instead of producing ambiguous output.

Pick the join shape

  • Join keeps only matching rows.
  • LeftJoin keeps every left row and pads unmatched right fields with empty strings.
  • RightJoin keeps every right row.
  • FullJoin keeps every row from both sides.

Duplicate keys produce the full cartesian product for those keys, which makes one-to-many exports explicit rather than silently dropping rows.

Join on composite keys

For keys such as country plus SKU, use JoinOn. The library uses a length-prefixed key internally, so separators inside values cannot create collisions.

joined, ok := inventory.JoinOn(
    prices,
    []string{"country", "sku"},
    []string{"market", "product_code"},
)
if !ok {
    log.Fatal("could not join inventory and prices")
}

Stack same-schema files

For monthly exports with the same headers and order, use ConcatE:

jan, err := simplecsv.ReadCsvFileE("sales-2026-01.csv")
if err != nil {
    log.Fatal(err)
}
feb, err := simplecsv.ReadCsvFileE("sales-2026-02.csv")
if err != nil {
    log.Fatal(err)
}

all, err := simplecsv.ConcatE(jan, feb)
if err != nil {
    log.Fatal(err)
}

If vendors add or reorder fields between files, use ConcatByNameE. It aligns cells by header name, appends new headers in first-seen order, and pads missing fields with empty strings.

all, err := simplecsv.ConcatByNameE(jan, feb, march)
if err != nil {
    log.Fatal(err)
}

Glue aligned files side by side

When two exports are already in the same row order and have the same row count, MergeColumns combines their columns positionally. It does not guess how to pad mismatched row counts.

Back to all recipes ->