From 5ad4ab2e237e8345f276d7672c2e059bddf55da8 Mon Sep 17 00:00:00 2001 From: William Petit Date: Wed, 1 Mar 2023 12:12:11 +0100 Subject: [PATCH] feat(storage,document,sqlite): implements order by and limit --- .../{query_option.go => query_options.go} | 12 +- pkg/storage/sqlite/document_store.go | 59 +++++ pkg/storage/testsuite/document_store_ops.go | 208 +++++++++++++++++- 3 files changed, 269 insertions(+), 10 deletions(-) rename pkg/storage/{query_option.go => query_options.go} (74%) diff --git a/pkg/storage/query_option.go b/pkg/storage/query_options.go similarity index 74% rename from pkg/storage/query_option.go rename to pkg/storage/query_options.go index ea26469..1e25e65 100644 --- a/pkg/storage/query_option.go +++ b/pkg/storage/query_options.go @@ -7,35 +7,35 @@ const ( OrderDirectionDesc OrderDirection = "DESC" ) -type QueryOption struct { +type QueryOptions struct { Limit *int Offset *int OrderBy *string OrderDirection *OrderDirection } -type QueryOptionFunc func(o *QueryOption) +type QueryOptionFunc func(o *QueryOptions) func WithLimit(limit int) QueryOptionFunc { - return func(o *QueryOption) { + return func(o *QueryOptions) { o.Limit = &limit } } func WithOffset(offset int) QueryOptionFunc { - return func(o *QueryOption) { + return func(o *QueryOptions) { o.Offset = &offset } } func WithOrderBy(orderBy string) QueryOptionFunc { - return func(o *QueryOption) { + return func(o *QueryOptions) { o.OrderBy = &orderBy } } func WithOrderDirection(direction OrderDirection) QueryOptionFunc { - return func(o *QueryOption) { + return func(o *QueryOptions) { o.OrderDirection = &direction } } diff --git a/pkg/storage/sqlite/document_store.go b/pkg/storage/sqlite/document_store.go index cceebb4..f7d5c73 100644 --- a/pkg/storage/sqlite/document_store.go +++ b/pkg/storage/sqlite/document_store.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "math" "sync" "time" @@ -90,6 +91,11 @@ func (s *DocumentStore) Get(ctx context.Context, collection string, id storage.D // Query implements storage.DocumentStore func (s *DocumentStore) Query(ctx context.Context, collection string, filter *filter.Filter, funcs ...storage.QueryOptionFunc) ([]storage.Document, error) { + opts := &storage.QueryOptions{} + for _, fn := range funcs { + fn(opts) + } + var documents []storage.Document err := s.withTx(ctx, func(tx *sql.Tx) error { @@ -120,6 +126,24 @@ func (s *DocumentStore) Query(ctx context.Context, collection string, filter *fi args = append([]interface{}{collection}, args...) + if opts.OrderBy != nil { + query, args = withOrderByClause(query, args, *opts.OrderBy, *opts.OrderDirection) + } + + if opts.Offset != nil || opts.Limit != nil { + offset := 0 + if opts.Offset != nil { + offset = *opts.Offset + } + + limit := math.MaxInt + if opts.Limit != nil { + limit = *opts.Limit + } + + query, args = withLimitOffsetClause(query, args, limit, offset) + } + logger.Debug( ctx, "executing query", logger.F("query", query), @@ -331,6 +355,41 @@ func (s *DocumentStore) ensureTables(ctx context.Context, db *sql.DB) error { return nil } +func withOrderByClause(query string, args []any, orderBy string, orderDirection storage.OrderDirection) (string, []any) { + direction := "ASC" + if orderDirection == storage.OrderDirectionDesc { + direction = "DESC" + } + + var column string + + switch orderBy { + case storage.DocumentAttrID: + column = "id" + + case storage.DocumentAttrCreatedAt: + column = "created_at" + + case storage.DocumentAttrUpdatedAt: + column = "updated_at" + + default: + column = fmt.Sprintf("json_extract(data, '$.' || $%d)", len(args)+1) + args = append(args, orderBy) + } + + query += fmt.Sprintf(`ORDER BY %s %s`, column, direction) + + return query, args +} + +func withLimitOffsetClause(query string, args []any, limit int, offset int) (string, []any) { + query += fmt.Sprintf(`LIMIT $%d OFFSET $%d`, len(args)+1, len(args)+2) + args = append(args, limit, offset) + + return query, args +} + func NewDocumentStore(path string) *DocumentStore { return &DocumentStore{ db: nil, diff --git a/pkg/storage/testsuite/document_store_ops.go b/pkg/storage/testsuite/document_store_ops.go index 5584446..efc2822 100644 --- a/pkg/storage/testsuite/document_store_ops.go +++ b/pkg/storage/testsuite/document_store_ops.go @@ -41,7 +41,7 @@ var documentStoreOpsTestCases = []documentStoreOpsTestCase{ }), ) - results, err := store.Query(ctx, collection, filter, nil) + results, err := store.Query(ctx, collection, filter) if err != nil { return errors.WithStack(err) } @@ -82,7 +82,7 @@ var documentStoreOpsTestCases = []documentStoreOpsTestCase{ }), ) - results, err := store.Query(ctx, collection, filter, nil) + results, err := store.Query(ctx, collection, filter) if err != nil { return errors.WithStack(err) } @@ -127,7 +127,7 @@ var documentStoreOpsTestCases = []documentStoreOpsTestCase{ ), ) - results, err := store.Query(ctx, collection, filter, nil) + results, err := store.Query(ctx, collection, filter) if err != nil { return errors.WithStack(err) } @@ -219,7 +219,7 @@ var documentStoreOpsTestCases = []documentStoreOpsTestCase{ // Verify that there is no additional created document in the collection - results, err := store.Query(ctx, collection, nil, nil) + results, err := store.Query(ctx, collection, nil) if err != nil { return errors.WithStack(err) } @@ -228,6 +228,206 @@ var documentStoreOpsTestCases = []documentStoreOpsTestCase{ return errors.Errorf("len(results): expected '%v', got '%v'", e, g) } + return nil + }, + }, + { + Name: "Query order by document field", + Run: func(ctx context.Context, store storage.DocumentStore) error { + docs := []storage.Document{ + { + "sortedField": 0, + "name": "Item 1", + }, + { + "sortedField": 1, + "name": "Item 2", + }, + { + "sortedField": 2, + "name": "Item 3", + }, + } + + collection := "ordered_query_by_document_field" + + for _, doc := range docs { + if _, err := store.Upsert(ctx, collection, doc); err != nil { + return errors.WithStack(err) + } + } + + results, err := store.Query( + ctx, collection, nil, + storage.WithOrderBy("sortedField"), + storage.WithOrderDirection(storage.OrderDirectionAsc), + ) + if err != nil { + return errors.WithStack(err) + } + + if e, g := 3, len(results); e != g { + return errors.Errorf("len(results): expected '%v', got '%v'", e, g) + } + + if e, g := docs[0]["name"], results[0]["name"]; e != g { + return errors.Errorf("results[0][\"name\"]: expected '%v', got '%v'", e, g) + } + + if e, g := docs[2]["name"], results[2]["name"]; e != g { + return errors.Errorf("results[2][\"name\"]: expected '%v', got '%v'", e, g) + } + + results, err = store.Query( + ctx, collection, nil, + storage.WithOrderBy("sortedField"), + storage.WithOrderDirection(storage.OrderDirectionDesc), + ) + if err != nil { + return errors.WithStack(err) + } + + if e, g := 3, len(results); e != g { + return errors.Errorf("len(results): expected '%v', got '%v'", e, g) + } + + if e, g := docs[2]["name"], results[0]["name"]; e != g { + return errors.Errorf("results[0][\"name\"]: expected '%v', got '%v'", e, g) + } + + if e, g := docs[0]["name"], results[2]["name"]; e != g { + return errors.Errorf("results[2][\"name\"]: expected '%v', got '%v'", e, g) + } + + return nil + }, + }, + { + Name: "Query order by special attr", + Run: func(ctx context.Context, store storage.DocumentStore) error { + docs := []storage.Document{ + { + "name": "Item 1", + }, + { + "name": "Item 2", + }, + { + "name": "Item 3", + }, + } + + collection := "ordered_query_by_special_attr" + + for _, doc := range docs { + if _, err := store.Upsert(ctx, collection, doc); err != nil { + return errors.WithStack(err) + } + } + + results, err := store.Query( + ctx, collection, nil, + storage.WithOrderBy(storage.DocumentAttrCreatedAt), + storage.WithOrderDirection(storage.OrderDirectionAsc), + ) + if err != nil { + return errors.WithStack(err) + } + + if e, g := 3, len(results); e != g { + return errors.Errorf("len(results): expected '%v', got '%v'", e, g) + } + + if e, g := docs[0]["name"], results[0]["name"]; e != g { + return errors.Errorf("results[0][\"name\"]: expected '%v', got '%v'", e, g) + } + + if e, g := docs[2]["name"], results[2]["name"]; e != g { + return errors.Errorf("results[2][\"name\"]: expected '%v', got '%v'", e, g) + } + + results, err = store.Query( + ctx, collection, nil, + storage.WithOrderBy(storage.DocumentAttrCreatedAt), + storage.WithOrderDirection(storage.OrderDirectionDesc), + ) + if err != nil { + return errors.WithStack(err) + } + + if e, g := 3, len(results); e != g { + return errors.Errorf("len(results): expected '%v', got '%v'", e, g) + } + + if e, g := docs[2]["name"], results[0]["name"]; e != g { + return errors.Errorf("results[0][\"name\"]: expected '%v', got '%v'", e, g) + } + + if e, g := docs[0]["name"], results[2]["name"]; e != g { + return errors.Errorf("results[2][\"name\"]: expected '%v', got '%v'", e, g) + } + + return nil + }, + }, + { + Name: "Query limit and offset", + Run: func(ctx context.Context, store storage.DocumentStore) error { + docs := []storage.Document{ + {"name": "Item 1"}, + {"name": "Item 2"}, + {"name": "Item 3"}, + {"name": "Item 4"}, + } + + collection := "query_limit_and_offset" + + for _, doc := range docs { + if _, err := store.Upsert(ctx, collection, doc); err != nil { + return errors.WithStack(err) + } + } + + results, err := store.Query( + ctx, collection, nil, + storage.WithLimit(2), + ) + if err != nil { + return errors.WithStack(err) + } + + if e, g := 2, len(results); e != g { + return errors.Errorf("len(results): expected '%v', got '%v'", e, g) + } + + if e, g := docs[0]["name"], results[0]["name"]; e != g { + return errors.Errorf("results[0][\"name\"]: expected '%v', got '%v'", e, g) + } + + if e, g := docs[1]["name"], results[1]["name"]; e != g { + return errors.Errorf("results[1][\"name\"]: expected '%v', got '%v'", e, g) + } + + results, err = store.Query( + ctx, collection, nil, + storage.WithOffset(2), + ) + if err != nil { + return errors.WithStack(err) + } + + if e, g := 2, len(results); e != g { + return errors.Errorf("len(results): expected '%v', got '%v'", e, g) + } + + if e, g := docs[2]["name"], results[0]["name"]; e != g { + return errors.Errorf("results[0][\"name\"]: expected '%v', got '%v'", e, g) + } + + if e, g := docs[3]["name"], results[1]["name"]; e != g { + return errors.Errorf("results[1][\"name\"]: expected '%v', got '%v'", e, g) + } + return nil }, },