feat(database): add file size metadata and range parameters

- Add byte_size and line_count columns to files table
- Increment SchemaVersion to 2 (requires re-indexing)
- Add DeclarationWithMetadata, FileRange, FileResult types
- Add GetDeclarationsWithMetadata method for file metadata lookup
- Add GetFileWithRange method for paginated file retrieval
- Implement countLines and applyLineRange helpers

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-04 01:30:39 +01:00
parent 128cc313dc
commit d9aab773c6
4 changed files with 226 additions and 19 deletions

View File

@@ -470,10 +470,18 @@ func (s *SQLiteStore) GetDeclarations(ctx context.Context, optionID int64) ([]*D
// CreateFile creates a new file record.
func (s *SQLiteStore) CreateFile(ctx context.Context, file *File) error {
// Compute metadata if not already set
if file.ByteSize == 0 {
file.ByteSize = len(file.Content)
}
if file.LineCount == 0 {
file.LineCount = countLines(file.Content)
}
result, err := s.db.ExecContext(ctx, `
INSERT INTO files (revision_id, file_path, extension, content)
VALUES (?, ?, ?, ?)`,
file.RevisionID, file.FilePath, file.Extension, file.Content,
INSERT INTO files (revision_id, file_path, extension, content, byte_size, line_count)
VALUES (?, ?, ?, ?, ?, ?)`,
file.RevisionID, file.FilePath, file.Extension, file.Content, file.ByteSize, file.LineCount,
)
if err != nil {
return fmt.Errorf("failed to create file: %w", err)
@@ -496,15 +504,23 @@ func (s *SQLiteStore) CreateFilesBatch(ctx context.Context, files []*File) error
defer tx.Rollback()
stmt, err := tx.PrepareContext(ctx, `
INSERT INTO files (revision_id, file_path, extension, content)
VALUES (?, ?, ?, ?)`)
INSERT INTO files (revision_id, file_path, extension, content, byte_size, line_count)
VALUES (?, ?, ?, ?, ?, ?)`)
if err != nil {
return fmt.Errorf("failed to prepare statement: %w", err)
}
defer stmt.Close()
for _, file := range files {
result, err := stmt.ExecContext(ctx, file.RevisionID, file.FilePath, file.Extension, file.Content)
// Compute metadata if not already set
if file.ByteSize == 0 {
file.ByteSize = len(file.Content)
}
if file.LineCount == 0 {
file.LineCount = countLines(file.Content)
}
result, err := stmt.ExecContext(ctx, file.RevisionID, file.FilePath, file.Extension, file.Content, file.ByteSize, file.LineCount)
if err != nil {
return fmt.Errorf("failed to insert file: %w", err)
}
@@ -522,9 +538,9 @@ func (s *SQLiteStore) CreateFilesBatch(ctx context.Context, files []*File) error
func (s *SQLiteStore) GetFile(ctx context.Context, revisionID int64, path string) (*File, error) {
file := &File{}
err := s.db.QueryRowContext(ctx, `
SELECT id, revision_id, file_path, extension, content
SELECT id, revision_id, file_path, extension, content, byte_size, line_count
FROM files WHERE revision_id = ? AND file_path = ?`, revisionID, path,
).Scan(&file.ID, &file.RevisionID, &file.FilePath, &file.Extension, &file.Content)
).Scan(&file.ID, &file.RevisionID, &file.FilePath, &file.Extension, &file.Content, &file.ByteSize, &file.LineCount)
if err == sql.ErrNoRows {
return nil, nil
}
@@ -533,3 +549,110 @@ func (s *SQLiteStore) GetFile(ctx context.Context, revisionID int64, path string
}
return file, nil
}
// GetDeclarationsWithMetadata retrieves declarations with file metadata for an option.
func (s *SQLiteStore) GetDeclarationsWithMetadata(ctx context.Context, revisionID, optionID int64) ([]*DeclarationWithMetadata, error) {
rows, err := s.db.QueryContext(ctx, `
SELECT d.id, d.option_id, d.file_path, d.line,
COALESCE(f.byte_size, 0), COALESCE(f.line_count, 0), (f.id IS NOT NULL)
FROM declarations d
LEFT JOIN files f ON f.revision_id = ? AND f.file_path = d.file_path
WHERE d.option_id = ?`, revisionID, optionID)
if err != nil {
return nil, fmt.Errorf("failed to get declarations with metadata: %w", err)
}
defer rows.Close()
var decls []*DeclarationWithMetadata
for rows.Next() {
decl := &DeclarationWithMetadata{}
if err := rows.Scan(&decl.ID, &decl.OptionID, &decl.FilePath, &decl.Line,
&decl.ByteSize, &decl.LineCount, &decl.HasFile); err != nil {
return nil, fmt.Errorf("failed to scan declaration: %w", err)
}
decls = append(decls, decl)
}
return decls, rows.Err()
}
// GetFileWithRange retrieves a file with a specified line range.
func (s *SQLiteStore) GetFileWithRange(ctx context.Context, revisionID int64, path string, r FileRange) (*FileResult, error) {
file, err := s.GetFile(ctx, revisionID, path)
if err != nil {
return nil, err
}
if file == nil {
return nil, nil
}
return applyLineRange(file, r), nil
}
// countLines counts the number of lines in content.
func countLines(content string) int {
if content == "" {
return 0
}
count := 1
for _, c := range content {
if c == '\n' {
count++
}
}
// Don't count trailing newline as extra line
if len(content) > 0 && content[len(content)-1] == '\n' {
count--
}
return count
}
// applyLineRange extracts a range of lines from a file.
func applyLineRange(file *File, r FileRange) *FileResult {
lines := strings.Split(file.Content, "\n")
totalLines := len(lines)
// Handle trailing newline
if totalLines > 0 && lines[totalLines-1] == "" {
totalLines--
lines = lines[:totalLines]
}
// Apply defaults
offset := r.Offset
if offset < 0 {
offset = 0
}
limit := r.Limit
if limit <= 0 {
limit = 250 // Default limit
}
// Calculate range
startLine := offset + 1 // 1-based
if offset >= totalLines {
// Beyond end of file
return &FileResult{
File: &File{ID: file.ID, RevisionID: file.RevisionID, FilePath: file.FilePath, Extension: file.Extension, Content: "", ByteSize: file.ByteSize, LineCount: file.LineCount},
TotalLines: totalLines,
StartLine: 0,
EndLine: 0,
}
}
endIdx := offset + limit
if endIdx > totalLines {
endIdx = totalLines
}
endLine := endIdx // 1-based (last line included)
// Extract lines
selectedLines := lines[offset:endIdx]
content := strings.Join(selectedLines, "\n")
return &FileResult{
File: &File{ID: file.ID, RevisionID: file.RevisionID, FilePath: file.FilePath, Extension: file.Extension, Content: content, ByteSize: file.ByteSize, LineCount: file.LineCount},
TotalLines: totalLines,
StartLine: startLine,
EndLine: endLine,
}
}