rx

package
v0.85.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 8, 2026 License: MIT Imports: 19 Imported by: 0

Documentation

Overview

Package rx provides a minimalistic objects to table-rows mapper by using the scanning capabilities of sqlx. It is also an SQL builder using SQL templates.

At runtime the templates get filled in with metadata (tables' and columns' names from the provided data structures) and WHERE clauses, written by you - the programmer in SQL. The rendered by fasttemplate SQL query is prepared and executed by sqlx.

In other words, package `rx` provides functions, interfaces and a generic data type Rx, which wraps data structures. The structs may be provided by you or generated from existing tables by Generate. Rx implements the provided interfaces to execute CRUD operations. The relations' constraints are left to be managed by the database.

To ease schema migrations, `rx` provides two functions - Migrate and Generate. The first executes sets of SQL statements from a file to migrate the the database schema to a new state and the second re-generates the structs, mappped to rows in tables.

By default the current implementation assumes that the primary key name is `ID`. Of course the primary key can be more than one column and with arbitrary name. You can mark such fields with tags.

Synopsis

// Have an existing or newly created database. Generate a model package
// from it using the companion commandline tool `rowx`.
cd to/your/project/root
// make a directory for your package, named for example "model"
mkdir -p internal/example/model
// Generate structures from all tables in the database, implementing
// SqlxMeta interface.
rowx generate -dsn /some/path/mydb-development.sqlite -package ./internal/example/model

// Use the structures in your application.
// ...
// Have a structure, mapping a table row, generated in
// ./internal/example/model/model_tables.go.
type Users struct {
	LoginName string
	// ...
	ID        int64
}

// Have a slice of Users to Insert.
var users = []Users{
	Users{LoginName: "first"},
	Users{LoginName: "the_second"},
	Users{LoginName: "the_third"},
}

// Insert them.
r, e := rx.NewRx(users).Insert()
if e != nil {
	fmt.Fprintf(os.Stderr, "Got error from m.Insert(): %s", e.Error())
	return
}
//... time passes

// Create a new migration file or add to an existing one a new set of SQL
// statements to migrate the database to a new state.
cd to/your/project/root
vim data/migrations_01.sql
// Migrate.
./rowx migrate -sql_file data/migrations_01.sql -dsn=/tmp/test.sqlite -direction=up
// Run generate again to reflect the changes in the schema.
rowx generate -dsn /some/path/mydb-development.sqlite -package ./internal/example/model
// Edit your code, which uses the structures, if needed.
// During deployment just run `rowx migrate` again on the production
// datatbase.
// ...and so the life of the application continues further on.

Index

Examples

Constants

View Source
const (
	// DefaultLimit is the default LIMIT for SQL queries.
	DefaultLimit = 100
	// DriverName is the name of the database engine to use. For now we only
	// support `sqlite3`. Support for PostreSQL and MySQL is planned.
	DriverName = `sqlite3`
	// MigrationsTable is where we keep information about executed schema
	// migrations.
	MigrationsTable = `rx_migrations`
)

Variables

View Source
var (
	// DefaultLogHeader is a template for rx logging.
	DefaultLogHeader = `${prefix}:${level}:${short_file}:${line}`
	// DefaultLogOutput is where the output from the Logger will go to.
	DefaultLogOutput = os.Stderr
	// DSN must be set before using DB() function. It is set by default to
	// `:memory:`, because the default DriverName = `sqlite3`. See also options
	// for the connection string when using sqlite3:
	// https://github.com/mattn/go-sqlite3?tab=readme-ov-file#connection-string
	// .
	DSN = `:memory:`
	// Logger is always instantiated and the log level is set to log.DEBUG. You
	// can change the log level as you wish. We use
	// `github.com/labstack/gommon/log` as logging engine.
	Logger = newLogger()
	// ReflectXTag sets the tag name for identifying tags, read and acted upon
	// by sqlx and Rx.
	ReflectXTag = `rx`
)
View Source
var (
	/*
		QueryTemplates is an SQLMap (~map[string]any), containing templates
		from which the queries are built. Some of the values may be parts of
		other templates and may be used for replacement in other entries, used
		as templates. We use [fasttemplate.ExecuteStringStd] to construct ready
		for use by [sqlx] queries.
	*/
	QueryTemplates = Map{
		`INSERT`: `INSERT INTO ${table} (${columns}) VALUES ${placeholders}`,
		`SELECT`: `SELECT ${columns} FROM ${table} ${WHERE} LIMIT ${limit} OFFSET ${offset}`,
		`GET`:    `SELECT ${columns} FROM ${table} ${WHERE} LIMIT 1`,
		`UPDATE`: `UPDATE ${table} ${SET} ${WHERE}`,
		`DELETE`: `DELETE FROM ${table} ${WHERE}`,
		`CREATE_MIGRATIONS_TABLE`: `
CREATE TABLE IF NOT EXISTS ${table} (
	version UNSIGNED INT NOT NULL,
	direction VARCHAR(4) NOT NULL CHECK(direction IN('up', 'down')),
	file_path TEXT NOT NULL,
	applied TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
	UNIQUE(version, direction)
)`,
		`SELECT_TABLE_INFO_sqlite3`: `
SELECT t.name AS table_name, c.cid as c_id, c.name AS c_name,
c.type as c_type, c."notnull" as not_null, c.dflt_value as default_value, c.pk as pk
-- TODO: Parse CHECK constraints(and later maybe foreign keys) from t.sql
-- , t.sql
FROM sqlite_master t, pragma_table_info(t.name) c
WHERE (
	-- We replace the ${and_t_name_in} with an IN clause with comma separated
	-- list of table names for which structures will be generated in Go.
	t.type='table' AND t.name NOT LIKE 'sqlite%' ${and_t_name_in} AND t.name !=?)
ORDER BY table_name, c_id;
`,
	}
)

