Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions cmds/db-manager/cleanup/evict.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ var (
deleteExpired = flags.Bool("delete", false, "set this flag to true to delete the expired entities")
locality = flags.String("locality", "", "self-identification string of this DSS instance")
timeout = flags.Duration("timeout", 5*time.Minute, "Timeout for the command")
scdLimit = flags.Int("scd_limit", 0, "maximum number of SCD entities deleted, defaults to unlimited")
ridLimit = flags.Int("rid_limit", 0, "maximum number of RID entities deleted, defaults to unlimited")
)

func init() {
Expand All @@ -41,7 +43,20 @@ func evict(cmd *cobra.Command, _ []string) error {
ctx = cmd.Context()
scdThreshold = time.Now().Add(-*scdTtl)
ridThreshold = time.Now().Add(-*ridTtl)
scdLimit = *scdLimit
ridLimit = *ridLimit
)
if scdLimit < 0 {
return fmt.Errorf("scd_limit must be equal to or greater than 0, got %d", scdLimit)
}
if ridLimit < 0 {
return fmt.Errorf("rid_limit must be equal to or greater than 0, got %d", ridLimit)
}
for _, limitFlag := range []string{"scd_limit", "rid_limit"} {
if cmd.Flags().Changed(limitFlag) && !*deleteExpired {
return fmt.Errorf("%s can only be set together with --delete", limitFlag)
}
}
log.Printf("WARNING: The usage of this tool may have an impact on performance when deleting entities. Read more in the README.")

ctx, cancel := context.WithTimeout(ctx, *timeout)
Expand Down Expand Up @@ -72,7 +87,7 @@ func evict(cmd *cobra.Command, _ []string) error {

if *checkScdOirs {
if *deleteExpired {
expiredOpIntents, err = scdRepo.DeleteExpiredOperationalIntents(ctx, scdThreshold)
expiredOpIntents, err = scdRepo.DeleteExpiredOperationalIntents(ctx, scdThreshold, scdLimit)
if err != nil {
return fmt.Errorf("failed to delete expired operational intents: %w", err)
}
Expand All @@ -86,7 +101,7 @@ func evict(cmd *cobra.Command, _ []string) error {

if *checkScdSubs {
if *deleteExpired {
scdExpiredSub, err = scdRepo.DeleteExpiredSubscriptions(ctx, scdThreshold)
scdExpiredSub, err = scdRepo.DeleteExpiredSubscriptions(ctx, scdThreshold, scdLimit)
if err != nil {
return fmt.Errorf("failed to delete expired SCD subscriptions: %w", err)
}
Expand All @@ -105,7 +120,7 @@ func evict(cmd *cobra.Command, _ []string) error {

if *checkRidISAs {
if *deleteExpired {
expiredISAs, err = ridRepo.DeleteExpiredISAs(ctx, *locality, ridThreshold)
expiredISAs, err = ridRepo.DeleteExpiredISAs(ctx, *locality, ridThreshold, ridLimit)
if err != nil {
return fmt.Errorf("failed to delete expired ISAs: %w", err)
}
Expand All @@ -119,7 +134,7 @@ func evict(cmd *cobra.Command, _ []string) error {

if *checkRidSubs {
if *deleteExpired {
ridExpiredSub, err = ridRepo.DeleteExpiredSubscriptions(ctx, *locality, ridThreshold)
ridExpiredSub, err = ridRepo.DeleteExpiredSubscriptions(ctx, *locality, ridThreshold, ridLimit)
if err != nil {
return fmt.Errorf("failed to delete expired RID subscriptions: %w", err)
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/rid/operations/subscription_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ func (r *fakeSubscriptionRepo) ListExpiredSubscriptions(_ context.Context, _ str
return nil, nil
}

func (r *fakeSubscriptionRepo) DeleteExpiredSubscriptions(_ context.Context, _ string, _ time.Time) ([]dssmodels.ID, error) {
func (r *fakeSubscriptionRepo) DeleteExpiredSubscriptions(_ context.Context, _ string, _ time.Time, _ int) ([]dssmodels.ID, error) {
return nil, nil
}

Expand Down Expand Up @@ -237,7 +237,7 @@ func (r *fakeSubscriptionRepo) ListExpiredISAs(_ context.Context, _ string, _ ti
panic("not implemented")
}

func (r *fakeSubscriptionRepo) DeleteExpiredISAs(_ context.Context, _ string, _ time.Time) ([]dssmodels.ID, error) {
func (r *fakeSubscriptionRepo) DeleteExpiredISAs(_ context.Context, _ string, _ time.Time, _ int) ([]dssmodels.ID, error) {
panic("not implemented")
}

Expand Down
4 changes: 2 additions & 2 deletions pkg/rid/repos/isa.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ type ISA interface {
// ListExpiredISAs lists the IDs of all expired ISAs based on writer
ListExpiredISAs(ctx context.Context, writer string, threshold time.Time) ([]dssmodels.ID, error)

// DeleteExpiredISAs deletes all expired ISAs based on writer and returns the IDs of the deleted ISAs.
DeleteExpiredISAs(ctx context.Context, writer string, threshold time.Time) ([]dssmodels.ID, error)
// DeleteExpiredISAs deletes up to `limit` expired ISAs based on writer and returns the IDs of the deleted ISAs. A limit of 0 means unlimited.
DeleteExpiredISAs(ctx context.Context, writer string, threshold time.Time, limit int) ([]dssmodels.ID, error)

// Count the number of existing ISA
CountISAs(ctx context.Context) (int64, error)
Expand Down
5 changes: 3 additions & 2 deletions pkg/rid/repos/subscription.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ type Subscription interface {
// ListExpiredSubscriptions lists the IDs of all expired Subscriptions based on writer.
ListExpiredSubscriptions(ctx context.Context, writer string, threshold time.Time) ([]dssmodels.ID, error)

// DeleteExpiredSubscriptions deletes all expired Subscriptions based on writer and returns the IDs of the deleted Subscriptions.
DeleteExpiredSubscriptions(ctx context.Context, writer string, threshold time.Time) ([]dssmodels.ID, error)
// DeleteExpiredSubscriptions deletes up to `limit` expired Subscriptions based on writer and
// returns the IDs of the deleted Subscriptions. A limit of 0 means unlimited.
DeleteExpiredSubscriptions(ctx context.Context, writer string, threshold time.Time, limit int) ([]dssmodels.ID, error)

// Count the number of existing subscriptions
CountSubscriptions(ctx context.Context) (int64, error)
Expand Down
2 changes: 1 addition & 1 deletion pkg/rid/store/memstore/identification_service_area.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ func (r *repo) ListExpiredISAs(_ context.Context, writer string, threshold time.
}

// TODO: Implement when raftstore evict is implemented (#1718)
func (r *repo) DeleteExpiredISAs(_ context.Context, writer string, threshold time.Time) ([]dssmodels.ID, error) {
func (r *repo) DeleteExpiredISAs(_ context.Context, writer string, threshold time.Time, limit int) ([]dssmodels.ID, error) {
return nil, stacktrace.NewErrorWithCode(dsserr.NotImplemented, "DeleteExpiredISAs not implemented for memstore")
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/rid/store/memstore/subscriptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ func (r *repo) ListExpiredSubscriptions(_ context.Context, writer string, thresh
}

// TODO: Implement when raftstore evict is implemented (#1718)
func (r *repo) DeleteExpiredSubscriptions(_ context.Context, writer string, threshold time.Time) ([]dssmodels.ID, error) {
func (r *repo) DeleteExpiredSubscriptions(_ context.Context, writer string, threshold time.Time, limit int) ([]dssmodels.ID, error) {
return nil, stacktrace.NewErrorWithCode(dsserr.NotImplemented, "DeleteExpiredSubscriptions not implemented for memstore")
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/rid/store/raftstore/identification_service_area.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ func (r *repo) ListExpiredISAs(ctx context.Context, writer string, threshold tim
return r.consensus.HandleReadRequest(ctx, listExpiredISAs, buf)
}

func (r *repo) DeleteExpiredISAs(_ context.Context, writer string, threshold time.Time) ([]dssmodels.ID, error) {
func (r *repo) DeleteExpiredISAs(_ context.Context, writer string, threshold time.Time, limit int) ([]dssmodels.ID, error) {
return nil, stacktrace.NewErrorWithCode(dsserr.NotImplemented, "DeleteExpiredISAs not implemented for raftstore")
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/rid/store/raftstore/subscriptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ func (r *repo) ListExpiredSubscriptions(ctx context.Context, writer string, thre
return r.consensus.HandleReadRequest(ctx, listExpiredSubscriptions, buf)
}

func (r *repo) DeleteExpiredSubscriptions(_ context.Context, writer string, threshold time.Time) ([]dssmodels.ID, error) {
func (r *repo) DeleteExpiredSubscriptions(_ context.Context, writer string, threshold time.Time, limit int) ([]dssmodels.ID, error) {
return nil, stacktrace.NewErrorWithCode(dsserr.NotImplemented, "DeleteExpiredSubscriptions not implemented for raftstore")
}

Expand Down
37 changes: 25 additions & 12 deletions pkg/rid/store/sqlstore/identification_service_area.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,24 +238,37 @@ func (r *repo) ListExpiredISAs(ctx context.Context, writer string, threshold tim
return dssql.FetchIDs(ctx, r.Queryable, isasInCellsQuery, threshold, writer)
}

// DeleteExpiredISAs deletes all expired ISAs based on writer and returns the IDs of the deleted ISAs.
// DeleteExpiredISAs deletes up to `limit` expired ISAs based on writer and returns the
// IDs of the deleted ISAs. A limit of 0 means unlimited.
// The function deletes both empty writer and null writer when passing empty string as a writer.
func (r *repo) DeleteExpiredISAs(ctx context.Context, writer string, threshold time.Time) ([]dssmodels.ID, error) {
func (r *repo) DeleteExpiredISAs(ctx context.Context, writer string, threshold time.Time, limit int) ([]dssmodels.ID, error) {
if len(writer) == 0 {
deleteExpiredQuery := `
expiredQuery, args := dssql.AppendLimitClause(`
SELECT id
FROM identification_service_areas
WHERE ends_at <= $1
AND (writer = '' OR writer IS NULL)`, []any{threshold}, limit)
deleteExpiredQuery := fmt.Sprintf(`
WITH expired AS (%s
)
DELETE FROM identification_service_areas
WHERE ends_at <= $1
AND (writer = '' OR writer IS NULL)
RETURNING id`
return dssql.FetchIDs(ctx, r.Queryable, deleteExpiredQuery, threshold)
WHERE id IN (SELECT id FROM expired)
RETURNING id`, expiredQuery)
return dssql.FetchIDs(ctx, r.Queryable, deleteExpiredQuery, args...)
}

deleteExpiredQuery := `
expiredQuery, args := dssql.AppendLimitClause(`
SELECT id
FROM identification_service_areas
WHERE ends_at <= $1
AND writer = $2`, []any{threshold, writer}, limit)
deleteExpiredQuery := fmt.Sprintf(`
WITH expired AS (%s
)
DELETE FROM identification_service_areas
WHERE ends_at <= $1
AND writer = $2
RETURNING id`
return dssql.FetchIDs(ctx, r.Queryable, deleteExpiredQuery, threshold, writer)
WHERE id IN (SELECT id FROM expired)
RETURNING id`, expiredQuery)
return dssql.FetchIDs(ctx, r.Queryable, deleteExpiredQuery, args...)
}

func (r *repo) CountISAs(ctx context.Context) (int64, error) {
Expand Down
38 changes: 25 additions & 13 deletions pkg/rid/store/sqlstore/subscriptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -322,25 +322,37 @@ func (r *repo) ListExpiredSubscriptions(ctx context.Context, writer string, thre
return dssql.FetchIDs(ctx, r.Queryable, query, threshold, writer)
}

// DeleteExpiredSubscriptions deletes all expired Subscriptions based on writer and returns the
// IDs of the deleted Subscriptions.
// DeleteExpiredSubscriptions deletes up to `limit` expired Subscriptions based on writer and returns
// the IDs of the deleted Subscriptions. A limit of 0 means unlimited.
// The function deletes both empty writer and null writer when passing empty string as a writer.
func (r *repo) DeleteExpiredSubscriptions(ctx context.Context, writer string, threshold time.Time) ([]dssmodels.ID, error) {
func (r *repo) DeleteExpiredSubscriptions(ctx context.Context, writer string, threshold time.Time, limit int) ([]dssmodels.ID, error) {
if len(writer) == 0 {
deleteExpiredQuery := `
expiredQuery, args := dssql.AppendLimitClause(`
SELECT id
FROM subscriptions
WHERE ends_at <= $1
AND (writer = '' OR writer IS NULL)`, []any{threshold}, limit)
deleteExpiredQuery := fmt.Sprintf(`
WITH expired AS (%s
)
DELETE FROM subscriptions
WHERE ends_at <= $1
AND (writer = '' OR writer IS NULL)
RETURNING id`
return dssql.FetchIDs(ctx, r.Queryable, deleteExpiredQuery, threshold)
WHERE id IN (SELECT id FROM expired)
RETURNING id`, expiredQuery)
return dssql.FetchIDs(ctx, r.Queryable, deleteExpiredQuery, args...)
}

deleteExpiredQuery := `
expiredQuery, args := dssql.AppendLimitClause(`
SELECT id
FROM subscriptions
WHERE ends_at <= $1
AND writer = $2`, []any{threshold, writer}, limit)
deleteExpiredQuery := fmt.Sprintf(`
WITH expired AS (%s
)
DELETE FROM subscriptions
WHERE ends_at <= $1
AND writer = $2
RETURNING id`
return dssql.FetchIDs(ctx, r.Queryable, deleteExpiredQuery, threshold, writer)
WHERE id IN (SELECT id FROM expired)
RETURNING id`, expiredQuery)
return dssql.FetchIDs(ctx, r.Queryable, deleteExpiredQuery, args...)
}

func (r *repo) CountSubscriptions(ctx context.Context) (int64, error) {
Expand Down
8 changes: 4 additions & 4 deletions pkg/scd/repos/repos.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ type OperationalIntent interface {
// Their age is determined by their end time, or by their update time if they do not have an end time.
ListExpiredOperationalIntents(ctx context.Context, threshold time.Time) ([]dssmodels.ID, error)

// DeleteExpiredOperationalIntents deletes all expired operational intents and returns the IDs of the deleted operational intents.
// DeleteExpiredOperationalIntents deletes up to `limit` expired operational intents and returns the IDs of the deleted operational intents. A limit of 0 means unlimited.
// Age is determined by their end time, or by their update time if they do not have an end time.
DeleteExpiredOperationalIntents(ctx context.Context, threshold time.Time) ([]dssmodels.ID, error)
DeleteExpiredOperationalIntents(ctx context.Context, threshold time.Time, limit int) ([]dssmodels.ID, error)

// Count the number of existing operational intent
CountOperationalIntents(ctx context.Context) (int64, error)
Expand Down Expand Up @@ -77,9 +77,9 @@ type Subscription interface {
// Their age is determined by their end time, or by their update time if they do not have an end time.
ListExpiredSubscriptions(ctx context.Context, threshold time.Time) ([]dssmodels.ID, error)

// DeleteExpiredSubscriptions deletes all expired subscriptions and returns the IDs of the deleted subscriptions.
// DeleteExpiredSubscriptions deletes up to `limit` expired subscriptions and returns the IDs of the deleted subscriptions. A limit of 0 means unlimited.
// Age is determined by their end time, or by their update time if they do not have an end time.
DeleteExpiredSubscriptions(ctx context.Context, threshold time.Time) ([]dssmodels.ID, error)
DeleteExpiredSubscriptions(ctx context.Context, threshold time.Time, limit int) ([]dssmodels.ID, error)

// Count the number of existing subscriptions
CountSubscriptions(ctx context.Context) (int64, error)
Expand Down
2 changes: 1 addition & 1 deletion pkg/scd/store/memstore/operational_intents.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ func (r *repo) ListExpiredOperationalIntents(ctx context.Context, threshold time
}

// TODO: Implement when raftstore evict is implemented (#1718)
func (r *repo) DeleteExpiredOperationalIntents(_ context.Context, threshold time.Time) ([]dssmodels.ID, error) {
func (r *repo) DeleteExpiredOperationalIntents(_ context.Context, threshold time.Time, limit int) ([]dssmodels.ID, error) {
return nil, stacktrace.NewErrorWithCode(dsserr.NotImplemented, "DeleteExpiredOperationalIntents not implemented for memstore")
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/scd/store/memstore/subscriptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ func (r *repo) ListExpiredSubscriptions(_ context.Context, threshold time.Time)
}

// TODO: Implement when raftstore evict is implemented (#1718)
func (r *repo) DeleteExpiredSubscriptions(_ context.Context, threshold time.Time) ([]dssmodels.ID, error) {
func (r *repo) DeleteExpiredSubscriptions(_ context.Context, threshold time.Time, limit int) ([]dssmodels.ID, error) {
return nil, stacktrace.NewErrorWithCode(dsserr.NotImplemented, "DeleteExpiredSubscriptions not implemented for memstore")
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/scd/store/raftstore/operational_intents.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func (r *repo) ListExpiredOperationalIntents(ctx context.Context, threshold time
return r.consensus.HandleReadRequest(ctx, listExpiredOperationalIntents, buf)
}

func (r *repo) DeleteExpiredOperationalIntents(_ context.Context, threshold time.Time) ([]dssmodels.ID, error) {
func (r *repo) DeleteExpiredOperationalIntents(_ context.Context, threshold time.Time, limit int) ([]dssmodels.ID, error) {
return nil, stacktrace.NewErrorWithCode(dsserr.NotImplemented, "DeleteExpiredOperationalIntents not implemented for raftstore")
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/scd/store/raftstore/subscriptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ func (r *repo) ListExpiredSubscriptions(ctx context.Context, threshold time.Time
return r.consensus.HandleReadRequest(ctx, listExpiredSubscriptions, buf)
}

func (r *repo) DeleteExpiredSubscriptions(_ context.Context, threshold time.Time) ([]dssmodels.ID, error) {
func (r *repo) DeleteExpiredSubscriptions(_ context.Context, threshold time.Time, limit int) ([]dssmodels.ID, error) {
return nil, stacktrace.NewErrorWithCode(dsserr.NotImplemented, "DeleteExpiredSubscriptions not implemented for raftstore")
}

Expand Down
25 changes: 16 additions & 9 deletions pkg/scd/store/sqlstore/operational_intents.go
Original file line number Diff line number Diff line change
Expand Up @@ -371,18 +371,25 @@ func (s *repo) ListExpiredOperationalIntents(ctx context.Context, threshold time
return ids, nil
}

// DeleteExpiredOperationalIntents deletes all expired operational intents and returns the IDs of the deleted operational intents.
// DeleteExpiredOperationalIntents deletes up to `limit` expired operational intents and returns the IDs of the deleted operational intents. A limit of 0 means unlimited.
// Age is determined by their end time, or by their update time if they do not have an end time.
func (s *repo) DeleteExpiredOperationalIntents(ctx context.Context, threshold time.Time) ([]dssmodels.ID, error) {
deleteExpiredQuery := `
func (s *repo) DeleteExpiredOperationalIntents(ctx context.Context, threshold time.Time, limit int) ([]dssmodels.ID, error) {
expiredQuery, args := dsssql.AppendLimitClause(`
SELECT id
FROM scd_operations
WHERE
(ends_at IS NOT NULL AND ends_at <= $1)
OR
(ends_at IS NULL AND updated_at <= $1)`, []any{threshold}, limit)

deleteExpiredQuery := fmt.Sprintf(`
WITH expired AS (%s
)
DELETE FROM scd_operations
WHERE
(ends_at IS NOT NULL AND ends_at <= $1)
OR
(ends_at IS NULL AND updated_at <= $1)
RETURNING id`
WHERE id IN (SELECT id FROM expired)
RETURNING id`, expiredQuery)

ids, err := dsssql.FetchIDs(ctx, s.q, deleteExpiredQuery, threshold)
ids, err := dsssql.FetchIDs(ctx, s.q, deleteExpiredQuery, args...)
if err != nil {
return nil, stacktrace.Propagate(err, "Error deleting expired Operations")
}
Expand Down
25 changes: 16 additions & 9 deletions pkg/scd/store/sqlstore/subscriptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -576,18 +576,25 @@ func (c *repo) ListExpiredSubscriptions(ctx context.Context, threshold time.Time
return ids, nil
}

// DeleteExpiredSubscriptions deletes all expired subscriptions and returns the IDs of the deleted subscriptions.
// DeleteExpiredSubscriptions deletes up to `limit` expired subscriptions and returns the IDs of the deleted subscriptions. A limit of 0 means unlimited.
// Age is determined by their end time, or by their update time if they do not have an end time.
func (c *repo) DeleteExpiredSubscriptions(ctx context.Context, threshold time.Time) ([]dssmodels.ID, error) {
deleteExpiredQuery := `
func (c *repo) DeleteExpiredSubscriptions(ctx context.Context, threshold time.Time, limit int) ([]dssmodels.ID, error) {
expiredQuery, args := dsssql.AppendLimitClause(`
SELECT id
FROM scd_subscriptions
WHERE
(ends_at IS NOT NULL AND ends_at <= $1)
OR
(ends_at IS NULL AND updated_at <= $1)`, []any{threshold}, limit)

deleteExpiredQuery := fmt.Sprintf(`
WITH expired AS (%s
)
DELETE FROM scd_subscriptions
WHERE
(ends_at IS NOT NULL AND ends_at <= $1)
OR
(ends_at IS NULL AND updated_at <= $1)
RETURNING id`
WHERE id IN (SELECT id FROM expired)
RETURNING id`, expiredQuery)

ids, err := dsssql.FetchIDs(ctx, c.q, deleteExpiredQuery, threshold)
ids, err := dsssql.FetchIDs(ctx, c.q, deleteExpiredQuery, args...)
if err != nil {
return nil, stacktrace.Propagate(err, "Unable to delete expired Subscriptions")
}
Expand Down
11 changes: 11 additions & 0 deletions pkg/sql/utils.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package sql

import (
"fmt"
"slices"
"time"

Expand Down Expand Up @@ -41,3 +42,13 @@ func MillisSinceMidnight() int {
midnight := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)
return int(now.Sub(midnight).Milliseconds())
}

// AppendLimitClause appends a "LIMIT $n" clause to query and the limit to args when limit is
// positive, n being the next parameter index after args. query and args are returned unchanged
// when limit <= 0 (unlimited).
func AppendLimitClause(query string, args []any, limit int) (string, []any) {
if limit <= 0 {
return query, args
}
return fmt.Sprintf("%s\nLIMIT $%d", query, len(args)+1), append(args, limit)
}
Loading