/scan

scan sql rows into any type powered by generics

Primary LanguageGoMIT LicenseMIT

Take a look at github.com/wroge/esquel.


go.dev reference Go Report Card golangci-lint codecov GitHub tag (latest SemVer)

Scan

This package offers a convenient and flexible way to scan SQL rows into any type, leveraging the power of generics.

Features

  • Efficient and Reusable: Avoid repetitive code and define the column-mapping in one place.
  • Auto Closing: No need to worry about resource leaks.
  • No Reflection: Faster than reflection based mappers.
  • Robust Error Handling: Best practices for managing errors.

Usage

import "github.com/wroge/scan"

type Author struct {
	ID   int64
	Name string
}

type Post struct {
	ID      int64
	Title   string
	Authors []Author
}

// Define mapping of database columns to struct fields.
var columns = scan.Columns[Post]{
	// Map the 'id' column to the 'ID' field in the 'Post' struct.
	// Uses the 'scan.Any' function for direct assignment without additional processing.
	"id": scan.Any(func(p *Post, id int64) { p.ID = id }),

	// Map the 'title' column to the 'Title' field in the 'Post' struct.
	// The 'scan.Null' function allows handling of nullable database columns.
	// If the 'title' column is null, 'default title' is used as the value.
	"title": scan.Null("default title", func(p *Post, title string) { p.Title = title }),

	// Map the 'authors' column, expected to be in JSON format, to the 'Authors' field in the 'Post' struct.
	// The 'scan.JSON' function automatically handles unmarshalling of the JSON data into the 'Author' struct slice.
	"authors": scan.JSON(func(p *Post, authors []Author) { p.Authors = authors }),

	// Or you could create a custom scanner with this function.
	// "column": scan.Func[Post, V](func(p *Post, value V) error {
	// 	return nil
	// }),
}

rows, err := db.Query("SELECT ...")
// handle error

Scanning all rows

posts, err := scan.All(rows, columns)
// handle error

Scanning the first row

post, err := scan.First(rows, columns)
if err != nil {
	if errors.Is(err, scan.ErrNoRows) {
		// handle no rows
	}

	// handle other error
}

Scanning exactly one row

post, err := scan.One(rows, columns)
if err != nil {
	if errors.Is(err, scan.ErrTooManyRows) {
		// handle too many rows
		// post is valid
	}

	if errors.Is(err, scan.ErrNoRows) {
		// handle no rows
	}

	// handle other error
}

Scanning a limited number of rows

posts, err := scan.Limit(10, rows, columns)
if err != nil {
	if errors.Is(err, scan.ErrTooManyRows) {
		// ignore if result set has more than 10 rows
		// len(posts) == 10
	}

	// handle other error
}

Using the Iterator directly

iter, err := scan.Iter(rows, columns)
// handle error

defer iter.Close()

for iter.Next() {
	var post Post

	err = iter.Scan(&post)
	// handle error

	// Or use the Value method:
	post, err := iter.Value()
	// handle error
}