Skip to content

Commit

Permalink
(no tests) collection indexes scaffoldings
Browse files Browse the repository at this point in the history
  • Loading branch information
ganigeorgiev committed Mar 19, 2023
1 parent 695c20a commit a0ec570
Show file tree
Hide file tree
Showing 11 changed files with 348 additions and 85 deletions.
2 changes: 1 addition & 1 deletion apis/collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ func (api *collectionApi) delete(c echo.Context) error {

handlerErr := api.app.OnCollectionBeforeDeleteRequest().Trigger(event, func(e *core.CollectionDeleteEvent) error {
if err := api.app.Dao().DeleteCollection(e.Collection); err != nil {
return NewBadRequestError("Failed to delete collection. Make sure that the collection is not referenced by other collections.", err)
return NewBadRequestError("Failed to delete collection due to existing dependency.", err)
}

return e.HttpContext.NoContent(http.StatusNoContent)
Expand Down
43 changes: 33 additions & 10 deletions daos/collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,11 @@ func (dao *Dao) DeleteCollection(collection *models.Collection) error {
}
}

// trigger views resave to check for dependencies
if err := txDao.resaveViewsWithChangedSchema(collection.Id); err != nil {
return fmt.Errorf("The collection has a view dependency - %w", err)
}

return txDao.Delete(collection)
})
}
Expand All @@ -169,7 +174,7 @@ func (dao *Dao) SaveCollection(collection *models.Collection) error {
}
}