Functions

func CamelToSnake added in v0.71.0

func CamelToSnake(text string) string

CamelToSnake is used to convert type names and structure fields to snake case table columns. We pass it to reflectx.NewMapperFunc together with ReflectXTag. For example the string `UserLastFiveComments` is transformed to `user_last_five_comments`.

func DB

func DB() *sqlx.DB

DB invokes sqlx.MustConnect and assigns the returned sqlx.DB pointer to a private package variable, if not assigned already. This private variable is simply returned on subsequent calls. Then DB sets the sqlx.DB.Mapper, using ReflectXTag, and CamelToSnake as parameters to reflectx.NewMapperFunc.

sqlx.DB is a wrapper around sql.DB. A DB instance is not a connection, but an abstraction representing a Database. This is why creating a *sqlx.DB does not return an error and will not panic. It maintains a connection pool internally, and will attempt to connect when a connection is first needed.

func Generate added in v0.80.0

func Generate(dsn string, packagePath string, tables string) error

Generate generates structures for tables, found in database, pointed to by `dsn` and dumps them to a given `packagePath` directory. Returns an error if unsuccessful at any point of the execution. The name of the last directory in the path is used as package name. The directory must exist already.

`tables` is expected to contain comma-separated tablenames, for which structures will be generated. If `tables` is an empty string, structures for all tables in the database are generated.

Two files are created. The first only declares the package and can be modified by the programmer. It will not be regenerated on subsequent runs. The second contains all the structures, mapped to tables. It will be regenerated again on the next run of this function to re-map the potentially migrated to a new state schema to Go structs.

func Migrate added in v0.80.0

func Migrate(filePath, dsn, direction string) error

Migrate executes all not applied schema migrations with the given `direction`, found in `filePath` and stores in MigrationsTable the version, direction and file path of every applied migration. The migrations comments (headers) are expected to mach `^--\s*(\d{1,12})\s*(up|down)$`. For example: `--202506092333 up`. All SQL statements in a migration are executed at once as one transaction.

If the `direction` is `up`, all migrations in a file are applied in FIFO order.

If the `direction` is `down`, all migrations in a file are applied in LIFO order.

The explained workflow allows to have more than one migration (a set of statements) in the same file for logically different parts of the application. For example different modules have their own different migrations but they in some cases have to be applied in one run - a new release.

Migrate is often followed by executing Generate, if the schema of the database is modified - new columns or tables are added, modified or removed etc.

func RenderSQLTemplate

func RenderSQLTemplate(key string, stash map[string]any) string

RenderSQLTemplate gets the template from QueryTemplates, replaces potential partial SQL keys from QueryTemplates and then the keys from the given stash with values. Returns the produced SQL. Panics if key was not found or is not of the expected type (string).

func ResetDB added in v0.80.0

func ResetDB()

ResetDB closes the connection to the database and undefines the underlying variable, holding the connection.

func SQLForSET

func SQLForSET(columns []string) string

