// // Copyright 2026 The InfiniFlow Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // package dao import ( "context" "errors" "ragflow/internal/entity" "strconv" "gorm.io/gorm" "gorm.io/gorm/clause" ) // toInterfaceSlice converts a string slice into an []interface{} for use as // parameterized query variables. func toInterfaceSlice(ss []string) []interface{} { out := make([]interface{}, len(ss)) for i := range ss { out[i] = ss[i] } return out } // FileCommitDAO file commit data access object type FileCommitDAO struct{} // NewFileCommitDAO create file commit DAO func NewFileCommitDAO() *FileCommitDAO { return &FileCommitDAO{} } // GetByID gets a file commit by ID func (dao *FileCommitDAO) GetByID(ctx context.Context, db *gorm.DB, id string) (*entity.FileCommit, error) { var commit entity.FileCommit err := db.WithContext(ctx).Where("id = ?", id).First(&commit).Error if err != nil { return nil, err } return &commit, nil } // Create creates a new file commit record func (dao *FileCommitDAO) Create(ctx context.Context, db *gorm.DB, commit *entity.FileCommit) error { return db.WithContext(ctx).Create(commit).Error } // UpdateTreeState updates the tree_state field for a commit func (dao *FileCommitDAO) UpdateTreeState(ctx context.Context, db *gorm.DB, id string, treeState string) error { return db.WithContext(ctx).Model(&entity.FileCommit{}).Where("id = ?", id).Update("tree_state", treeState).Error } // GetLatestByFolderID gets the latest (most recent) commit for a folder func (dao *FileCommitDAO) GetLatestByFolderID(ctx context.Context, db *gorm.DB, folderID string) (*entity.FileCommit, error) { var commit entity.FileCommit err := db.WithContext(ctx).Where("folder_id = ?", folderID). Order("create_time DESC"). First(&commit).Error if err != nil { return nil, err } return &commit, nil } // allowedFileCommitSorts is the whitelist of safe column names for // ListByFolderID's orderBy parameter to prevent SQL injection. var allowedFileCommitSorts = map[string]string{ "create_time": "create_time", "create_date": "create_date", "update_time": "update_time", "update_date": "update_date", } // ListByFolderID lists commits for a folder with pagination func (dao *FileCommitDAO) ListByFolderID(ctx context.Context, db *gorm.DB, folderID string, page, pageSize int, orderBy string, desc bool) ([]*entity.FileCommit, int64, error) { var commits []*entity.FileCommit var total int64 query := db.WithContext(ctx).Model(&entity.FileCommit{}).Where("folder_id = ?", folderID) if err := query.Count(&total).Error; err != nil { return nil, 0, err } // Sanitize orderBy against whitelist; fall back to create_time. safeCol, ok := allowedFileCommitSorts[orderBy] if !ok { safeCol = "create_time" } orderDirection := "DESC" if !desc { orderDirection = "ASC" } orderClause := safeCol + " " + orderDirection if page > 0 && pageSize > 0 { offset := (page - 1) * pageSize if err := query.Order(orderClause).Offset(offset).Limit(pageSize).Find(&commits).Error; err != nil { return nil, 0, err } } else { if err := query.Order(orderClause).Find(&commits).Error; err != nil { return nil, 0, err } } return commits, total, nil } // ListByIDs lists commits whose IDs are in the given set, preserving the // order of commitIDs (most-recent-first for page-edit history). Returns the // total matched count for pagination. func (dao *FileCommitDAO) ListByIDs(ctx context.Context, db *gorm.DB, commitIDs []string, page, pageSize int) ([]*entity.FileCommit, int64, error) { if len(commitIDs) == 0 { return []*entity.FileCommit{}, 0, nil } var commits []*entity.FileCommit var total int64 query := db.WithContext(ctx).Model(&entity.FileCommit{}).Where("id IN ?", commitIDs) if err := query.Count(&total).Error; err != nil { return nil, 0, err } // Preserve the incoming (most-recent-first) order with a parameterized // CASE expression rather than a MySQL-specific FIELD() interpolation. orderExpr := "CASE id" for i := range commitIDs { orderExpr += " WHEN ? THEN " + strconv.Itoa(i) } orderExpr += " END" ordered := query.Clauses(clause.OrderBy{Expression: clause.Expr{ SQL: orderExpr, Vars: toInterfaceSlice(commitIDs), }}) // Apply pagination in the query itself so long histories never load the // full set into memory before slicing. if page > 0 && pageSize > 0 { ordered = ordered.Offset((page - 1) * pageSize).Limit(pageSize) } if err := ordered.Find(&commits).Error; err != nil { return nil, 0, err } return commits, total, nil } // FileCommitItemDAO file commit item data access object type FileCommitItemDAO struct{} // NewFileCommitItemDAO create file commit item DAO func NewFileCommitItemDAO() *FileCommitItemDAO { return &FileCommitItemDAO{} } // Create creates a new file commit item record func (dao *FileCommitItemDAO) Create(ctx context.Context, db *gorm.DB, item *entity.FileCommitItem) error { return db.WithContext(ctx).Create(item).Error } // ListByCommitID lists all items for a commit func (dao *FileCommitItemDAO) ListByCommitID(ctx context.Context, db *gorm.DB, commitID string) ([]*entity.FileCommitItem, error) { var items []*entity.FileCommitItem err := db.WithContext(ctx).Where("commit_id = ?", commitID).Order("create_time ASC").Find(&items).Error return items, err } // ListByFileID lists all commit items for a specific file (for version history) func (dao *FileCommitItemDAO) ListByFileID(ctx context.Context, db *gorm.DB, fileID string) ([]*entity.FileCommitItem, error) { var items []*entity.FileCommitItem err := db.WithContext(ctx).Where("file_id = ?", fileID).Order("create_time DESC").Find(&items).Error return items, err } // GetByCommitIDAndFileID gets a single commit item by commit and file ID func (dao *FileCommitItemDAO) GetByCommitIDAndFileID(ctx context.Context, db *gorm.DB, commitID, fileID string) (*entity.FileCommitItem, error) { var item entity.FileCommitItem err := db.WithContext(ctx).Where("commit_id = ? AND file_id = ?", commitID, fileID).First(&item).Error if err != nil { return nil, err } return &item, nil } // GetLatestCommitIDByFileID returns the most recent commit_id that modified the // given wiki page file key, or "" if none. Used to build the parent chain for // page-edit commits. func (dao *FileCommitItemDAO) GetLatestCommitIDByFileID(ctx context.Context, db *gorm.DB, fileID string) (string, error) { var item entity.FileCommitItem err := db.WithContext(ctx). Where("file_id = ?", fileID). // Order by the monotonic auto-increment sequence so the most recently // inserted item is always selected even when multiple edits share the // same create_time millisecond. Order("seq DESC"). First(&item).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return "", nil } return "", err } return item.CommitID, nil }