Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/mchmarny/pgxmap
Go library to handle interface types when JSONB serialized in PostgreSQL
https://github.com/mchmarny/pgxmap
Last synced: 5 days ago
JSON representation
Go library to handle interface types when JSONB serialized in PostgreSQL
- Host: GitHub
- URL: https://github.com/mchmarny/pgxmap
- Owner: mchmarny
- License: apache-2.0
- Created: 2022-05-24T03:11:10.000Z (over 2 years ago)
- Default Branch: main
- Last Pushed: 2023-03-06T23:07:10.000Z (over 1 year ago)
- Last Synced: 2024-06-20T12:06:44.423Z (5 months ago)
- Language: Go
- Size: 1.47 MB
- Stars: 1
- Watchers: 2
- Forks: 0
- Open Issues: 2
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# pgxmap
[![test](https://github.com/mchmarny/pgxmap/actions/workflows/test-on-push.yaml/badge.svg?branch=main)](https://github.com/mchmarny/pgxmap/actions/workflows/test-on-push.yaml)
[![Go Report Card](https://goreportcard.com/badge/github.com/mchmarny/pgxmap)](https://goreportcard.com/report/github.com/mchmarny/pgxmap)
![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/mchmarny/pgxmap)
[![codecov](https://codecov.io/gh/mchmarny/pgxmap/branch/main/graph/badge.svg?token=00H8S7GMPP)](https://codecov.io/gh/mchmarny/pgxmap)
[![GoDoc](https://godoc.org/github.com/mchmarny/pgxmap?status.svg)](https://godoc.org/github.com/mchmarny/pgxmap)Custom map to use with [pgx](https://github.com/jackc/pgx) help with `map[string]interface{}` decoding and encoding correctly map items types (e.g. `int64`) when saved and retrieved from Postgres DB.
## Why
`pgx` stores JSON-marshalled objects in DB as JSONB type. For Go `structs` with priory know types, `pgx` driver encodes Go types to JSON and back automatically. However, when unmarshalling into a Go interface (e.g. `map[string]interface{}`) Go decoding has to do some guessing to map certain types when scanning DB selection results (e.g. `int64` vs `float64`). There isn't really much `pgx` can actually do here since JavaScript itself does not distinguish between integers and floats (see [this](https://tools.ietf.org/html/rfc7159#section-6) for details). There are similar issues with time precision as well.
> The full demo of the error resulting from an idiomatic implementation is available [here](examples/idiom/main.go).
There are 4 diff ways you can deal with this issue:
1. Avoid using map with interface value altogether
2. Use map with all values as strings and parse on return
3. Use the `driver.Valuer` interface to do your own encoding and decoding
4. Use the "wrapper struct" (assuming you have the luxury of using generics)The `pgxmap` uses the 3rd approach and ensures that the `int`, `float` derivative types (`int64`, `int32`, `uint16`, `float64` etc.) and `time` in your map are automatically correctly encoded and decoded to ensure exact same type/precision while still providing that basic Go map-like functionality.
## Usage
The basic usage of the `pgxmap` library falls into 3 patterns:
* Creation of data (config map or state struct)
* Saving it to DB
* Retrieving it from DB### Creation
#### Config Map
> For illustration purposes, this example creates a config map with only 3 items with the different types. See [map_test.go](./map_test.go) for broader example.
```go
bigInt := time.Now().UnixNano()m := pgxmap.ConfigMap{
"int64": bigInt,
"uint32": uint32(bigInt),
"float64": float64(bigInt) / float64(3),
}
```#### State Struct
Assuming the data you want to persist into DB looks something like this:
```go
type MyThing struct {
String string `json:"s"`
Number int64 `json:"n"`
Float float64 `json:"f"`
Time time.Time `json:"t"`
Bool bool `json:"b"`
}
```You wrap it into the `State` struct like this:
```go
s := pgxmap.State[MyThing]{
Data: MyThing{
String: "hello",
Number: 1653398649063529000,
Float: 1653398731.740085,
Time: time.Now(),
Bool: true,
},
}
```### Saving
Assuming the DB table schema looks something like this (key is the `JSONB` column type):
```sql
CREATE TABLE IF NOT EXISTS demo (
id varchar NOT NULL PRIMARY KEY,
cf JSONB
);
```Saving the map or state object into DB then is as simple as using it as yet another parameter in your `Exec` method:
> Assuming the `sql` variable holding your `SELECT` statement is already defined.
```go
func save(ctx context.Context, p *pgxpool.Pool, id string, m pgxmap.ConfigMap) error {
// input validation skipped for brevity
if _, err := pool.Exec(ctx, sql, id, m); err != nil {
return errors.Wrap(err, "error inserting row")
}
return nil
}
```### Retrieval
Similarly, during retrieval you simply provide a pointer to the `ConfigMap` or `State` and use the `row.Scan` method:
> Assuming the `sql` variable holding your `INSERT` statement is already defined.
```go
func save[T any](ctx context.Context, p *pgxpool.Pool, id string, s *pgxmap.State[T]) error {
// input validation skipped for brevity
if _, err := p.Exec(ctx, insertSQL, id, s); err != nil {
return errors.Wrap(err, "error inserting row")
}
return nil
}
```## Disclaimer
This is my personal project and it does not represent my employer. I take no responsibility for issues caused by this code. I do my best to ensure that everything works, but if something goes wrong, my apologies is all you will get.