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
39 changes: 33 additions & 6 deletions completer.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ var rootSuggestions = []prompt.Suggest{
}

var (
selectSuggestion = prompt.Suggest{Text: "SELECT", Description: "SELECT [field...]"}
whereSuggestion = prompt.Suggest{Text: "WHERE", Description: "WHERE [field] [operator] [value]"}
orderBySuggestion = prompt.Suggest{Text: "ORDER BY", Description: "ORDER BY [field] [ASC/DESC]"}
limitSuggestion = prompt.Suggest{Text: "LIMIT", Description: "LIMIT [count]"}
selectSuggestion = prompt.Suggest{Text: "SELECT", Description: "SELECT [field...]"}
whereSuggestion = prompt.Suggest{Text: "WHERE", Description: "WHERE [field] [operator] [value]"}
orderBySuggestion = prompt.Suggest{Text: "ORDER BY", Description: "ORDER BY [field] [ASC/DESC]"}
limitSuggestion = prompt.Suggest{Text: "LIMIT", Description: "LIMIT [count]"}
collectionGroupSuggestion = prompt.Suggest{Text: "COLLECTION_GROUP", Description: "COLLECTION_GROUP [collection]"}
)

var querySuggestions = []prompt.Suggest{
Expand Down Expand Up @@ -83,12 +84,24 @@ func (c *Completer) Parse() ([]prompt.Suggest, error) {
}

func (c *Completer) parseQueryOperation() ([]prompt.Suggest, error) {
if !c.expectPeek(IDENT) {
if !c.peekTokenIs(IDENT) && !c.peekTokenIs(COLLECTION_GROUP) {
return []prompt.Suggest{}, nil
}
c.nextToken()

collectionGroupMode := false
if c.curTokenIs(COLLECTION_GROUP) {
collectionGroupMode = true
if !c.expectPeek(IDENT) {
return []prompt.Suggest{}, nil
}
}

// collection := c.curToken.Literal
if c.peekTokenIs(EOF) {
if collectionGroupMode {
return querySuggestions, nil
}
collection := normalizeFirestorePath(c.curToken.Literal)
parts := strings.Split(collection, "/")
if len(parts)%2 == 0 {
Expand All @@ -109,6 +122,7 @@ func (c *Completer) parseQueryOperation() ([]prompt.Suggest, error) {
for _, col := range collections {
suggestions = append(suggestions, newCollectionSuggestion(baseDoc, col))
}
suggestions = append(suggestions, collectionGroupSuggestion)

return prompt.FilterHasPrefix(suggestions, c.curToken.Literal, false), nil
}
Expand Down Expand Up @@ -292,11 +306,23 @@ func (c *Completer) parseGetOperation() ([]prompt.Suggest, error) {
}

func (c *Completer) parseCountOperation() ([]prompt.Suggest, error) {
if !c.expectPeek(IDENT) {
if !c.peekTokenIs(IDENT) && !c.peekTokenIs(COLLECTION_GROUP) {
return []prompt.Suggest{}, nil
}
c.nextToken()

collectionGroupMode := false
if c.curTokenIs(COLLECTION_GROUP) {
collectionGroupMode = true
if !c.expectPeek(IDENT) {
return []prompt.Suggest{}, nil
}
}

if c.peekTokenIs(EOF) {
if collectionGroupMode {
return []prompt.Suggest{whereSuggestion}, nil
}
collection := normalizeFirestorePath(c.curToken.Literal)
parts := strings.Split(collection, "/")
if len(parts)%2 == 0 {
Expand All @@ -317,6 +343,7 @@ func (c *Completer) parseCountOperation() ([]prompt.Suggest, error) {
for _, col := range collections {
suggestions = append(suggestions, newCollectionSuggestion(baseDoc, col))
}
suggestions = append(suggestions, collectionGroupSuggestion)

return prompt.FilterHasPrefix(suggestions, c.curToken.Literal, false), nil
}
Expand Down
30 changes: 30 additions & 0 deletions completer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,21 @@ func TestCompleter(t *testing.T) {
input: `QUERY us`,
want: []prompt.Suggest{newCollectionSuggestion("", "user")},
},
{
desc: "middle of query with collection_group keyword",
input: `QUERY C`,
want: []prompt.Suggest{collectionGroupSuggestion},
},
{
desc: "query collection_group with name and where",
input: `QUERY COLLECTION_GROUP posts WH`,
want: []prompt.Suggest{whereSuggestion},
},
{
desc: "query collection_group with name",
input: `QUERY COLLECTION_GROUP posts`,
want: querySuggestions,
},
{
desc: "middle of query with sub collection",
input: `QUERY user/1/p`,
Expand Down Expand Up @@ -174,6 +189,21 @@ func TestCompleter(t *testing.T) {
input: `COUNT us`,
want: []prompt.Suggest{newCollectionSuggestion("", "user")},
},
{
desc: "middle of count with collection_group keyword",
input: `COUNT C`,
want: []prompt.Suggest{collectionGroupSuggestion},
},
{
desc: "count collection_group with name and where",
input: `COUNT COLLECTION_GROUP posts WH`,
want: []prompt.Suggest{whereSuggestion},
},
{
desc: "count collection_group with name",
input: `COUNT COLLECTION_GROUP posts`,
want: []prompt.Suggest{whereSuggestion},
},
{
desc: "middle of count with sub collection",
input: `COUNT user/1/p`,
Expand Down
16 changes: 16 additions & 0 deletions docs/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Query documents in a collection. Supports `SELECT`, `WHERE`, `ORDER BY`, and `LI

```
QUERY <collection_path> [SELECT ...] [WHERE ...] [ORDER BY ...] [LIMIT ...]
QUERY COLLECTION_GROUP <collection_id> [SELECT ...] [WHERE ...] [ORDER BY ...] [LIMIT ...]
```

### Examples
Expand All @@ -21,6 +22,9 @@ QUERY users WHERE age = 20

-- Subcollection query
QUERY users/abc123/posts

-- Collection group query
QUERY COLLECTION_GROUP posts WHERE title = "post-1-1"
```

## GET
Expand Down Expand Up @@ -49,6 +53,7 @@ Count documents in a collection. Supports `WHERE` clause for filtering.

```
COUNT <collection_path> [WHERE ...]
COUNT COLLECTION_GROUP <collection_id> [WHERE ...]
```

Returns a single integer.
Expand All @@ -61,6 +66,9 @@ COUNT users

-- Count with filter
COUNT users WHERE name = "takashi"

-- Collection group count
COUNT COLLECTION_GROUP posts WHERE title = "post-1-1"
```

## Collection Path
Expand All @@ -75,3 +83,11 @@ collection/docId/subcollection/docId/subcollection/...
- **Document path** (odd number of segments): `users/abc123`, `users/abc123/posts/xyz789`

Leading slashes are automatically stripped.

## Collection Group

`COLLECTION_GROUP` targets all collections with the same collection ID across the database hierarchy.

- Use a single collection ID (for example, `posts`)
- Slash-separated paths are not allowed (for example, `users/posts` is invalid)
- Supported with `QUERY` and `COUNT`
36 changes: 26 additions & 10 deletions executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,25 @@ func NewExecutor(ctx context.Context, fs *firestore.Client) *Executor {
}

func (exe *Executor) ExecuteQuery(ctx context.Context, op *QueryOperation) ([]*firestore.DocumentSnapshot, error) {
collection := exe.fs.Collection(op.Collection())
if collection == nil {
return nil, ErrInvalidCollection
var q firestore.Query
if op.IsCollectionGroup() {
q = exe.fs.CollectionGroup(op.Collection()).Query
} else {
collection := exe.fs.Collection(op.Collection())
if collection == nil {
return nil, ErrInvalidCollection
}
q = collection.Query
Comment thread
maruware marked this conversation as resolved.
}
q := collection.Query

for _, filter := range op.filters {
fieldName := filter.FieldName()
value := filter.Value()
if fieldName == FieldDocumentID {
fieldName = firestore.DocumentID
value = toDocRefValue(collection, value)
if !op.IsCollectionGroup() {
value = toDocRefValue(exe.fs.Collection(op.Collection()), value)
}
}
q = q.Where(fieldName, string(filter.Operator()), value)
}
Expand Down Expand Up @@ -76,17 +84,25 @@ func (exe *Executor) ExecuteGet(ctx context.Context, op *GetOperation) (*firesto
}

func (exe *Executor) ExecuteCount(ctx context.Context, op *CountOperation) (int64, error) {
collection := exe.fs.Collection(op.Collection())
if collection == nil {
return 0, ErrInvalidCollection
var q firestore.Query
if op.IsCollectionGroup() {
q = exe.fs.CollectionGroup(op.Collection()).Query
} else {
collection := exe.fs.Collection(op.Collection())
if collection == nil {
return 0, ErrInvalidCollection
}
q = collection.Query
}
q := collection.Query

for _, filter := range op.filters {
fieldName := filter.FieldName()
value := filter.Value()
if fieldName == FieldDocumentID {
fieldName = firestore.DocumentID
value = toDocRefValue(collection, value)
if !op.IsCollectionGroup() {
value = toDocRefValue(exe.fs.Collection(op.Collection()), value)
}
}
q = q.Where(fieldName, string(filter.Operator()), value)
}
Expand Down
19 changes: 19 additions & 0 deletions executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,18 @@ func TestQuery(t *testing.T) {
},
},
},
{
desc: "query with collection group",
input: NewCollectionGroupQueryOperation("posts", nil, []Filter{
NewStringFilter("title", "==", "post-1-1"),
}, nil, 0),
want: []map[string]any{
{
"title": "post-1-1",
"publishedAt": time.Date(2025, 1, 2, 0, 0, 0, 0, time.UTC),
},
},
},
{
desc: "query with not equal",
input: &QueryOperation{collection: "users", filters: []Filter{
Expand Down Expand Up @@ -448,6 +460,13 @@ func TestCount(t *testing.T) {
}),
want: 3,
},
{
desc: "count with collection group",
input: NewCollectionGroupCountOperation("posts", []Filter{
NewStringFilter("title", "==", "post-1-1"),
}),
want: 1,
},
}

for _, tt := range tests {
Expand Down
32 changes: 25 additions & 7 deletions operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,17 +133,22 @@ type OrderBy struct {

type QueryOperation struct {
BaseOperation
collection string
selects []string
filters []Filter
orderBys []OrderBy
limit int
collection string
collectionGroup bool
selects []string
filters []Filter
orderBys []OrderBy
limit int
}

func NewQueryOperation(collection string, selects []string, filters []Filter, orderBys []OrderBy, limit int) *QueryOperation {
return &QueryOperation{collection: collection, selects: selects, filters: filters, orderBys: orderBys, limit: limit}
}

func NewCollectionGroupQueryOperation(collectionGroup string, selects []string, filters []Filter, orderBys []OrderBy, limit int) *QueryOperation {
return &QueryOperation{collection: collectionGroup, collectionGroup: true, selects: selects, filters: filters, orderBys: orderBys, limit: limit}
}

func (op *QueryOperation) OperationType() OperationType {
return OPERATION_TYPE_QUERY
}
Expand All @@ -152,6 +157,10 @@ func (op *QueryOperation) Collection() string {
return op.collection
}

func (op *QueryOperation) IsCollectionGroup() bool {
return op.collectionGroup
}

type GetOperation struct {
BaseOperation
collection string
Expand Down Expand Up @@ -181,18 +190,27 @@ func (op *GetOperation) Selects() []string {

type CountOperation struct {
BaseOperation
collection string
filters []Filter
collection string
collectionGroup bool
filters []Filter
}

func NewCountOperation(collection string, filters []Filter) *CountOperation {
return &CountOperation{collection: collection, filters: filters}
}

func NewCollectionGroupCountOperation(collectionGroup string, filters []Filter) *CountOperation {
return &CountOperation{collection: collectionGroup, collectionGroup: true, filters: filters}
}

func (op *CountOperation) OperationType() OperationType {
return OPERATION_TYPE_COUNT
}

func (op *CountOperation) Collection() string {
return op.collection
}

func (op *CountOperation) IsCollectionGroup() bool {
return op.collectionGroup
}
Loading
Loading