@@ -2,134 +2,243 @@ package database
22
33import (
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