Skip to content

Commit

Permalink
[pocketbase#519] improved query performance for relations lookup
Browse files Browse the repository at this point in the history
  • Loading branch information
ganigeorgiev committed Sep 17, 2022
1 parent b8c5456 commit 9cf8987
Show file tree
Hide file tree
Showing 12 changed files with 58 additions and 51 deletions.
61 changes: 34 additions & 27 deletions resolvers/record_field_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac
props := strings.Split(fieldName, ".")

currentCollectionName := r.baseCollection.Name
currentTableAlias := currentCollectionName
currentTableAlias := inflector.Columnify(currentCollectionName)

// check for @collection field (aka. non-relational join)
// must be in the format "@collection.COLLECTION_NAME.FIELD[.FIELD2....]"
Expand All @@ -106,14 +106,14 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac
}

currentCollectionName = props[1]
currentTableAlias = "__collection_" + currentCollectionName
currentTableAlias = inflector.Columnify("__collection_" + currentCollectionName)

collection, err := r.loadCollection(currentCollectionName)
if err != nil {
return "", nil, fmt.Errorf("Failed to load collection %q from field path %q.", currentCollectionName, fieldName)
}

r.addJoin(collection.Name, currentTableAlias, nil)
r.addJoin(inflector.Columnify(collection.Name), currentTableAlias, nil)

props = props[2:] // leave only the collection fields
} else if props[0] == "@request" {
Expand All @@ -129,7 +129,7 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac

// resolve the profile collection fields
currentCollectionName = models.ProfileCollectionName
currentTableAlias = "__user_" + currentCollectionName
currentTableAlias = inflector.Columnify("__user_" + currentCollectionName)

collection, err := r.loadCollection(currentCollectionName)
if err != nil {
Expand All @@ -146,12 +146,16 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac
}

// join the profile collection
r.addJoin(collection.Name, currentTableAlias, dbx.NewExp(fmt.Sprintf(
// aka. profiles.id = profileId
"[[%s.id]] = %s",
inflector.Columnify(currentTableAlias),
profileIdPlaceholder,
), profileIdPlaceholderParam))
r.addJoin(
inflector.Columnify(collection.Name),
currentTableAlias,
dbx.NewExp(fmt.Sprintf(
// aka. profiles.id = profileId
"[[%s.id]] = %s",
currentTableAlias,
profileIdPlaceholder,
), profileIdPlaceholderParam),
)

props = props[3:] // leave only the profile fields
}
Expand All @@ -168,7 +172,7 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac

// base model prop (always available but not part of the collection schema)
if list.ExistInSlice(prop, baseModelFields) {
return fmt.Sprintf("[[%s.%s]]", inflector.Columnify(currentTableAlias), inflector.Columnify(prop)), nil, nil
return fmt.Sprintf("[[%s.%s]]", currentTableAlias, inflector.Columnify(prop)), nil, nil
}

field := collection.Schema.GetFieldByName(prop)
Expand All @@ -178,7 +182,7 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac

// last prop
if i == totalProps-1 {
return fmt.Sprintf("[[%s.%s]]", inflector.Columnify(currentTableAlias), inflector.Columnify(prop)), nil, nil
return fmt.Sprintf("[[%s.%s]]", currentTableAlias, inflector.Columnify(prop)), nil, nil
}

// check if it is a relation field
Expand All @@ -198,20 +202,27 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac
if relErr != nil {
return "", nil, fmt.Errorf("Failed to find field %q collection.", prop)
}

cleanFieldName := inflector.Columnify(field.Name)
newCollectionName := relCollection.Name
newTableAlias := currentTableAlias + "_" + field.Name
newTableAlias := currentTableAlias + "_" + cleanFieldName

jeTable := currentTableAlias + "_" + cleanFieldName + "_je"
jePair := currentTableAlias + "." + cleanFieldName