SQLForSET produces the `SET column = :column,...` for an UPDATE query from a slice of columns` names. It also makes each column snake_case if it contains a capital letter.

func SnakeToCamel added in v0.71.0

func SnakeToCamel(snake_case_word string) string

SnakeToCamel converts words from snake_case to CamelCase. It will be used to convert table_name to TableName and column_names to ColumnNames. This will be done during generation of structures out from tables.

func TypeToSnake added in v0.71.0

func TypeToSnake[R Rowx](row R) string

TypeToSnake converts struct type name like AVeryLongAndComplexTableName to 'a_very_long_and_complex_table_name' and returns it. Panics if the structure is annonimous.

Types

type Ext added in v0.83.0

type Ext interface {
	sqlx.Ext
	PrepareNamed(query string) (*sqlx.NamedStmt, error)
}

Ext is a generic constraint for *sqlx.Tx and *sqlx.DB.

type Map added in v0.80.0

type Map map[string]any

Map is a map of [string]any. It is used for QueryTemplates, for binding variables for sqlx statements, for variables for fasttemplate functions and wherever else appllicable.

type Migrations added in v0.80.0

type Migrations struct {
	Applied   time.Time `rx:"applied,auto"`
	Version   string
	Direction string
	FilePath  string
}

Migrations is an object, mapped to MigrationsTable.

func (*Migrations) Table added in v0.80.0

func (r *Migrations) Table() string

Table returns the table for Migrations.

type Rowx added in v0.71.0

type Rowx interface{}

Rowx is an empty interface and generic constraint for database records. Any struct type implements it. The fields of such a struct are expected to map to cells (columns) of a row in a database table.

type Rx

type Rx[R Rowx] struct {
	// contains filtered or unexported fields
}

Rx implements the SqlxModel interface and can be used right away or embedded (extended) to override some methods for a struct or set of structs.

func (*Rx[R]) Columns

func (m *Rx[R]) Columns() []string

Columns returns a slice with the names of the table's columns. If the underlying type implements the method Columns from SqlxMeta, the type is instantiated (if not already) and the method is called.

Example
package main

import (
	"fmt"

	"github.com/kberov/rowx/rx"
	_ "github.com/mattn/go-sqlite3"
)

func main() {
	type Books struct {
		Title  string
		Author string
		Body   string
		ID     uint32
		// ...
	}

	b := Books{Title: `Нова земя`, Author: `Иванъ Вазовъ`, Body: `По стръмната южна урва на Амбарица...`}
	columns := rx.NewRx(b).Columns()
	fmt.Printf("Columns: %+v\n", columns)

}
Output:
Columns: [title author body id]

func (*Rx[R]) Data

func (m *Rx[R]) Data() []R

Data returns the slice of structs, passed to NewRx or selected from the database. It may return nil if no rows were passed to NewRx.

Example
package main

import (
	"database/sql"
	"fmt"

	"github.com/kberov/rowx/rx"
	_ "github.com/mattn/go-sqlite3"
)

func main() {
	type Users struct {
		LoginName string
		GroupID   sql.NullInt64
		ChangedBy sql.NullInt64
		ID        int32 `rx:"id,auto"`
	}
	// []Users to be inserted (or updated, (LoginName is UNIQUE)).
	var users = []Users{
		Users{LoginName: "first", ChangedBy: sql.NullInt64{1, true}},
		Users{LoginName: "the_second", ChangedBy: sql.NullInt64{1, true}},
	}
	// Type parameter is guessed from the type of the parameters.
	m := rx.NewRx(users...)
	for _, u := range m.Data() {
		fmt.Printf("User.LoginName: %s, User.ChangedBy.Int64: %d\n", u.LoginName, u.ChangedBy.Int64)
	}
}
Output:
User.LoginName: first, User.ChangedBy.Int64: 1
User.LoginName: the_second, User.ChangedBy.Int64: 1

func (*Rx[R]) Delete

func (m *Rx[R]) Delete(where string, bindData any) (sql.Result, error)

Delete deletes records from the database.

func (*Rx[R]) Get

func (m *Rx[R]) Get(where string, bindData ...any) (*R, error)

Get executes sqlx.DB.Get and returns the result scanned into an instantiated Rowx object or an error.

Example
package main

import (
	"database/sql"
	"fmt"

	"github.com/kberov/rowx/rx"
	_ "github.com/mattn/go-sqlite3"
)

type Users struct {
	LoginName string
	Passwword string `rx:"password"`
	GroupID   sql.NullInt64
	ChangedBY sql.NullInt64
	ID        int64 `rx:"id,auto"`
}

func main() {
	// A long time ago in a galaxy far, far away....
	// m := rx.NewRx(users...)
	// ...
	// r, e := m.Insert()
	// fmt.Printf("sql.Result:%#v; Error:%#v;", r, e)
	// ...
	// d, e := rx.NewRx[Users]().Select(`id>0`, nil)
	// fmt.Printf("%+v; e:%+v", d, e)
	// ...
	// Now
	bindVars := struct{ ID int32 }{ID: 4}
	u, err := rx.NewRx[Users]().Get(`id=:id`, bindVars)
	if err == nil {
		fmt.Println(u.LoginName)

		return
	}
	fmt.Printf("err: %s\n", err)
}
Output:
fourth

func (*Rx[R]) Insert

func (m *Rx[R]) Insert() (sql.Result, error)

Insert inserts a slice of Rowx instances (without their primary key values) and returns sql.Result and [error]. The value for the autoincremented primary key (usually ID column) is left to be set by the database.

If the records to be inserted are more than one, the data is inserted in a transaction. sql.Result.RowsAffected will always return 1, because every row is inserted in its own statement. This may change in a future release. If there are no records to be inserted, Rx.Insert panics.

If you need to insert a Rowx structure with a specific value for ID, add a tag to the ID column `rx:"id,no_auto"` or use directly sqlx.

If you want to skip any field during insert (including `id`) add, a tag to it `rx:"field_name,auto"`.

Example
package main

import (
	"database/sql"
	"fmt"

	"github.com/kberov/rowx/rx"
	_ "github.com/mattn/go-sqlite3"
)

type Users struct {
	LoginName string
	Passwword string `rx:"password"`
	GroupID   sql.NullInt64
	ChangedBY sql.NullInt64
	ID        int64 `rx:"id,auto"`
}

var users = []Users{
	Users{LoginName: "first", ChangedBY: sql.NullInt64{0, false}, Passwword: `a`},
	Users{LoginName: "the_second", ChangedBY: sql.NullInt64{1, true}, Passwword: `b`},
	Users{LoginName: "the_third", ChangedBY: sql.NullInt64{1, true}, Passwword: `c`},
}

type Groups struct {
	Name      string
	ChangedBy sql.NullInt64
	ID        int64 `rx:"id,auto"`
}

func main() {
	_, e := rx.NewRx(users...).Insert()
	if e != nil {
		println(`Error inserting new users:`, e)
	}
	// users, e := rx.NewRx[Users]().Select(`id>=0`, nil)
	// fmt.Printf("Selected []Users %+v; %+v\n", users, e)
	groupRs, e := rx.NewRx[Groups](Groups{Name: `fifth`}).Insert()
	if e != nil {
		println(`Error inserting new group:`, e.Error())
	}
	lastGroupID, _ := groupRs.LastInsertId()
	fmt.Printf("Inserted new group with id: %d\n", lastGroupID)

	usrs := []Users{
		Users{LoginName: `fourth`, GroupID: sql.NullInt64{Int64: 4, Valid: true}, Passwword: `qwe4`},
		Users{LoginName: `fifth`, GroupID: sql.NullInt64{Int64: 5, Valid: true}, Passwword: `5th`},
	}
	r, err := rx.NewRx(usrs...).Insert()

	if err == nil {
		last, _ := r.LastInsertId()
		fmt.Println(`Last inserted user id:`, last)

		return
	}
	fmt.Printf("err: %s", err)
}
Output:
Inserted new group with id: 5
Last inserted user id: 5

func (*Rx[R]) Select

func (m *Rx[R]) Select(where string, bindData any, limitAndOffset ...int) ([]R, error)

Select prepares, executes a SELECT statement and returns the collected result as a slice. Selected records can also be used with Rx.Data.

  • `where` is expected to contain the `WHERE` clause with potentially subsequent `ORDER BY` clause. the keyword `WHERE` can be omitted.
  • `bindData` can be a struct (even unnamed) or map[string]any.
  • `limitAndOffset` is expected to be used as a variadic parameter. If passed, it is expected to consist of two values limit and offset - in that order. The default value for LIMIT can be set by DefaultLimit. OFFSET is 0 by default.
Example
package main

import (
	"database/sql"
	"fmt"

	"github.com/kberov/rowx/rx"
	_ "github.com/mattn/go-sqlite3"
)

type Users struct {
	LoginName string
	Passwword string `rx:"password"`
	GroupID   sql.NullInt64
	ChangedBY sql.NullInt64
	ID        int64 `rx:"id,auto"`
}

func main() {
	bind := struct{ IDs []uint }{IDs: []uint{4, 5}}
	u := rx.NewRx[Users]()
	data, err := u.Select(`id IN(:ids) ORDER BY id DESC`, bind)
	if err != nil {
		fmt.Println(err.Error())
	}
	fmt.Println(`Last two records in descending order:`)
	for _, u := range data {
		fmt.Printf("%d: %s\n", u.ID, u.LoginName)
	}

	// We can reuse the *Rx object for this parameter type for many and
	// different SQL queries.
	fmt.Println("\nUp to DefaultLimit records with OFFSET 0 in the default order:")
	data, err = u.Select(``, nil)
	if err != nil {
		fmt.Println(err.Error())
	}
	for _, u := range data {
		fmt.Printf("%d: %s\n", u.ID, u.LoginName)
	}
}
Output:
Last two records in descending order:
5: fifth
4: fourth

Up to DefaultLimit records with OFFSET 0 in the default order:
0: superadmin
1: first
2: the_second
3: the_third
4: fourth
5: fifth

func (*Rx[R]) SetData

func (m *Rx[R]) SetData(data []R) SqlxModel[R]

SetData sets a slice of R to be inserted or updated in the database. Returns the current instance of Rx.

Example
package main

import (
	"fmt"

	"github.com/kberov/rowx/rx"
	_ "github.com/mattn/go-sqlite3"
)

type UserGroup struct {
	rx.Rx[UserGroup]
	UserID  int64
	GroupID int64

	Where whereParams `rx:"where,-"`
}
type whereParams struct{ GroupID int64 }

func main() {
	ugDataIns := []UserGroup{
		UserGroup{UserID: 1, GroupID: 1},
		UserGroup{UserID: 2, GroupID: 2},
		UserGroup{UserID: 3, GroupID: 3},
		UserGroup{UserID: 1, GroupID: 4},
		UserGroup{UserID: 2, GroupID: 4},
	}
	ug := rx.NewRx[UserGroup]().SetData(ugDataIns)
	for i, row := range ug.Data() {
		fmt.Printf("%d: UserID: %d; GroupID: %d\n", i+1, row.UserID, row.GroupID)
	}
}
Output:

1: UserID: 1; GroupID: 1
2: UserID: 2; GroupID: 2
3: UserID: 3; GroupID: 3
4: UserID: 1; GroupID: 4
5: UserID: 2; GroupID: 4

func (*Rx[R]) Table

func (m *Rx[R]) Table() string

Table returns the converted to snake_case name of the type to be used as table name in sql queries. If the underlying type implements the method Table from SqlxMeta, the type is instantiated (if not already) and the method is called.

Example
package main

import (
	"fmt"

	"github.com/kberov/rowx/rx"
	_ "github.com/mattn/go-sqlite3"
)

func main() {
	type WishYouWereHere struct {
		SongName string
		ID       uint32
	}
	f := WishYouWereHere{SongName: `Shine On You Crazy Diamond`}
	fmt.Printf("TableName: %s\n", rx.NewRx(f).Table())

}
Output:
TableName: wish_you_were_here

func (*Rx[R]) Tx added in v0.83.0

func (m *Rx[R]) Tx() *sqlx.Tx

Tx returns an *sqlx.Tx so you do not have to make type assertion when you want to invoke *sqlx.Tx.Commit or *sqlx.Tx.Rollback. It creates a new one if needed.

Example
package main

import (
	"database/sql"
	"fmt"
	"strings"

	"github.com/jmoiron/sqlx"
	"github.com/labstack/gommon/log"

	"github.com/kberov/rowx/rx"
	_ "github.com/mattn/go-sqlite3"
)

var schema = `
PRAGMA foreign_keys = OFF;
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
login_name varchar(100) UNIQUE,
password varchar(64) NOT NULL UNIQUE,
group_id INTEGER DEFAULT NULL REFERENCES groups(id),
changed_by INTEGER DEFAULT NULL REFERENCES users(id) ON DELETE SET DEFAULT);

INSERT INTO users(id,group_id,changed_by,login_name,password) VALUES (0,0,0,'superadmin','qwerty');

CREATE TABLE groups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(100) UNIQUE NOT NULL,
changed_by INTEGER DEFAULT NULL REFERENCES users(id) ON DELETE SET DEFAULT);

INSERT INTO groups(id,name, changed_by) VALUES (0,'superadmin',0);
INSERT INTO groups(id,name, changed_by) VALUES (1,'admins',NULL);
INSERT INTO groups(id,name, changed_by) VALUES (2,'guests',NULL);
INSERT INTO groups(id,name, changed_by) VALUES (3,'editors',NULL);
INSERT INTO groups(id,name, changed_by) VALUES (4,'commenters',NULL);
CREATE TABLE user_group (
--  'ID of the user belonging to the group with group_id.'
  user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
--  'ID of the group to which the user with user_id belongs.'
  group_id INTEGER REFERENCES groups(id) ON DELETE CASCADE,
  PRIMARY KEY(user_id, group_id)
);
CREATE TABLE foo(
	bar INTEGER PRIMARY KEY AUTOINCREMENT,
	description VARCHAR(255) NOT NULL DEFAULT '',
	id VARCHAR(56) UNIQUE NOT NULL DEFAULT ''
);
PRAGMA foreign_keys = ON;
`

type Users struct {
	LoginName string
	Passwword string `rx:"password"`
	GroupID   sql.NullInt64
	ChangedBY sql.NullInt64
	ID        int64 `rx:"id,auto"`
}

func NewUsers(u ...Users) rx.SqlxModel[Users] {
	return rx.NewRx[Users](u...)
}

type Groups struct {
	Name      string
	ChangedBy sql.NullInt64
	ID        int64 `rx:"id,auto"`
}

// Stollen from sqlx_test.go.
func multiExec(e sqlx.Execer, query string) {
	stmts := strings.Split(query, ";\n")
	if len(strings.Trim(stmts[len(stmts)-1], " \n\t\r")) == 0 {
		stmts = stmts[:len(stmts)-1]
	}
	for _, s := range stmts {
		_, err := e.Exec(s)
		if err != nil {
			fmt.Println(err, s)
		}
	}
}

func init() {
	rx.Logger.SetLevel(log.WARN)
	multiExec(rx.DB(), schema)
}

type UserGroup struct {
	rx.Rx[UserGroup]
	UserID  int64
	GroupID int64

	Where whereParams `rx:"where,-"`
}
type whereParams struct{ GroupID int64 }

func main() {
	superAdmin := NewUsers()
	tx := superAdmin.Tx() // A new transaction just begun.
	defer func() { _ = tx.Rollback() }()
	admin, _ := superAdmin.Get(`login_name='superadmin'`)
	adminGroup := rx.NewRx(Groups{
		Name:      `MoreAdmins`,
		ChangedBy: sql.NullInt64{Int64: admin.ID, Valid: true}},
	).WithTx(tx)
	res, err := adminGroup.Insert()
	if err != nil {
		fmt.Println("adminGroup.Insert Error:", err.Error())
		return
	}
	gID, _ := res.LastInsertId()
	rx.NewRx(UserGroup{UserID: admin.ID, GroupID: gID}).WithTx(tx).Insert()
	if err := tx.Commit(); err != nil {
		fmt.Println(`tx.Commit Error`, err.Error())
		return
	}
	gr, _ := rx.NewRx[Groups]().Get(`id=:id`, rx.Map{`id`: gID})
	fmt.Printf(`new Group: %q`, gr.Name)
}
Output:
new Group: "MoreAdmins"

func (*Rx[R]) Update

func (m *Rx[R]) Update(fields []string, where string) (sql.Result, error)

Update constructs a Named UPDATE query, prepares it and executes it for each row of data in a transaction. It panics if there is no data to be updated.

We pass as bind parameters for each sqlx.NamedStmt.Exec each element of the slice of passed Rowx to NewRx or to Rx.SetData.

This is somehow problematic with named queries. What if we want to `SET group_id=1 WHERE group_id=2. How to differntiate between columns to be updated and parameters for the WHERE clause? We need different name for the bind parameter. Something like `:where.group_id` to hold the existing value in the database. Or maybe use a nested select statement in the WHERE clause to match the needed row for update by primary key column. A solution is to have a nested structure in the passed record, used only as parameters for the query. We can enrich our structure, representing the database record with a `Where` field which is a structure and holds the current values. Look in the tests for an example of updating such an enriched record. Also we can use for our columns types like sql.NullInt32 and such, provided by the sql package.