return dao.RunInTransaction(func(txDao *Dao) error {
txErr := dao.RunInTransaction(func(txDao *Dao) error {
// set default collection type
if collection.Type == "" {
collection.Type = models.CollectionTypeBase
Expand All @@ -192,12 +197,18 @@ func (dao *Dao) SaveCollection(collection *models.Collection) error {
}
}

// trigger an update for all views with changed schema as a result of the current collection save
// (ignoring view errors to allow users to update the query from the UI)
txDao.resaveViewsWithChangedSchema(collection.Id)

return nil
})

if txErr != nil {
return txErr
}

// trigger an update for all views with changed schema as a result of the current collection save
// (ignoring view errors to allow users to update the query from the UI)
dao.resaveViewsWithChangedSchema(collection.Id)

return nil
}

// ImportCollections imports the provided collections list within a single transaction.
Expand Down Expand Up @@ -343,7 +354,7 @@ func (dao *Dao) ImportCollections(
// - saves the newCollection
//
// This method returns an error if newCollection is not a "view".
func (dao *Dao) saveViewCollection(newCollection *models.Collection, oldCollection *models.Collection) error {
func (dao *Dao) saveViewCollection(newCollection, oldCollection *models.Collection) error {
if !newCollection.IsView() {
return errors.New("not a view collection")
}
Expand All @@ -358,7 +369,7 @@ func (dao *Dao) saveViewCollection(newCollection *models.Collection, oldCollecti
}

// delete old renamed view
if oldCollection != nil && newCollection.Name != oldCollection.Name {
if oldCollection != nil {
if err := txDao.DeleteView(oldCollection.Name); err != nil {
return err
}
Expand Down Expand Up @@ -388,20 +399,32 @@ func (dao *Dao) resaveViewsWithChangedSchema(excludeIds ...string) error {
continue
}

query := collection.ViewOptions().Query
// clone the existing schema so that it is safe for temp modifications
oldSchema, err := collection.Schema.Clone()
if err != nil {
return err
}

// generate a new schema from the query
newSchema, err := txDao.CreateViewSchema(query)
newSchema, err := txDao.CreateViewSchema(collection.ViewOptions().Query)
if err != nil {
return err
}

// unset the schema field ids to exclude from the comparison
for _, f := range oldSchema.Fields() {
f.Id = ""
}
for _, f := range newSchema.Fields() {
f.Id = ""
}

encodedNewSchema, err := json.Marshal(newSchema)
if err != nil {
return err
}

encodedOldSchema, err := json.Marshal(collection.Schema)
encodedOldSchema, err := json.Marshal(oldSchema)
if err != nil {
return err
}
Expand Down
193 changes: 133 additions & 60 deletions daos/record_table_sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,84 +2,85 @@ package daos

import (
"fmt"
"strconv"
"strings"

validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/models/schema"
"github.com/pocketbase/pocketbase/tools/dbutils"
"github.com/pocketbase/pocketbase/tools/list"
"github.com/pocketbase/pocketbase/tools/security"
"github.com/spf13/cast"
)

// SyncRecordTableSchema compares the two provided collections
// and applies the necessary related record table changes.
//
// If `oldCollection` is null, then only `newCollection` is used to create the record table.
func (dao *Dao) SyncRecordTableSchema(newCollection *models.Collection, oldCollection *models.Collection) error {
// create
if oldCollection == nil {
cols := map[string]string{
schema.FieldNameId: "TEXT PRIMARY KEY DEFAULT ('r'||lower(hex(randomblob(7)))) NOT NULL",
schema.FieldNameCreated: "TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL",
schema.FieldNameUpdated: "TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL",
}

if newCollection.IsAuth() {
cols[schema.FieldNameUsername] = "TEXT NOT NULL"
cols[schema.FieldNameEmail] = "TEXT DEFAULT '' NOT NULL"
cols[schema.FieldNameEmailVisibility] = "BOOLEAN DEFAULT FALSE NOT NULL"
cols[schema.FieldNameVerified] = "BOOLEAN DEFAULT FALSE NOT NULL"
cols[schema.FieldNameTokenKey] = "TEXT NOT NULL"
cols[schema.FieldNamePasswordHash] = "TEXT NOT NULL"
cols[schema.FieldNameLastResetSentAt] = "TEXT DEFAULT '' NOT NULL"
cols[schema.FieldNameLastVerificationSentAt] = "TEXT DEFAULT '' NOT NULL"
}

// ensure that the new collection has an id
if !newCollection.HasId() {
newCollection.RefreshId()
newCollection.MarkAsNew()
}
return dao.RunInTransaction(func(txDao *Dao) error {
// create
// -----------------------------------------------------------
if oldCollection == nil {
cols := map[string]string{
schema.FieldNameId: "TEXT PRIMARY KEY DEFAULT ('r'||lower(hex(randomblob(7)))) NOT NULL",
schema.FieldNameCreated: "TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL",
schema.FieldNameUpdated: "TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL",
}

tableName := newCollection.Name
if newCollection.IsAuth() {
cols[schema.FieldNameUsername] = "TEXT NOT NULL"
cols[schema.FieldNameEmail] = "TEXT DEFAULT '' NOT NULL"
cols[schema.FieldNameEmailVisibility] = "BOOLEAN DEFAULT FALSE NOT NULL"
cols[schema.FieldNameVerified] = "BOOLEAN DEFAULT FALSE NOT NULL"
cols[schema.FieldNameTokenKey] = "TEXT NOT NULL"
cols[schema.FieldNamePasswordHash] = "TEXT NOT NULL"
cols[schema.FieldNameLastResetSentAt] = "TEXT DEFAULT '' NOT NULL"
cols[schema.FieldNameLastVerificationSentAt] = "TEXT DEFAULT '' NOT NULL"
}

// add schema field definitions
for _, field := range newCollection.Schema.Fields() {
cols[field.Name] = field.ColDefinition()
}
// ensure that the new collection has an id
if !newCollection.HasId() {
newCollection.RefreshId()
newCollection.MarkAsNew()
}

// create table
if _, err := dao.DB().CreateTable(tableName, cols).Execute(); err != nil {
return err
}
tableName := newCollection.Name

// add named index on the base `created` column
if _, err := dao.DB().CreateIndex(tableName, "_"+newCollection.Id+"_created_idx", "created").Execute(); err != nil {
return err
}
// add schema field definitions
for _, field := range newCollection.Schema.Fields() {
cols[field.Name] = field.ColDefinition()
}

// add named unique index on the email and tokenKey columns
if newCollection.IsAuth() {
_, err := dao.DB().NewQuery(fmt.Sprintf(
`
CREATE UNIQUE INDEX _%s_username_idx ON {{%s}} ([[username]]);
CREATE UNIQUE INDEX _%s_email_idx ON {{%s}} ([[email]]) WHERE [[email]] != '';
CREATE UNIQUE INDEX _%s_tokenKey_idx ON {{%s}} ([[tokenKey]]);
`,
newCollection.Id, tableName,
newCollection.Id, tableName,
newCollection.Id, tableName,
)).Execute()
if err != nil {
// create table
if _, err := txDao.DB().CreateTable(tableName, cols).Execute(); err != nil {
return err
}
}

return nil
}
// add named unique index on the email and tokenKey columns
if newCollection.IsAuth() {
_, err := txDao.DB().NewQuery(fmt.Sprintf(
`
CREATE UNIQUE INDEX _%s_username_idx ON {{%s}} ([[username]]);
CREATE UNIQUE INDEX _%s_email_idx ON {{%s}} ([[email]]) WHERE [[email]] != '';
CREATE UNIQUE INDEX _%s_tokenKey_idx ON {{%s}} ([[tokenKey]]);
`,
newCollection.Id, tableName,
newCollection.Id, tableName,
newCollection.Id, tableName,
)).Execute()
if err != nil {
return err
}
}

// update
return dao.RunInTransaction(func(txDao *Dao) error {
return txDao.createCollectionIndexes(newCollection)
}

// update
// -----------------------------------------------------------
oldTableName := oldCollection.Name
newTableName := newCollection.Name
oldSchema := oldCollection.Schema
Expand All @@ -89,12 +90,17 @@ func (dao *Dao) SyncRecordTableSchema(newCollection *models.Collection, oldColle

// check for renamed table
if !strings.EqualFold(oldTableName, newTableName) {
_, err := txDao.DB().RenameTable(oldTableName, newTableName).Execute()
_, err := txDao.DB().RenameTable("{{"+oldTableName+"}}", "{{"+newTableName+"}}").Execute()
if err != nil {
return err
}
}

// drop old indexes (if any)
if err := txDao.dropCollectionIndex(oldCollection); err != nil {
return err
}

// check for deleted columns
for _, oldField := range oldSchema.Fields() {
if f := newSchema.GetFieldById(oldField.Id); f != nil {
Expand All @@ -103,7 +109,7 @@ func (dao *Dao) SyncRecordTableSchema(newCollection *models.Collection, oldColle

_, err := txDao.DB().DropColumn(newTableName, oldField.Name).Execute()
if err != nil {
return err
return fmt.Errorf("failed to drop column %s - %w", oldField.Name, err)
}

deletedFieldNames = append(deletedFieldNames, oldField.Name)
Expand All @@ -126,7 +132,7 @@ func (dao *Dao) SyncRecordTableSchema(newCollection *models.Collection, oldColle
// add
_, err := txDao.DB().AddColumn(newTableName, tempName, field.ColDefinition()).Execute()
if err != nil {
return err
return fmt.Errorf("failed to add column %s - %w", field.Name, err)
}
} else if oldField.Name != field.Name {
tempName := field.Name + security.PseudorandomString(5)
Expand All @@ -135,7 +141,7 @@ func (dao *Dao) SyncRecordTableSchema(newCollection *models.Collection, oldColle
// rename
_, err := txDao.DB().RenameColumn(newTableName, oldField.Name, tempName).Execute()
if err != nil {
return err
return fmt.Errorf("failed to rename column %s - %w", oldField.Name, err)
}

renamedFieldNames[oldField.Name] = field.Name
Expand All @@ -154,7 +160,11 @@ func (dao *Dao) SyncRecordTableSchema(newCollection *models.Collection, oldColle
return err
}

return txDao.syncRelationDisplayFieldsChanges(newCollection, renamedFieldNames, deletedFieldNames)
if err := txDao.syncRelationDisplayFieldsChanges(newCollection, renamedFieldNames, deletedFieldNames); err != nil {
return err
}

return txDao.createCollectionIndexes(newCollection)
})
}

Expand Down Expand Up @@ -291,3 +301,66 @@ func (dao *Dao) syncRelationDisplayFieldsChanges(collection *models.Collection,

return nil
}

func (dao *Dao) dropCollectionIndex(collection *models.Collection) error {
if collection.IsView() {
return nil // views don't have indexes
}

return dao.RunInTransaction(func(txDao *Dao) error {
for _, raw := range collection.Indexes {
parsed := dbutils.ParseIndex(cast.ToString(raw))

if !parsed.IsValid() {
continue
}

if _, err := txDao.DB().NewQuery(fmt.Sprintf("DROP INDEX IF EXISTS [[%s]]", parsed.IndexName)).Execute(); err != nil {
return err
}
}

return nil
})
}

func (dao *Dao) createCollectionIndexes(collection *models.Collection) error {
if collection.IsView() {
return nil // views don't have indexes
}

return dao.RunInTransaction(func(txDao *Dao) error {
// upsert new indexes
//
// note: we are returning validation errors because the indexes cannot be
// validated in a form, aka. before persisting the related collection
// record table changes
errs := validation.Errors{}
for i, idx := range collection.Indexes {
idxString := cast.ToString(idx)
parsed := dbutils.ParseIndex(idxString)

if !parsed.IsValid() {
errs[strconv.Itoa(i)] = validation.NewError(
"validation_invalid_index_expression",
fmt.Sprintf("Invalid CREATE INDEX expression."),
)
continue
}

if _, err := txDao.DB().NewQuery(idxString).Execute(); err != nil {
errs[strconv.Itoa(i)] = validation.NewError(
"validation_invalid_index_expression",
fmt.Sprintf("Failed to create index %s - %v.", parsed.IndexName, err.Error()),
)
continue
}
}

if len(errs) > 0 {
return validation.Errors{"indexes": errs}
}

return nil
})
}
4 changes: 4 additions & 0 deletions daos/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ func (dao *Dao) SaveView(name string, selectQuery string) error {
// fetch the view table info to ensure that the view was created
// because missing tables or columns won't return an error
if _, err := txDao.GetTableInfo(name); err != nil {
// manually cleanup previously created view in case the func
// is called in a nested transaction and the error is discarded
txDao.DeleteView(name)

return err
}

Expand Down
Loading

0 comments on commit a0ec570

Please sign in to comment.