Skip to content

Commit 279942d

Browse files
committed
Extend functional query builders
1 parent 2a133da commit 279942d

File tree

1 file changed

+201
-92
lines changed

1 file changed

+201
-92
lines changed

database/optionally.go

Lines changed: 201 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -2,134 +2,243 @@ package database
22

33
import (
44
"context"
5-
"fmt"
65
"github.com/icinga/icinga-go-library/com"
76
"github.com/pkg/errors"
87
)
98

10-
// Upsert inserts new rows into a table or updates rows of a table if the primary key already exists.
11-
type Upsert interface {
12-
// Stream bulk upserts the specified entities via NamedBulkExec.
13-
// If not explicitly specified, the upsert statement is created using
14-
// BuildUpsertStmt with the first entity from the entities stream.
15-
Stream(ctx context.Context, entities <-chan Entity) error
16-
}
9+
// QueryType represents the type of database query, expressed as an enum-like integer value.
10+
type QueryType int
1711

18-
// UpsertOption is a functional option for NewUpsert.
19-
type UpsertOption func(u *upsert)
12+
const (
13+
// SelectQuery represents a SQL SELECT query type, used for retrieving data from a database.
14+
SelectQuery QueryType = iota
2015

21-
// WithOnUpsert adds callback(s) to bulk upserts. Entities for which the
22-
// operation was performed successfully are passed to the callbacks.
23-
func WithOnUpsert(onUpsert ...OnSuccess[Entity]) UpsertOption {
24-
return func(u *upsert) {
25-
u.onUpsert = onUpsert
26-
}
16+
// InsertQuery represents the constant value for an INSERT database query.
17+
InsertQuery
18+
19+
// UpsertQuery represents the constant value used for an UPSERT (INSERT or UPDATE) database query.
20+
UpsertQuery
21+
22+
// UpdateQuery represents the constant value for an UPDATE database query.
23+
UpdateQuery
24+
25+
// DeleteQuery represents the constant value for a DELETE query.
26+
DeleteQuery
27+
)
28+
29+
// Queryable defines methods for bulk executing database entities such as upsert, insert, and update.
30+
type Queryable interface {
31+
// Stream bulk executes database Entity(ies) for the following three database query types.
32+
// * Upsert - Stream consumes from the provided entities channel and bulk upserts them via DB.NamedBulkExec.
33+
// If not explicitly specified via WithStatement, the upsert statement is generated dynamically via the
34+
// QueryBuilder. The bulk size is controlled via Options.MaxPlaceholdersPerStatement and concurrency
35+
// via the Options.MaxConnectionsPerTable.
36+
// * Insert(Ignore) - Stream does likewise for insert statement and bulk inserts the entities via DB.NamedBulkExec.
37+
// If not explicitly specified via WithStatement, the insert statement is generated dynamically via the
38+
// QueryBuilder. The bulk size is controlled via Options.MaxPlaceholdersPerStatement and concurrency
39+
// via the Options.MaxConnectionsPerTable.
40+
// * Update - Stream bulk updates the entities via DB.NamedBulkExecTx. If not explicitly specified via
41+
// WithStatement, the update statement is generated dynamically via the QueryBuilder. The bulk size is
42+
// controlled via Options.MaxRowsPerTransaction and concurrency via the Options.MaxConnectionsPerTable.
43+
// Entities for which the query ran successfully will be passed to the onSuccess handlers (if provided).
44+
Stream(ctx context.Context, entities <-chan Entity, onSuccess ...OnSuccess[Entity]) error
45+
46+
// StreamAny bulk executes the streamed items of type any using the [DB.BulkExec] method.
47+
StreamAny(ctx context.Context, args <-chan any, onSuccess ...OnSuccess[any]) error
2748
}
2849

29-
// WithStatement uses the specified statement for bulk upserts instead of automatically creating one.
30-
func WithStatement(stmt string, placeholders int) UpsertOption {
31-
return func(u *upsert) {
32-
u.stmt = stmt
33-
u.placeholders = placeholders
34-
}
50+
// NewSelect initializes a new Queryable object of type SelectQuery for a given [DB], subject.
51+
func NewSelect(db *DB, subject any, options ...QueryableOption) Queryable {
52+
return newQuery(db, subject, append([]QueryableOption{withSetQueryType(SelectQuery)}, options...)...)
3553
}
3654

37-
// NewUpsert creates a new Upsert initalized with a database.
38-
func NewUpsert(db *DB, options ...UpsertOption) Upsert {
39-
u := &upsert{db: db}
55+
// NewInsert initializes a new Queryable object of type InsertQuery for a given [DB], subject.
56+
func NewInsert(db *DB, subject any, options ...QueryableOption) Queryable {
57+
return newQuery(db, subject, append([]QueryableOption{withSetQueryType(InsertQuery)}, options...)...)
58+
}
4059

41-
for _, option := range options {
42-
option(u)
43-
}
60+
// NewUpsert initializes a new Queryable object of type UpsertQuery for a given [DB], subject.
61+
func NewUpsert(db *DB, subject any, options ...QueryableOption) Queryable {
62+
return newQuery(db, subject, append([]QueryableOption{withSetQueryType(UpsertQuery)}, options...)...)
63+
}
4464

45-
return u
65+
// NewUpdate initializes a new Queryable object of type UpdateQuery for a given [DB], subject.
66+
func NewUpdate(db *DB, subject any, options ...QueryableOption) Queryable {
67+
return newQuery(db, subject, append([]QueryableOption{withSetQueryType(UpdateQuery)}, options...)...)
4668
}
4769

48-
type upsert struct {
49-
db *DB
50-
onUpsert []OnSuccess[Entity]
51-
stmt string
52-
placeholders int
70+
// NewDelete initializes a new Queryable object of type DeleteQuery for a given [DB], subject.
71+
func NewDelete(db *DB, subject any, options ...QueryableOption) Queryable {
72+
return newQuery(db, subject, append([]QueryableOption{withSetQueryType(DeleteQuery)}, options...)...)
5373
}
5474

55-
func (u *upsert) Stream(ctx context.Context, entities <-chan Entity) error {
56-
first, forward, err := com.CopyFirst(ctx, entities)
57-
if err != nil {
58-
return errors.Wrap(err, "can't copy first entity")
59-
}
75+
// queryable represents a database query type with customizable behavior for dynamic and static SQL statements.
76+
type queryable struct {
77+
db *DB
6078

61-
sem := u.db.GetSemaphoreForTable(TableName(first))
62-
var stmt string
63-
var placeholders int
79+
// qb is the query builder used to construct SQL statements for various database
80+
// statements if, and only if stmt is not set.
81+
qb *QueryBuilder
6482

65-
if u.stmt != "" {
66-
stmt = u.stmt
67-
placeholders = u.placeholders
68-
} else {
69-
stmt, placeholders = u.db.BuildUpsertStmt(first)
70-
}
83+
// qtype defines the type of database query (e.g., SELECT, INSERT) to perform, influencing query construction behavior.
84+
qtype QueryType
85+
86+
// scoper is used to dynamically generate scoped database queries if, and only if stmt is not set.
87+
scoper any
88+
89+
// stmt is used to cache statically provided database statements.
90+
stmt string
7191

72-
return u.db.NamedBulkExec(
73-
ctx, stmt, u.db.BatchSizeByPlaceholders(placeholders), sem,
74-
forward, SplitOnDupId[Entity], u.onUpsert...,
75-
)
92+
// placeholders is used to determine the entities bulk/chunk size for statically provided statements.
93+
placeholders int
94+
95+
// ignoreOnError is only used to generate special insert statements that silently suppress duplicate key errors.
96+
ignoreOnError bool
7697
}
7798

78-
// Delete deletes rows of a table.
79-
type Delete interface {
80-
// Stream bulk deletes rows from the table specified in from using the given args stream via BulkExec.
81-
// Unless explicitly specified, the DELETE statement is created using BuildDeleteStmt.
82-
Stream(ctx context.Context, from any, args <-chan any) error
99+
// Assert that *queryable type satisfies the Queryable interface.
100+
var _ Queryable = (*queryable)(nil)
101+
102+
// Stream implements the [Queryable.Stream] method.
103+
func (q *queryable) Stream(ctx context.Context, entities <-chan Entity, onSuccess ...OnSuccess[Entity]) error {
104+
sem := q.db.GetSemaphoreForTable(TableName(q.qb.subject))
105+
stmt, placeholders := q.buildStmt()
106+
batchSize := q.db.BatchSizeByPlaceholders(placeholders)
107+
108+
switch q.qtype {
109+
case SelectQuery: // TODO: support select statements?
110+
case InsertQuery:
111+
return q.db.NamedBulkExec(ctx, stmt, batchSize, sem, entities, com.NeverSplit[Entity], onSuccess...)
112+
case UpsertQuery:
113+
return q.db.NamedBulkExec(ctx, stmt, batchSize, sem, entities, SplitOnDupId[Entity], onSuccess...)
114+
case UpdateQuery:
115+
return q.db.NamedBulkExecTx(ctx, stmt, q.db.Options.MaxRowsPerTransaction, sem, entities)
116+
case DeleteQuery:
117+
return errors.Errorf("can't stream entities for 'DELETE' query")
118+
}
119+
120+
return errors.Errorf("unsupported query type: %v", q.qtype)
83121
}
84122

85-
// DeleteOption is a functional option for NewDelete.
86-
type DeleteOption func(options *delete)
123+
// StreamAny implements the [Queryable.StreamAny] method.
124+
func (q *queryable) StreamAny(ctx context.Context, args <-chan any, onSuccess ...OnSuccess[any]) error {
125+
stmt, _ := q.buildStmt()
126+
sem := q.db.GetSemaphoreForTable(TableName(q.qb.subject))
87127

88-
// WithOnDelete adds callback(s) to bulk deletes. Arguments for which the
89-
// operation was performed successfully are passed to the callbacks.
90-
func WithOnDelete(onDelete ...OnSuccess[any]) DeleteOption {
91-
return func(d *delete) {
92-
d.onDelete = onDelete
93-
}
128+
return q.db.BulkExec(ctx, stmt, q.db.Options.MaxPlaceholdersPerStatement, sem, args, onSuccess...)
94129
}
95130

96-
// ByColumn uses the given column for the WHERE clause that the rows must
97-
// satisfy in order to be deleted, instead of automatically using ID.
98-
func ByColumn(column string) DeleteOption {
99-
return func(d *delete) {
100-
d.column = column
131+
// buildStmt constructs the SQL statement based on the type of query (Select, Insert, Upsert, Update, Delete).
132+
// It also determines the number of placeholders to be used in the statement.
133+
func (q *queryable) buildStmt() (string, int) {
134+
if q.stmt != "" {
135+
return q.stmt, q.placeholders
101136
}
102-
}
103137

104-
// NewDelete creates a new Delete initalized with a database.
105-
func NewDelete(db *DB, options ...DeleteOption) Delete {
106-
d := &delete{db: db}
138+
var stmt string
139+
var placeholders int
107140

108-
for _, option := range options {
109-
option(d)
141+
switch q.qtype {
142+
case SelectQuery: // TODO: support select statements?
143+
case InsertQuery:
144+
if q.ignoreOnError {
145+
stmt, placeholders = q.qb.InsertIgnore(q.db)
146+
} else {
147+
stmt, placeholders = q.qb.Insert(q.db)
148+
}
149+
case UpsertQuery:
150+
if q.stmt != "" {
151+
stmt, placeholders = q.stmt, q.placeholders
152+
} else {
153+
stmt, placeholders = q.qb.Upsert(q.db)
154+
}
155+
case UpdateQuery:
156+
stmt = q.stmt
157+
if stmt == "" {
158+
if q.scoper != nil && q.scoper.(string) != "" {
159+
stmt, _ = q.qb.UpdateScoped(q.db, q.scoper)
160+
} else {
161+
stmt, _ = q.qb.Update(q.db)
162+
}
163+
}
164+
case DeleteQuery:
165+
if q.stmt != "" {
166+
stmt, placeholders = q.stmt, q.placeholders
167+
} else if q.scoper != "" {
168+
stmt = q.qb.DeleteBy(q.scoper.(string))
169+
} else {
170+
stmt = q.qb.Delete()
171+
}
110172
}
111173

112-
return d
174+
return stmt, placeholders
113175
}
114176

115-
type delete struct {
116-
db *DB
117-
column string
118-
onDelete []OnSuccess[any]
119-
}
177+
// newQuery initializes a new Queryable object for a given [DB], subject, and query type.
178+
// It also applies optional query options to the just created queryable object.
179+
//
180+
// Note: If the query type is not explicitly set using WithSetQueryType, it will default to SELECT queries.
181+
func newQuery(db *DB, subject any, options ...QueryableOption) Queryable {
182+
q := &queryable{db: db, qb: &QueryBuilder{subject: subject}}
183+
for _, option := range options {
184+
option(q)
185+
}
120186

121-
func (d *delete) Stream(ctx context.Context, from any, args <-chan any) error {
122-
var stmt string
187+
return q
188+
}
123189

124-
if d.column != "" {
125-
stmt = fmt.Sprintf(`DELETE FROM "%s" WHERE %s IN (?)`, TableName(from), d.column)
126-
} else {
127-
stmt = d.db.BuildDeleteStmt(from)
190+
// QueryableOption describes the base functional specification for all the queryable types.
191+
type QueryableOption func(*queryable)
192+
193+
// withSetQueryType sets the type of database query to be executed/generated.
194+
func withSetQueryType(qtype QueryType) QueryableOption { return func(q *queryable) { q.qtype = qtype } }
195+
196+
// WithStatement configures a static SQL statement and its associated placeholders for a queryable entity.
197+
//
198+
// Note that using WithStatement always suppresses all other available queryable options and unlike
199+
// some other options, this can be used to explicitly provide a custom query for all kinds of DB stmts.
200+
//
201+
// Returns a function that lazily modifies a given queryable type by setting its stmt and placeholders fields.
202+
func WithStatement(stmt string, placeholders int) QueryableOption {
203+
return func(q *queryable) {
204+
q.stmt = stmt
205+
q.placeholders = placeholders
128206
}
207+
}
208+
209+
// WithColumns statically configures the DB columns to be used for building the database statements.
210+
//
211+
// Setting the queryable columns while using WithStatement has no behavioural effects, thus these columns are never
212+
// used. Additionally, for upsert statements, WithColumns not only defines the columns to be actually inserted but
213+
// the columns to be updated when a duplicate key error occurs as well. However, to maintain the compatibility with
214+
// legacy implementations, a query subject that implements the Upserter interface takes a higher precedence over
215+
// those explicitly set columns for the "update on duplicate key error" part.
216+
//
217+
// Note that using this option for Delete statements has no effect as well, hence its usage is discouraged.
218+
//
219+
// Returns a function that lazily modifies a given queryable type by setting its columns.
220+
func WithColumns(columns ...string) QueryableOption {
221+
return func(q *queryable) { q.qb.SetColumns(columns...) }
222+
}
129223

130-
sem := d.db.GetSemaphoreForTable(TableName(from))
224+
// WithoutColumns returns a QueryableOption callback that excludes the DB columns from the generated DB statements.
225+
//
226+
// Setting the excludable columns while using WithStatement has no behavioural effects, so these columns may or may
227+
// not be excluded depending on the explicitly set statement. Also, note that using this option for Delete statements
228+
// has no effect as well, hence its usage is prohibited.
229+
func WithoutColumns(columns ...string) QueryableOption {
230+
return func(q *queryable) { q.qb.SetExcludedColumns(columns...) }
231+
}
131232

132-
return d.db.BulkExec(
133-
ctx, stmt, d.db.Options.MaxPlaceholdersPerStatement, sem, args, d.onDelete...,
134-
)
233+
// WithIgnoreOnError returns a InsertOption callback that sets the ignoreOnError flag DB insert statements.
234+
//
235+
// When this flag is set, the dynamically generated insert statement will cause to suppress all duplicate key errors.
236+
//
237+
// Setting this flag while using WithStatement has no behavioural effects, so the final database statement
238+
// may or may not silently suppress "duplicate key errors" depending on the explicitly set statement.
239+
func WithIgnoreOnError() QueryableOption { return func(q *queryable) { q.ignoreOnError = true } }
240+
241+
// WithByColumn returns a functional option for DeleteOption or UpdateOption, setting the scoper to the provided column.
242+
func WithByColumn(column string) QueryableOption {
243+
return func(q *queryable) { q.scoper = column }
135244
}

0 commit comments

Comments
 (0)