`fields` is the list of columns to be updated - used to construct the `SET col = :col...` part of the query. If a field starts with UppercaseLetter it is converted to snake_case.

For any case in which this method is not suitable, use directly sqlx.

Example
package main

import (
	"fmt"

	"github.com/kberov/rowx/rx"
	_ "github.com/mattn/go-sqlite3"
)

func main() {
	type whereBind struct{ GroupID uint32 }
	type UserGroup struct {
		rx.Rx[UserGroup]
		UserID  uint32
		GroupID uint32
		// Used only as bind parameters during UPDATE and maybe in other
		// queries. Must be a named struct, known at compile time!
		Where whereBind `rx:"where,-"` // - : Do not treat this field as column.
	}
	// rx.Rx can be embedded and used from within your record structure or
	// specialized type.
	ug := new(UserGroup)
	ugData := []UserGroup{
		UserGroup{UserID: 4, GroupID: 4},
		UserGroup{UserID: 5, GroupID: 5},
	}
	ug.SetData(ugData)
	_, e := ug.Insert()
	if e != nil {
		fmt.Println("Error inserting into user_group:", e.Error())
	}

	// Update one or many rows - move some user(5) to another group(4).
	ugDataUpd := []UserGroup{
		UserGroup{
			UserID: 5,
			// new value (to be updated in the database). Current value: 5
			GroupID: 4,
			Where: whereBind{
				// existing in the database value: 5
				GroupID: 5,
			},
		},
	}
	ug.SetData(ugDataUpd)
	//                    columns to be set                             the Where.GroupID field
	rs, err := ug.Update([]string{`group_id`}, `user_id=:user_id AND group_id=:where.group_id`)
	if err != nil {
		fmt.Println(err.Error())
	}
	affected, _ := rs.RowsAffected()
	fmt.Printf("RowsAffected: %d; err: %+v", affected, err)

}
Output:
RowsAffected: 1; err: <nil>

