package store import ( "context" "database/sql" "fmt" "github.com/M1ngdaXie/realtime-collab/internal/models" "github.com/google/uuid" ) // CreateDocumentVersion creates a new version snapshot func (s *PostgresStore) CreateDocumentVersion( ctx context.Context, documentID, userID uuid.UUID, snapshot []byte, textPreview *string, label *string, isAuto bool, ) (*models.DocumentVersion, error) { tx, err := s.db.BeginTx(ctx, nil) if err != nil { return nil, fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback() // Get next version number var versionNumber int err = tx.QueryRowContext(ctx, `SELECT get_next_version_number($1)`, documentID, ).Scan(&versionNumber) if err != nil { return nil, fmt.Errorf("failed to get version number: %w", err) } // Insert version query := ` INSERT INTO document_versions (document_id, yjs_snapshot, text_preview, version_number, created_by, version_label, is_auto_generated) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, document_id, text_preview, version_number, created_by, version_label, is_auto_generated, created_at ` var version models.DocumentVersion err = tx.QueryRowContext(ctx, query, documentID, snapshot, textPreview, versionNumber, userID, label, isAuto, ).Scan( &version.ID, &version.DocumentID, &version.TextPreview, &version.VersionNumber, &version.CreatedBy, &version.VersionLabel, &version.IsAutoGenerated, &version.CreatedAt, ) if err != nil { return nil, fmt.Errorf("failed to create version: %w", err) } // Update document metadata _, err = tx.ExecContext(ctx, ` UPDATE documents SET version_count = version_count + 1, last_snapshot_at = NOW() WHERE id = $1 `, documentID) if err != nil { return nil, fmt.Errorf("failed to update document metadata: %w", err) } if err := tx.Commit(); err != nil { return nil, fmt.Errorf("failed to commit transaction: %w", err) } return &version, nil } // ListDocumentVersions returns paginated version list with author info func (s *PostgresStore) ListDocumentVersions( ctx context.Context, documentID uuid.UUID, limit int, offset int, ) ([]models.DocumentVersionWithAuthor, int, error) { // Get total count var total int err := s.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM document_versions WHERE document_id = $1`, documentID, ).Scan(&total) if err != nil { return nil, 0, fmt.Errorf("failed to count versions: %w", err) } // Get paginated versions with author (LEFT JOIN to handle deleted users) query := ` SELECT v.id, v.document_id, v.text_preview, v.version_number, v.created_by, v.version_label, v.is_auto_generated, v.created_at, u.id, u.email, u.name, u.avatar_url FROM document_versions v LEFT JOIN users u ON v.created_by = u.id WHERE v.document_id = $1 ORDER BY v.version_number DESC LIMIT $2 OFFSET $3 ` rows, err := s.db.QueryContext(ctx, query, documentID, limit, offset) if err != nil { return nil, 0, fmt.Errorf("failed to list versions: %w", err) } defer rows.Close() var versions []models.DocumentVersionWithAuthor for rows.Next() { var v models.DocumentVersionWithAuthor var authorID, authorEmail, authorName sql.NullString var authorAvatar *string err := rows.Scan( &v.ID, &v.DocumentID, &v.TextPreview, &v.VersionNumber, &v.CreatedBy, &v.VersionLabel, &v.IsAutoGenerated, &v.CreatedAt, &authorID, &authorEmail, &authorName, &authorAvatar, ) if err != nil { return nil, 0, fmt.Errorf("failed to scan version: %w", err) } // Populate author if exists if authorID.Valid { authorUUID, _ := uuid.Parse(authorID.String) v.Author = &models.User{ ID: authorUUID, Email: authorEmail.String, Name: authorName.String, AvatarURL: authorAvatar, } } versions = append(versions, v) } return versions, total, nil } // GetDocumentVersion retrieves a specific version with full snapshot func (s *PostgresStore) GetDocumentVersion(ctx context.Context, versionID uuid.UUID) (*models.DocumentVersion, error) { query := ` SELECT id, document_id, yjs_snapshot, text_preview, version_number, created_by, version_label, is_auto_generated, created_at FROM document_versions WHERE id = $1 ` var version models.DocumentVersion err := s.db.QueryRowContext(ctx, query, versionID).Scan( &version.ID, &version.DocumentID, &version.YjsSnapshot, &version.TextPreview, &version.VersionNumber, &version.CreatedBy, &version.VersionLabel, &version.IsAutoGenerated, &version.CreatedAt, ) if err == sql.ErrNoRows { return nil, fmt.Errorf("version not found") } if err != nil { return nil, fmt.Errorf("failed to get version: %w", err) } return &version, nil } // GetLatestDocumentVersion gets the most recent version for auto-snapshot comparison func (s *PostgresStore) GetLatestDocumentVersion(ctx context.Context, documentID uuid.UUID) (*models.DocumentVersion, error) { query := ` SELECT id, document_id, yjs_snapshot, text_preview, version_number, created_by, version_label, is_auto_generated, created_at FROM document_versions WHERE document_id = $1 ORDER BY version_number DESC LIMIT 1 ` var version models.DocumentVersion err := s.db.QueryRowContext(ctx, query, documentID).Scan( &version.ID, &version.DocumentID, &version.YjsSnapshot, &version.TextPreview, &version.VersionNumber, &version.CreatedBy, &version.VersionLabel, &version.IsAutoGenerated, &version.CreatedAt, ) if err == sql.ErrNoRows { return nil, nil // No versions yet } if err != nil { return nil, fmt.Errorf("failed to get latest version: %w", err) } return &version, nil }