v4 without errors

This commit is contained in:
adlerhurst
2025-04-30 09:30:48 +02:00
parent 597781b389
commit 050aa7dd48
11 changed files with 314 additions and 130 deletions

View File

@@ -50,6 +50,17 @@ var _ Change = Changes(nil)
var _ Change = (*change[string])(nil)
type Columns []Column
func (m Columns) writeTo(builder *statementBuilder) {
for i, col := range m {
if i > 0 {
builder.WriteString(", ")
}
col.writeTo(builder)
}
}
type Column interface {
writeTo(builder *statementBuilder)
}

View File

@@ -59,6 +59,8 @@ CREATE TABLE users (
-- , CONSTRAINT fk_instances FOREIGN KEY (instance_id) REFERENCES instances(id)
) INHERITS (org_objects);
CREATE INDEX idx_users_username ON users(username);
CREATE TRIGGER set_updated_at
BEFORE UPDATE
ON users
@@ -74,6 +76,8 @@ CREATE TABLE human_users(
, CONSTRAINT fk_instances FOREIGN KEY (instance_id) REFERENCES instances(id)
) INHERITS (users);
CREATE INDEX idx_human_users_username ON human_users(username);
CREATE TRIGGER set_updated_at
BEFORE UPDATE
ON human_users
@@ -88,23 +92,15 @@ CREATE TABLE machine_users(
, CONSTRAINT fk_instances FOREIGN KEY (instance_id) REFERENCES instances(id)
) INHERITS (users);
CREATE INDEX idx_machine_users_username ON machine_users(username);
CREATE TRIGGER set_updated_at
BEFORE UPDATE
ON machine_users
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
select u.*, hu.first_name, hu.last_name, mu.description from users u
left join human_users hu on u.instance_id = hu.instance_id and u.org_id = hu.org_id and u.id = hu.id
left join machine_users mu on u.instance_id = mu.instance_id and u.org_id = mu.org_id and u.id = mu.id
-- where
-- u.instance_id = 1
-- and u.org_id = 3
-- and u.id = 7
;
create view users_view as (
CREATE VIEW users_view AS (
SELECT
id
, created_at
@@ -113,27 +109,16 @@ SELECT
, instance_id
, org_id
, username
, first_name
, last_name
, description
FROM (
(SELECT
id
, created_at
, updated_at
, deleted_at
, instance_id
, org_id
, username
, tableoid::regclass::TEXT AS type
, first_name
, last_name
, NULL AS description
FROM
human_users)
human_users
UNION
(SELECT
SELECT
id
, created_at
, updated_at
@@ -141,9 +126,10 @@ UNION
, instance_id
, org_id
, username
, tableoid::regclass::TEXT AS type
, NULL AS first_name
, NULL AS last_name
, description
FROM
machine_users)
));
machine_users
);

View File

@@ -0,0 +1,66 @@
package v4
type queryOpts struct {
condition Condition
orderBy Columns
limit uint32
offset uint32
}
func (opts *queryOpts) writeCondition(builder *statementBuilder) {
if opts.condition == nil {
return
}
builder.WriteString(" WHERE ")
opts.condition.writeTo(builder)
}
func (opts *queryOpts) writeOrderBy(builder *statementBuilder) {
if len(opts.orderBy) == 0 {
return
}
builder.WriteString(" ORDER BY ")
opts.orderBy.writeTo(builder)
}
func (opts *queryOpts) writeLimit(builder *statementBuilder) {
if opts.limit == 0 {
return
}
builder.WriteString(" LIMIT ")
builder.writeArg(opts.limit)
}
func (opts *queryOpts) writeOffset(builder *statementBuilder) {
if opts.offset == 0 {
return
}
builder.WriteString(" OFFSET ")
builder.writeArg(opts.offset)
}
type QueryOption func(*queryOpts)
func WithCondition(condition Condition) QueryOption {
return func(opts *queryOpts) {
opts.condition = condition
}
}
func WithOrderBy(orderBy ...Column) QueryOption {
return func(opts *queryOpts) {
opts.orderBy = orderBy
}
}
func WithLimit(limit uint32) QueryOption {
return func(opts *queryOpts) {
opts.limit = limit
}
}
func WithOffset(offset uint32) QueryOption {
return func(opts *queryOpts) {
opts.offset = offset
}
}

View File