func (*Rx[R]) WithTx added in v0.83.0

func (m *Rx[R]) WithTx(queryer *sqlx.Tx) SqlxModel[R]

WithTx allows you to set an sqlx.Tx to be shared among several objects to execute several SQL statements in one transaction.

Example
package main

import (
	"crypto/sha256"
	"database/sql"
	"encoding/hex"
	"fmt"
	"strings"

	"github.com/jmoiron/sqlx"
	"github.com/labstack/gommon/log"

	"github.com/kberov/rowx/rx"
	_ "github.com/mattn/go-sqlite3"
)

var schema = `
PRAGMA foreign_keys = OFF;
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
login_name varchar(100) UNIQUE,
password varchar(64) NOT NULL UNIQUE,
group_id INTEGER DEFAULT NULL REFERENCES groups(id),
changed_by INTEGER DEFAULT NULL REFERENCES users(id) ON DELETE SET DEFAULT);

INSERT INTO users(id,group_id,changed_by,login_name,password) VALUES (0,0,0,'superadmin','qwerty');

CREATE TABLE groups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(100) UNIQUE NOT NULL,
changed_by INTEGER DEFAULT NULL REFERENCES users(id) ON DELETE SET DEFAULT);

INSERT INTO groups(id,name, changed_by) VALUES (0,'superadmin',0);
INSERT INTO groups(id,name, changed_by) VALUES (1,'admins',NULL);
INSERT INTO groups(id,name, changed_by) VALUES (2,'guests',NULL);
INSERT INTO groups(id,name, changed_by) VALUES (3,'editors',NULL);
INSERT INTO groups(id,name, changed_by) VALUES (4,'commenters',NULL);
CREATE TABLE user_group (
--  'ID of the user belonging to the group with group_id.'
  user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
--  'ID of the group to which the user with user_id belongs.'
  group_id INTEGER REFERENCES groups(id) ON DELETE CASCADE,
  PRIMARY KEY(user_id, group_id)
);
CREATE TABLE foo(
	bar INTEGER PRIMARY KEY AUTOINCREMENT,
	description VARCHAR(255) NOT NULL DEFAULT '',
	id VARCHAR(56) UNIQUE NOT NULL DEFAULT ''
);
PRAGMA foreign_keys = ON;
`

