| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | package core | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							|  |  |  | 	"errors" | 
					
						
							|  |  |  | 	"fmt" | 
					
						
							|  |  |  | 	"io" | 
					
						
							|  |  |  | 	"regexp" | 
					
						
							|  |  |  | 	"strings" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	"github.com/pocketbase/dbx" | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 	"github.com/pocketbase/pocketbase/tools/dbutils" | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 	"github.com/pocketbase/pocketbase/tools/inflector" | 
					
						
							|  |  |  | 	"github.com/pocketbase/pocketbase/tools/security" | 
					
						
							|  |  |  | 	"github.com/pocketbase/pocketbase/tools/tokenizer" | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // DeleteView drops the specified view name.
 | 
					
						
							|  |  |  | //
 | 
					
						
							|  |  |  | // This method is a no-op if a view with the provided name doesn't exist.
 | 
					
						
							|  |  |  | //
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | // NB! Be aware that this method is vulnerable to SQL injection and the
 | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | // "name" argument must come only from trusted input!
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | func (app *BaseApp) DeleteView(name string) error { | 
					
						
							|  |  |  | 	_, err := app.DB().NewQuery(fmt.Sprintf( | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 		"DROP VIEW IF EXISTS {{%s}}", | 
					
						
							|  |  |  | 		name, | 
					
						
							|  |  |  | 	)).Execute() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return err | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // SaveView creates (or updates already existing) persistent SQL view.
 | 
					
						
							|  |  |  | //
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | // NB! Be aware that this method is vulnerable to SQL injection and the
 | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | // "selectQuery" argument must come only from trusted input!
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | func (app *BaseApp) SaveView(name string, selectQuery string) error { | 
					
						
							|  |  |  | 	return app.RunInTransaction(func(txApp App) error { | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 		// delete old view (if exists)
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 		if err := txApp.DeleteView(name); err != nil { | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 			return err | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-10-05 14:31:24 +08:00
										 |  |  | 		selectQuery = strings.Trim(strings.TrimSpace(selectQuery), ";") | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 		// try to loosely detect multiple inline statements
 | 
					
						
							| 
									
										
										
										
											2023-10-05 14:31:24 +08:00
										 |  |  | 		tk := tokenizer.NewFromString(selectQuery) | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 		tk.Separators(';') | 
					
						
							|  |  |  | 		if queryParts, _ := tk.ScanAll(); len(queryParts) > 1 { | 
					
						
							|  |  |  | 			return errors.New("multiple statements are not supported") | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// (re)create the view
 | 
					
						
							|  |  |  | 		//
 | 
					
						
							|  |  |  | 		// note: the query is wrapped in a secondary SELECT as a rudimentary
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 		// measure to discourage multiple inline sql statements execution
 | 
					
						
							| 
									
										
										
										
											2023-10-05 14:31:24 +08:00
										 |  |  | 		viewQuery := fmt.Sprintf("CREATE VIEW {{%s}} AS SELECT * FROM (%s)", name, selectQuery) | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 		if _, err := txApp.DB().NewQuery(viewQuery).Execute(); err != nil { | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 			return err | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// fetch the view table info to ensure that the view was created
 | 
					
						
							|  |  |  | 		// because missing tables or columns won't return an error
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 		if _, err := txApp.TableInfo(name); err != nil { | 
					
						
							| 
									
										
										
										
											2023-03-19 22:18:33 +08:00
										 |  |  | 			// manually cleanup previously created view in case the func
 | 
					
						
							|  |  |  | 			// is called in a nested transaction and the error is discarded
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 			txApp.DeleteView(name) | 
					
						
							| 
									
										
										
										
											2023-03-19 22:18:33 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 			return err | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		return nil | 
					
						
							|  |  |  | 	}) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | // CreateViewFields creates a new FieldsList from the provided select query.
 | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | //
 | 
					
						
							|  |  |  | // There are some caveats:
 | 
					
						
							|  |  |  | // - The select query must have an "id" column.
 | 
					
						
							|  |  |  | // - Wildcard ("*") columns are not supported to avoid accidentally leaking sensitive data.
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | func (app *BaseApp) CreateViewFields(selectQuery string) (FieldsList, error) { | 
					
						
							|  |  |  | 	result := NewFieldsList() | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 	suggestedFields, err := parseQueryToFields(app, selectQuery) | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return result, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// note wrap in a transaction in case the selectQuery contains
 | 
					
						
							|  |  |  | 	// multiple statements allowing us to rollback on any error
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 	txErr := app.RunInTransaction(func(txApp App) error { | 
					
						
							|  |  |  | 		info, err := getQueryTableInfo(txApp, selectQuery) | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			return err | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		var hasId bool | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		for _, row := range info { | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 			if row.Name == FieldNameId { | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 				hasId = true | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 			var field Field | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 			if f, ok := suggestedFields[row.Name]; ok { | 
					
						
							|  |  |  | 				field = f.field | 
					
						
							|  |  |  | 			} else { | 
					
						
							|  |  |  | 				field = defaultViewField(row.Name) | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 			result.Add(field) | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		if !hasId { | 
					
						
							| 
									
										
										
										
											2023-05-24 14:25:39 +08:00
										 |  |  | 			return errors.New("missing required id column (you can use `(ROW_NUMBER() OVER()) as id` if you don't have one)") | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		return nil | 
					
						
							|  |  |  | 	}) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return result, txErr | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | // FindRecordByViewFile returns the original Record of the provided view collection file.
 | 
					
						
							|  |  |  | func (app *BaseApp) FindRecordByViewFile(viewCollectionModelOrIdentifier any, fileFieldName string, filename string) (*Record, error) { | 
					
						
							|  |  |  | 	view, err := getCollectionByModelOrIdentifier(app, viewCollectionModelOrIdentifier) | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if !view.IsView() { | 
					
						
							|  |  |  | 		return nil, errors.New("not a view collection") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	var findFirstNonViewQueryFileField func(int) (*queryField, error) | 
					
						
							|  |  |  | 	findFirstNonViewQueryFileField = func(level int) (*queryField, error) { | 
					
						
							|  |  |  | 		// check the level depth to prevent infinite circular recursion
 | 
					
						
							|  |  |  | 		// (the limit is arbitrary and may change in the future)
 | 
					
						
							|  |  |  | 		if level > 5 { | 
					
						
							|  |  |  | 			return nil, errors.New("reached the max recursion level of view collection file field queries") | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 		queryFields, err := parseQueryToFields(app, view.ViewQuery) | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			return nil, err | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		for _, item := range queryFields { | 
					
						
							|  |  |  | 			if item.collection == nil || | 
					
						
							|  |  |  | 				item.original == nil || | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 				item.field.GetName() != fileFieldName { | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 				continue | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			if item.collection.IsView() { | 
					
						
							|  |  |  | 				view = item.collection | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 				fileFieldName = item.original.GetName() | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 				return findFirstNonViewQueryFileField(level + 1) | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			return item, nil | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		return nil, errors.New("no query file field found") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	qf, err := findFirstNonViewQueryFileField(1) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 	cleanFieldName := inflector.Columnify(qf.original.GetName()) | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 	record := &Record{} | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 	query := app.RecordQuery(qf.collection).Limit(1) | 
					
						
							| 
									
										
										
										
											2023-03-08 05:28:35 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 	if opt, ok := qf.original.(MultiValuer); !ok || !opt.IsMultiple() { | 
					
						
							| 
									
										
										
										
											2023-03-08 05:28:35 +08:00
										 |  |  | 		query.AndWhere(dbx.HashExp{cleanFieldName: filename}) | 
					
						
							|  |  |  | 	} else { | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 		query.InnerJoin( | 
					
						
							|  |  |  | 			fmt.Sprintf(`%s as {{_je_file}}`, dbutils.JSONEach(cleanFieldName)), | 
					
						
							|  |  |  | 			dbx.HashExp{"_je_file.value": filename}, | 
					
						
							|  |  |  | 		) | 
					
						
							| 
									
										
										
										
											2023-03-08 05:28:35 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if err := query.One(record); err != nil { | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 		return nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-02-21 22:38:12 +08:00
										 |  |  | 	return record, nil | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // -------------------------------------------------------------------
 | 
					
						
							|  |  |  | // Raw query to schema helpers
 | 
					
						
							|  |  |  | // -------------------------------------------------------------------
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | type queryField struct { | 
					
						
							|  |  |  | 	// field is the final resolved field.
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 	field Field | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// collection refers to the original field's collection model.
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 	// It could be nil if the found query field is not from a collection
 | 
					
						
							|  |  |  | 	collection *Collection | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// original is the original found collection field.
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 	// It could be nil if the found query field is not from a collection
 | 
					
						
							|  |  |  | 	original Field | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | func defaultViewField(name string) Field { | 
					
						
							|  |  |  | 	return &JSONField{ | 
					
						
							|  |  |  | 		Name:    name, | 
					
						
							|  |  |  | 		MaxSize: 1, // unused for views
 | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-08-11 19:29:18 +08:00
										 |  |  | var castRegex = regexp.MustCompile(`(?i)^cast\s*\(.*\s+as\s+(\w+)\s*\)$`) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | func parseQueryToFields(app App, selectQuery string) (map[string]*queryField, error) { | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 	p := new(identifiersParser) | 
					
						
							|  |  |  | 	if err := p.parse(selectQuery); err != nil { | 
					
						
							|  |  |  | 		return nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 	collections, err := findCollectionsByIdentifiers(app, p.tables) | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	result := make(map[string]*queryField, len(p.columns)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	var mainTable identifier | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if len(p.tables) > 0 { | 
					
						
							|  |  |  | 		mainTable = p.tables[0] | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	for _, col := range p.columns { | 
					
						
							|  |  |  | 		colLower := strings.ToLower(col.original) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 		// pk (always assume text field for now)
 | 
					
						
							|  |  |  | 		if col.alias == FieldNameId { | 
					
						
							|  |  |  | 			result[col.alias] = &queryField{ | 
					
						
							|  |  |  | 				field: &TextField{ | 
					
						
							|  |  |  | 					Name:       col.alias, | 
					
						
							|  |  |  | 					System:     true, | 
					
						
							|  |  |  | 					Required:   true, | 
					
						
							|  |  |  | 					PrimaryKey: true, | 
					
						
							| 
									
										
										
										
											2024-11-19 23:21:25 +08:00
										 |  |  | 					Pattern:    `^[a-z0-9]+$`, | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 				}, | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			continue | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-08-11 19:29:18 +08:00
										 |  |  | 		// numeric aggregations
 | 
					
						
							|  |  |  | 		if strings.HasPrefix(colLower, "count(") || strings.HasPrefix(colLower, "total(") { | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 			result[col.alias] = &queryField{ | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 				field: &NumberField{ | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 					Name: col.alias, | 
					
						
							|  |  |  | 				}, | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			continue | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-08-11 19:29:18 +08:00
										 |  |  | 		castMatch := castRegex.FindStringSubmatch(colLower) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// numeric casts
 | 
					
						
							|  |  |  | 		if len(castMatch) == 2 { | 
					
						
							|  |  |  | 			switch castMatch[1] { | 
					
						
							|  |  |  | 			case "real", "integer", "int", "decimal", "numeric": | 
					
						
							|  |  |  | 				result[col.alias] = &queryField{ | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 					field: &NumberField{ | 
					
						
							| 
									
										
										
										
											2023-08-11 19:29:18 +08:00
										 |  |  | 						Name: col.alias, | 
					
						
							|  |  |  | 					}, | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 				continue | 
					
						
							|  |  |  | 			case "text": | 
					
						
							|  |  |  | 				result[col.alias] = &queryField{ | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 					field: &TextField{ | 
					
						
							| 
									
										
										
										
											2023-08-11 19:29:18 +08:00
										 |  |  | 						Name: col.alias, | 
					
						
							|  |  |  | 					}, | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 				continue | 
					
						
							|  |  |  | 			case "boolean", "bool": | 
					
						
							|  |  |  | 				result[col.alias] = &queryField{ | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 					field: &BoolField{ | 
					
						
							| 
									
										
										
										
											2023-08-11 19:29:18 +08:00
										 |  |  | 						Name: col.alias, | 
					
						
							|  |  |  | 					}, | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 				continue | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 		parts := strings.Split(col.original, ".") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		var fieldName string | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 		var collection *Collection | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		if len(parts) == 2 { | 
					
						
							|  |  |  | 			fieldName = parts[1] | 
					
						
							|  |  |  | 			collection = collections[parts[0]] | 
					
						
							|  |  |  | 		} else { | 
					
						
							|  |  |  | 			fieldName = parts[0] | 
					
						
							|  |  |  | 			collection = collections[mainTable.alias] | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 		// fallback to the default field
 | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 		if collection == nil { | 
					
						
							|  |  |  | 			result[col.alias] = &queryField{ | 
					
						
							|  |  |  | 				field: defaultViewField(col.alias), | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			continue | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		if fieldName == "*" { | 
					
						
							|  |  |  | 			return nil, errors.New("dynamic column names are not supported") | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// find the first field by name (case insensitive)
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 		var field Field | 
					
						
							|  |  |  | 		for _, f := range collection.Fields { | 
					
						
							|  |  |  | 			if strings.EqualFold(f.GetName(), fieldName) { | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 				field = f | 
					
						
							|  |  |  | 				break | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 		// fallback to the default field
 | 
					
						
							|  |  |  | 		if field == nil { | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 			result[col.alias] = &queryField{ | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 				field:      defaultViewField(col.alias), | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 				collection: collection, | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			continue | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 		// convert to relation since it is an id reference
 | 
					
						
							|  |  |  | 		if strings.EqualFold(fieldName, FieldNameId) { | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 			result[col.alias] = &queryField{ | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 				field: &RelationField{ | 
					
						
							|  |  |  | 					Name:         col.alias, | 
					
						
							|  |  |  | 					MaxSelect:    1, | 
					
						
							|  |  |  | 					CollectionId: collection.Id, | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 				}, | 
					
						
							|  |  |  | 				collection: collection, | 
					
						
							|  |  |  | 			} | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 			continue | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// we fetch a brand new collection object to avoid using reflection
 | 
					
						
							|  |  |  | 		// or having a dedicated Clone method for each field type
 | 
					
						
							|  |  |  | 		tempCollection, err := app.FindCollectionByNameOrId(collection.Id) | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			return nil, err | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		clone := tempCollection.Fields.GetById(field.GetId()) | 
					
						
							|  |  |  | 		if clone == nil { | 
					
						
							|  |  |  | 			return nil, fmt.Errorf("missing expected field %q (%q) in collection %q", field.GetName(), field.GetId(), tempCollection.Name) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		// set new random id to prevent duplications if the same field is aliased multiple times
 | 
					
						
							|  |  |  | 		clone.SetId("_clone_" + security.PseudorandomString(4)) | 
					
						
							|  |  |  | 		clone.SetName(col.alias) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		result[col.alias] = &queryField{ | 
					
						
							|  |  |  | 			original:   field, | 
					
						
							|  |  |  | 			field:      clone, | 
					
						
							|  |  |  | 			collection: collection, | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return result, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | func findCollectionsByIdentifiers(app App, tables []identifier) (map[string]*Collection, error) { | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 	names := make([]any, 0, len(tables)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	for _, table := range tables { | 
					
						
							|  |  |  | 		if strings.Contains(table.alias, "(") { | 
					
						
							|  |  |  | 			continue // skip expressions
 | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		names = append(names, table.original) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if len(names) == 0 { | 
					
						
							|  |  |  | 		return nil, nil | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 	result := make(map[string]*Collection, len(names)) | 
					
						
							|  |  |  | 	collections := make([]*Collection, 0, len(names)) | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 	err := app.CollectionQuery(). | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 		AndWhere(dbx.In("name", names...)). | 
					
						
							|  |  |  | 		All(&collections) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	for _, table := range tables { | 
					
						
							|  |  |  | 		for _, collection := range collections { | 
					
						
							|  |  |  | 			if collection.Name == table.original { | 
					
						
							|  |  |  | 				result[table.alias] = collection | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return result, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | func getQueryTableInfo(app App, selectQuery string) ([]*TableInfoRow, error) { | 
					
						
							|  |  |  | 	tempView := "_temp_" + security.PseudorandomString(6) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	var info []*TableInfoRow | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	txErr := app.RunInTransaction(func(txApp App) error { | 
					
						
							|  |  |  | 		// create a temp view with the provided query
 | 
					
						
							|  |  |  | 		err := txApp.SaveView(tempView, selectQuery) | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			return err | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// extract the generated view table info
 | 
					
						
							|  |  |  | 		info, err = txApp.TableInfo(tempView) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		return errors.Join(err, txApp.DeleteView(tempView)) | 
					
						
							|  |  |  | 	}) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if txErr != nil { | 
					
						
							|  |  |  | 		return nil, txErr | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return info, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | // -------------------------------------------------------------------
 | 
					
						
							|  |  |  | // Raw query identifiers parser
 | 
					
						
							|  |  |  | // -------------------------------------------------------------------
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | var joinReplaceRegex = regexp.MustCompile(`(?im)\s+(full\s+outer\s+join|left\s+outer\s+join|right\s+outer\s+join|full\s+join|cross\s+join|inner\s+join|outer\s+join|left\s+join|right\s+join|join)\s+?`) | 
					
						
							|  |  |  | var discardReplaceRegex = regexp.MustCompile(`(?im)\s+(where|group\s+by|having|order|limit|with)\s+?`) | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | var commentsReplaceRegex = regexp.MustCompile(`(?m)(\/\*[\s\S]+\*\/)|(--.+$)`) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | type identifier struct { | 
					
						
							|  |  |  | 	original string | 
					
						
							|  |  |  | 	alias    string | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | type identifiersParser struct { | 
					
						
							|  |  |  | 	columns []identifier | 
					
						
							|  |  |  | 	tables  []identifier | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (p *identifiersParser) parse(selectQuery string) error { | 
					
						
							| 
									
										
										
										
											2023-10-05 14:31:24 +08:00
										 |  |  | 	str := strings.Trim(strings.TrimSpace(selectQuery), ";") | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 	str = joinReplaceRegex.ReplaceAllString(str, " _join_ ") | 
					
						
							|  |  |  | 	str = discardReplaceRegex.ReplaceAllString(str, " _discard_ ") | 
					
						
							|  |  |  | 	str = commentsReplaceRegex.ReplaceAllString(str, "") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	tk := tokenizer.NewFromString(str) | 
					
						
							|  |  |  | 	tk.Separators(',', ' ', '\n', '\t') | 
					
						
							|  |  |  | 	tk.KeepSeparator(true) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	var skip bool | 
					
						
							|  |  |  | 	var partType string | 
					
						
							|  |  |  | 	var activeBuilder *strings.Builder | 
					
						
							|  |  |  | 	var selectParts strings.Builder | 
					
						
							|  |  |  | 	var fromParts strings.Builder | 
					
						
							|  |  |  | 	var joinParts strings.Builder | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	for { | 
					
						
							|  |  |  | 		token, err := tk.Scan() | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			if err != io.EOF { | 
					
						
							|  |  |  | 				return err | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			break | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		trimmed := strings.ToLower(strings.TrimSpace(token)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		switch trimmed { | 
					
						
							|  |  |  | 		case "select": | 
					
						
							|  |  |  | 			skip = false | 
					
						
							|  |  |  | 			partType = "select" | 
					
						
							|  |  |  | 			activeBuilder = &selectParts | 
					
						
							| 
									
										
										
										
											2023-04-24 17:53:27 +08:00
										 |  |  | 		case "distinct": | 
					
						
							|  |  |  | 			continue // ignore as it is not important for the identifiers parsing
 | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 		case "from": | 
					
						
							|  |  |  | 			skip = false | 
					
						
							|  |  |  | 			partType = "from" | 
					
						
							|  |  |  | 			activeBuilder = &fromParts | 
					
						
							|  |  |  | 		case "_join_": | 
					
						
							|  |  |  | 			skip = false | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// the previous part was also a join
 | 
					
						
							|  |  |  | 			if partType == "join" { | 
					
						
							|  |  |  | 				joinParts.WriteString(",") | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			partType = "join" | 
					
						
							|  |  |  | 			activeBuilder = &joinParts | 
					
						
							|  |  |  | 		case "_discard_": | 
					
						
							| 
									
										
										
										
											2023-04-24 17:53:27 +08:00
										 |  |  | 			// skip following tokens
 | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 			skip = true | 
					
						
							|  |  |  | 		default: | 
					
						
							|  |  |  | 			isJoin := partType == "join" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			if isJoin && trimmed == "on" { | 
					
						
							|  |  |  | 				skip = true | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			if !skip && activeBuilder != nil { | 
					
						
							|  |  |  | 				activeBuilder.WriteString(" ") | 
					
						
							|  |  |  | 				activeBuilder.WriteString(token) | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	selects, err := extractIdentifiers(selectParts.String()) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	froms, err := extractIdentifiers(fromParts.String()) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	joins, err := extractIdentifiers(joinParts.String()) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	p.columns = selects | 
					
						
							|  |  |  | 	p.tables = froms | 
					
						
							|  |  |  | 	p.tables = append(p.tables, joins...) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func extractIdentifiers(rawExpression string) ([]identifier, error) { | 
					
						
							|  |  |  | 	rawTk := tokenizer.NewFromString(rawExpression) | 
					
						
							|  |  |  | 	rawTk.Separators(',') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	rawIdentifiers, err := rawTk.ScanAll() | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	result := make([]identifier, 0, len(rawIdentifiers)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	for _, rawIdentifier := range rawIdentifiers { | 
					
						
							|  |  |  | 		tk := tokenizer.NewFromString(rawIdentifier) | 
					
						
							|  |  |  | 		tk.Separators(' ', '\n', '\t') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		parts, err := tk.ScanAll() | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			return nil, err | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		resolved, err := identifierFromParts(parts) | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			return nil, err | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		result = append(result, resolved) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return result, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func identifierFromParts(parts []string) (identifier, error) { | 
					
						
							|  |  |  | 	var result identifier | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	switch len(parts) { | 
					
						
							|  |  |  | 	case 3: | 
					
						
							|  |  |  | 		if !strings.EqualFold(parts[1], "as") { | 
					
						
							|  |  |  | 			return result, fmt.Errorf(`invalid identifier part - expected "as", got %v`, parts[1]) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		result.original = parts[0] | 
					
						
							|  |  |  | 		result.alias = parts[2] | 
					
						
							|  |  |  | 	case 2: | 
					
						
							|  |  |  | 		result.original = parts[0] | 
					
						
							|  |  |  | 		result.alias = parts[1] | 
					
						
							|  |  |  | 	case 1: | 
					
						
							|  |  |  | 		subParts := strings.Split(parts[0], ".") | 
					
						
							|  |  |  | 		result.original = parts[0] | 
					
						
							|  |  |  | 		result.alias = subParts[len(subParts)-1] | 
					
						
							|  |  |  | 	default: | 
					
						
							|  |  |  | 		return result, fmt.Errorf(`invalid identifier parts %v`, parts) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	result.original = trimRawIdentifier(result.original) | 
					
						
							| 
									
										
										
										
											2023-10-05 14:31:24 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// 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, "'") | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	return result, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-10-05 14:31:24 +08:00
										 |  |  | func trimRawIdentifier(rawIdentifier string, extraTrimChars ...string) string { | 
					
						
							|  |  |  | 	trimChars := "`\"[];" | 
					
						
							|  |  |  | 	if len(extraTrimChars) > 0 { | 
					
						
							|  |  |  | 		trimChars += strings.Join(extraTrimChars, "") | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2023-02-19 01:33:42 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	parts := strings.Split(rawIdentifier, ".") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	for i := range parts { | 
					
						
							|  |  |  | 		parts[i] = strings.Trim(parts[i], trimChars) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return strings.Join(parts, ".") | 
					
						
							|  |  |  | } |