r.addJoin(
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)`,
jePair, jePair, jePair,
),
jeTable,
nil,
)
r.addJoin(
newCollectionName,
inflector.Columnify(newCollectionName),
newTableAlias,
dbx.NewExp(fmt.Sprintf(
// 'LIKE' expr is used to handle the case when the reference field supports multiple values (aka. is json array)
"[[%s.%s]] LIKE ('%%' || [[%s.%s]] || '%%')",
inflector.Columnify(currentTableAlias),
inflector.Columnify(field.Name),
inflector.Columnify(newTableAlias),
inflector.Columnify("id"),
)),
dbx.NewExp(fmt.Sprintf("[[%s.id]] = [[%s.value]]", newTableAlias, jeTable)),
)

currentCollectionName = newCollectionName
Expand Down Expand Up @@ -296,11 +307,7 @@ func (r *RecordFieldResolver) loadCollection(collectionNameOrId string) (*models
}

func (r *RecordFieldResolver) addJoin(tableName string, tableAlias string, on dbx.Expression) {
tableExpr := fmt.Sprintf(
"%s %s",
inflector.Columnify(tableName),
inflector.Columnify(tableAlias),
)
tableExpr := fmt.Sprintf("%s %s", tableName, tableAlias)

join := join{
id: tableAlias,
Expand Down
20 changes: 10 additions & 10 deletions resolvers/record_field_resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,32 +52,32 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) {
{
"single rel",
[]string{"onerel.title"},
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `demo4_onerel` ON [[demo4.onerel]] LIKE ('%' || [[demo4_onerel.id]] || '%')",
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.onerel]]) THEN [[demo4.onerel]] ELSE json_array([[demo4.onerel]]) END) `demo4_onerel_je` LEFT JOIN `demo4` `demo4_onerel` ON [[demo4_onerel.id]] = [[demo4_onerel_je.value]]",
},
{
"non-relation field + single rel",
[]string{"title", "onerel.title"},
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `demo4_onerel` ON [[demo4.onerel]] LIKE ('%' || [[demo4_onerel.id]] || '%')",
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.onerel]]) THEN [[demo4.onerel]] ELSE json_array([[demo4.onerel]]) END) `demo4_onerel_je` LEFT JOIN `demo4` `demo4_onerel` ON [[demo4_onerel.id]] = [[demo4_onerel_je.value]]",
},
{
"nested incomplete rels",
[]string{"manyrels.onerel"},
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4.manyrels]] LIKE ('%' || [[demo4_manyrels.id]] || '%')",
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.manyrels]]) THEN [[demo4.manyrels]] ELSE json_array([[demo4.manyrels]]) END) `demo4_manyrels_je` LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4_manyrels.id]] = [[demo4_manyrels_je.value]]",
},
{
"nested complete rels",
[]string{"manyrels.onerel.title"},
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4.manyrels]] LIKE ('%' || [[demo4_manyrels.id]] || '%') LEFT JOIN `demo4` `demo4_manyrels_onerel` ON [[demo4_manyrels.onerel]] LIKE ('%' || [[demo4_manyrels_onerel.id]] || '%')",
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.manyrels]]) THEN [[demo4.manyrels]] ELSE json_array([[demo4.manyrels]]) END) `demo4_manyrels_je` LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4_manyrels.id]] = [[demo4_manyrels_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_manyrels.onerel]]) THEN [[demo4_manyrels.onerel]] ELSE json_array([[demo4_manyrels.onerel]]) END) `demo4_manyrels_onerel_je` LEFT JOIN `demo4` `demo4_manyrels_onerel` ON [[demo4_manyrels_onerel.id]] = [[demo4_manyrels_onerel_je.value]]",
},
{
"repeated nested rels",
[]string{"manyrels.onerel.manyrels.onerel.title"},
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4.manyrels]] LIKE ('%' || [[demo4_manyrels.id]] || '%') LEFT JOIN `demo4` `demo4_manyrels_onerel` ON [[demo4_manyrels.onerel]] LIKE ('%' || [[demo4_manyrels_onerel.id]] || '%') LEFT JOIN `demo4` `demo4_manyrels_onerel_manyrels` ON [[demo4_manyrels_onerel.manyrels]] LIKE ('%' || [[demo4_manyrels_onerel_manyrels.id]] || '%') LEFT JOIN `demo4` `demo4_manyrels_onerel_manyrels_onerel` ON [[demo4_manyrels_onerel_manyrels.onerel]] LIKE ('%' || [[demo4_manyrels_onerel_manyrels_onerel.id]] || '%')",
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.manyrels]]) THEN [[demo4.manyrels]] ELSE json_array([[demo4.manyrels]]) END) `demo4_manyrels_je` LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4_manyrels.id]] = [[demo4_manyrels_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_manyrels.onerel]]) THEN [[demo4_manyrels.onerel]] ELSE json_array([[demo4_manyrels.onerel]]) END) `demo4_manyrels_onerel_je` LEFT JOIN `demo4` `demo4_manyrels_onerel` ON [[demo4_manyrels_onerel.id]] = [[demo4_manyrels_onerel_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_manyrels_onerel.manyrels]]) THEN [[demo4_manyrels_onerel.manyrels]] ELSE json_array([[demo4_manyrels_onerel.manyrels]]) END) `demo4_manyrels_onerel_manyrels_je` LEFT JOIN `demo4` `demo4_manyrels_onerel_manyrels` ON [[demo4_manyrels_onerel_manyrels.id]] = [[demo4_manyrels_onerel_manyrels_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_manyrels_onerel_manyrels.onerel]]) THEN [[demo4_manyrels_onerel_manyrels.onerel]] ELSE json_array([[demo4_manyrels_onerel_manyrels.onerel]]) END) `demo4_manyrels_onerel_manyrels_onerel_je` LEFT JOIN `demo4` `demo4_manyrels_onerel_manyrels_onerel` ON [[demo4_manyrels_onerel_manyrels_onerel.id]] = [[demo4_manyrels_onerel_manyrels_onerel_je.value]]",
},
{
"multiple rels",
[]string{"manyrels.title", "onerel.onefile"},
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4.manyrels]] LIKE ('%' || [[demo4_manyrels.id]] || '%') LEFT JOIN `demo4` `demo4_onerel` ON [[demo4.onerel]] LIKE ('%' || [[demo4_onerel.id]] || '%')",
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.manyrels]]) THEN [[demo4.manyrels]] ELSE json_array([[demo4.manyrels]]) END) `demo4_manyrels_je` LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4_manyrels.id]] = [[demo4_manyrels_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4.onerel]]) THEN [[demo4.onerel]] ELSE json_array([[demo4.onerel]]) END) `demo4_onerel_je` LEFT JOIN `demo4` `demo4_onerel` ON [[demo4_onerel.id]] = [[demo4_onerel_je.value]]",
},
{
"@collection join",
Expand All @@ -95,12 +95,12 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) {
"^" +
regexp.QuoteMeta("SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `profiles` `__user_profiles` ON [[__user_profiles.id]] =") +
" {:.*} " +
regexp.QuoteMeta("LEFT JOIN `profiles` `__user_profiles_rel` ON [[__user_profiles.rel]] LIKE ('%' || [[__user_profiles_rel.id]] || '%')") +
regexp.QuoteMeta("LEFT JOIN json_each(CASE WHEN json_valid([[__user_profiles.rel]]) THEN [[__user_profiles.rel]] ELSE json_array([[__user_profiles.rel]]) END) `__user_profiles_rel_je` LEFT JOIN `profiles` `__user_profiles_rel` ON [[__user_profiles_rel.id]] = [[__user_profiles_rel_je.value]]") +
"$",
},
}

for i, s := range scenarios {
for _, s := range scenarios {
query := app.Dao().RecordQuery(collection)

r := resolvers.NewRecordFieldResolver(app.Dao(), collection, requestData)
Expand All @@ -109,14 +109,14 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) {
}

if err := r.UpdateQuery(query); err != nil {
t.Errorf("(%d) UpdateQuery failed with error %v", i, err)
t.Errorf("(%s) UpdateQuery failed with error %v", s.name, err)
continue
}

rawQuery := query.Build().SQL()

if !list.ExistInSliceWithRegex(rawQuery, []string{s.expectQuery}) {
t.Errorf("(%d) Expected query\n %v \ngot:\n %v", i, s.expectQuery, rawQuery)
t.Errorf("(%s) Expected query\n %v \ngot:\n %v", s.name, s.expectQuery, rawQuery)
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion ui/.env
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ PB_PROFILE_COLLECTION = "profiles"
PB_INSTALLER_PARAM = "installer"
PB_RULES_SYNTAX_DOCS = "https://pocketbase.io/docs/manage-collections#rules-filters-syntax"
PB_RELEASES = "https://github.com/pocketbase/pocketbase/releases"
PB_VERSION = "v0.7.3"
PB_VERSION = "v0.7.4"

Large diffs are not rendered by default.

Loading

0 comments on commit 9cf8987

Please sign in to comment.