// `--csv` glob expansion: the pattern matcher (`*`, and nothing else), the
// pattern stem that names a multi-file table, and the per-file stem used when a
// file is named explicitly.
///|
/// Expand one --csv glob pattern into sorted file paths. Only a single
/// `*` (matching any run within one directory level) is supported;
/// directories are skipped.
fn expand_glob(pattern : String) -> Array[String] raise {
let mut slash = -1
for i, ch in pattern {
if ch == '/' {
slash = i
}
}
let dir = if slash >= 0 { pattern[:slash + 1].to_owned() } else { "" }
let pat = pattern[slash + 1:].to_owned()
let entries = @fs.read_dir(if dir == "" { "." } else { dir }) catch {
_ => raise cli_fail("cannot read directory at \{dir}")
}
let hits : Array[String] = []
for name in entries {
if glob_match(pat, name) && @fs.is_file(dir + name) {
hits.push(dir + name)
}
}
hits.sort()
hits
}
///|
/// Classic single-star glob match: `*` spans any run of characters,
/// everything else compares literally.
fn glob_match(pattern : String, name : String) -> Bool {
let ps : Array[Char] = []
for ch in pattern {
ps.push(ch)
}
let ns : Array[Char] = []
for ch in name {
ns.push(ch)
}
let mut pi = 0
let mut ni = 0
let mut star = -1
let mut mark = 0
while ni < ns.length() {
if pi < ps.length() && (ps[pi] == '*' || ps[pi] == ns[ni]) {
if ps[pi] == '*' {
star = pi
mark = ni
pi += 1
} else {
pi += 1
ni += 1
}
} else if star >= 0 {
pi = star + 1
mark += 1
ni = mark
} else {
return false
}
}
while pi < ps.length() && ps[pi] == '*' {
pi += 1
}
pi == ps.length()
}
///|
/// Table name for a multi-file glob pattern: the basename with `*`
/// removed ("parts/part-*.csv" scans into table "part-.csv"? no —
/// into "part-", keeping the suffix strip of table_stem).
fn pattern_stem(pattern : String) -> String {
let mut start = 0
for i, ch in pattern {
if ch == '/' {
start = i + 1
}
}
let base = pattern[start:].to_owned()
let mut stem = ""
for ch in base {
if ch != '*' {
stem = stem + ch.to_string()
}
}
for suffix in [".csv", ".parquet"] {
if stem.has_suffix(suffix) {
stem = stem[:stem.length() - suffix.length()].to_owned()
}
}
// "part-*.csv" -> "part": the star leaves a separator behind that
// would force quoting in every query
let mut end = stem.length()
while end > 0 {
let last = stem[end - 1]
if last == '-' || last == '_' || last == '.' {
end -= 1
} else {
break
}
}
stem[:end].to_owned()
}
///|
/// File stem: strip directories and a trailing ".csv" or ".parquet".
fn table_stem(path : String) -> String {
let mut start = 0
for i, ch in path {
if ch == '/' {
start = i + 1
}
}
let base = path[start:].to_owned()
for suffix in [".csv", ".parquet"] {
if base.length() > suffix.length() && base.has_suffix(suffix) {
return base[:base.length() - suffix.length()].to_owned()
}
}
base
}
///|
test {
assert_true(usage().contains("moonlake"))
assert_eq(table_stem("lineitem.csv"), "lineitem")
assert_eq(table_stem("/tmp/data/lineitem.csv"), "lineitem")
assert_eq(table_stem("plain.tsv"), "plain.tsv")
// single-star glob matching
assert_true(glob_match("part-*.csv", "part-001.csv"))
assert_true(glob_match("*.csv", "anything.csv"))
assert_true(glob_match("a*b.csv", "aXxYb.csv"))
assert_false(glob_match("part-*.csv", "part-001.tsv"))
assert_false(glob_match("part-*.csv", "xpart-001.csv"))
assert_false(glob_match("a*b.csv", "abcsv"))
// pattern stem: star removed, suffix stripped, separator trimmed
assert_eq(pattern_stem("parts/part-*.csv"), "part")
// a bare "*.csv" keeps no name: build_groups rejects it rather than register
// every file under table ""
assert_eq(pattern_stem("*.csv"), "")
assert_eq(pattern_stem("data-*.csv"), "data") // trailing separator trimmed
}