Skip to content

Commit

Permalink
Merge pull request #11 from go-andiamo/extend
Browse files Browse the repository at this point in the history
Add mapper Extend
  • Loading branch information
marrow16 authored Nov 5, 2024
2 parents f713e04 + 7d0d9b1 commit 4f62625
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 12 deletions.
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,51 @@
[![Go Report Card](https://goreportcard.com/badge/github.com/go-andiamo/columbus)](https://goreportcard.com/report/github.com/go-andiamo/columbus)

Columbus is a SQL row mapper that converts rows to a `map[string]any` - saving the need to scan rows into structs and then marshalling those structs as JSON.

## Installation
To install columbus, use go get:

go get github.com/go-andiamo/columbus

To update columbus to the latest version, run:

go get -u github.com/go-andiamo/columbus

## Usage / Examples

A basic example to map all columns from any table...
```go
package main

import (
"context"
"database/sql"
"github.com/go-andiamo/columbus"
)

func ReadRows(ctx context.Context, db *sql.DB, tableName string) ([]map[string]any, error) {
return columbus.MustNewMapper("*", nil, columbus.Query("FROM "+tableName)).
Rows(ctx, db, nil)
}
```

Re-using the same mapper to read all rows or a specific row...
```go
package main

import (
"context"
"database/sql"
"github.com/go-andiamo/columbus"
)

var mapper = columbus.MustNewMapper("*", nil, columbus.Query(`FROM people`))

func ReadAll(ctx context.Context, db *sql.DB) ([]map[string]any, error) {
return mapper.Rows(ctx, db, nil)
}

func ReadById(ctx context.Context, db *sql.DB, id any) (map[string]any, error) {
return mapper.ExactlyOneRow(ctx, db, []any{id}, columbus.AddClause(`WHERE id = ?`))
}
```
2 changes: 1 addition & 1 deletion exclude_properties.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func (f ConditionalExclude) Exclude(property string, path []string) bool {
return f(property, path)
}

var _ PropertyExcluder = AllowedProperties{}
var _ PropertyExcluder = (AllowedProperties)(nil)

func (xp AllowedProperties) Exclude(property string, path []string) bool {
if cx, ok := xp[property]; ok {
Expand Down
61 changes: 55 additions & 6 deletions mapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,34 @@ import (
"sync"
)

// Mapper is the main row mapper interface
type Mapper interface {
// Rows reads all rows and maps them into a slice of `map[string]any`
Rows(ctx context.Context, sqli SqlInterface, args []any, options ...any) ([]map[string]any, error)
// FirstRow reads just the first row and maps it into a `map[string]any`
//
// if there are no rows, returns nil
FirstRow(ctx context.Context, sqli SqlInterface, args []any, options ...any) (map[string]any, error)
// ExactlyOneRow reads exactly one row and maps it into a `map[string]any`
//
// if there are no rows, returns error sql.ErrNoRows
ExactlyOneRow(ctx context.Context, sqli SqlInterface, args []any, options ...any) (map[string]any, error)
// WriteRows reads all rows and writes them as JSON to the supplied writer
WriteRows(ctx context.Context, writer io.Writer, sqli SqlInterface, args []any, options ...any) error
// WriteFirstRow reads just the first row and writes it as JSON to the supplied writer
//
// if there are no rows, nothing is written to the writer
WriteFirstRow(ctx context.Context, writer io.Writer, sqli SqlInterface, args []any, options ...any) error
// WriteExactlyOneRow reads exactly one row and writes it as JSON to the supplied writer
//
// if there are no rows, returns error sql.ErrNoRows (and nothing is written to the writer)
WriteExactlyOneRow(ctx context.Context, writer io.Writer, sqli SqlInterface, args []any, options ...any) error
// Iterate iterates over the rows and calls the supplied handler with each row
//
// iteration stops at the end of rows - or an error is encountered - or the supplied handler returns false for `cont` (continue)
Iterate(ctx context.Context, sqli SqlInterface, args []any, handler func(row map[string]any) (cont bool, err error), options ...any) error
// Extend creates a new Mapper adding the specified columns, mappings and options
Extend(addColumns []string, mappings Mappings, options ...any) (Mapper, error)
}

// UseDecimals is an option that determines whether float/numeric/decimal columns should be mapped as decimal.Decimal properties
Expand All @@ -27,13 +47,15 @@ type Mapper interface {
type UseDecimals bool

// NewMapper creates a new row mapper
func NewMapper[T string | []string](cols T, mappings Mappings, options ...any) (Mapper, error) {
return newMapper(cols, mappings, options...)
//
// options can be any of: Query, RowPostProcessor, SubQuery or UseDecimals
func NewMapper[T string | []string](columns T, mappings Mappings, options ...any) (Mapper, error) {
return newMapper(columns, mappings, options...)
}

// MustNewMapper is the same as NewMapper, except it panics on error
func MustNewMapper[T string | []string](cols T, mappings Mappings, options ...any) Mapper {
m, err := NewMapper[T](cols, mappings, options...)
func MustNewMapper[T string | []string](columns T, mappings Mappings, options ...any) Mapper {
m, err := NewMapper[T](columns, mappings, options...)
if err != nil {
panic(err)
}
Expand Down Expand Up @@ -67,7 +89,7 @@ type mapper struct {
defaultQuery *Query
useDecimals bool
// subQuery is set by parent sub-query
subQuery SubQuery
subQuery internalSubQuery
subPath []string
}

Expand Down Expand Up @@ -252,6 +274,31 @@ func (m *mapper) Iterate(ctx context.Context, sqli SqlInterface, args []any, han
return err
}

func (m *mapper) Extend(addColumns []string, mappings Mappings, options ...any) (Mapper, error) {
result := &mapper{
mappings: m.copyMappings(),
cols: m.cols,
rowPostProcessors: append([]RowPostProcessor{}, m.rowPostProcessors...),
rowSubQueries: append([]SubQuery{}, m.rowSubQueries...),
defaultQuery: m.defaultQuery,
useDecimals: m.useDecimals,
}
if len(addColumns) != 0 {
if result.cols != "" {
result.cols += "," + strings.Join(addColumns, ",")
} else {
result.cols = strings.Join(addColumns, ",")
}
}
for k, v := range mappings {
result.mappings[k] = v
}
if err := result.addOptions(options...); err != nil {
return nil, err
}
return result, nil
}

func (m *mapper) rowMapOptions(options ...any) (query string, mappings Mappings, postProcesses []RowPostProcessor, subQueries []SubQuery, exclusions PropertyExclusions, err error) {
mappings = m.mappings
mappingsCopied := false
Expand Down Expand Up @@ -318,6 +365,7 @@ func (m *mapper) copyMappings() Mappings {
}

func (m *mapper) addOptions(options ...any) error {
seenQuery := false
for _, o := range options {
if o != nil {
switch option := o.(type) {
Expand All @@ -326,9 +374,10 @@ func (m *mapper) addOptions(options ...any) error {
case SubQuery:
m.rowSubQueries = append(m.rowSubQueries, option)
case Query:
if m.defaultQuery != nil {
if seenQuery {
return errors.New("cannot use multiple default queries")
}
seenQuery = true
qStr := Query("SELECT " + m.cols + " " + string(option))
m.defaultQuery = &qStr
case UseDecimals:
Expand Down
48 changes: 48 additions & 0 deletions mapper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -954,6 +954,54 @@ func TestMapper_Iterate_MapRowErrors(t *testing.T) {
require.Equal(t, "foo", err.Error())
}

func TestMapper_Extend(t *testing.T) {
m, err := NewMapper("a",
Mappings{"a": {Path: []string{"sub_obj"}}},
Query(`FROM table`),
&dummyRowPostProcessor{},
NewSubQuery("x", `SELECT * FROM other`, nil, nil, false),
)
require.NoError(t, err)
require.NotNil(t, m)
mt := m.(*mapper)
require.Equal(t, "SELECT a FROM table", string(*mt.defaultQuery))
require.Equal(t, "a", mt.cols)
require.Equal(t, 1, len(mt.rowPostProcessors))
require.Equal(t, 1, len(mt.rowSubQueries))
require.Equal(t, 1, len(mt.mappings))

m2, err := m.Extend([]string{"b", "c"},
Mappings{"b": {Path: []string{"sub_obj"}}},
Query(`FROM other_table`),
UseDecimals(false),
&dummyRowPostProcessor{},
NewSubQuery("x", `SELECT * FROM other`, nil, nil, false),
)
require.NoError(t, err)
require.NotNil(t, m2)
m2t := m2.(*mapper)
require.Equal(t, m2, m2t)
require.Equal(t, "SELECT a,b,c FROM other_table", string(*m2t.defaultQuery))
require.Equal(t, "a,b,c", m2t.cols)
require.Equal(t, 2, len(m2t.rowPostProcessors))
require.Equal(t, 2, len(m2t.rowSubQueries))
require.Equal(t, 2, len(m2t.mappings))
require.False(t, m2t.useDecimals)

_, err = m.Extend(nil, nil, Query(`FROM other_table`), Query(`FROM other_table`))
require.Error(t, err)

m, err = NewMapper("", nil)
require.NoError(t, err)
require.NotNil(t, m)
m2, err = m.Extend([]string{"a", "b"}, nil)
require.NoError(t, err)
require.NotNil(t, m2)
m2t = m2.(*mapper)
require.Equal(t, m2, m2t)
require.Equal(t, "a,b", m2t.cols)
}

func hasProperties(obj map[string]any, keys ...string) bool {
for _, key := range keys {
if _, ok := obj[key]; !ok {
Expand Down
14 changes: 9 additions & 5 deletions sub_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import (
type SubQuery interface {
Execute(ctx context.Context, sqli SqlInterface, row map[string]any, exclusions PropertyExclusions) error
ProvidesProperty() string
}

type internalSubQuery interface {
SubQuery
getQuery() string
}

Expand Down Expand Up @@ -86,7 +90,7 @@ type sliceSubQuery struct {
subQuery
}

var _ SubQuery = &sliceSubQuery{}
var _ internalSubQuery = (*sliceSubQuery)(nil)

func (sq *sliceSubQuery) Execute(ctx context.Context, sqli SqlInterface, row map[string]any, exclusions PropertyExclusions) error {
rm := sq.rowMapper(sq)
Expand All @@ -108,7 +112,7 @@ type objectSubQuery struct {
subQuery
}

var _ SubQuery = &objectSubQuery{}
var _ internalSubQuery = (*objectSubQuery)(nil)

func (sq *objectSubQuery) Execute(ctx context.Context, sqli SqlInterface, row map[string]any, exclusions PropertyExclusions) error {
rm := sq.rowMapper(sq)
Expand All @@ -130,7 +134,7 @@ type exactObjectSubQuery struct {
subQuery
}

var _ SubQuery = &exactObjectSubQuery{}
var _ internalSubQuery = (*exactObjectSubQuery)(nil)

func (sq *exactObjectSubQuery) Execute(ctx context.Context, sqli SqlInterface, row map[string]any, exclusions PropertyExclusions) error {
rm := sq.rowMapper(sq)
Expand All @@ -151,7 +155,7 @@ type mergeSubQuery struct {
subQuery
}

var _ SubQuery = &mergeSubQuery{}
var _ internalSubQuery = (*mergeSubQuery)(nil)

func (sq *mergeSubQuery) Execute(ctx context.Context, sqli SqlInterface, row map[string]any, exclusions PropertyExclusions) error {
rm := sq.rowMapper(sq)
Expand Down Expand Up @@ -187,7 +191,7 @@ func (sq *subQuery) getArgs(row map[string]any) ([]any, error) {
return result, nil
}

func (sq *subQuery) rowMapper(asq SubQuery) *mapper {
func (sq *subQuery) rowMapper(asq internalSubQuery) *mapper {
sq.mutex.RLock()
if sq.mapper != nil {
sq.mutex.RUnlock()
Expand Down

0 comments on commit 4f62625

Please sign in to comment.