type Users struct {
	LoginName string
	Passwword string `rx:"password"`
	GroupID   sql.NullInt64
	ChangedBY sql.NullInt64
	ID        int64 `rx:"id,auto"`
}

type Groups struct {
	Name      string
	ChangedBy sql.NullInt64
	ID        int64 `rx:"id,auto"`
}

// Stollen from sqlx_test.go.
func multiExec(e sqlx.Execer, query string) {
	stmts := strings.Split(query, ";\n")
	if len(strings.Trim(stmts[len(stmts)-1], " \n\t\r")) == 0 {
		stmts = stmts[:len(stmts)-1]
	}
	for _, s := range stmts {
		_, err := e.Exec(s)
		if err != nil {
			fmt.Println(err, s)
		}
	}
}

func init() {
	rx.Logger.SetLevel(log.WARN)
	multiExec(rx.DB(), schema)
}

type UserGroup struct {
	rx.Rx[UserGroup]
	UserID  int64
	GroupID int64

	Where whereParams `rx:"where,-"`
}
type whereParams struct{ GroupID int64 }

func main() {
	superAdmin, _ := rx.NewRx[Users]().Get(`login_name='superadmin'`)
	superID := superAdmin.ID
	uname := `kberov`
	pswd := `123qwerty!`
	// This is how we usually begin a transaction!
	// We could have also started one automatically with tx := group.Tx().
	group := rx.NewRx(Groups{Name: uname}).WithTx(rx.DB().MustBegin())
	// The rollback will be ignored if tx has been committed already.
	defer func() { _ = group.Tx().Rollback() }()
	res, err := group.Insert()
	if err != nil {
		fmt.Println("group.Insert() Error:", err.Error())
	}
	groupID, err := res.LastInsertId()
	if err != nil {
		fmt.Println("group.LastInsertId Error:", err.Error())
	}
	passwd := hashPasswordWithSaltAndIterations(pswd, uname, groupID)
	user := rx.NewRx(Users{
		LoginName: `kberov`,
		Passwword: passwd,
		GroupID:   sql.NullInt64{groupID, true},
		ChangedBY: sql.NullInt64{superID, true},
		// Using the same transaction!
	}).WithTx(group.Tx())
	res, err = user.Insert()
	if err != nil {
		fmt.Println("user.Insert Error:", err.Error())
	}
	userID, err := res.LastInsertId()
	if err != nil {
		fmt.Println("user.LastInsertId Error:", err.Error())
	}
	res, err = rx.NewRx(UserGroup{
		UserID:  userID,
		GroupID: groupID,
		// Using the same transaction!
	}).WithTx(group.Tx()).Insert()
	if err != nil {
		fmt.Println("UserGroup.Insert Error:", err.Error())
	}
	// Commit the transaction. It is the same started with group.WithTx(...)
	if err = user.Tx().Commit(); err != nil {
		fmt.Println("Commit Error:", err.Error())
	}
	// Not using any transaction.
	if kberov, err := rx.NewRx[Users]().Get(`login_name='kberov'`); err == nil {
		fmt.Println(`Passwword:`, kberov.Passwword[:6])
	} else {
		fmt.Println("Users.Get Error:", err.Error())
	}
}

func hashPasswordWithSaltAndIterations(password, salt string, iterations int64) string {
	hash := salt + password
	for range iterations {
		hasher := sha256.New()
		hasher.Write([]byte(hash))
		hash = hex.EncodeToString(hasher.Sum(nil))
	}
	return hash
}
Output:
Passwword: 7fc19e

type SqlxDeleter added in v0.71.0

type SqlxDeleter[R Rowx] interface {
	Delete(where string, binData any) (sql.Result, error)
}

SqlxDeleter can be implemented to delete records from a table. It is fully implemented by Rx.

type SqlxGetter added in v0.71.0

type SqlxGetter[R Rowx] interface {
	/*
		Get expects a string to be used as where clause and optional bindata
		(struct or map[string]any).
	*/
	Get(where string, binData ...any) (*R, error)
}

SqlxGetter can be implemented to get one record from the database. It is fully implemented by Rx.

type SqlxInserter added in v0.71.0

type SqlxInserter[R Rowx] interface {
	/*
	   Insert inserts a set of Rowx instances (without their primary key values) and
	   returns [sql.Result] and [error]. The value for the autoincremented primary key
	   (usually ID column) is left to be set by the database.
	*/
	Insert() (sql.Result, error)
}

