Skip to content

Commit

Permalink
up
Browse files Browse the repository at this point in the history
  • Loading branch information
WinPooh32 committed Jan 4, 2024
0 parents commit 579f8bf
Show file tree
Hide file tree
Showing 8 changed files with 1,232 additions and 0 deletions.
21 changes: 21 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
on: [push, pull_request]
name: Test with Docker
jobs:
test:
runs-on: ubuntu-latest
services:
dind:
image: docker:23.0-rc-dind-rootless
ports:
- 2375:2375
steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: "1.19"

- name: Test with Docker
run: go test -v ./...
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2024 WinPooh32

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# NORM

![test](https://github.com/WinPooh32/norm/actions/workflows/test.yml/badge.svg)
[![Go Report Card](https://goreportcard.com/badge/github.com/WinPooh32/norm)](https://goreportcard.com/report/github.com/WinPooh32/norm)
[![Go Reference](https://pkg.go.dev/badge/github.com/WinPooh32/norm.svg)](https://pkg.go.dev/github.com/WinPooh32/norm)

NORM is **N**ot an **ORM**.

It does:

- provide **unified** CRUD API for your queries;
- use generics for your types;
- **not** generate sql migrations;
- **not** generate queries from structs;
- **not** manage transactions.
205 changes: 205 additions & 0 deletions driver/sql/sql.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
package sql

import (
"context"
"database/sql"
"errors"
"fmt"
"reflect"

"github.com/VauntDev/tqla"
"github.com/blockloop/scan/v2"

"github.com/WinPooh32/norm"
)

var tq, _ = tqla.New(tqla.WithPlaceHolder(tqla.Dollar))

func SetPlaceHolder(p tqla.Placeholder) {
tq, _ = tqla.New(tqla.WithPlaceHolder(p))
}

type txKey struct{}

func WithTransaction(ctx context.Context, tx *sql.Tx) context.Context {
return context.WithValue(ctx, txKey{}, tx)
}

func txValue(ctx context.Context) *sql.Tx {
tx := ctx.Value(txKey{})
if tx == nil {
return nil
}
return tx.(*sql.Tx)
}

func NewObject[Model, Args any](db *sql.DB, c, r, u, d string) norm.Object[Model, Args] {
var m Model

return norm.NewObject[Model, Args](
&pgCreator[Model, Args]{
pgWriter[Model, Args]{db, c},
},
&pgReader[Model, Args]{
db, r, isSlice(m),
},
&pgUpdater[Model, Args]{
pgWriter[Model, Args]{db, u},
},
&pgDeleter[Model, Args]{
pgWriter[Model, Args]{db, d},
},
)
}

func NewView[Model, Args any](db *sql.DB, r string) norm.View[Model, Args] {
var m Model

reader := &pgReader[Model, Args]{
db: db,
tpl: r,
scanSlice: isSlice(m),
}

return norm.NewView[Model, Args](reader)
}

type preparer interface {
PrepareContext(ctx context.Context, query string) (*sql.Stmt, error)
}

type a[Args any] struct {
A Args
}

type ma[Model, Args any] struct {
M Model
A Args
}

type pgCreator[Model, Args any] struct {
pgWriter[Model, Args]
}

func (c *pgCreator[Model, Args]) Create(ctx context.Context, args Args, value Model) error {
return c.affect(ctx, args, value)
}

type pgUpdater[Model, Args any] struct {
pgWriter[Model, Args]
}

func (u *pgUpdater[Model, Args]) Update(ctx context.Context, args Args, value Model) error {
return u.affect(ctx, args, value)
}

type pgDeleter[Model, Args any] struct {
pgWriter[Model, Args]
}

func (d *pgDeleter[Model, Args]) Delete(ctx context.Context, args Args) error {
var nop Model
return d.affect(ctx, args, nop)
}

type pgWriter[Model, Args any] struct {
db *sql.DB
tpl string
}

func (w *pgWriter[Model, Args]) affect(ctx context.Context, args Args, value Model) error {
pr := newPreparer(txValue(ctx), w.db)
if err := w.exec(ctx, pr, args, value); err != nil {
return err
}

return nil
}

func (w *pgWriter[Model, Args]) exec(ctx context.Context, pr preparer, args Args, value Model) error {
stmtRaw, stmtArgs, err := tq.Compile(w.tpl, ma[Model, Args]{M: value, A: args})
if err != nil {
return fmt.Errorf("compile query template: %w", err)
}

stmt, err := pr.PrepareContext(ctx, stmtRaw)
if err != nil {
return fmt.Errorf("prepare query: %w", err)
}

res, err := stmt.ExecContext(ctx, stmtArgs...)
if err != nil {
return err
}

n, err := res.RowsAffected()
if err == nil && n <= 0 {
return norm.ErrNotAffected
}

return nil
}

type pgReader[Model, Args any] struct {
db *sql.DB
tpl string
scanSlice bool
}

func (r *pgReader[Model, Args]) Read(ctx context.Context, args Args) (value Model, err error) {
pr := newPreparer(txValue(ctx), r.db)

rows, err := r.query(ctx, pr, args)
if err != nil {
return value, err
}

if r.scanSlice {
err = scan.RowsStrict(&value, rows)
if err != nil {
return value, fmt.Errorf("scan rows: %w", err)
}
} else {
err = scan.RowStrict(&value, rows)
if errors.Is(err, sql.ErrNoRows) {
return value, norm.ErrNotFound
}
if err != nil {
return value, fmt.Errorf("scan one row: %w", err)
}
}

return value, nil
}

func (r *pgReader[Model, Args]) query(ctx context.Context, pr preparer, args Args) (rows *sql.Rows, err error) {
stmtRaw, stmtArgs, err := tq.Compile(r.tpl, a[Args]{A: args})
if err != nil {
return nil, fmt.Errorf("compile query template: %w", err)
}

stmt, err := pr.PrepareContext(ctx, stmtRaw)
if err != nil {
return nil, fmt.Errorf("prepare query: %w", err)
}

rows, err = stmt.QueryContext(ctx, stmtArgs...)
if err != nil {
return nil, fmt.Errorf("run query: %w", err)
}

return rows, nil
}

func isSlice(v any) bool {
return reflect.TypeOf(v).Kind() == reflect.Slice
}

func newPreparer(tx *sql.Tx, db *sql.DB) (pr preparer) {
if tx != nil {
pr = tx
} else {
pr = db
}
return pr
}
Loading

0 comments on commit 579f8bf

Please sign in to comment.