@@ -29,17 +29,13 @@ type userTrait interface {
Type() UserType
}
const userQuery = `SELECT u.instance_id, u.org_id, u.id, u.username, u.type, u.created_at, u.updated_at, u.deleted_at,` +
` h.first_name, h.last_name, h.email_address, h.email_verified_at, h.phone_number, h.phone_verified_at, m.description` +
` FROM users u` +
` LEFT JOIN user_humans h ON u.instance_id = h.instance_id AND u.org_id = h.org_id AND u.id = h.id` +
` LEFT JOIN user_machines m ON u.instance_id = m.instance_id AND u.org_id = m.org_id AND u.id = m.id`
const queryUserStmt = `SELECT instance_id, org_id, id, username, type, created_at, updated_at, deleted_at,` +
` first_name, last_name, email_address, email_verified_at, phone_number, phone_verified_at, description` +
` FROM users_view`
type user struct {
builder statementBuilder
client database.QueryExecutor
condition Condition
}
func UserRepository(client database.QueryExecutor) *user {
@@ -48,20 +44,17 @@ func UserRepository(client database.QueryExecutor) *user {
}
}
func (u *user) WithCondition(condition Condition) *user {
u.condition = condition
return u
}
func (u *user) List(ctx context.Context, opts ...QueryOption) (users []*User, err error) {
options := new(queryOpts)
for _, opt := range opts {
opt(options)
}
func (u *user) Get(ctx context.Context) (*User, error) {
u.builder.WriteString(userQuery)
u.writeCondition()
return scanUser(u.client.QueryRow(ctx, u.builder.String(), u.builder.args...))
}
func (u *user) List(ctx context.Context) (users []*User, err error) {
u.builder.WriteString(userQuery)
u.writeCondition()
u.builder.WriteString(queryUserStmt)
options.writeCondition(&u.builder)
options.writeOrderBy(&u.builder)
options.writeLimit(&u.builder)
options.writeOffset(&u.builder)
rows, err := u.client.Query(ctx, u.builder.String(), u.builder.args...)
if err != nil {
@@ -87,7 +80,23 @@ func (u *user) List(ctx context.Context) (users []*User, err error) {
return users, nil
}
func (u *user) Get(ctx context.Context, opts ...QueryOption) (*User, error) {
options := new(queryOpts)
for _, opt := range opts {
opt(options)
}
u.builder.WriteString(queryUserStmt)
options.writeCondition(&u.builder)
options.writeOrderBy(&u.builder)
options.writeLimit(&u.builder)
options.writeOffset(&u.builder)
return scanUser(u.client.QueryRow(ctx, u.builder.String(), u.builder.args...))
}
const (
// TODO: change to separate statements and tables
createUserCte = `WITH user AS (` +
`INSERT INTO users (instance_id, org_id, id, username, type) VALUES ($1, $2, $3, $4, $5)` +
` RETURNING *)`
@@ -111,11 +120,24 @@ func (u *user) Create(ctx context.Context, user *User) error {
u.builder.WriteString(createMachineStmt)
u.builder.appendArgs(trait.Description)
}
return u.client.QueryRow(ctx, u.builder.String(), u.builder.args...).Scan(user.CreatedAt, user.UpdatedAt)
return u.client.QueryRow(ctx, u.builder.String(), u.builder.args...).Scan(&user.Dates.CreatedAt, &user.Dates.UpdatedAt)
}
func (u *user) Update(ctx context.Context, condition Condition, changes ...Change) error {
u.builder.WriteString("UPDATE users SET ")
Changes(changes).writeTo(&u.builder)
u.writeCondition(condition)
return u.client.Exec(ctx, u.builder.String(), u.builder.args...)
}
func (u *user) Delete(ctx context.Context, condition Condition) error {
u.builder.WriteString("DELETE FROM users")
u.writeCondition(condition)
return u.client.Exec(ctx, u.builder.String(), u.builder.args...)
}
func (u *user) InstanceIDColumn() Column {
return column{name: "u.instance_id"}
return column{name: "instance_id"}
}
func (u *user) InstanceIDCondition(instanceID string) Condition {
@@ -123,7 +145,7 @@ func (u *user) InstanceIDCondition(instanceID string) Condition {
}
func (u *user) OrgIDColumn() Column {
return column{name: "u.org_id"}
return column{name: "org_id"}
}
func (u *user) OrgIDCondition(orgID string) Condition {
@@ -131,7 +153,7 @@ func (u *user) OrgIDCondition(orgID string) Condition {
}
func (u *user) IDColumn() Column {
return column{name: "u.id"}
return column{name: "id"}
}
func (u *user) IDCondition(userID string) Condition {
@@ -140,7 +162,7 @@ func (u *user) IDCondition(userID string) Condition {
func (u *user) UsernameColumn() Column {
return ignoreCaseCol{
column: column{name: "u.username"},
column: column{name: "username"},
suffix: "_lower",
}
}
@@ -154,7 +176,7 @@ func (u *user) UsernameCondition(op TextOperator, username string) Condition {
}
func (u *user) CreatedAtColumn() Column {
return column{name: "u.created_at"}
return column{name: "created_at"}
}
func (u *user) CreatedAtCondition(op NumberOperator, createdAt time.Time) Condition {
@@ -162,7 +184,7 @@ func (u *user) CreatedAtCondition(op NumberOperator, createdAt time.Time) Condit
}
func (u *user) UpdatedAtColumn() Column {
return column{name: "u.updated_at"}
return column{name: "updated_at"}
}
func (u *user) UpdatedAtCondition(op NumberOperator, updatedAt time.Time) Condition {
@@ -170,7 +192,7 @@ func (u *user) UpdatedAtCondition(op NumberOperator, updatedAt time.Time) Condit
}
func (u *user) DeletedAtColumn() Column {
return column{name: "u.deleted_at"}
return column{name: "deleted_at"}
}
func (u *user) DeletedCondition(isDeleted bool) Condition {
@@ -184,12 +206,24 @@ func (u *user) DeletedAtCondition(op NumberOperator, deletedAt time.Time) Condit
return newNumberCondition(u.DeletedAtColumn(), op, deletedAt)
}
func (u *user) writeCondition() {
if u.condition == nil {
func (u *user) writeCondition(condition Condition) {
if condition == nil {
return
}
u.builder.WriteString(" WHERE ")
u.condition.writeTo(&u.builder)
condition.writeTo(&u.builder)
}
func (u user) columns() Columns {
return Columns{
u.InstanceIDColumn(),
u.OrgIDColumn(),
u.IDColumn(),
u.UsernameColumn(),
u.CreatedAtColumn(),
u.UpdatedAtColumn(),
u.DeletedAtColumn(),
}
}
func scanUser(scanner database.Scanner) (*User, error) {

View File

@@ -46,11 +46,11 @@ func (u *user) Human() *userHuman {
const userEmailQuery = `SELECT h.email_address, h.email_verified_at FROM user_humans h`
func (u *userHuman) GetEmail(ctx context.Context) (*Email, error) {
func (u *userHuman) GetEmail(ctx context.Context, condition Condition) (*Email, error) {
var email Email
u.builder.WriteString(userEmailQuery)
u.writeCondition()
u.writeCondition(condition)
err := u.client.QueryRow(ctx, u.builder.String(), u.builder.args...).Scan(
&email.Address,
@@ -63,10 +63,10 @@ func (u *userHuman) GetEmail(ctx context.Context) (*Email, error) {
return &email, nil
}
func (h userHuman) Update(ctx context.Context, changes ...Change) error {
h.builder.WriteString(`UPDATE human_users h SET `)
func (h userHuman) Update(ctx context.Context, condition Condition, changes ...Change) error {
h.builder.WriteString(`UPDATE human_users SET `)
Changes(changes).writeTo(&h.builder)
h.writeCondition()
h.writeCondition(condition)
stmt := h.builder.String()
@@ -78,7 +78,7 @@ func (h userHuman) SetFirstName(firstName string) Change {
}
func (h userHuman) FirstNameColumn() Column {
return column{"h.first_name"}
return column{"first_name"}
}
func (h userHuman) FirstNameCondition(op TextOperator, firstName string) Condition {
@@ -90,7 +90,7 @@ func (h userHuman) SetLastName(lastName string) Change {
}
func (h userHuman) LastNameColumn() Column {
return column{"h.last_name"}
return column{"last_name"}
}
func (h userHuman) LastNameCondition(op TextOperator, lastName string) Condition {
@@ -99,7 +99,7 @@ func (h userHuman) LastNameCondition(op TextOperator, lastName string) Condition
func (h userHuman) EmailAddressColumn() Column {
return ignoreCaseCol{
column: column{"h.email_address"},
column: column{"email_address"},
suffix: "_lower",
}
}
@@ -109,7 +109,7 @@ func (h userHuman) EmailAddressCondition(op TextOperator, email string) Conditio
}
func (h userHuman) EmailVerifiedAtColumn() Column {
return column{"h.email_verified_at"}
return column{"email_verified_at"}
}
func (h *userHuman) EmailAddressVerifiedCondition(isVerified bool) Condition {
@@ -144,7 +144,7 @@ func (h userHuman) SetEmail(address string, verified *time.Time) Change {
}
func (h userHuman) PhoneNumberColumn() Column {
return column{"h.phone_number"}
return column{"phone_number"}
}
func (h userHuman) SetPhoneNumber(number string) Change {
@@ -156,7 +156,7 @@ func (h userHuman) PhoneNumberCondition(op TextOperator, phoneNumber string) Con
}
func (h userHuman) PhoneVerifiedAtColumn() Column {
return column{"h.phone_verified_at"}
return column{"phone_verified_at"}
}
func (h userHuman) PhoneNumberVerifiedCondition(isVerified bool) Condition {
@@ -185,3 +185,19 @@ func (h userHuman) SetPhone(number string, verifiedAt *time.Time) Change {
newUpdatePtrColumn(h.PhoneVerifiedAtColumn(), verifiedAt),
)
}
func (h userHuman) columns() Columns {
return append(h.user.columns(),
h.FirstNameColumn(),
h.LastNameColumn(),
h.EmailAddressColumn(),
h.EmailVerifiedAtColumn(),
h.PhoneNumberColumn(),
h.PhoneVerifiedAtColumn(),
)
}
func (h userHuman) writeReturning(builder *statementBuilder) {
builder.WriteString(" RETURNING ")
h.columns().writeTo(builder)
}

View File

@@ -24,12 +24,34 @@ func (u *user) Machine() *userMachine {
return &userMachine{user: u}
}
func (m userMachine) Update(ctx context.Context, cols ...Change) (*Machine, error) {
return nil, nil
func (m userMachine) Update(ctx context.Context, condition Condition, changes ...Change) ([]*Machine, error) {
m.builder.WriteString("UPDATE user_machines SET ")
Changes(changes).writeTo(&m.builder)
m.writeCondition(condition)
m.writeReturning()
var machines []*Machine
rows, err := m.client.Query(ctx, m.builder.String(), m.builder.args...)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
machine := new(Machine)
if err := rows.Scan(&machine.Description); err != nil {
return nil, err
}
machines = append(machines, machine)
}
if err := rows.Err(); err != nil {
return nil, err
}
return machines, nil
}
func (userMachine) DescriptionColumn() Column {
return column{"m.description"}
return column{"description"}
}
func (m userMachine) SetDescription(description string) Change {
@@ -39,3 +61,12 @@ func (m userMachine) SetDescription(description string) Change {
func (m userMachine) DescriptionCondition(op TextOperator, description string) Condition {
return newTextCondition(m.DescriptionColumn(), op, description)
}
func (m userMachine) columns() Columns {
return append(m.user.columns(), m.DescriptionColumn())
}
func (m *userMachine) writeReturning() {
m.builder.WriteString(" RETURNING ")
m.columns().writeTo(&m.builder)
}

View File

@@ -11,32 +11,38 @@ import (
func TestQueryUser(t *testing.T) {
t.Run("User filters", func(t *testing.T) {
user := v4.UserRepository(nil)
user.WithCondition(
v4.And(
v4.Or(
user.IDCondition("test"),
user.IDCondition("2"),
u, err := user.Get(context.Background(),
v4.WithCondition(
v4.And(
v4.Or(
user.IDCondition("test"),
user.IDCondition("2"),
),
user.UsernameCondition(v4.TextOperatorStartsWithIgnoreCase, "test"),
),
user.UsernameCondition(v4.TextOperatorStartsWithIgnoreCase, "test"),
),
).Get(context.Background())
v4.WithOrderBy(user.CreatedAtColumn()),
)
assert.NoError(t, err)
assert.NotNil(t, u)
})
t.Run("machine and human filters", func(t *testing.T) {
user := v4.UserRepository(nil)
machine := user.Machine()
human := user.Human()
user.WithCondition(
v4.And(
user.UsernameCondition(v4.TextOperatorStartsWithIgnoreCase, "test"),
v4.Or(
machine.DescriptionCondition(v4.TextOperatorStartsWithIgnoreCase, "test"),
human.EmailAddressVerifiedCondition(true),
v4.IsNotNull(machine.DescriptionColumn()),
),
email, err := human.GetEmail(context.Background(), v4.And(
user.UsernameCondition(v4.TextOperatorStartsWithIgnoreCase, "test"),
v4.Or(
machine.DescriptionCondition(v4.TextOperatorStartsWithIgnoreCase, "test"),
human.EmailAddressVerifiedCondition(true),
v4.IsNotNull(machine.DescriptionColumn()),
),
)
human.GetEmail(context.Background())
))
assert.NoError(t, err)
assert.NotNil(t, email)
})
}
@@ -56,10 +62,6 @@ func TestArg(t *testing.T) {
func TestWriteUser(t *testing.T) {
t.Run("update user", func(t *testing.T) {
user := v4.UserRepository(nil)
user.WithCondition(user.IDCondition("test")).Human().Update(
context.Background(),
user.SetUsername("test"),
)
user.Update(context.Background(), user.IDCondition("test"), user.SetUsername("test"))
})
}