SqlxInserter can be implemented to insert records in a table. It is fully implemented by Rx.

type SqlxMeta added in v0.71.0

type SqlxMeta[R Rowx] interface {
	Table() string
	Columns() []string
}

SqlxMeta can be implemented to return the name of the table in the database for the implementing type and the slice with its column names. It is fully implemented by Rx.

If you implement this interface for a struct, its methods will be called by Rx everywhere where table name or a slice of columns are needed. You can even implement it partially, if you want to provide only the table name or only the column names to be used by Rx.

If you use the commandline tool `rowx`, it will generate for you structures for all tables in the database and these structs will implement SqlxMeta.

Example
package main

import (
	"fmt"

	"github.com/kberov/rowx/rx"
	_ "github.com/mattn/go-sqlite3"
)

// A custom type, which implements rx.SqlxMeta.
type U struct {
	table     string
	LoginName string
	ID        int32
}

func (u *U) Table() string {
	if u.table == "" {
		u.table = `users`
	}
	return u.table
}

var uColumns = []string{`id`, `login_name`}

func (u *U) Columns() []string {
	return uColumns
}

func main() {
	// A custom type, which implements rx.SqlxMeta[U].
	/*
	   type U struct {
	   	table     string
	   	LoginName string
	   	ID        int32 `rx:"id,auto"`
	   }
	   func (u *U) Table() string {
	   	if u.table == "" {
	   		u.table = `users`
	   	}
	   	return u.table
	   }
	   func (u *U) Columns() []string {
	   	return []string{`id`, `login_name`}
	   }
	*/
	m := rx.NewRx[U]()
	u, e := m.Get(`id=:id`, U{ID: 1})
	if e != nil {
		fmt.Println("Error:", e.Error())
	}
	fmt.Printf("ID: %d, LoginName: %s", u.ID, u.LoginName)
}
Output:
ID: 1, LoginName: first

type SqlxModel

type SqlxModel[R Rowx] interface {
	Data() []R
	SetData(data []R) (rx SqlxModel[R])
	SqlxDeleter[R]
	SqlxGetter[R]
	SqlxInserter[R]
	SqlxMeta[R]
	SqlxSelector[R]
	SqlxUpdater[R]
	Tx() *sqlx.Tx
	WithTx(queryer *sqlx.Tx) SqlxModel[R]
}

SqlxModel is an interface and generic constraint for working with a set of database records. Rx fully implements SqlxModel. You can embed (extend) Rx to get automatically its implementation and override some of its methods.

func NewRx

func NewRx[R Rowx](rows ...R) SqlxModel[R]

NewRx returns a new instance of a table model with optionally provided data rows as a variadic parameter. Providing the specific type parameter to instantiate is mandatory if a variadic parameter is not passed.

Example
package main

import (
	"database/sql"
	"fmt"

	"github.com/kberov/rowx/rx"
	_ "github.com/mattn/go-sqlite3"
)

type Users struct {
	LoginName string
	Passwword string `rx:"password"`
	GroupID   sql.NullInt64
	ChangedBY sql.NullInt64
	ID        int64 `rx:"id,auto"`
}

func main() {
	// If no Rowx are passed, NewRx needs a type parameter to know
	// which type to instantiate for subsequent call to Select(...) or Delete(...)....
	m := rx.NewRx[Users]()
	fmt.Printf(" %#T\n", m)
}
Output:
*rx.Rx[github.com/kberov/rowx/rx_test.Users]
Example (With_param)
package main

import (
	"database/sql"
	"fmt"

	"github.com/kberov/rowx/rx"
	_ "github.com/mattn/go-sqlite3"
)

type Users struct {
	LoginName string
	Passwword string `rx:"password"`
	GroupID   sql.NullInt64
	ChangedBY sql.NullInt64
	ID        int64 `rx:"id,auto"`
}

var users = []Users{
	Users{LoginName: "first", ChangedBY: sql.NullInt64{0, false}, Passwword: `a`},
	Users{LoginName: "the_second", ChangedBY: sql.NullInt64{1, true}, Passwword: `b`},
	Users{LoginName: "the_third", ChangedBY: sql.NullInt64{1, true}, Passwword: `c`},
}

func main() {
	// To Inser(...)  Update(...) []Users in the database, no type parameter is
	// needed.
	m := rx.NewRx(users...)
	last := m.Data()[len(m.Data())-1]
	fmt.Printf("Last user: %s", last.LoginName)
}
Output:
Last user: the_third

type SqlxSelector added in v0.71.0

type SqlxSelector[R Rowx] interface {
	Select(where string, binData any, limitAndOffset ...int) ([]R, error)
}

SqlxSelector can be implemented to select records from a table or view. It is fully implemented by Rx.

type SqlxUpdater added in v0.71.0

type SqlxUpdater[R Rowx] interface {
	Update(fields []string, where string) (sql.Result, error)
}

SqlxUpdater can be implemented to update records in a table. It is fully implemented by Rx.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL