2026-05-22 20:26:11 -04:00
|
|
|
package pathscope
|
|
|
|
|
|
|
|
|
|
import (
|
2026-07-16 19:58:44 +02:00
|
|
|
"fmt"
|
2026-05-22 20:26:11 -04:00
|
|
|
"path/filepath"
|
|
|
|
|
"strings"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var likeEscaper = strings.NewReplacer(
|
|
|
|
|
`\`, `\\`,
|
|
|
|
|
`%`, `\%`,
|
|
|
|
|
`_`, `\_`,
|
|
|
|
|
)
|
|
|
|
|
|
2026-07-06 18:52:44 -04:00
|
|
|
// EscapeLike escapes LIKE wildcards in a literal fragment for use with
|
|
|
|
|
// ESCAPE '\' queries.
|
|
|
|
|
func EscapeLike(literal string) string {
|
|
|
|
|
return likeEscaper.Replace(literal)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-22 20:26:11 -04:00
|
|
|
func PrefixLike(pathPrefix string) string {
|
|
|
|
|
clean := filepath.Clean(pathPrefix)
|
|
|
|
|
if clean == string(filepath.Separator) {
|
|
|
|
|
return likeEscaper.Replace(clean) + "%"
|
|
|
|
|
}
|
|
|
|
|
return likeEscaper.Replace(clean) + string(filepath.Separator) + "%"
|
|
|
|
|
}
|
2026-07-16 19:58:44 +02:00
|
|
|
|
|
|
|
|
// CoverageClauses builds one SQL predicate per root matching rows whose
|
|
|
|
|
// column value lies at or under that root, using the exact-match + escaped
|
|
|
|
|
// prefix-LIKE shape (`column = $n OR column LIKE $n+1 ESCAPE '\'`). The
|
|
|
|
|
// prefix pattern ends with a path separator, so a sibling root that merely
|
|
|
|
|
// shares a string prefix (/mnt/movies2 vs /mnt/movies) never matches.
|
|
|
|
|
// Placeholder numbering starts at firstArg; the returned args bind pairwise
|
|
|
|
|
// (root, prefix pattern) in clause order.
|
|
|
|
|
func CoverageClauses(column string, roots []string, firstArg int) ([]string, []any) {
|
|
|
|
|
clauses := make([]string, 0, len(roots))
|
|
|
|
|
args := make([]any, 0, len(roots)*2)
|
|
|
|
|
for i, root := range roots {
|
|
|
|
|
pathArg := firstArg + i*2
|
|
|
|
|
clauses = append(clauses, fmt.Sprintf(`(%s = $%d OR %s LIKE $%d ESCAPE '\')`, column, pathArg, column, pathArg+1))
|
|
|
|
|
args = append(args, root, PrefixLike(root))
|
|
|
|
|
}
|
|
|
|
|
return clauses, args
|
|
|
|
|
}
|