///|
/// checkpoint parquet 解析与快照恢复(M3)。
///
/// checkpoint 每行恰好一个 action(各 action 列中恰一列非空)。不同 writer
/// (Spark 经典 v1 / delta-rs)对列的可空性标注不同,因此这里不依赖声明式
/// schema,而是按 shredded 叶列的实际 def/rep levels 逐记录解析:
/// - 标量叶:def == 该叶 max_definition_level ⇔ 值存在;
/// - map/list 叶组:首条目 rep == 外层深度,后续条目 rep == 组最大深度,
/// "字段存在但无条目"占 1 个 def 较低的槽位;
/// - action 是否出现:以该 action 的规范必填标量(add.path 等)为判据。
/// 已知但未解释的叶(tags 等)按记录消耗槽位保持游标同步;
/// 未知 action 与删除向量必须拒绝,不能静默跳过。
///|
priv struct CpCursor {
c : LeafCursor
mut token : Int
}
///|
fn cp_group(cursors : Array[CpCursor], action : String) -> Array[CpCursor] {
let out : Array[CpCursor] = []
for cc in cursors {
if cc.c.leaf.path.length() > 0 && cc.c.leaf.path[0] == action {
out.push(cc)
}
}
out
}
///|
/// 标量叶:路径最后一个分量等于字段名(不限深度,如 metaData.format.provider)
fn cp_find_scalar(group : Array[CpCursor], name : String) -> CpCursor? {
for cc in group {
let n = cc.c.leaf.path.length()
if n >= 2 && cc.c.leaf.path[n - 1] == name {
return Some(cc)
}
}
None
}
///|
/// list 叶:path = [action, <字段>, element|item]
fn cp_find_list(group : Array[CpCursor], name : String) -> CpCursor? {
for cc in group {
let n = cc.c.leaf.path.length()
if n >= 3 {
let last = cc.c.leaf.path[n - 1]
if (last == "element" || last == "item") && cc.c.leaf.path[n - 2] == name {
return Some(cc)
}
}
}
None
}
///|
/// map 叶:path = [action, <字段>, key|value]
fn cp_find_map(
group : Array[CpCursor],
name : String,
child : String,
) -> CpCursor? {
for cc in group {
let n = cc.c.leaf.path.length()
if n >= 3 && cc.c.leaf.path[n - 1] == child && cc.c.leaf.path[n - 2] == name {
return Some(cc)
}
}
None
}
///|
/// 消耗一个标量叶的当前槽位并返回其值(def == max ⇔ 值存在)
fn cp_take_scalar(cc : CpCursor, rec : Int) -> Cell raise DeltaError {
if cc.c.at_end() {
raise DeltaError::ParseError(
"checkpoint column '\{path_text(cc.c.leaf.path)}' ended early",
)
}
let def = cc.c.peek_def()
let cell = if def == cc.c.leaf.max_definition_level {
shred_value_to_cell(cc.c.leaf.values[cc.c.pos])
} else {
CellNull
}
cc.c.pos = cc.c.pos + 1
cc.token = rec
cell
}
///|
/// 消耗标量槽位;字段在文件中不存在时返回 None
fn cp_scalar(
group : Array[CpCursor],
name : String,
rec : Int,
) -> Cell? raise DeltaError {
match cp_find_scalar(group, name) {
Some(cc) => Some(cp_take_scalar(cc, rec))
None => None
}
}
///|
/// 消耗一个记录的槽位(标量 1 个;map/list 叶按 rep 边界走完本记录)
fn cp_skip_record(cc : CpCursor, rec : Int) -> Unit raise DeltaError {
if cc.c.at_end() {
raise DeltaError::ParseError(
"checkpoint column '\{path_text(cc.c.leaf.path)}' ended early",
)
}
cc.c.pos = cc.c.pos + 1
let max_rep = cc.c.leaf.max_repetition_level
if max_rep > 0 {
while !cc.c.at_end() && cc.c.peek_rep() == max_rep {
cc.c.pos = cc.c.pos + 1
}
}
cc.token = rec
}
///|
fn cp_cell_str(c : Cell?) -> String? {
match c {
Some(CellStr(s)) => Some(s)
_ => None
}
}
///|
/// 消耗一个 list 叶组本记录的槽位;存在条目时返回字符串列表
fn cp_list_strings(
group : Array[CpCursor],
name : String,
rec : Int,
) -> Array[String]? raise DeltaError {
let cc = match cp_find_list(group, name) {
Some(c) => c
None => return None
}
if cc.c.at_end() {
raise DeltaError::ParseError(
"checkpoint column '\{path_text(cc.c.leaf.path)}' ended early",
)
}
let out : Array[String] = []
let present_def = cc.c.leaf.max_definition_level - 1
let def0 = cc.c.peek_def()
if def0 <= present_def {
// list 为 null(def 更低)或存在但为空(def == present_def):各占 1 槽
cc.c.pos = cc.c.pos + 1
cc.token = rec
return Some(out)
}
let max_rep = cc.c.leaf.max_repetition_level
let mut first = true
while !cc.c.at_end() {
let rep = cc.c.peek_rep()
if !first && rep < max_rep {
break
}
if cc.c.peek_def() == cc.c.leaf.max_definition_level {
match shred_value_to_cell(cc.c.leaf.values[cc.c.pos]) {
CellStr(s) => out.push(s)
_ =>
raise DeltaError::ParseError(
"checkpoint list '\{name}' has a non-string element",
)
}
} else {
out.push("")
}
cc.c.pos = cc.c.pos + 1
first = false
}
cc.token = rec
Some(out)
}
///|
/// 消耗一个 string→string map 叶组本记录的槽位;存在条目时返回键值对
fn cp_map_pairs(
group : Array[CpCursor],
name : String,
rec : Int,
) -> Array[(String, String)]? raise DeltaError {
let kcc = match cp_find_map(group, name, "key") {
Some(c) => c
None => return None
}
let vcc = match cp_find_map(group, name, "value") {
Some(c) => c
None => return None
}
if kcc.c.at_end() || vcc.c.at_end() {
raise DeltaError::ParseError("checkpoint map '\{name}' ended early")
}
let out : Array[(String, String)] = []
let present_def = kcc.c.leaf.max_definition_level - 1
let def0 = kcc.c.peek_def()
if def0 <= present_def {
// map 为 null 或存在但为空:key/value 各占 1 槽
kcc.c.pos = kcc.c.pos + 1
vcc.c.pos = vcc.c.pos + 1
kcc.token = rec
vcc.token = rec
return Some(out)
}
let max_rep = kcc.c.leaf.max_repetition_level
let mut first = true
while !kcc.c.at_end() && !vcc.c.at_end() {
let rep = kcc.c.peek_rep()
if !first && rep < max_rep {
break
}
let key = match shred_value_to_cell(kcc.c.leaf.values[kcc.c.pos]) {
CellStr(s) => s
_ =>
raise DeltaError::ParseError(
"checkpoint map '\{name}' has a non-string key",
)
}
let value = if vcc.c.peek_def() == vcc.c.leaf.max_definition_level {
match shred_value_to_cell(vcc.c.leaf.values[vcc.c.pos]) {
CellStr(s) => s
_ => ""
}
} else {
""
}
kcc.c.pos = kcc.c.pos + 1
vcc.c.pos = vcc.c.pos + 1
out.push((key, value))
first = false
}
kcc.token = rec
vcc.token = rec
Some(out)
}
///|
/// 删除向量的任一叶存在即表示该 add 需要 DV 语义;读取端不支持时必须拒绝。
fn cp_has_deletion_vector(
group : Array[CpCursor],
rec : Int,
) -> Bool raise DeltaError {
for cc in group {
let path = cc.c.leaf.path
if path.length() >= 2 && path[1] == "deletionVector" {
let present = cp_scalar([cc], path[path.length() - 1], rec)
match present {
Some(cell) =>
match cell {
CellNull => ()
_ => return true
}
None => ()
}
}
}
false
}
///|
fn checkpoint_file_name(version : Int32) -> String {
let s = version.to_string()
let sb = StringBuilder()
let mut i = 0
while i < 20 - s.length() {
sb.write_char('0')
i = i + 1
}
sb.write_string(s)
sb.write_string(".checkpoint.parquet")
sb.to_string()
}
///|
/// 列出 _delta_log 中单文件 checkpoint 的版本(升序)。
/// 多部分 checkpoint(`.checkpoint..parquet`)显式不支持并报错。
pub fn list_checkpoint_versions(root : String) -> Array[Int32] raise DeltaError {
let dir = log_dir(root)
if !@fs.path_exists(dir) {
return []
}
let names = @fs.read_dir(dir) catch {
@fs.IOError(msg) => raise DeltaError::IoError(msg)
}
let out : Array[Int32] = []
for name in names {
if name.has_suffix(".checkpoint.parquet") {
let stem_len = name.char_length() - ".checkpoint.parquet".char_length()
if stem_len == 20 {
let mut ok = true
let mut v = 0
let mut i = 0
for c in name {
if i < 20 {
let d = c.to_int() - 48
if d < 0 || d > 9 {
ok = false
break
}
v = v * 10 + d
}
i = i + 1
}
if ok && !out.contains(v) {
out.push(v)
}
} else {
raise DeltaError::Unsupported(
"multi-part or nonstandard checkpoint file '\{name}' is not supported (single-file <20-digit>.checkpoint.parquet only)",
)
}
}
}
out.sort()
out
}
///|
/// 解析 checkpoint parquet,返回其中的 protocol / metaData / add 集合。
pub fn parse_checkpoint(
path : String,
) -> (Protocol?, TableInfo?, Array[AddFile]) raise DeltaError {
let data = @fs.read_file_to_bytes(path) catch {
_ => raise DeltaError::IoError("cannot read '\{path}'")
}
let (leaves, row_count) = @parquet.read_bytes_shredded(data) catch {
@parquet.Unsupported(msg) =>
raise DeltaError::Unsupported("cannot read '\{path}': \{msg}")
@parquet.InvalidData(msg) =>
raise DeltaError::ParseError("corrupt parquet '\{path}': \{msg}")
@parquet.Io(msg) =>
raise DeltaError::IoError("cannot read '\{path}': \{msg}")
}
let cursors : Array[CpCursor] = []
let unknown_actions : Array[String] = []
for leaf in leaves {
if leaf.path.length() == 0 {
raise DeltaError::ParseError("checkpoint contains an empty column path")
}
let action = leaf.path[0]
if action != "protocol" && action != "metaData" && action != "add" &&
action != "remove" && action != "txn" && action != "commitInfo" &&
action != "cdc" && action != "domainMetadata" &&
!unknown_actions.contains(action) {
unknown_actions.push(action)
}
cursors.push({ c: { leaf, pos: 0 }, token: -1 })
}
let mut protocol : Protocol? = None
let mut metadata : TableInfo? = None
let files : Array[AddFile] = []
let removed : Array[String] = []
let mut rec = 0
while rec < row_count {
// ---- protocol ----
let min_r = cp_scalar(cp_group(cursors, "protocol"), "minReaderVersion", rec)
let vr = match min_r {
Some(CellI32(x)) => Some(x)
_ => None
}
if vr is Some(_) {
let min_w = cp_scalar(cp_group(cursors, "protocol"), "minWriterVersion", rec)
let vw = match min_w {
Some(CellI32(x)) => x
_ =>
raise DeltaError::ParseError(
"checkpoint protocol.minWriterVersion is not an int",
)
}
let rf = match cp_list_strings(
cp_group(cursors, "protocol"), "readerFeatures", rec,
) {
Some(items) => items
None => []
}
let wf = match cp_list_strings(
cp_group(cursors, "protocol"), "writerFeatures", rec,
) {
Some(items) => items
None => []
}
protocol = Some({
min_reader_version: vr.unwrap(),
min_writer_version: vw,
reader_features: rf,
writer_features: wf,
})
}
// ---- metaData ----
let meta_id = cp_scalar(cp_group(cursors, "metaData"), "id", rec)
if meta_id is Some(CellStr(_)) {
let id = match cp_cell_str(meta_id) {
Some(s) => s
None => ""
}
let schema_string = match cp_cell_str(
cp_scalar(cp_group(cursors, "metaData"), "schemaString", rec),
) {
Some(s) => s
None =>
raise DeltaError::ParseError(
"checkpoint metaData.schemaString is missing",
)
}
let provider = match cp_cell_str(
cp_scalar(cp_group(cursors, "metaData"), "provider", rec),
) {
Some(s) => s
None => "parquet"
}
let partition_columns = match cp_list_strings(
cp_group(cursors, "metaData"), "partitionColumns", rec,
) {
Some(items) => items
None => []
}
let configuration = match cp_map_pairs(
cp_group(cursors, "metaData"), "configuration", rec,
) {
Some(pairs) => pairs
None => []
}
metadata = Some({
id,
name: None,
schema: parse_schema(schema_string),
partition_columns,
format_provider: provider,
configuration,
})
}
// ---- add ----
let add_path = cp_scalar(cp_group(cursors, "add"), "path", rec)
if add_path is Some(CellStr(_)) {
let p = match cp_cell_str(add_path) {
Some(s) => s
None => ""
}
let size = match cp_scalar(cp_group(cursors, "add"), "size", rec) {
Some(CellI64(x)) => x
_ => 0L
}
let modification_time = match cp_scalar(
cp_group(cursors, "add"), "modificationTime", rec,
) {
Some(CellI64(x)) => x
_ => 0L
}
let data_change = match cp_scalar(cp_group(cursors, "add"), "dataChange", rec) {
Some(CellBool(b)) => b
_ => true
}
let stats = cp_cell_str(cp_scalar(cp_group(cursors, "add"), "stats", rec))
let partition_values = match cp_map_pairs(
cp_group(cursors, "add"), "partitionValues", rec,
) {
Some(pairs) => pairs
None => []
}
files.push({
path: decode_action_path(p),
partition_values,
size,
modification_time,
data_change,
stats,
has_deletion_vector: cp_has_deletion_vector(
cp_group(cursors, "add"),
rec,
),
})
}
// ---- remove(收集被剔除路径) ----
let remove_path = cp_scalar(cp_group(cursors, "remove"), "path", rec)
match cp_cell_str(remove_path) {
Some(s) => removed.push(decode_action_path(s))
None => ()
}
// 未知 action 仅在当前记录真实出现(def > 0)时拒绝;
// checkpoint schema 常预声明全空列,不能把列存在本身当作 action。
for action in unknown_actions {
for cc in cp_group(cursors, action) {
if !cc.c.at_end() && cc.c.peek_def() > 0 {
raise DeltaError::Unsupported(
"unknown action '\{action}' in checkpoint '\{path}'",
)
}
}
}
// ---- 全组兜底:未解释/漏掉的叶按记录消耗,保持游标同步 ----
for cc in cursors {
if cc.token != rec {
cp_skip_record(cc, rec)
}
}
rec = rec + 1
}
let kept : Array[AddFile] = []
for f in files {
let mut removed_f = false
for rp in removed {
if f.path == rp {
removed_f = true
}
}
if !removed_f {
kept.push(f)
}
}
(protocol, metadata, kept)
}