added back relation filter reference support
This commit is contained in:
parent
4743c1ce72
commit
4937acb3e2
26
CHANGELOG.md
26
CHANGELOG.md
|
@ -1,10 +1,16 @@
|
||||||
## (WIP) v0.22.0
|
## (WIP) v0.22.0
|
||||||
|
|
||||||
- Admin UI improvements:
|
- Added support for back/indirect relation `filter`/`sort` (single and multiple).
|
||||||
- Sync collection changes across multiple opened tabs.
|
The syntax to reference back relation fields is `yourCollection_via_yourRelField.*`.
|
||||||
- Fixed vertical image popup preview scrolling.
|
⚠️ To avoid excessive joins, the nested relations resolver is now limited to max 6 level depth (similar to `expand`).
|
||||||
- Added options to export a subset of collections.
|
_Note that in the future there will be also more advanced and granular options to specify a subset of the fields that are filterable/sortable._
|
||||||
- Added option to import a subset of collections without deleting the others ([#3403](https://github.com/pocketbase/pocketbase/issues/3403)).
|
@todo add Admin UI autocomplete
|
||||||
|
@todo update "Working with relations" docs
|
||||||
|
|
||||||
|
- Added support for multiple back/indirect relation `expand` and updated the keys to use the `_via_` reference syntax (`yourCollection_via_yourRelField`).
|
||||||
|
_To minimize the breaking changes, the old parenthesis reference syntax (`yourCollection(yourRelField)`) will still continue to work but it is soft-deprecated and there will be a console log reminding you to change it to the new one._
|
||||||
|
|
||||||
|
- ⚠️ Collections and fields are no longer allowed to have `_via_` in their name to avoid collisions with the back/indirect relation reference syntax.
|
||||||
|
|
||||||
- Added `jsvm.Config.OnInit` optional config function to allow registering custom Go bindings to the JSVM.
|
- Added `jsvm.Config.OnInit` optional config function to allow registering custom Go bindings to the JSVM.
|
||||||
|
|
||||||
|
@ -18,9 +24,15 @@
|
||||||
oauth2
|
oauth2
|
||||||
```
|
```
|
||||||
|
|
||||||
- Upgraded to `aws-sdk-go-v2` and added special handling for GCS to workaround the previous [headers signature issue](https://github.com/pocketbase/pocketbase/issues/2231) that we had with v2.
|
- Upgraded to `aws-sdk-go-v2` and added special handling for GCS to workaround the previous [GCS headers signature issue](https://github.com/pocketbase/pocketbase/issues/2231) that we had with v2.
|
||||||
_This should also fix the SVG/JSON zero response when using Cloudflare R2 ([#4287](https://github.com/pocketbase/pocketbase/issues/4287#issuecomment-1925168142), [#2068](https://github.com/pocketbase/pocketbase/discussions/2068), [#2952](https://github.com/pocketbase/pocketbase/discussions/2952))._
|
_This should also fix the SVG/JSON zero response when using Cloudflare R2 ([#4287](https://github.com/pocketbase/pocketbase/issues/4287#issuecomment-1925168142), [#2068](https://github.com/pocketbase/pocketbase/discussions/2068), [#2952](https://github.com/pocketbase/pocketbase/discussions/2952))._
|
||||||
_If you are using S3, please verify that you have a green check in the Admin UI for your S3 configuration (I've tested the new version with GCS, MinIO, Cloudflare R2 and Wasabi)._
|
_If you are using S3 for uploaded files or backups, please verify that you have a green check in the Admin UI for your S3 configuration (I've tested the new version with GCS, MinIO, Cloudflare R2 and Wasabi)._
|
||||||
|
|
||||||
|
- Admin UI improvements:
|
||||||
|
- Sync collection changes across multiple opened tabs.
|
||||||
|
- Fixed vertical image popup preview scrolling.
|
||||||
|
- Added options to export a subset of collections.
|
||||||
|
- Added option to import a subset of collections without deleting the others ([#3403](https://github.com/pocketbase/pocketbase/issues/3403)).
|
||||||
|
|
||||||
- Other minor improvements (updated the `ghupdate` plugin to use the configured executable name when printing to the console, fixed the error reporting of `admin update/delete` commands, etc.).
|
- Other minor improvements (updated the `ghupdate` plugin to use the configured executable name when printing to the console, fixed the error reporting of `admin update/delete` commands, etc.).
|
||||||
|
|
||||||
|
|
|
@ -893,7 +893,8 @@ func TestCollectionUpdate(t *testing.T) {
|
||||||
{"type":"text","name":"password"},
|
{"type":"text","name":"password"},
|
||||||
{"type":"text","name":"passwordConfirm"},
|
{"type":"text","name":"passwordConfirm"},
|
||||||
{"type":"text","name":"oldPassword"}
|
{"type":"text","name":"oldPassword"}
|
||||||
]
|
],
|
||||||
|
"indexes": []
|
||||||
}`),
|
}`),
|
||||||
RequestHeaders: map[string]string{
|
RequestHeaders: map[string]string{
|
||||||
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
|
||||||
|
|
|
@ -153,9 +153,11 @@ func TestFindCollectionReferences(t *testing.T) {
|
||||||
"rel_one_no_cascade",
|
"rel_one_no_cascade",
|
||||||
"rel_one_no_cascade_required",
|
"rel_one_no_cascade_required",
|
||||||
"rel_one_cascade",
|
"rel_one_cascade",
|
||||||
|
"rel_one_unique",
|
||||||
"rel_many_no_cascade",
|
"rel_many_no_cascade",
|
||||||
"rel_many_no_cascade_required",
|
"rel_many_no_cascade_required",
|
||||||
"rel_many_cascade",
|
"rel_many_cascade",
|
||||||
|
"rel_many_unique",
|
||||||
}
|
}
|
||||||
|
|
||||||
for col, fields := range result {
|
for col, fields := range result {
|
||||||
|
@ -756,7 +758,7 @@ func TestImportCollections(t *testing.T) {
|
||||||
"demo1": 15,
|
"demo1": 15,
|
||||||
"demo2": 2,
|
"demo2": 2,
|
||||||
"demo3": 2,
|
"demo3": 2,
|
||||||
"demo4": 11,
|
"demo4": 13,
|
||||||
"demo5": 6,
|
"demo5": 6,
|
||||||
"new_import": 1,
|
"new_import": 1,
|
||||||
}
|
}
|
||||||
|
@ -774,24 +776,24 @@ func TestImportCollections(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, scenario := range scenarios {
|
for _, s := range scenarios {
|
||||||
|
t.Run(s.name, func(t *testing.T) {
|
||||||
testApp, _ := tests.NewTestApp()
|
testApp, _ := tests.NewTestApp()
|
||||||
defer testApp.Cleanup()
|
defer testApp.Cleanup()
|
||||||
|
|
||||||
importedCollections := []*models.Collection{}
|
importedCollections := []*models.Collection{}
|
||||||
|
|
||||||
// load data
|
// load data
|
||||||
loadErr := json.Unmarshal([]byte(scenario.jsonData), &importedCollections)
|
loadErr := json.Unmarshal([]byte(s.jsonData), &importedCollections)
|
||||||
if loadErr != nil {
|
if loadErr != nil {
|
||||||
t.Fatalf("[%s] Failed to load data: %v", scenario.name, loadErr)
|
t.Fatalf("Failed to load data: %v", loadErr)
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := testApp.Dao().ImportCollections(importedCollections, scenario.deleteMissing, scenario.beforeRecordsSync)
|
err := testApp.Dao().ImportCollections(importedCollections, s.deleteMissing, s.beforeRecordsSync)
|
||||||
|
|
||||||
hasErr := err != nil
|
hasErr := err != nil
|
||||||
if hasErr != scenario.expectError {
|
if hasErr != s.expectError {
|
||||||
t.Errorf("[%s] Expected hasErr to be %v, got %v (%v)", scenario.name, scenario.expectError, hasErr, err)
|
t.Fatalf("Expected hasErr to be %v, got %v (%v)", s.expectError, hasErr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// check collections count
|
// check collections count
|
||||||
|
@ -799,12 +801,13 @@ func TestImportCollections(t *testing.T) {
|
||||||
if err := testApp.Dao().CollectionQuery().All(&collections); err != nil {
|
if err := testApp.Dao().CollectionQuery().All(&collections); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if len(collections) != scenario.expectCollectionsCount {
|
if len(collections) != s.expectCollectionsCount {
|
||||||
t.Errorf("[%s] Expected %d collections, got %d", scenario.name, scenario.expectCollectionsCount, len(collections))
|
t.Fatalf("Expected %d collections, got %d", s.expectCollectionsCount, len(collections))
|
||||||
}
|
}
|
||||||
|
|
||||||
if scenario.afterTestFunc != nil {
|
if s.afterTestFunc != nil {
|
||||||
scenario.afterTestFunc(testApp, collections)
|
s.afterTestFunc(testApp, collections)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -198,8 +198,6 @@ func (dao *Dao) FindRecordsByIds(
|
||||||
return records, nil
|
return records, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// @todo consider to depricate as it may be easier to just use dao.RecordQuery()
|
|
||||||
//
|
|
||||||
// FindRecordsByExpr finds all records by the specified db expression.
|
// FindRecordsByExpr finds all records by the specified db expression.
|
||||||
//
|
//
|
||||||
// Returns all collection records if no expressions are provided.
|
// Returns all collection records if no expressions are provided.
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
package daos
|
package daos
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -9,13 +11,14 @@ import (
|
||||||
"github.com/pocketbase/pocketbase/models"
|
"github.com/pocketbase/pocketbase/models"
|
||||||
"github.com/pocketbase/pocketbase/models/schema"
|
"github.com/pocketbase/pocketbase/models/schema"
|
||||||
"github.com/pocketbase/pocketbase/tools/dbutils"
|
"github.com/pocketbase/pocketbase/tools/dbutils"
|
||||||
"github.com/pocketbase/pocketbase/tools/inflector"
|
|
||||||
"github.com/pocketbase/pocketbase/tools/list"
|
"github.com/pocketbase/pocketbase/tools/list"
|
||||||
"github.com/pocketbase/pocketbase/tools/security"
|
"github.com/pocketbase/pocketbase/tools/security"
|
||||||
"github.com/pocketbase/pocketbase/tools/types"
|
"github.com/pocketbase/pocketbase/tools/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MaxExpandDepth specifies the max allowed nested expand depth path.
|
// MaxExpandDepth specifies the max allowed nested expand depth path.
|
||||||
|
//
|
||||||
|
// @todo Consider eventually reusing resolvers.maxNestedRels
|
||||||
const MaxExpandDepth = 6
|
const MaxExpandDepth = 6
|
||||||
|
|
||||||
// ExpandFetchFunc defines the function that is used to fetch the expanded relation records.
|
// ExpandFetchFunc defines the function that is used to fetch the expanded relation records.
|
||||||
|
@ -51,13 +54,15 @@ func (dao *Dao) ExpandRecords(records []*models.Record, expands []string, optFet
|
||||||
return failed
|
return failed
|
||||||
}
|
}
|
||||||
|
|
||||||
var indirectExpandRegex = regexp.MustCompile(`^(\w+)\((\w+)\)$`)
|
// Deprecated
|
||||||
|
var indirectExpandRegexOld = regexp.MustCompile(`^(\w+)\((\w+)\)$`)
|
||||||
|
|
||||||
|
var indirectExpandRegex = regexp.MustCompile(`^(\w+)_via_(\w+)$`)
|
||||||
|
|
||||||
// notes:
|
// notes:
|
||||||
// - if fetchFunc is nil, dao.FindRecordsByIds will be used
|
// - if fetchFunc is nil, dao.FindRecordsByIds will be used
|
||||||
// - all records are expected to be from the same collection
|
// - all records are expected to be from the same collection
|
||||||
// - if MaxExpandDepth is reached, the function returns nil ignoring the remaining expand path
|
// - if MaxExpandDepth is reached, the function returns nil ignoring the remaining expand path
|
||||||
// - indirect expands are supported only with single relation fields
|
|
||||||
func (dao *Dao) expandRecords(records []*models.Record, expandPath string, fetchFunc ExpandFetchFunc, recursionLevel int) error {
|
func (dao *Dao) expandRecords(records []*models.Record, expandPath string, fetchFunc ExpandFetchFunc, recursionLevel int) error {
|
||||||
if fetchFunc == nil {
|
if fetchFunc == nil {
|
||||||
// load a default fetchFunc
|
// load a default fetchFunc
|
||||||
|
@ -77,7 +82,22 @@ func (dao *Dao) expandRecords(records []*models.Record, expandPath string, fetch
|
||||||
var relCollection *models.Collection
|
var relCollection *models.Collection
|
||||||
|
|
||||||
parts := strings.SplitN(expandPath, ".", 2)
|
parts := strings.SplitN(expandPath, ".", 2)
|
||||||
matches := indirectExpandRegex.FindStringSubmatch(parts[0])
|
var matches []string
|
||||||
|
|
||||||
|
// @todo remove the old syntax support
|
||||||
|
if strings.Contains(parts[0], "(") {
|
||||||
|
matches = indirectExpandRegexOld.FindStringSubmatch(parts[0])
|
||||||
|
if len(matches) == 3 {
|
||||||
|
log.Printf(
|
||||||
|
"%s expand format is deprecated and will be removed in the future. Consider replacing it with %s_via_%s.\n",
|
||||||
|
matches[0],
|
||||||
|
matches[1],
|
||||||
|
matches[2],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
matches = indirectExpandRegex.FindStringSubmatch(parts[0])
|
||||||
|
}
|
||||||
|
|
||||||
if len(matches) == 3 {
|
if len(matches) == 3 {
|
||||||
indirectRel, _ := dao.FindCollectionByNameOrId(matches[1])
|
indirectRel, _ := dao.FindCollectionByNameOrId(matches[1])
|
||||||
|
@ -95,47 +115,47 @@ func (dao *Dao) expandRecords(records []*models.Record, expandPath string, fetch
|
||||||
if indirectRelFieldOptions == nil || indirectRelFieldOptions.CollectionId != mainCollection.Id {
|
if indirectRelFieldOptions == nil || indirectRelFieldOptions.CollectionId != mainCollection.Id {
|
||||||
return fmt.Errorf("Invalid indirect relation field path %q.", parts[0])
|
return fmt.Errorf("Invalid indirect relation field path %q.", parts[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add the related id(s) as a dynamic relation field value to
|
||||||
|
// allow further expand checks at later stage in a more unified manner
|
||||||
|
prepErr := func() error {
|
||||||
|
q := dao.DB().Select("id").From(indirectRel.Name)
|
||||||
|
|
||||||
if indirectRelFieldOptions.IsMultiple() {
|
if indirectRelFieldOptions.IsMultiple() {
|
||||||
// for now don't allow multi-relation indirect fields expand
|
q.AndWhere(dbx.Exists(dbx.NewExp(fmt.Sprintf(
|
||||||
// due to eventual poor query performance with large data sets.
|
"SELECT 1 FROM %s je WHERE je.value = {:id}",
|
||||||
return fmt.Errorf("Multi-relation fields cannot be indirectly expanded in %q.", parts[0])
|
dbutils.JsonEach(indirectRelField.Name),
|
||||||
|
))))
|
||||||
|
} else {
|
||||||
|
q.AndWhere(dbx.NewExp("[[" + indirectRelField.Name + "]] = {:id}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
recordIds := make([]any, len(records))
|
pq := q.Build().Prepare()
|
||||||
for i, record := range records {
|
|
||||||
recordIds[i] = record.Id
|
|
||||||
}
|
|
||||||
|
|
||||||
// @todo after the index optimizations consider allowing
|
|
||||||
// indirect expand for multi-relation fields
|
|
||||||
indirectRecords, err := dao.FindRecordsByExpr(
|
|
||||||
indirectRel.Id,
|
|
||||||
dbx.In(inflector.Columnify(matches[2]), recordIds...),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
mappedIndirectRecordIds := make(map[string][]string, len(indirectRecords))
|
|
||||||
for _, indirectRecord := range indirectRecords {
|
|
||||||
recId := indirectRecord.GetString(matches[2])
|
|
||||||
if recId != "" {
|
|
||||||
mappedIndirectRecordIds[recId] = append(mappedIndirectRecordIds[recId], indirectRecord.Id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// add the indirect relation ids as a new relation field value
|
|
||||||
for _, record := range records {
|
for _, record := range records {
|
||||||
relIds, ok := mappedIndirectRecordIds[record.Id]
|
var relIds []string
|
||||||
if ok && len(relIds) > 0 {
|
|
||||||
|
err := pq.Bind(dbx.Params{"id": record.Id}).Column(&relIds)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Join(err, pq.Close())
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(relIds) > 0 {
|
||||||
record.Set(parts[0], relIds)
|
record.Set(parts[0], relIds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return pq.Close()
|
||||||
|
}()
|
||||||
|
if prepErr != nil {
|
||||||
|
return prepErr
|
||||||
|
}
|
||||||
|
|
||||||
relFieldOptions = &schema.RelationOptions{
|
relFieldOptions = &schema.RelationOptions{
|
||||||
MaxSelect: nil,
|
MaxSelect: nil,
|
||||||
CollectionId: indirectRel.Id,
|
CollectionId: indirectRel.Id,
|
||||||
}
|
}
|
||||||
if isRelFieldUnique(indirectRel, indirectRelField.Name) {
|
if dbutils.HasSingleColumnUniqueIndex(indirectRelField.Name, indirectRel.Indexes) {
|
||||||
relFieldOptions.MaxSelect = types.Pointer(1)
|
relFieldOptions.MaxSelect = types.Pointer(1)
|
||||||
}
|
}
|
||||||
// indirect relation
|
// indirect relation
|
||||||
|
|
|
@ -163,7 +163,7 @@ func TestExpandRecords(t *testing.T) {
|
||||||
0,
|
0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"simple indirect expand",
|
"simple back single relation field expand (deprecated syntax)",
|
||||||
"demo3",
|
"demo3",
|
||||||
[]string{"lcl9d87w22ml6jy"},
|
[]string{"lcl9d87w22ml6jy"},
|
||||||
[]string{"demo4(rel_one_no_cascade_required)"},
|
[]string{"demo4(rel_one_no_cascade_required)"},
|
||||||
|
@ -174,11 +174,22 @@ func TestExpandRecords(t *testing.T) {
|
||||||
0,
|
0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"nested indirect expand",
|
"simple back expand via single relation field",
|
||||||
|
"demo3",
|
||||||
|
[]string{"lcl9d87w22ml6jy"},
|
||||||
|
[]string{"demo4_via_rel_one_no_cascade_required"},
|
||||||
|
func(c *models.Collection, ids []string) ([]*models.Record, error) {
|
||||||
|
return app.Dao().FindRecordsByIds(c.Id, ids, nil)
|
||||||
|
},
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"nested back expand via single relation field",
|
||||||
"demo3",
|
"demo3",
|
||||||
[]string{"lcl9d87w22ml6jy"},
|
[]string{"lcl9d87w22ml6jy"},
|
||||||
[]string{
|
[]string{
|
||||||
"demo4(rel_one_no_cascade_required).self_rel_many.self_rel_many.self_rel_one",
|
"demo4_via_rel_one_no_cascade_required.self_rel_many.self_rel_many.self_rel_one",
|
||||||
},
|
},
|
||||||
func(c *models.Collection, ids []string) ([]*models.Record, error) {
|
func(c *models.Collection, ids []string) ([]*models.Record, error) {
|
||||||
return app.Dao().FindRecordsByIds(c.Id, ids, nil)
|
return app.Dao().FindRecordsByIds(c.Id, ids, nil)
|
||||||
|
@ -186,6 +197,19 @@ func TestExpandRecords(t *testing.T) {
|
||||||
5,
|
5,
|
||||||
0,
|
0,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"nested back expand via multiple relation field",
|
||||||
|
"demo3",
|
||||||
|
[]string{"lcl9d87w22ml6jy"},
|
||||||
|
[]string{
|
||||||
|
"demo4_via_rel_many_no_cascade_required.self_rel_many.rel_many_no_cascade_required.demo4_via_rel_many_no_cascade_required",
|
||||||
|
},
|
||||||
|
func(c *models.Collection, ids []string) ([]*models.Record, error) {
|
||||||
|
return app.Dao().FindRecordsByIds(c.Id, ids, nil)
|
||||||
|
},
|
||||||
|
7,
|
||||||
|
0,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"expand multiple relations sharing a common path",
|
"expand multiple relations sharing a common path",
|
||||||
"demo4",
|
"demo4",
|
||||||
|
@ -332,7 +356,7 @@ func TestExpandRecord(t *testing.T) {
|
||||||
0,
|
0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"simple indirect expand",
|
"simple indirect expand via single relation field (deprecated syntax)",
|
||||||
"demo3",
|
"demo3",
|
||||||
"lcl9d87w22ml6jy",
|
"lcl9d87w22ml6jy",
|
||||||
[]string{"demo4(rel_one_no_cascade_required)"},
|
[]string{"demo4(rel_one_no_cascade_required)"},
|
||||||
|
@ -343,7 +367,18 @@ func TestExpandRecord(t *testing.T) {
|
||||||
0,
|
0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"nested indirect expand",
|
"simple indirect expand via single relation field",
|
||||||
|
"demo3",
|
||||||
|
"lcl9d87w22ml6jy",
|
||||||
|
[]string{"demo4_via_rel_one_no_cascade_required"},
|
||||||
|
func(c *models.Collection, ids []string) ([]*models.Record, error) {
|
||||||
|
return app.Dao().FindRecordsByIds(c.Id, ids, nil)
|
||||||
|
},
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"nested indirect expand via single relation field",
|
||||||
"demo3",
|
"demo3",
|
||||||
"lcl9d87w22ml6jy",
|
"lcl9d87w22ml6jy",
|
||||||
[]string{
|
[]string{
|
||||||
|
@ -355,6 +390,19 @@ func TestExpandRecord(t *testing.T) {
|
||||||
5,
|
5,
|
||||||
0,
|
0,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"nested indirect expand via single relation field",
|
||||||
|
"demo3",
|
||||||
|
"lcl9d87w22ml6jy",
|
||||||
|
[]string{
|
||||||
|
"demo4_via_rel_many_no_cascade_required.self_rel_many.rel_many_no_cascade_required.demo4_via_rel_many_no_cascade_required",
|
||||||
|
},
|
||||||
|
func(c *models.Collection, ids []string) ([]*models.Record, error) {
|
||||||
|
return app.Dao().FindRecordsByIds(c.Id, ids, nil)
|
||||||
|
},
|
||||||
|
7,
|
||||||
|
0,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, s := range scenarios {
|
for _, s := range scenarios {
|
||||||
|
@ -388,21 +436,23 @@ func TestIndirectExpandSingeVsArrayResult(t *testing.T) {
|
||||||
|
|
||||||
// non-unique indirect expand
|
// non-unique indirect expand
|
||||||
{
|
{
|
||||||
errs := app.Dao().ExpandRecord(record, []string{"demo4(rel_one_cascade)"}, func(c *models.Collection, ids []string) ([]*models.Record, error) {
|
errs := app.Dao().ExpandRecord(record, []string{"demo4_via_rel_one_cascade"}, func(c *models.Collection, ids []string) ([]*models.Record, error) {
|
||||||
return app.Dao().FindRecordsByIds(c.Id, ids, nil)
|
return app.Dao().FindRecordsByIds(c.Id, ids, nil)
|
||||||
})
|
})
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
t.Fatal(errs)
|
t.Fatal(errs)
|
||||||
}
|
}
|
||||||
|
|
||||||
result, ok := record.Expand()["demo4(rel_one_cascade)"].([]*models.Record)
|
result, ok := record.Expand()["demo4_via_rel_one_cascade"].([]*models.Record)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatalf("Expected the expanded result to be a slice, got %v", result)
|
t.Fatalf("Expected the expanded result to be a slice, got %v", result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// mock a unique constraint for the rel_one_cascade field
|
// unique indirect expand
|
||||||
{
|
{
|
||||||
|
// mock a unique constraint for the rel_one_cascade field
|
||||||
|
// ---
|
||||||
demo4, err := app.Dao().FindCollectionByNameOrId("demo4")
|
demo4, err := app.Dao().FindCollectionByNameOrId("demo4")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -413,18 +463,16 @@ func TestIndirectExpandSingeVsArrayResult(t *testing.T) {
|
||||||
if err := app.Dao().SaveCollection(demo4); err != nil {
|
if err := app.Dao().SaveCollection(demo4); err != nil {
|
||||||
t.Fatalf("Failed to mock unique constraint: %v", err)
|
t.Fatalf("Failed to mock unique constraint: %v", err)
|
||||||
}
|
}
|
||||||
}
|
// ---
|
||||||
|
|
||||||
// non-unique indirect expand
|
errs := app.Dao().ExpandRecord(record, []string{"demo4_via_rel_one_cascade"}, func(c *models.Collection, ids []string) ([]*models.Record, error) {
|
||||||
{
|
|
||||||
errs := app.Dao().ExpandRecord(record, []string{"demo4(rel_one_cascade)"}, func(c *models.Collection, ids []string) ([]*models.Record, error) {
|
|
||||||
return app.Dao().FindRecordsByIds(c.Id, ids, nil)
|
return app.Dao().FindRecordsByIds(c.Id, ids, nil)
|
||||||
})
|
})
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
t.Fatal(errs)
|
t.Fatal(errs)
|
||||||
}
|
}
|
||||||
|
|
||||||
result, ok := record.Expand()["demo4(rel_one_cascade)"].(*models.Record)
|
result, ok := record.Expand()["demo4_via_rel_one_cascade"].(*models.Record)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatalf("Expected the expanded result to be a single model, got %v", result)
|
t.Fatalf("Expected the expanded result to be a single model, got %v", result)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||||
"github.com/pocketbase/pocketbase/core"
|
"github.com/pocketbase/pocketbase/core"
|
||||||
|
@ -131,6 +132,7 @@ func (form *CollectionUpsert) Validate() error {
|
||||||
validation.Match(collectionNameRegex),
|
validation.Match(collectionNameRegex),
|
||||||
validation.By(form.ensureNoSystemNameChange),
|
validation.By(form.ensureNoSystemNameChange),
|
||||||
validation.By(form.checkUniqueName),
|
validation.By(form.checkUniqueName),
|
||||||
|
validation.By(form.checkForVia),
|
||||||
),
|
),
|
||||||
// validates using the type's own validation rules + some collection's specifics
|
// validates using the type's own validation rules + some collection's specifics
|
||||||
validation.Field(
|
validation.Field(
|
||||||
|
@ -163,6 +165,19 @@ func (form *CollectionUpsert) Validate() error {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (form *CollectionUpsert) checkForVia(value any) error {
|
||||||
|
v, _ := value.(string)
|
||||||
|
if v == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(strings.ToLower(v), "_via_") {
|
||||||
|
return validation.NewError("validation_invalid_name", "The name of the collection cannot contain '_via_'.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (form *CollectionUpsert) checkUniqueName(value any) error {
|
func (form *CollectionUpsert) checkUniqueName(value any) error {
|
||||||
v, _ := value.(string)
|
v, _ := value.(string)
|
||||||
|
|
||||||
|
|
|
@ -105,6 +105,17 @@ func TestCollectionUpsertValidateAndSubmit(t *testing.T) {
|
||||||
{"empty create (auth)", "", `{"type":"auth"}`, []string{"name"}},
|
{"empty create (auth)", "", `{"type":"auth"}`, []string{"name"}},
|
||||||
{"empty create (view)", "", `{"type":"view"}`, []string{"name", "options"}},
|
{"empty create (view)", "", `{"type":"view"}`, []string{"name", "options"}},
|
||||||
{"empty update", "demo2", "{}", []string{}},
|
{"empty update", "demo2", "{}", []string{}},
|
||||||
|
{
|
||||||
|
"collection and field with _via_ names",
|
||||||
|
"",
|
||||||
|
`{
|
||||||
|
"name": "a_via_b",
|
||||||
|
"schema": [
|
||||||
|
{"name":"c_via_d","type":"text"}
|
||||||
|
]
|
||||||
|
}`,
|
||||||
|
[]string{"name", "schema"},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"create failure",
|
"create failure",
|
||||||
"",
|
"",
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||||
"github.com/go-ozzo/ozzo-validation/v4/is"
|
"github.com/go-ozzo/ozzo-validation/v4/is"
|
||||||
|
@ -211,6 +212,7 @@ func (f SchemaField) Validate() error {
|
||||||
validation.Length(1, 255),
|
validation.Length(1, 255),
|
||||||
validation.Match(schemaFieldNameRegex),
|
validation.Match(schemaFieldNameRegex),
|
||||||
validation.NotIn(list.ToInterfaceSlice(excludeNames)...),
|
validation.NotIn(list.ToInterfaceSlice(excludeNames)...),
|
||||||
|
validation.By(f.checkForVia),
|
||||||
),
|
),
|
||||||
validation.Field(&f.Type, validation.Required, validation.In(list.ToInterfaceSlice(FieldTypes())...)),
|
validation.Field(&f.Type, validation.Required, validation.In(list.ToInterfaceSlice(FieldTypes())...)),
|
||||||
// currently file fields cannot be unique because a proper
|
// currently file fields cannot be unique because a proper
|
||||||
|
@ -228,6 +230,20 @@ func (f *SchemaField) checkOptions(value any) error {
|
||||||
return v.Validate()
|
return v.Validate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @todo merge with the collections during the refactoring
|
||||||
|
func (f *SchemaField) checkForVia(value any) error {
|
||||||
|
v, _ := value.(string)
|
||||||
|
if v == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(strings.ToLower(v), "_via_") {
|
||||||
|
return validation.NewError("validation_invalid_name", "The name of the field cannot contain '_via_'.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// InitOptions initializes the current field options based on its type.
|
// InitOptions initializes the current field options based on its type.
|
||||||
//
|
//
|
||||||
// Returns error on unknown field type.
|
// Returns error on unknown field type.
|
||||||
|
|
|
@ -298,6 +298,15 @@ func TestSchemaFieldValidate(t *testing.T) {
|
||||||
},
|
},
|
||||||
[]string{"name"},
|
[]string{"name"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name with _via_",
|
||||||
|
schema.SchemaField{
|
||||||
|
Type: schema.FieldTypeText,
|
||||||
|
Id: "1234567890",
|
||||||
|
Name: "a_via_b",
|
||||||
|
},
|
||||||
|
[]string{"name"},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"reserved name (null)",
|
"reserved name (null)",
|
||||||
schema.SchemaField{
|
schema.SchemaField{
|
||||||
|
|
|
@ -3,18 +3,23 @@ package resolvers
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/pocketbase/dbx"
|
"github.com/pocketbase/dbx"
|
||||||
"github.com/pocketbase/pocketbase/models"
|
"github.com/pocketbase/pocketbase/models"
|
||||||
"github.com/pocketbase/pocketbase/models/schema"
|
"github.com/pocketbase/pocketbase/models/schema"
|
||||||
|
"github.com/pocketbase/pocketbase/tools/dbutils"
|
||||||
"github.com/pocketbase/pocketbase/tools/inflector"
|
"github.com/pocketbase/pocketbase/tools/inflector"
|
||||||
"github.com/pocketbase/pocketbase/tools/list"
|
"github.com/pocketbase/pocketbase/tools/list"
|
||||||
"github.com/pocketbase/pocketbase/tools/search"
|
"github.com/pocketbase/pocketbase/tools/search"
|
||||||
"github.com/pocketbase/pocketbase/tools/security"
|
"github.com/pocketbase/pocketbase/tools/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// maxNestedRels defines the max allowed nested relations depth.
|
||||||
|
const maxNestedRels = 6
|
||||||
|
|
||||||
// parseAndRun starts a new one-off RecordFieldResolver.Resolve execution.
|
// parseAndRun starts a new one-off RecordFieldResolver.Resolve execution.
|
||||||
func parseAndRun(fieldName string, resolver *RecordFieldResolver) (*search.ResolverResult, error) {
|
func parseAndRun(fieldName string, resolver *RecordFieldResolver) (*search.ResolverResult, error) {
|
||||||
r := &runner{
|
r := &runner{
|
||||||
|
@ -334,6 +339,8 @@ func (r *runner) processRequestInfoRelationField(dataField *schema.SchemaField)
|
||||||
return r.processActiveProps()
|
return r.processActiveProps()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var viaRegex = regexp.MustCompile(`^(\w+)_via_(\w+)$`)
|
||||||
|
|
||||||
func (r *runner) processActiveProps() (*search.ResolverResult, error) {
|
func (r *runner) processActiveProps() (*search.ResolverResult, error) {
|
||||||
totalProps := len(r.activeProps)
|
totalProps := len(r.activeProps)
|
||||||
|
|
||||||
|
@ -393,12 +400,12 @@ func (r *runner) processActiveProps() (*search.ResolverResult, error) {
|
||||||
jePair := r.activeTableAlias + "." + cleanFieldName
|
jePair := r.activeTableAlias + "." + cleanFieldName
|
||||||
|
|
||||||
result := &search.ResolverResult{
|
result := &search.ResolverResult{
|
||||||
Identifier: jsonArrayLength(jePair),
|
Identifier: dbutils.JsonArrayLength(jePair),
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.withMultiMatch {
|
if r.withMultiMatch {
|
||||||
jePair2 := r.multiMatchActiveTableAlias + "." + cleanFieldName
|
jePair2 := r.multiMatchActiveTableAlias + "." + cleanFieldName
|
||||||
r.multiMatch.valueIdentifier = jsonArrayLength(jePair2)
|
r.multiMatch.valueIdentifier = dbutils.JsonArrayLength(jePair2)
|
||||||
result.MultiMatchSubQuery = r.multiMatch
|
result.MultiMatchSubQuery = r.multiMatch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -410,7 +417,7 @@ func (r *runner) processActiveProps() (*search.ResolverResult, error) {
|
||||||
if field.Type == schema.FieldTypeSelect && modifier == eachModifier {
|
if field.Type == schema.FieldTypeSelect && modifier == eachModifier {
|
||||||
jePair := r.activeTableAlias + "." + cleanFieldName
|
jePair := r.activeTableAlias + "." + cleanFieldName
|
||||||
jeAlias := r.activeTableAlias + "_" + cleanFieldName + "_je"
|
jeAlias := r.activeTableAlias + "_" + cleanFieldName + "_je"
|
||||||
r.resolver.registerJoin(jsonEach(jePair), jeAlias, nil)
|
r.resolver.registerJoin(dbutils.JsonEach(jePair), jeAlias, nil)
|
||||||
|
|
||||||
result := &search.ResolverResult{
|
result := &search.ResolverResult{
|
||||||
Identifier: fmt.Sprintf("[[%s.value]]", jeAlias),
|
Identifier: fmt.Sprintf("[[%s.value]]", jeAlias),
|
||||||
|
@ -431,7 +438,7 @@ func (r *runner) processActiveProps() (*search.ResolverResult, error) {
|
||||||
jeAlias2 := r.multiMatchActiveTableAlias + "_" + cleanFieldName + "_je"
|
jeAlias2 := r.multiMatchActiveTableAlias + "_" + cleanFieldName + "_je"
|
||||||
|
|
||||||
r.multiMatch.joins = append(r.multiMatch.joins, &join{
|
r.multiMatch.joins = append(r.multiMatch.joins, &join{
|
||||||
tableName: jsonEach(jePair2),
|
tableName: dbutils.JsonEach(jePair2),
|
||||||
tableAlias: jeAlias2,
|
tableAlias: jeAlias2,
|
||||||
})
|
})
|
||||||
r.multiMatch.valueIdentifier = fmt.Sprintf("[[%s.value]]", jeAlias2)
|
r.multiMatch.valueIdentifier = fmt.Sprintf("[[%s.value]]", jeAlias2)
|
||||||
|
@ -458,9 +465,9 @@ func (r *runner) processActiveProps() (*search.ResolverResult, error) {
|
||||||
// (https://github.com/pocketbase/pocketbase/issues/4068)
|
// (https://github.com/pocketbase/pocketbase/issues/4068)
|
||||||
if field.Type == schema.FieldTypeJson {
|
if field.Type == schema.FieldTypeJson {
|
||||||
result.NoCoalesce = true
|
result.NoCoalesce = true
|
||||||
result.Identifier = jsonExtract(r.activeTableAlias+"."+cleanFieldName, "")
|
result.Identifier = dbutils.JsonExtract(r.activeTableAlias+"."+cleanFieldName, "")
|
||||||
if r.withMultiMatch {
|
if r.withMultiMatch {
|
||||||
r.multiMatch.valueIdentifier = jsonExtract(r.multiMatchActiveTableAlias+"."+cleanFieldName, "")
|
r.multiMatch.valueIdentifier = dbutils.JsonExtract(r.multiMatchActiveTableAlias+"."+cleanFieldName, "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -468,23 +475,19 @@ func (r *runner) processActiveProps() (*search.ResolverResult, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
field := collection.Schema.GetFieldByName(prop)
|
field := collection.Schema.GetFieldByName(prop)
|
||||||
if field == nil {
|
|
||||||
if r.nullifyMisingField {
|
|
||||||
return &search.ResolverResult{Identifier: "NULL"}, nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("unknown field %q", prop)
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if it is a json field
|
// json field -> treat the rest of the props as json path
|
||||||
if field.Type == schema.FieldTypeJson {
|
if field != nil && field.Type == schema.FieldTypeJson {
|
||||||
var jsonPath strings.Builder
|
var jsonPath strings.Builder
|
||||||
for _, p := range r.activeProps[i+1:] {
|
for j, p := range r.activeProps[i+1:] {
|
||||||
if _, err := strconv.Atoi(p); err == nil {
|
if _, err := strconv.Atoi(p); err == nil {
|
||||||
jsonPath.WriteString("[")
|
jsonPath.WriteString("[")
|
||||||
jsonPath.WriteString(inflector.Columnify(p))
|
jsonPath.WriteString(inflector.Columnify(p))
|
||||||
jsonPath.WriteString("]")
|
jsonPath.WriteString("]")
|
||||||
} else {
|
} else {
|
||||||
|
if j > 0 {
|
||||||
jsonPath.WriteString(".")
|
jsonPath.WriteString(".")
|
||||||
|
}
|
||||||
jsonPath.WriteString(inflector.Columnify(p))
|
jsonPath.WriteString(inflector.Columnify(p))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -492,18 +495,130 @@ func (r *runner) processActiveProps() (*search.ResolverResult, error) {
|
||||||
|
|
||||||
result := &search.ResolverResult{
|
result := &search.ResolverResult{
|
||||||
NoCoalesce: true,
|
NoCoalesce: true,
|
||||||
Identifier: jsonExtract(r.activeTableAlias+"."+inflector.Columnify(prop), jsonPathStr),
|
Identifier: dbutils.JsonExtract(r.activeTableAlias+"."+inflector.Columnify(prop), jsonPathStr),
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.withMultiMatch {
|
if r.withMultiMatch {
|
||||||
r.multiMatch.valueIdentifier = jsonExtract(r.multiMatchActiveTableAlias+"."+inflector.Columnify(prop), jsonPathStr)
|
r.multiMatch.valueIdentifier = dbutils.JsonExtract(r.multiMatchActiveTableAlias+"."+inflector.Columnify(prop), jsonPathStr)
|
||||||
result.MultiMatchSubQuery = r.multiMatch
|
result.MultiMatchSubQuery = r.multiMatch
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if it is a relation field
|
if i >= maxNestedRels {
|
||||||
|
return nil, fmt.Errorf("max nested relations reached for field %q", prop)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for back relation (eg. yourCollection_via_yourRelField)
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
if field == nil {
|
||||||
|
parts := viaRegex.FindStringSubmatch(prop)
|
||||||
|
if len(parts) != 3 {
|
||||||
|
if r.nullifyMisingField {
|
||||||
|
return &search.ResolverResult{Identifier: "NULL"}, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unknown field %q", prop)
|
||||||
|
}
|
||||||
|
|
||||||
|
backCollection, err := r.resolver.loadCollection(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to resolve field %q", prop)
|
||||||
|
}
|
||||||
|
backField := backCollection.Schema.GetFieldByName(parts[2])
|
||||||
|
if backField == nil || backField.Type != schema.FieldTypeRelation {
|
||||||
|
return nil, fmt.Errorf("invalid or missing back relation field %q", parts[2])
|
||||||
|
}
|
||||||
|
backField.InitOptions()
|
||||||
|
backFieldOptions, ok := backField.Options.(*schema.RelationOptions)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("failed to initialize back relation field %q options", backField.Name)
|
||||||
|
}
|
||||||
|
if backFieldOptions.CollectionId != collection.Id {
|
||||||
|
return nil, fmt.Errorf("invalid back relation field %q collection reference", backField.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// join the back relation to the main query
|
||||||
|
// ---
|
||||||
|
cleanProp := inflector.Columnify(prop)
|
||||||
|
cleanBackFieldName := inflector.Columnify(backField.Name)
|
||||||
|
newTableAlias := r.activeTableAlias + "_" + cleanProp
|
||||||
|
newCollectionName := inflector.Columnify(backCollection.Name)
|
||||||
|
|
||||||
|
isBackRelMultiple := backFieldOptions.IsMultiple()
|
||||||
|
if !isBackRelMultiple {
|
||||||
|
// additionally check if the rel field has a single column unique index
|
||||||
|
isBackRelMultiple = !dbutils.HasSingleColumnUniqueIndex(backField.Name, backCollection.Indexes)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isBackRelMultiple {
|
||||||
|
r.resolver.registerJoin(
|
||||||
|
newCollectionName,
|
||||||
|
newTableAlias,
|
||||||
|
dbx.NewExp(fmt.Sprintf("[[%s.%s]] = [[%s.id]]", newTableAlias, cleanBackFieldName, r.activeTableAlias)),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
jeAlias := r.activeTableAlias + "_" + cleanProp + "_je"
|
||||||
|
r.resolver.registerJoin(
|
||||||
|
newCollectionName,
|
||||||
|
newTableAlias,
|
||||||
|
dbx.NewExp(fmt.Sprintf(
|
||||||
|
"[[%s.id]] IN (SELECT [[%s.value]] FROM %s {{%s}})",
|
||||||
|
r.activeTableAlias,
|
||||||
|
jeAlias,
|
||||||
|
dbutils.JsonEach(newTableAlias+"."+cleanBackFieldName),
|
||||||
|
jeAlias,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.activeCollectionName = newCollectionName
|
||||||
|
r.activeTableAlias = newTableAlias
|
||||||
|
// ---
|
||||||
|
|
||||||
|
// join the back relation to the multi-match subquery
|
||||||
|
// ---
|
||||||
|
if isBackRelMultiple {
|
||||||
|
r.withMultiMatch = true // enable multimatch if not already
|
||||||
|
}
|
||||||
|
|
||||||
|
newTableAlias2 := r.multiMatchActiveTableAlias + "_" + cleanProp
|
||||||
|
|
||||||
|
if !isBackRelMultiple {
|
||||||
|
r.multiMatch.joins = append(
|
||||||
|
r.multiMatch.joins,
|
||||||
|
&join{
|
||||||
|
tableName: newCollectionName,
|
||||||
|
tableAlias: newTableAlias2,
|
||||||
|
on: dbx.NewExp(fmt.Sprintf("[[%s.%s]] = [[%s.id]]", newTableAlias2, cleanBackFieldName, r.multiMatchActiveTableAlias)),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
jeAlias2 := r.multiMatchActiveTableAlias + "_" + cleanProp + "_je"
|
||||||
|
r.multiMatch.joins = append(
|
||||||
|
r.multiMatch.joins,
|
||||||
|
&join{
|
||||||
|
tableName: newCollectionName,
|
||||||
|
tableAlias: newTableAlias2,
|
||||||
|
on: dbx.NewExp(fmt.Sprintf(
|
||||||
|
"[[%s.id]] IN (SELECT [[%s.value]] FROM %s {{%s}})",
|
||||||
|
r.multiMatchActiveTableAlias,
|
||||||
|
jeAlias2,
|
||||||
|
dbutils.JsonEach(newTableAlias2+"."+cleanBackFieldName),
|
||||||
|
jeAlias2,
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.multiMatchActiveTableAlias = newTableAlias2
|
||||||
|
// ---
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
|
||||||
|
// check for direct relation
|
||||||
if field.Type != schema.FieldTypeRelation {
|
if field.Type != schema.FieldTypeRelation {
|
||||||
return nil, fmt.Errorf("field %q is not a valid relation", prop)
|
return nil, fmt.Errorf("field %q is not a valid relation", prop)
|
||||||
}
|
}
|
||||||
|
@ -534,7 +649,7 @@ func (r *runner) processActiveProps() (*search.ResolverResult, error) {
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
jeAlias := r.activeTableAlias + "_" + cleanFieldName + "_je"
|
jeAlias := r.activeTableAlias + "_" + cleanFieldName + "_je"
|
||||||
r.resolver.registerJoin(jsonEach(prefixedFieldName), jeAlias, nil)
|
r.resolver.registerJoin(dbutils.JsonEach(prefixedFieldName), jeAlias, nil)
|
||||||
r.resolver.registerJoin(
|
r.resolver.registerJoin(
|
||||||
inflector.Columnify(newCollectionName),
|
inflector.Columnify(newCollectionName),
|
||||||
newTableAlias,
|
newTableAlias,
|
||||||
|
@ -549,7 +664,7 @@ func (r *runner) processActiveProps() (*search.ResolverResult, error) {
|
||||||
// join the relation to the multi-match subquery
|
// join the relation to the multi-match subquery
|
||||||
// ---
|
// ---
|
||||||
if options.IsMultiple() {
|
if options.IsMultiple() {
|
||||||
r.withMultiMatch = true
|
r.withMultiMatch = true // enable multimatch if not already
|
||||||
}
|
}
|
||||||
|
|
||||||
newTableAlias2 := r.multiMatchActiveTableAlias + "_" + cleanFieldName
|
newTableAlias2 := r.multiMatchActiveTableAlias + "_" + cleanFieldName
|
||||||
|
@ -569,7 +684,7 @@ func (r *runner) processActiveProps() (*search.ResolverResult, error) {
|
||||||
r.multiMatch.joins = append(
|
r.multiMatch.joins = append(
|
||||||
r.multiMatch.joins,
|
r.multiMatch.joins,
|
||||||
&join{
|
&join{
|
||||||
tableName: jsonEach(prefixedFieldName2),
|
tableName: dbutils.JsonEach(prefixedFieldName2),
|
||||||
tableAlias: jeAlias2,
|
tableAlias: jeAlias2,
|
||||||
},
|
},
|
||||||
&join{
|
&join{
|
||||||
|
@ -587,34 +702,6 @@ func (r *runner) processActiveProps() (*search.ResolverResult, error) {
|
||||||
return nil, fmt.Errorf("failed to resolve field %q", r.fieldName)
|
return nil, fmt.Errorf("failed to resolve field %q", r.fieldName)
|
||||||
}
|
}
|
||||||
|
|
||||||
func jsonArrayLength(tableColumnPair string) string {
|
|
||||||
return fmt.Sprintf(
|
|
||||||
// note: the case is used to normalize value access for single and multiple relations.
|
|
||||||
`json_array_length(CASE WHEN json_valid([[%s]]) THEN [[%s]] ELSE (CASE WHEN [[%s]] = '' OR [[%s]] IS NULL THEN json_array() ELSE json_array([[%s]]) END) END)`,
|
|
||||||
tableColumnPair, tableColumnPair, tableColumnPair, tableColumnPair, tableColumnPair,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func jsonEach(tableColumnPair string) string {
|
|
||||||
return fmt.Sprintf(
|
|
||||||
// note: the case is used to normalize value access for single and multiple relations.
|
|
||||||
`json_each(CASE WHEN json_valid([[%s]]) THEN [[%s]] ELSE json_array([[%s]]) END)`,
|
|
||||||
tableColumnPair, tableColumnPair, tableColumnPair,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func jsonExtract(tableColumnPair string, path string) string {
|
|
||||||
return fmt.Sprintf(
|
|
||||||
// note: the extra object wrapping is needed to workaround the cases where a json_extract is used with non-json columns.
|
|
||||||
"(CASE WHEN json_valid([[%s]]) THEN JSON_EXTRACT([[%s]], '$%s') ELSE JSON_EXTRACT(json_object('pb', [[%s]]), '$.pb%s') END)",
|
|
||||||
tableColumnPair,
|
|
||||||
tableColumnPair,
|
|
||||||
path,
|
|
||||||
tableColumnPair,
|
|
||||||
path,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolvableSystemFieldNames(collection *models.Collection) []string {
|
func resolvableSystemFieldNames(collection *models.Collection) []string {
|
||||||
result := schema.BaseModelFieldNames()
|
result := schema.BaseModelFieldNames()
|
||||||
|
|
||||||
|
|
|
@ -144,7 +144,8 @@ func (r *RecordFieldResolver) UpdateQuery(query *dbx.SelectQuery) error {
|
||||||
// id
|
// id
|
||||||
// someSelect.each
|
// someSelect.each
|
||||||
// project.screen.status
|
// project.screen.status
|
||||||
// @request.status
|
// @request.context
|
||||||
|
// @request.method
|
||||||
// @request.query.filter
|
// @request.query.filter
|
||||||
// @request.headers.x_token
|
// @request.headers.x_token
|
||||||
// @request.auth.someRelation.name
|
// @request.auth.someRelation.name
|
||||||
|
|
|
@ -99,61 +99,103 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) {
|
||||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `demo4_self_rel_one` ON [[demo4_self_rel_one.id]] = [[demo4.self_rel_one]] WHERE ([[demo4.title]] > 1 OR [[demo4_self_rel_one.title]] > 1)",
|
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `demo4_self_rel_one` ON [[demo4_self_rel_one.id]] = [[demo4.self_rel_one]] WHERE ([[demo4.title]] > 1 OR [[demo4_self_rel_one.title]] > 1)",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"nested incomplete rels (opt/any operator)",
|
"nested incomplete relations (opt/any operator)",
|
||||||
"demo4",
|
"demo4",
|
||||||
"self_rel_many.self_rel_one ?> true",
|
"self_rel_many.self_rel_one ?> true",
|
||||||
false,
|
false,
|
||||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] WHERE [[demo4_self_rel_many.self_rel_one]] > 1",
|
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] WHERE [[demo4_self_rel_many.self_rel_one]] > 1",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"nested incomplete rels (multi-match operator)",
|
"nested incomplete relations (multi-match operator)",
|
||||||
"demo4",
|
"demo4",
|
||||||
"self_rel_many.self_rel_one > true",
|
"self_rel_many.self_rel_one > true",
|
||||||
false,
|
false,
|
||||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] WHERE ((([[demo4_self_rel_many.self_rel_one]] > 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo4_self_rel_many.self_rel_one]] as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo4.self_rel_many]]) THEN [[__mm_demo4.self_rel_many]] ELSE json_array([[__mm_demo4.self_rel_many]]) END) `__mm_demo4_self_rel_many_je` LEFT JOIN `demo4` `__mm_demo4_self_rel_many` ON [[__mm_demo4_self_rel_many.id]] = [[__mm_demo4_self_rel_many_je.value]] WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{TEST}} WHERE ((NOT ([[TEST.multiMatchValue]] > 1)) OR ([[TEST.multiMatchValue]] IS NULL))))))",
|
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] WHERE ((([[demo4_self_rel_many.self_rel_one]] > 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo4_self_rel_many.self_rel_one]] as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo4.self_rel_many]]) THEN [[__mm_demo4.self_rel_many]] ELSE json_array([[__mm_demo4.self_rel_many]]) END) `__mm_demo4_self_rel_many_je` LEFT JOIN `demo4` `__mm_demo4_self_rel_many` ON [[__mm_demo4_self_rel_many.id]] = [[__mm_demo4_self_rel_many_je.value]] WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{TEST}} WHERE ((NOT ([[TEST.multiMatchValue]] > 1)) OR ([[TEST.multiMatchValue]] IS NULL))))))",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"nested complete rels (opt/any operator)",
|
"nested complete relations (opt/any operator)",
|
||||||
"demo4",
|
"demo4",
|
||||||
"self_rel_many.self_rel_one.title ?> true",
|
"self_rel_many.self_rel_one.title ?> true",
|
||||||
false,
|
false,
|
||||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one` ON [[demo4_self_rel_many_self_rel_one.id]] = [[demo4_self_rel_many.self_rel_one]] WHERE [[demo4_self_rel_many_self_rel_one.title]] > 1",
|
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one` ON [[demo4_self_rel_many_self_rel_one.id]] = [[demo4_self_rel_many.self_rel_one]] WHERE [[demo4_self_rel_many_self_rel_one.title]] > 1",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"nested complete rels (multi-match operator)",
|
"nested complete relations (multi-match operator)",
|
||||||
"demo4",
|
"demo4",
|
||||||
"self_rel_many.self_rel_one.title > true",
|
"self_rel_many.self_rel_one.title > true",
|
||||||
false,
|
false,
|
||||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one` ON [[demo4_self_rel_many_self_rel_one.id]] = [[demo4_self_rel_many.self_rel_one]] WHERE ((([[demo4_self_rel_many_self_rel_one.title]] > 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo4_self_rel_many_self_rel_one.title]] as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo4.self_rel_many]]) THEN [[__mm_demo4.self_rel_many]] ELSE json_array([[__mm_demo4.self_rel_many]]) END) `__mm_demo4_self_rel_many_je` LEFT JOIN `demo4` `__mm_demo4_self_rel_many` ON [[__mm_demo4_self_rel_many.id]] = [[__mm_demo4_self_rel_many_je.value]] LEFT JOIN `demo4` `__mm_demo4_self_rel_many_self_rel_one` ON [[__mm_demo4_self_rel_many_self_rel_one.id]] = [[__mm_demo4_self_rel_many.self_rel_one]] WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{__smTEST}} WHERE ((NOT ([[__smTEST.multiMatchValue]] > 1)) OR ([[__smTEST.multiMatchValue]] IS NULL))))))",
|
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one` ON [[demo4_self_rel_many_self_rel_one.id]] = [[demo4_self_rel_many.self_rel_one]] WHERE ((([[demo4_self_rel_many_self_rel_one.title]] > 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo4_self_rel_many_self_rel_one.title]] as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo4.self_rel_many]]) THEN [[__mm_demo4.self_rel_many]] ELSE json_array([[__mm_demo4.self_rel_many]]) END) `__mm_demo4_self_rel_many_je` LEFT JOIN `demo4` `__mm_demo4_self_rel_many` ON [[__mm_demo4_self_rel_many.id]] = [[__mm_demo4_self_rel_many_je.value]] LEFT JOIN `demo4` `__mm_demo4_self_rel_many_self_rel_one` ON [[__mm_demo4_self_rel_many_self_rel_one.id]] = [[__mm_demo4_self_rel_many.self_rel_one]] WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{__smTEST}} WHERE ((NOT ([[__smTEST.multiMatchValue]] > 1)) OR ([[__smTEST.multiMatchValue]] IS NULL))))))",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"repeated nested rels (opt/any operator)",
|
"repeated nested relations (opt/any operator)",
|
||||||
"demo4",
|
"demo4",
|
||||||
"self_rel_many.self_rel_one.self_rel_many.self_rel_one.title ?> true",
|
"self_rel_many.self_rel_one.self_rel_many.self_rel_one.title ?> true",
|
||||||
false,
|
false,
|
||||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one` ON [[demo4_self_rel_many_self_rel_one.id]] = [[demo4_self_rel_many.self_rel_one]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_self_rel_many_self_rel_one.self_rel_many]]) THEN [[demo4_self_rel_many_self_rel_one.self_rel_many]] ELSE json_array([[demo4_self_rel_many_self_rel_one.self_rel_many]]) END) `demo4_self_rel_many_self_rel_one_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one_self_rel_many` ON [[demo4_self_rel_many_self_rel_one_self_rel_many.id]] = [[demo4_self_rel_many_self_rel_one_self_rel_many_je.value]] LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one` ON [[demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one.id]] = [[demo4_self_rel_many_self_rel_one_self_rel_many.self_rel_one]] WHERE [[demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one.title]] > 1",
|
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one` ON [[demo4_self_rel_many_self_rel_one.id]] = [[demo4_self_rel_many.self_rel_one]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_self_rel_many_self_rel_one.self_rel_many]]) THEN [[demo4_self_rel_many_self_rel_one.self_rel_many]] ELSE json_array([[demo4_self_rel_many_self_rel_one.self_rel_many]]) END) `demo4_self_rel_many_self_rel_one_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one_self_rel_many` ON [[demo4_self_rel_many_self_rel_one_self_rel_many.id]] = [[demo4_self_rel_many_self_rel_one_self_rel_many_je.value]] LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one` ON [[demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one.id]] = [[demo4_self_rel_many_self_rel_one_self_rel_many.self_rel_one]] WHERE [[demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one.title]] > 1",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"repeated nested rels (multi-match operator)",
|
"repeated nested relations (multi-match operator)",
|
||||||
"demo4",
|
"demo4",
|
||||||
"self_rel_many.self_rel_one.self_rel_many.self_rel_one.title > true",
|
"self_rel_many.self_rel_one.self_rel_many.self_rel_one.title > true",
|
||||||
false,
|
false,
|
||||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one` ON [[demo4_self_rel_many_self_rel_one.id]] = [[demo4_self_rel_many.self_rel_one]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_self_rel_many_self_rel_one.self_rel_many]]) THEN [[demo4_self_rel_many_self_rel_one.self_rel_many]] ELSE json_array([[demo4_self_rel_many_self_rel_one.self_rel_many]]) END) `demo4_self_rel_many_self_rel_one_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one_self_rel_many` ON [[demo4_self_rel_many_self_rel_one_self_rel_many.id]] = [[demo4_self_rel_many_self_rel_one_self_rel_many_je.value]] LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one` ON [[demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one.id]] = [[demo4_self_rel_many_self_rel_one_self_rel_many.self_rel_one]] WHERE ((([[demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one.title]] > 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one.title]] as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo4.self_rel_many]]) THEN [[__mm_demo4.self_rel_many]] ELSE json_array([[__mm_demo4.self_rel_many]]) END) `__mm_demo4_self_rel_many_je` LEFT JOIN `demo4` `__mm_demo4_self_rel_many` ON [[__mm_demo4_self_rel_many.id]] = [[__mm_demo4_self_rel_many_je.value]] LEFT JOIN `demo4` `__mm_demo4_self_rel_many_self_rel_one` ON [[__mm_demo4_self_rel_many_self_rel_one.id]] = [[__mm_demo4_self_rel_many.self_rel_one]] LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo4_self_rel_many_self_rel_one.self_rel_many]]) THEN [[__mm_demo4_self_rel_many_self_rel_one.self_rel_many]] ELSE json_array([[__mm_demo4_self_rel_many_self_rel_one.self_rel_many]]) END) `__mm_demo4_self_rel_many_self_rel_one_self_rel_many_je` LEFT JOIN `demo4` `__mm_demo4_self_rel_many_self_rel_one_self_rel_many` ON [[__mm_demo4_self_rel_many_self_rel_one_self_rel_many.id]] = [[__mm_demo4_self_rel_many_self_rel_one_self_rel_many_je.value]] LEFT JOIN `demo4` `__mm_demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one` ON [[__mm_demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one.id]] = [[__mm_demo4_self_rel_many_self_rel_one_self_rel_many.self_rel_one]] WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{__smTEST}} WHERE ((NOT ([[__smTEST.multiMatchValue]] > 1)) OR ([[__smTEST.multiMatchValue]] IS NULL))))))",
|
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one` ON [[demo4_self_rel_many_self_rel_one.id]] = [[demo4_self_rel_many.self_rel_one]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_self_rel_many_self_rel_one.self_rel_many]]) THEN [[demo4_self_rel_many_self_rel_one.self_rel_many]] ELSE json_array([[demo4_self_rel_many_self_rel_one.self_rel_many]]) END) `demo4_self_rel_many_self_rel_one_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one_self_rel_many` ON [[demo4_self_rel_many_self_rel_one_self_rel_many.id]] = [[demo4_self_rel_many_self_rel_one_self_rel_many_je.value]] LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one` ON [[demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one.id]] = [[demo4_self_rel_many_self_rel_one_self_rel_many.self_rel_one]] WHERE ((([[demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one.title]] > 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one.title]] as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo4.self_rel_many]]) THEN [[__mm_demo4.self_rel_many]] ELSE json_array([[__mm_demo4.self_rel_many]]) END) `__mm_demo4_self_rel_many_je` LEFT JOIN `demo4` `__mm_demo4_self_rel_many` ON [[__mm_demo4_self_rel_many.id]] = [[__mm_demo4_self_rel_many_je.value]] LEFT JOIN `demo4` `__mm_demo4_self_rel_many_self_rel_one` ON [[__mm_demo4_self_rel_many_self_rel_one.id]] = [[__mm_demo4_self_rel_many.self_rel_one]] LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo4_self_rel_many_self_rel_one.self_rel_many]]) THEN [[__mm_demo4_self_rel_many_self_rel_one.self_rel_many]] ELSE json_array([[__mm_demo4_self_rel_many_self_rel_one.self_rel_many]]) END) `__mm_demo4_self_rel_many_self_rel_one_self_rel_many_je` LEFT JOIN `demo4` `__mm_demo4_self_rel_many_self_rel_one_self_rel_many` ON [[__mm_demo4_self_rel_many_self_rel_one_self_rel_many.id]] = [[__mm_demo4_self_rel_many_self_rel_one_self_rel_many_je.value]] LEFT JOIN `demo4` `__mm_demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one` ON [[__mm_demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one.id]] = [[__mm_demo4_self_rel_many_self_rel_one_self_rel_many.self_rel_one]] WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{__smTEST}} WHERE ((NOT ([[__smTEST.multiMatchValue]] > 1)) OR ([[__smTEST.multiMatchValue]] IS NULL))))))",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"multiple rels (opt/any operators)",
|
"multiple relations (opt/any operators)",
|
||||||
"demo4",
|
"demo4",
|
||||||
"self_rel_many.title ?= 'test' || self_rel_one.json_object.a ?> true",
|
"self_rel_many.title ?= 'test' || self_rel_one.json_object.a ?> true",
|
||||||
false,
|
false,
|
||||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] LEFT JOIN `demo4` `demo4_self_rel_one` ON [[demo4_self_rel_one.id]] = [[demo4.self_rel_one]] WHERE ([[demo4_self_rel_many.title]] = {:TEST} OR (CASE WHEN json_valid([[demo4_self_rel_one.json_object]]) THEN JSON_EXTRACT([[demo4_self_rel_one.json_object]], '$.a') ELSE JSON_EXTRACT(json_object('pb', [[demo4_self_rel_one.json_object]]), '$.pb.a') END) > 1)",
|
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] LEFT JOIN `demo4` `demo4_self_rel_one` ON [[demo4_self_rel_one.id]] = [[demo4.self_rel_one]] WHERE ([[demo4_self_rel_many.title]] = {:TEST} OR (CASE WHEN json_valid([[demo4_self_rel_one.json_object]]) THEN JSON_EXTRACT([[demo4_self_rel_one.json_object]], '$.a') ELSE JSON_EXTRACT(json_object('pb', [[demo4_self_rel_one.json_object]]), '$.pb.a') END) > 1)",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"multiple rels (multi-match operators)",
|
"multiple relations (multi-match operators)",
|
||||||
"demo4",
|
"demo4",
|
||||||
"self_rel_many.title = 'test' || self_rel_one.json_object.a > true",
|
"self_rel_many.title = 'test' || self_rel_one.json_object.a > true",
|
||||||
false,
|
false,
|
||||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] LEFT JOIN `demo4` `demo4_self_rel_one` ON [[demo4_self_rel_one.id]] = [[demo4.self_rel_one]] WHERE ((([[demo4_self_rel_many.title]] = {:TEST}) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo4_self_rel_many.title]] as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo4.self_rel_many]]) THEN [[__mm_demo4.self_rel_many]] ELSE json_array([[__mm_demo4.self_rel_many]]) END) `__mm_demo4_self_rel_many_je` LEFT JOIN `demo4` `__mm_demo4_self_rel_many` ON [[__mm_demo4_self_rel_many.id]] = [[__mm_demo4_self_rel_many_je.value]] WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{__smTEST}} WHERE NOT ([[__smTEST.multiMatchValue]] = {:TEST})))) OR (CASE WHEN json_valid([[demo4_self_rel_one.json_object]]) THEN JSON_EXTRACT([[demo4_self_rel_one.json_object]], '$.a') ELSE JSON_EXTRACT(json_object('pb', [[demo4_self_rel_one.json_object]]), '$.pb.a') END) > 1)",
|
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] LEFT JOIN `demo4` `demo4_self_rel_one` ON [[demo4_self_rel_one.id]] = [[demo4.self_rel_one]] WHERE ((([[demo4_self_rel_many.title]] = {:TEST}) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo4_self_rel_many.title]] as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo4.self_rel_many]]) THEN [[__mm_demo4.self_rel_many]] ELSE json_array([[__mm_demo4.self_rel_many]]) END) `__mm_demo4_self_rel_many_je` LEFT JOIN `demo4` `__mm_demo4_self_rel_many` ON [[__mm_demo4_self_rel_many.id]] = [[__mm_demo4_self_rel_many_je.value]] WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{__smTEST}} WHERE NOT ([[__smTEST.multiMatchValue]] = {:TEST})))) OR (CASE WHEN json_valid([[demo4_self_rel_one.json_object]]) THEN JSON_EXTRACT([[demo4_self_rel_one.json_object]], '$.a') ELSE JSON_EXTRACT(json_object('pb', [[demo4_self_rel_one.json_object]]), '$.pb.a') END) > 1)",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"back relations via single relation field (without unique index)",
|
||||||
|
"demo3",
|
||||||
|
"demo4_via_rel_one_cascade.id = true",
|
||||||
|
false,
|
||||||
|
"SELECT DISTINCT `demo3`.* FROM `demo3` LEFT JOIN `demo4` `demo3_demo4_via_rel_one_cascade` ON [[demo3.id]] IN (SELECT [[demo3_demo4_via_rel_one_cascade_je.value]] FROM json_each(CASE WHEN json_valid([[demo3_demo4_via_rel_one_cascade.rel_one_cascade]]) THEN [[demo3_demo4_via_rel_one_cascade.rel_one_cascade]] ELSE json_array([[demo3_demo4_via_rel_one_cascade.rel_one_cascade]]) END) {{demo3_demo4_via_rel_one_cascade_je}}) WHERE ((([[demo3_demo4_via_rel_one_cascade.id]] = 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo3_demo4_via_rel_one_cascade.id]] as [[multiMatchValue]] FROM `demo3` `__mm_demo3` LEFT JOIN `demo4` `__mm_demo3_demo4_via_rel_one_cascade` ON [[__mm_demo3.id]] IN (SELECT [[__mm_demo3_demo4_via_rel_one_cascade_je.value]] FROM json_each(CASE WHEN json_valid([[__mm_demo3_demo4_via_rel_one_cascade.rel_one_cascade]]) THEN [[__mm_demo3_demo4_via_rel_one_cascade.rel_one_cascade]] ELSE json_array([[__mm_demo3_demo4_via_rel_one_cascade.rel_one_cascade]]) END) {{__mm_demo3_demo4_via_rel_one_cascade_je}}) WHERE `__mm_demo3`.`id` = `demo3`.`id`) {{__smTEST}} WHERE NOT ([[__smTEST.multiMatchValue]] = 1)))))",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"back relations via single relation field (with unique index)",
|
||||||
|
"demo3",
|
||||||
|
"demo4_via_rel_one_unique.id = true",
|
||||||
|
false,
|
||||||
|
"SELECT DISTINCT `demo3`.* FROM `demo3` LEFT JOIN `demo4` `demo3_demo4_via_rel_one_unique` ON [[demo3_demo4_via_rel_one_unique.rel_one_unique]] = [[demo3.id]] WHERE [[demo3_demo4_via_rel_one_unique.id]] = 1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"back relations via multiple relation field (opt/any operators)",
|
||||||
|
"demo3",
|
||||||
|
"demo4_via_rel_many_cascade.id ?= true",
|
||||||
|
false,
|
||||||
|
"SELECT DISTINCT `demo3`.* FROM `demo3` LEFT JOIN `demo4` `demo3_demo4_via_rel_many_cascade` ON [[demo3.id]] IN (SELECT [[demo3_demo4_via_rel_many_cascade_je.value]] FROM json_each(CASE WHEN json_valid([[demo3_demo4_via_rel_many_cascade.rel_many_cascade]]) THEN [[demo3_demo4_via_rel_many_cascade.rel_many_cascade]] ELSE json_array([[demo3_demo4_via_rel_many_cascade.rel_many_cascade]]) END) {{demo3_demo4_via_rel_many_cascade_je}}) WHERE [[demo3_demo4_via_rel_many_cascade.id]] = 1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"back relations via multiple relation field (multi-match operators)",
|
||||||
|
"demo3",
|
||||||
|
"demo4_via_rel_many_cascade.id = true",
|
||||||
|
false,
|
||||||
|
"SELECT DISTINCT `demo3`.* FROM `demo3` LEFT JOIN `demo4` `demo3_demo4_via_rel_many_cascade` ON [[demo3.id]] IN (SELECT [[demo3_demo4_via_rel_many_cascade_je.value]] FROM json_each(CASE WHEN json_valid([[demo3_demo4_via_rel_many_cascade.rel_many_cascade]]) THEN [[demo3_demo4_via_rel_many_cascade.rel_many_cascade]] ELSE json_array([[demo3_demo4_via_rel_many_cascade.rel_many_cascade]]) END) {{demo3_demo4_via_rel_many_cascade_je}}) WHERE ((([[demo3_demo4_via_rel_many_cascade.id]] = 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo3_demo4_via_rel_many_cascade.id]] as [[multiMatchValue]] FROM `demo3` `__mm_demo3` LEFT JOIN `demo4` `__mm_demo3_demo4_via_rel_many_cascade` ON [[__mm_demo3.id]] IN (SELECT [[__mm_demo3_demo4_via_rel_many_cascade_je.value]] FROM json_each(CASE WHEN json_valid([[__mm_demo3_demo4_via_rel_many_cascade.rel_many_cascade]]) THEN [[__mm_demo3_demo4_via_rel_many_cascade.rel_many_cascade]] ELSE json_array([[__mm_demo3_demo4_via_rel_many_cascade.rel_many_cascade]]) END) {{__mm_demo3_demo4_via_rel_many_cascade_je}}) WHERE `__mm_demo3`.`id` = `demo3`.`id`) {{__smTEST}} WHERE NOT ([[__smTEST.multiMatchValue]] = 1)))))",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"back relations via unique multiple relation field (should be the same as multi-match)",
|
||||||
|
"demo3",
|
||||||
|
"demo4_via_rel_many_unique.id = true",
|
||||||
|
false,
|
||||||
|
"SELECT DISTINCT `demo3`.* FROM `demo3` LEFT JOIN `demo4` `demo3_demo4_via_rel_many_unique` ON [[demo3.id]] IN (SELECT [[demo3_demo4_via_rel_many_unique_je.value]] FROM json_each(CASE WHEN json_valid([[demo3_demo4_via_rel_many_unique.rel_many_unique]]) THEN [[demo3_demo4_via_rel_many_unique.rel_many_unique]] ELSE json_array([[demo3_demo4_via_rel_many_unique.rel_many_unique]]) END) {{demo3_demo4_via_rel_many_unique_je}}) WHERE ((([[demo3_demo4_via_rel_many_unique.id]] = 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo3_demo4_via_rel_many_unique.id]] as [[multiMatchValue]] FROM `demo3` `__mm_demo3` LEFT JOIN `demo4` `__mm_demo3_demo4_via_rel_many_unique` ON [[__mm_demo3.id]] IN (SELECT [[__mm_demo3_demo4_via_rel_many_unique_je.value]] FROM json_each(CASE WHEN json_valid([[__mm_demo3_demo4_via_rel_many_unique.rel_many_unique]]) THEN [[__mm_demo3_demo4_via_rel_many_unique.rel_many_unique]] ELSE json_array([[__mm_demo3_demo4_via_rel_many_unique.rel_many_unique]]) END) {{__mm_demo3_demo4_via_rel_many_unique_je}}) WHERE `__mm_demo3`.`id` = `demo3`.`id`) {{__smTEST}} WHERE NOT ([[__smTEST.multiMatchValue]] = 1)))))",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"recursive back relations",
|
||||||
|
"demo3",
|
||||||
|
"demo4_via_rel_many_cascade.rel_one_cascade.demo4_via_rel_many_cascade.id ?= true",
|
||||||
|
false,
|
||||||
|
"SELECT DISTINCT `demo3`.* FROM `demo3` LEFT JOIN `demo4` `demo3_demo4_via_rel_many_cascade` ON [[demo3.id]] IN (SELECT [[demo3_demo4_via_rel_many_cascade_je.value]] FROM json_each(CASE WHEN json_valid([[demo3_demo4_via_rel_many_cascade.rel_many_cascade]]) THEN [[demo3_demo4_via_rel_many_cascade.rel_many_cascade]] ELSE json_array([[demo3_demo4_via_rel_many_cascade.rel_many_cascade]]) END) {{demo3_demo4_via_rel_many_cascade_je}}) LEFT JOIN `demo3` `demo3_demo4_via_rel_many_cascade_rel_one_cascade` ON [[demo3_demo4_via_rel_many_cascade_rel_one_cascade.id]] = [[demo3_demo4_via_rel_many_cascade.rel_one_cascade]] LEFT JOIN `demo4` `demo3_demo4_via_rel_many_cascade_rel_one_cascade_demo4_via_rel_many_cascade` ON [[demo3_demo4_via_rel_many_cascade_rel_one_cascade.id]] IN (SELECT [[demo3_demo4_via_rel_many_cascade_rel_one_cascade_demo4_via_rel_many_cascade_je.value]] FROM json_each(CASE WHEN json_valid([[demo3_demo4_via_rel_many_cascade_rel_one_cascade_demo4_via_rel_many_cascade.rel_many_cascade]]) THEN [[demo3_demo4_via_rel_many_cascade_rel_one_cascade_demo4_via_rel_many_cascade.rel_many_cascade]] ELSE json_array([[demo3_demo4_via_rel_many_cascade_rel_one_cascade_demo4_via_rel_many_cascade.rel_many_cascade]]) END) {{demo3_demo4_via_rel_many_cascade_rel_one_cascade_demo4_via_rel_many_cascade_je}}) WHERE [[demo3_demo4_via_rel_many_cascade_rel_one_cascade_demo4_via_rel_many_cascade.id]] = 1",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"@collection join (opt/any operators)",
|
"@collection join (opt/any operators)",
|
||||||
"demo4",
|
"demo4",
|
||||||
|
@ -403,13 +445,28 @@ func TestRecordFieldResolverResolveSchemaFields(t *testing.T) {
|
||||||
{"self_rel_many.unknown", true, ""},
|
{"self_rel_many.unknown", true, ""},
|
||||||
{"self_rel_many.title", false, "[[demo4_self_rel_many.title]]"},
|
{"self_rel_many.title", false, "[[demo4_self_rel_many.title]]"},
|
||||||
{"self_rel_many.self_rel_one.self_rel_many.title", false, "[[demo4_self_rel_many_self_rel_one_self_rel_many.title]]"},
|
{"self_rel_many.self_rel_one.self_rel_many.title", false, "[[demo4_self_rel_many_self_rel_one_self_rel_many.title]]"},
|
||||||
|
|
||||||
|
// max relations limit
|
||||||
|
{"self_rel_many.self_rel_many.self_rel_many.self_rel_many.self_rel_many.self_rel_many.id", false, "[[demo4_self_rel_many_self_rel_many_self_rel_many_self_rel_many_self_rel_many_self_rel_many.id]]"},
|
||||||
|
{"self_rel_many.self_rel_many.self_rel_many.self_rel_many.self_rel_many.self_rel_many.self_rel_many.id", true, ""},
|
||||||
|
|
||||||
|
// back relations
|
||||||
|
{"rel_one_cascade.demo4_via_title.id", true, ""}, // non-relation via field
|
||||||
|
{"rel_one_cascade.demo4_via_rel_one_cascade.id", false, "[[demo4_rel_one_cascade_demo4_via_rel_one_cascade.id]]"},
|
||||||
|
{"rel_one_cascade.demo4_via_rel_one_cascade.rel_one_cascade.demo4_via_rel_one_cascade.id", false, "[[demo4_rel_one_cascade_demo4_via_rel_one_cascade_rel_one_cascade_demo4_via_rel_one_cascade.id]]"},
|
||||||
|
|
||||||
// json_extract
|
// json_extract
|
||||||
{"json_array.0", false, "(CASE WHEN json_valid([[demo4.json_array]]) THEN JSON_EXTRACT([[demo4.json_array]], '$[0]') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_array]]), '$.pb[0]') END)"},
|
{"json_array.0", false, "(CASE WHEN json_valid([[demo4.json_array]]) THEN JSON_EXTRACT([[demo4.json_array]], '$[0]') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_array]]), '$.pb[0]') END)"},
|
||||||
{"json_object.a.b.c", false, "(CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$.a.b.c') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb.a.b.c') END)"},
|
{"json_object.a.b.c", false, "(CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$.a.b.c') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb.a.b.c') END)"},
|
||||||
// @request.auth relation join:
|
|
||||||
|
// max relations limit shouldn't apply for json paths
|
||||||
|
{"json_object.a.b.c.e.f.g.h.i.j.k.l.m.n.o.p", false, "(CASE WHEN json_valid([[demo4.json_object]]) THEN JSON_EXTRACT([[demo4.json_object]], '$.a.b.c.e.f.g.h.i.j.k.l.m.n.o.p') ELSE JSON_EXTRACT(json_object('pb', [[demo4.json_object]]), '$.pb.a.b.c.e.f.g.h.i.j.k.l.m.n.o.p') END)"},
|
||||||
|
|
||||||
|
// @request.auth relation join
|
||||||
{"@request.auth.rel", false, "[[__auth_users.rel]]"},
|
{"@request.auth.rel", false, "[[__auth_users.rel]]"},
|
||||||
{"@request.auth.rel.title", false, "[[__auth_users_rel.title]]"},
|
{"@request.auth.rel.title", false, "[[__auth_users_rel.title]]"},
|
||||||
// @collection fieds:
|
|
||||||
|
// @collection fieds
|
||||||
{"@collect", true, ""},
|
{"@collect", true, ""},
|
||||||
{"collection.demo4.title", true, ""},
|
{"collection.demo4.title", true, ""},
|
||||||
{"@collection", true, ""},
|
{"@collection", true, ""},
|
||||||
|
@ -441,12 +498,12 @@ func TestRecordFieldResolverResolveSchemaFields(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.Identifier != s.expectName {
|
if r.Identifier != s.expectName {
|
||||||
t.Fatalf("Expected r.Identifier %q, got %q", s.expectName, r.Identifier)
|
t.Fatalf("Expected r.Identifier\n%q\ngot\n%q", s.expectName, r.Identifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
// params should be empty for non @request fields
|
// params should be empty for non @request fields
|
||||||
if len(r.Params) != 0 {
|
if len(r.Params) != 0 {
|
||||||
t.Fatalf("Expected 0 r.Params, got %v", r.Params)
|
t.Fatalf("Expected 0 r.Params, got\n%v", r.Params)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
|
@ -192,3 +192,16 @@ func ParseIndex(createIndexExpr string) Index {
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasColumnUniqueIndex loosely checks whether the specified column has
|
||||||
|
// a single column unique index (WHERE statements are ignored).
|
||||||
|
func HasSingleColumnUniqueIndex(column string, indexes []string) bool {
|
||||||
|
for _, idx := range indexes {
|
||||||
|
parsed := ParseIndex(idx)
|
||||||
|
if parsed.Unique && len(parsed.Columns) == 1 && strings.EqualFold(parsed.Columns[0].Name, column) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package dbutils_test
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/pocketbase/pocketbase/tools/dbutils"
|
"github.com/pocketbase/pocketbase/tools/dbutils"
|
||||||
|
@ -68,21 +69,23 @@ func TestParseIndex(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, s := range scenarios {
|
for i, s := range scenarios {
|
||||||
|
t.Run(fmt.Sprintf("scenario_%d", i), func(t *testing.T) {
|
||||||
result := dbutils.ParseIndex(s.index)
|
result := dbutils.ParseIndex(s.index)
|
||||||
|
|
||||||
resultRaw, err := json.Marshal(result)
|
resultRaw, err := json.Marshal(result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("[%d] %v", i, err)
|
t.Fatalf("Faild to marshalize parse result: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
expectedRaw, err := json.Marshal(s.expected)
|
expectedRaw, err := json.Marshal(s.expected)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("[%d] %v", i, err)
|
t.Fatalf("Failed to marshalize expected index: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !bytes.Equal(resultRaw, expectedRaw) {
|
if !bytes.Equal(resultRaw, expectedRaw) {
|
||||||
t.Errorf("[%d] Expected \n%s \ngot \n%s", i, expectedRaw, resultRaw)
|
t.Errorf("Expected \n%s \ngot \n%s", expectedRaw, resultRaw)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -146,11 +149,12 @@ func TestIndexIsValid(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, s := range scenarios {
|
for _, s := range scenarios {
|
||||||
|
t.Run(s.name, func(t *testing.T) {
|
||||||
result := s.index.IsValid()
|
result := s.index.IsValid()
|
||||||
|
|
||||||
if result != s.expected {
|
if result != s.expected {
|
||||||
t.Errorf("[%s] Expected %v, got %v", s.name, s.expected, result)
|
t.Fatalf("Expected %v, got %v", s.expected, result)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -218,10 +222,93 @@ func TestIndexBuild(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, s := range scenarios {
|
for _, s := range scenarios {
|
||||||
|
t.Run(s.name, func(t *testing.T) {
|
||||||
result := s.index.Build()
|
result := s.index.Build()
|
||||||
|
|
||||||
if result != s.expected {
|
if result != s.expected {
|
||||||
t.Errorf("[%s] Expected \n%v \ngot \n%v", s.name, s.expected, result)
|
t.Fatalf("Expected \n%v \ngot \n%v", s.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHasSingleColumnUniqueIndex(t *testing.T) {
|
||||||
|
scenarios := []struct {
|
||||||
|
name string
|
||||||
|
column string
|
||||||
|
indexes []string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"empty indexes",
|
||||||
|
"test",
|
||||||
|
nil,
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"empty column",
|
||||||
|
"",
|
||||||
|
[]string{
|
||||||
|
"CREATE UNIQUE INDEX `index1` ON `example` (`test`)",
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mismatched column",
|
||||||
|
"test",
|
||||||
|
[]string{
|
||||||
|
"CREATE UNIQUE INDEX `index1` ON `example` (`test2`)",
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"non unique index",
|
||||||
|
"test",
|
||||||
|
[]string{
|
||||||
|
"CREATE INDEX `index1` ON `example` (`test`)",
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matching columnd and unique index",
|
||||||
|
"test",
|
||||||
|
[]string{
|
||||||
|
"CREATE UNIQUE INDEX `index1` ON `example` (`test`)",
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"multiple columns",
|
||||||
|
"test",
|
||||||
|
[]string{
|
||||||
|
"CREATE UNIQUE INDEX `index1` ON `example` (`test`, `test2`)",
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"multiple indexes",
|
||||||
|
"test",
|
||||||
|
[]string{
|
||||||
|
"CREATE UNIQUE INDEX `index1` ON `example` (`test`, `test2`)",
|
||||||
|
"CREATE UNIQUE INDEX `index2` ON `example` (`test`)",
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"partial unique index",
|
||||||
|
"test",
|
||||||
|
[]string{
|
||||||
|
"CREATE UNIQUE INDEX `index` ON `example` (`test`) where test != ''",
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range scenarios {
|
||||||
|
t.Run(s.name, func(t *testing.T) {
|
||||||
|
result := dbutils.HasSingleColumnUniqueIndex(s.column, s.indexes)
|
||||||
|
if result != s.expected {
|
||||||
|
t.Fatalf("Expected %v got %v", s.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
package dbutils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JsonEach returns JSON_EACH SQLite string expression with
|
||||||
|
// some normalizations for non-json columns.
|
||||||
|
func JsonEach(column string) string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
`json_each(CASE WHEN json_valid([[%s]]) THEN [[%s]] ELSE json_array([[%s]]) END)`,
|
||||||
|
column, column, column,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JsonArrayLength returns JSON_ARRAY_LENGTH SQLite string expression
|
||||||
|
// with some normalizations for non-json columns.
|
||||||
|
//
|
||||||
|
// It works with both json and non-json column values.
|
||||||
|
//
|
||||||
|
// Returns 0 for empty string or NULL column values.
|
||||||
|
func JsonArrayLength(column string) string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
`json_array_length(CASE WHEN json_valid([[%s]]) THEN [[%s]] ELSE (CASE WHEN [[%s]] = '' OR [[%s]] IS NULL THEN json_array() ELSE json_array([[%s]]) END) END)`,
|
||||||
|
column, column, column, column, column,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JsonExtract returns a JSON_EXTRACT SQLite string expression with
|
||||||
|
// some normalizations for non-json columns.
|
||||||
|
func JsonExtract(column string, path string) string {
|
||||||
|
// prefix the path with dot if it is not starting with array notation
|
||||||
|
if path != "" && !strings.HasPrefix(path, "[") {
|
||||||
|
path = "." + path
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(
|
||||||
|
// note: the extra object wrapping is needed to workaround the cases where a json_extract is used with non-json columns.
|
||||||
|
"(CASE WHEN json_valid([[%s]]) THEN JSON_EXTRACT([[%s]], '$%s') ELSE JSON_EXTRACT(json_object('pb', [[%s]]), '$.pb%s') END)",
|
||||||
|
column,
|
||||||
|
column,
|
||||||
|
path,
|
||||||
|
column,
|
||||||
|
path,
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
package dbutils_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/pocketbase/pocketbase/tools/dbutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestJsonEach(t *testing.T) {
|
||||||
|
result := dbutils.JsonEach("a.b")
|
||||||
|
|
||||||
|
expected := "json_each(CASE WHEN json_valid([[a.b]]) THEN [[a.b]] ELSE json_array([[a.b]]) END)"
|
||||||
|
|
||||||
|
if result != expected {
|
||||||
|
t.Fatalf("Expected\n%v\ngot\n%v", expected, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJsonArrayLength(t *testing.T) {
|
||||||
|
result := dbutils.JsonArrayLength("a.b")
|
||||||
|
|
||||||
|
expected := "json_array_length(CASE WHEN json_valid([[a.b]]) THEN [[a.b]] ELSE (CASE WHEN [[a.b]] = '' OR [[a.b]] IS NULL THEN json_array() ELSE json_array([[a.b]]) END) END)"
|
||||||
|
|
||||||
|
if result != expected {
|
||||||
|
t.Fatalf("Expected\n%v\ngot\n%v", expected, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJsonExtract(t *testing.T) {
|
||||||
|
scenarios := []struct {
|
||||||
|
name string
|
||||||
|
column string
|
||||||
|
path string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"empty path",
|
||||||
|
"a.b",
|
||||||
|
"",
|
||||||
|
"(CASE WHEN json_valid([[a.b]]) THEN JSON_EXTRACT([[a.b]], '$') ELSE JSON_EXTRACT(json_object('pb', [[a.b]]), '$.pb') END)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"starting with array index",
|
||||||
|
"a.b",
|
||||||
|
"[1].a[2]",
|
||||||
|
"(CASE WHEN json_valid([[a.b]]) THEN JSON_EXTRACT([[a.b]], '$[1].a[2]') ELSE JSON_EXTRACT(json_object('pb', [[a.b]]), '$.pb[1].a[2]') END)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"starting with key",
|
||||||
|
"a.b",
|
||||||
|
"a.b[2].c",
|
||||||
|
"(CASE WHEN json_valid([[a.b]]) THEN JSON_EXTRACT([[a.b]], '$.a.b[2].c') ELSE JSON_EXTRACT(json_object('pb', [[a.b]]), '$.pb.a.b[2].c') END)",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range scenarios {
|
||||||
|
t.Run(s.name, func(t *testing.T) {
|
||||||
|
result := dbutils.JsonExtract(s.column, s.path)
|
||||||
|
|
||||||
|
if result != s.expected {
|
||||||
|
t.Fatalf("Expected\n%v\ngot\n%v", s.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue