diff --git a/daos/collection.go b/daos/collection.go index fee14f6c..773afbf3 100644 --- a/daos/collection.go +++ b/daos/collection.go @@ -402,6 +402,8 @@ func (dao *Dao) saveViewCollection(newCollection, oldCollection *models.Collecti // currently we don't support non-string model ids // (see https://github.com/pocketbase/pocketbase/issues/3110). func (dao *Dao) normalizeViewQueryId(query string) (string, error) { + query = strings.Trim(strings.TrimSpace(query), ";") + parsed, err := dao.parseQueryToFields(query) if err != nil { return "", err diff --git a/daos/view.go b/daos/view.go index 2e38d3bc..40d2ad8a 100644 --- a/daos/view.go +++ b/daos/view.go @@ -43,10 +43,10 @@ func (dao *Dao) SaveView(name string, selectQuery string) error { return err } - trimmed := strings.Trim(selectQuery, ";") + selectQuery = strings.Trim(strings.TrimSpace(selectQuery), ";") // try to eagerly detect multiple inline statements - tk := tokenizer.NewFromString(trimmed) + tk := tokenizer.NewFromString(selectQuery) tk.Separators(';') if queryParts, _ := tk.ScanAll(); len(queryParts) > 1 { return errors.New("multiple statements are not supported") @@ -56,7 +56,7 @@ func (dao *Dao) SaveView(name string, selectQuery string) error { // // note: the query is wrapped in a secondary SELECT as a rudimentary // measure to discourage multiple inline sql statements execution. - viewQuery := fmt.Sprintf("CREATE VIEW {{%s}} AS SELECT * FROM (%s)", name, trimmed) + viewQuery := fmt.Sprintf("CREATE VIEW {{%s}} AS SELECT * FROM (%s)", name, selectQuery) if _, err := txDao.DB().NewQuery(viewQuery).Execute(); err != nil { return err } @@ -458,7 +458,7 @@ type identifiersParser struct { } func (p *identifiersParser) parse(selectQuery string) error { - str := strings.Trim(selectQuery, ";") + str := strings.Trim(strings.TrimSpace(selectQuery), ";") str = joinReplaceRegex.ReplaceAllString(str, " _join_ ") str = discardReplaceRegex.ReplaceAllString(str, " _discard_ ") str = commentsReplaceRegex.ReplaceAllString(str, "") @@ -599,13 +599,20 @@ func identifierFromParts(parts []string) (identifier, error) { } result.original = trimRawIdentifier(result.original) - result.alias = trimRawIdentifier(result.alias) + + // we trim the single quote even though it is not a valid column quote character + // because SQLite allows it if the context expects an identifier and not string literal + // (https://www.sqlite.org/lang_keywords.html) + result.alias = trimRawIdentifier(result.alias, "'") return result, nil } -func trimRawIdentifier(rawIdentifier string) string { - const trimChars = "`\"[];" +func trimRawIdentifier(rawIdentifier string, extraTrimChars ...string) string { + trimChars := "`\"[];" + if len(extraTrimChars) > 0 { + trimChars += strings.Join(extraTrimChars, "") + } parts := strings.Split(rawIdentifier, ".") diff --git a/daos/view_test.go b/daos/view_test.go index f5997a94..5c797d84 100644 --- a/daos/view_test.go +++ b/daos/view_test.go @@ -147,34 +147,33 @@ func TestSaveView(t *testing.T) { } for _, s := range scenarios { - err := app.Dao().SaveView(s.viewName, s.query) + t.Run(s.scenarioName, func(t *testing.T) { + err := app.Dao().SaveView(s.viewName, s.query) - hasErr := err != nil - if hasErr != s.expectError { - t.Errorf("[%s] Expected hasErr %v, got %v (%v)", s.scenarioName, s.expectError, hasErr, err) - continue - } - - if hasErr { - continue - } - - infoRows, err := app.Dao().TableInfo(s.viewName) - if err != nil { - t.Errorf("[%s] Failed to fetch table info for %s: %v", s.scenarioName, s.viewName, err) - continue - } - - if len(s.expectColumns) != len(infoRows) { - t.Errorf("[%s] Expected %d columns, got %d", s.scenarioName, len(s.expectColumns), len(infoRows)) - continue - } - - for _, row := range infoRows { - if !list.ExistInSlice(row.Name, s.expectColumns) { - t.Errorf("[%s] Missing %q column in %v", s.scenarioName, row.Name, s.expectColumns) + hasErr := err != nil + if hasErr != s.expectError { + t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) } - } + + if hasErr { + return + } + + infoRows, err := app.Dao().TableInfo(s.viewName) + if err != nil { + t.Fatalf("Failed to fetch table info for %s: %v", s.viewName, err) + } + + if len(s.expectColumns) != len(infoRows) { + t.Fatalf("Expected %d columns, got %d", len(s.expectColumns), len(infoRows)) + } + + for _, row := range infoRows { + if !list.ExistInSlice(row.Name, s.expectColumns) { + t.Fatalf("Missing %q column in %v", row.Name, s.expectColumns) + } + } + }) } ensureNoTempViews(app, t) @@ -272,24 +271,26 @@ func TestCreateViewSchema(t *testing.T) { "datetime", "json", "rel_one", - "rel_many" + "rel_many", + 'single_quoted_custom_literal' as 'single_quoted_column' from demo1 `, false, map[string]string{ - "text": schema.FieldTypeText, - "bool": schema.FieldTypeBool, - "url": schema.FieldTypeUrl, - "select_one": schema.FieldTypeSelect, - "select_many": schema.FieldTypeSelect, - "file_one": schema.FieldTypeFile, - "file_many": schema.FieldTypeFile, - "number_alias": schema.FieldTypeNumber, - "email": schema.FieldTypeEmail, - "datetime": schema.FieldTypeDate, - "json": schema.FieldTypeJson, - "rel_one": schema.FieldTypeRelation, - "rel_many": schema.FieldTypeRelation, + "text": schema.FieldTypeText, + "bool": schema.FieldTypeBool, + "url": schema.FieldTypeUrl, + "select_one": schema.FieldTypeSelect, + "select_many": schema.FieldTypeSelect, + "file_one": schema.FieldTypeFile, + "file_many": schema.FieldTypeFile, + "number_alias": schema.FieldTypeNumber, + "email": schema.FieldTypeEmail, + "datetime": schema.FieldTypeDate, + "json": schema.FieldTypeJson, + "rel_one": schema.FieldTypeRelation, + "rel_many": schema.FieldTypeRelation, + "single_quoted_column": schema.FieldTypeJson, }, }, {