
后端【免费下载链接】automergeA JSON-like data structure (a CRDT) that can be modified concurrently by different users, and merged again automatically.项目地址https://gitcode.com/gh_mirrors/au/automerge点击查看免费下载注意1.0.0-alpha.2本文档描述的 0.2 时代ColumnDataCAPI下文统称v0已被移除v1 API 现位于 crate 根部不再有v1::前缀。本指南作为面向仍基于 0.2 编写代码的移植参考继续保留。详见 CHANGELOG。Automerge Hexane 列式存储迁移实战从 v0ColumnDataC到 v1PrefixColumnT/ColumnTHexane 是 Automerge 项目底层的列式压缩存储引擎直接以 Automerge 二进制文档格式的列格式存储类型化序列并支持原地编辑插入、删除、splice 直接作用于压缩字节。本文以 rust/hexane/V1_MIGRATION.md 为核心系统讲解如何把基于 v0ColumnDataC游标 API 的代码迁移到 v1 的PrefixColumnT与ColumnT从类型声明、构建、splice、前缀和查询、迭代器 seek到编码器与内存列存储两大迁移战线全部给出可复制的对照代码、API 映射表与 automerge 仓库内的真实迁移案例。读完你可以独立完成一次从 v0 到 v1 的完整迁移并理解背后的 Fenwick 树BIT索引原理与交叉验证脚手架迁移方法。为什么迁移从 O(n) 线性迭代到 O(log n) 树索引v0 的ColumnDataC对前缀和查询advance_acc_by、shift_acc、get_acc_delta使用线性迭代器每次调用都是 O(n)。当列中数据量大时这些按累计值 seek的操作会成为性能瓶颈。v1 的两类列解决了这个问题PrefixColumnT基于 slab 的 Fenwick 树树状数组BIT实现前缀和索引get_prefix、get_index_for_total、sum_range均为 O(log n)ColumnT当你只需要值不需要前缀和时提供 O(log n) 的随机访问且没有 BIT 的额外开销。在 rust/hexane/src/prefix.rs 中可以看到 BIT 节点的实现PrefixSlabWeight同时携带len条目数和prefix前缀和两个分量一棵 Fenwick 树同时支持 O(log S) 的位置查询与 O(log S) 的前缀和查询。README 中给出的整体复杂度表格印证了这一设计| 操作 | 成本 | |-|-| |get(i)、iter_range(a..b)seek | O(log S runs in slab) | |insert/remove/splice| O(log S bytes in slab) | | 前缀和 / find-by-value 查询 | O(log S runs in slab) | |save| O(total bytes)合并边界 run | |load| O(total bytes)全量校验 |复杂度表摘自 rust/hexane/README.md。slab 拆分/合并机制保证树与 slab 在任意编辑模式下保持平衡——每次变更只落在一个 slab 内大删除波及邻居通过 B 树 O(log S) 定位 slab仅重写受影响 run 的字节回程路径上更新一个聚合值。选择正确的 v1 类型迁移的第一步是判断每个列到底需不需要前缀和语义| 需要前缀和吗 | 使用 | |-|-| | 是 —— 计数、按累计值 seek、区间求和 |hexane::PrefixColumnT| | 否 —— 仅随机访问、迭代、splice |hexane::ColumnT|判断依据很直接当列的值是长度或数量时累计值本身就是最关心的数字——它能把第 2 条记录翻译成第 8..15 字节。这正是 automerge 文本索引text列存字符宽度、top列存行首标记使用PrefixColumn的原因。而像visible这种只做布尔标志读取、从不求和的列用Columnbool即可避免 BIT 的维护开销。类型声明迁移最典型的迁移样例是 automerge 的Indexes结构体文本索引三件套// Before text: ColumnDataUIntCursor, // Optionu64 with prefix sums top: ColumnDataBooleanCursor, // bool with prefix sums visible: ColumnDataBooleanCursor, // bool, no prefix sums needed // After text: hexane::PrefixColumnOptionu32, // prefix sums over Optionu32 top: hexane::PrefixColumnbool, // prefix sums over bool visible: hexane::Columnbool, // values only注意两个细节值类型从Optionu64收窄为Optionu32字符宽度用 32 位足够线格式更省是否加Prefix前缀完全取决于是否需要前缀和与值的类型无关——bool也可以有前缀和此时前缀类型是u32。PrefixColumn要求T: PrefixValue。已实现的类型包括bool、u32、u64、i64、Optionu32、Optionu64、Optioni64、NonZeroU32等。PrefixValue的前缀累加类型通常比值类型宽一级以避免溢出见 rust/hexane/src/prefix.rs值类型Prefix累加类型u64u128i64i128boolu32Optionu64u128Optioni64i128PrefixValue还要求实现accumulate与accumulate_run两个方法slab_sum/partial_sum提供默认实现直接解码 run 计算O(segments) 而非 O(items)。构建列FromIteratorPrefixColumn和Column都实现了FromIterator因此.collect()可以直接工作// Before let text: ColumnDataUIntCursor widths.iter() .map(|w| if visible { Some(*w) } else { None }) .collect(); // After let text: PrefixColumnOptionu32 widths.iter() .map(|w| if visible { Some(*w as u32) } else { None }) .collect();如果数据本来就是有序到达的构建 change、解析文件等流式场景还可以跳过 Column 直接走流式编码器Encoder/DeltaEncoder见 rust/hexane/README.md。Splice 操作迁移v1 对删除提供了更明确的语义方法remove_n所有列类型都有见 CHANGELOG// Before (delete) col.splice::u64, _(pos, del, []); // After (delete) col.remove_n(pos, del); // 参数与 splice(pos, del, []) 相同 // Before (insert/replace) col.splice(pos, 1, [Some(value)]); // After —— 相同调用只需匹配新的元素类型 col.splice(pos, 1, [Some(value as u32)]);v1 的splice在内部把每个值视为 1 个条目map(|v| (v, 1))批量插入均匀数据时建议用splice_runs走 run 感知的快速路径rust/hexane/src/prefix.rs。API 迁移参考逐方法对照获取单个值// v0 let val col.get(pos); // → OptionOptionCow_, T // v1 PrefixColumn let val col.values().get(pos); // → OptionT (owned无 Cow) // v1 Column let val col.get(pos); // → OptionTv1 消除了Cow包装Copy类型直接返回 owned 值String/Vecu8这类引用类型返回str/[u8]零拷贝读取见 rust/hexane/README.md。PrefixColumn自身的get返回带前缀上下文的PrefixedValue不需要前缀时通过values()拿到底层Column的普通值读取。迭代区间内的值// v0 let iter col.iter_range(range); // yields OptionCow_, T // v1 PrefixColumn let iter col.values().iter_range(range); // yields T // v1 Column let iter col.iter_range(range); // yields T位置前缀和0..pos 之和// v0 —— 无直接调用必须迭代 // v1 let sum col.get_prefix(pos); // O(log n), returns T::Prefixget_prefix是排他前缀和0..index的和配套的get_total(index)是包含前缀和等于get_prefix(index 1)实现见 rust/hexane/src/prefix.rs。区间前缀和增量// v0 let (delta, item) col.get_acc_delta(start, end); let index delta.as_usize(); let visible item.is_some(); // v1 let index col.sum_range(start..end) as usize; // O(log n), 同 slab 快速路径 let visible col.values().get(end).is_some();sum_range在内部通过迭代器两次advance_to起点、终点一次遍历算出区间和rust/hexane/src/prefix.rs同 slab 内的计算走 BIT 的快速路径。按累计值 seek ——advance_prefixv0 的shift_acc(n)把前进 n 个单位和告诉我落点在哪合二为一。v1 的等价物是PrefixIter上的advance_prefix(n)// v0 let mut iter col.iter_range(range).with_acc(); let start_acc iter.acc(); let tx iter.shift_acc(n)?; let pos tx.pos; let consumed (tx.acc - start_acc).as_usize(); // v1 let mut iter col.iter_range(range); let tx iter.advance_prefix(n)?; let pos tx.pos; let consumed tx.delta as usize; // 迭代器现在位于 pos1可以继续 iter.next() 等advance_prefix返回PrefixSeek { pos, delta, pv }其中pos—— 落点所在条目位置delta——[from, to)区间内消费的前缀量不含该条目本身pv—— 该条目作为PrefixedValue.value、.prefix()排他、.total()包含。边界契约automerge 文本索引依赖此语义单位宽度[1, 1, 1]时advance_prefix(0)落在条目 0advance_prefix(1)落在条目1——越过条目 0 的 1 个单位意味着越过它累计值恰好等于n的条目会被跳过而非返回rust/hexane/src/prefix.rs。实现上它通过get_index_for_total在 B 树上做一次 O(log S) 逆查询定位 slab再在 slab 内做 ceiling 除法定位 run。定位到已知位置 ——advance_to/delta_nthv0 的get_acc_delta(start, pos)返回某位置处的增量与值。v1 的等价物是PrefixIter上的advance_to/delta_nth// v0 let (delta, item) col.get_acc_delta(start, pos); let visible item.is_some(); let index delta.as_usize(); // v1 let tx col.delta(obj_range.start, pos).unwrap(); let visible tx.pv.value.is_some(); let index tx.delta as usize; // 或者在已有的 PrefixIter 上用 iter.delta_nth(pos - start)PrefixColumn上的便捷方法一次性查找、之后不再需要迭代器时用delta方法内部创建临时迭代器// Value prefix context at a position, in one shot let tx col.delta(start, pos)?; let value tx.pv.value; let consumed tx.delta; // [start, pos) 上的和delta(from, to)要求to from反向区间会 panic属于调用方错误见 rust/hexane/src/prefix.rs。已经持有区间或需要继续迭代时直接使用迭代器方法advance_prefix/delta_nth/advance_to避免重复创建迭代器的开销。交叉验证脚手架模式迁移期保平安迁移过程中把旧列暂时留在结构体里做对照验证是文档推荐的稳妥做法// In the struct text: hexane::PrefixColumnOptionu32, text_old: ColumnDataUIntCursor, // 临时保留验证完成后删除 // At every usage site let new_result col.sum_range(start..end) as usize; let (old_delta, _) col_old.get_acc_delta(start, end); assert_eq!(new_result, old_delta.as_usize(), v1 vs v0 mismatch); // Keep text_old in sync at every splice/insert/remove要点每个使用点都做一次assert_eq!双写对照每次 splice/insert/remove 都要同步维护_old列测试全部通过后移除_old字段与所有断言。automerge 仓库内已完成的迁移脚手架均已移除insertColumnDataBooleanCursor→hexane::PrefixColumnbool完成脚手架已移除topColumnDataBooleanCursor→hexane::PrefixColumnbool完成脚手架已移除visibleColumnDataBooleanCursor→hexane::Columnbool完成textColumnDataUIntCursor→hexane::PrefixColumnOptionu32进行中脚手架仍活跃在当前仓库 rust/automerge/src/op_set2/columns.rs 中可以确认text、top、visible已是 hexane 类型text为PrefixColumnOptionu32、top为PrefixColumnbool、visible为Columnbool与迁移目标完全一致。API 摘要与迁移映射所有 seek 类方法统一返回PrefixSeek { pos, delta, pv }delta—— 从 seek 起点到该条目之前不含的排他累计和pv—— 条目本身作为PrefixedValue.value、.prefix()、.total()。迭代器方法推荐 —— 已持有区间时使用| 方法 | 说明 | |-|-| |iter.advance_prefix(n)| 越过n个前缀单位返回落点条目 | |iter.advance_to(pos)| 跳转到位置pos返回条目 前缀增量 |列便捷方法一次性查询| 方法 | 说明 | |-|-| |col.delta(from, to)| seek 到todelta携带[from, to)上的和 |迁移映射表| v0 模式 | v1 替代 | |-|-| |iter.shift_acc(n)→ pos consumed |iter.advance_prefix(n)| |col.get_acc_delta(start, pos)→ delta value |col.delta(start, pos)/iter.delta_nth(n)| |prefix_delta(range)仅求和无值 |sum_range(range)|注意prefix_delta在 v1 中已更名为sum_range见 CHANGELOG。编码器迁移状态automerge 侧XYZCursor::encode_unless_empty的语义陷阱这是迁移中最容易踩的坑v0 的encode_unless_empty只在完全没有追加任何值时才省略编码即迭代器为空且编码器状态为空v1 的save_to_unless(default)只要所有值都等于 default 就省略。对可空列两者恰好一致空列即全 null但对不可空列两者会分叉——不可空列请使用 v1 的encode_to不带_unless以保持与 v0 线格式一致。v1 的save_to_unless检查条件是self.slabs.is_empty() self.state.is_single_run_of(value)见 rust/hexane/src/encoder.rs。已迁移列v0 脚手架已移除 —— v1 为唯一事实来源| 文件 | 列 | 类型 | 备注 | |-|-|-|-| |op_set2/change.rs|obj_actor|OptionActorIdx| 带 remap | |op_set2/change.rs|obj_ctr|Optionu64| | |op_set2/change.rs|key_actor|OptionActorIdx| 带 remap | |op_set2/change.rs|key_str|OptionString| | |op_set2/change.rs|insert|bool| 不可空encode_to| |op_set2/change.rs|action|Action| 不可空encode_to| |op_set2/change.rs|value_meta|ValueMeta| 不可空encode_to| |op_set2/change.rs|pred_count|u32| 不可空encode_to| |op_set2/change.rs|pred_actor|ActorIdx| 不可空带 remap | |op_set2/change.rs|key_ctr|Optioni64| DeltaDeltaEncoder::encode_to_unless(None)| |op_set2/change.rs|pred_ctr|i64| DeltaDeltaEncoder::encode_to| |op_set2/change.rs|expand|bool| 可空-按-默认 | |op_set2/change.rs|mark_name|OptionString| | |op_set2/change/collector.rs::ProgressiveEncoder|key_ctr|Optioni64| DeltaDeltaEncoder::save_to_unless(None)| |op_set2/change/collector.rs::ProgressiveEncoder|pred_ctr|i64| DeltaDeltaEncoder::save_to| |change_graph.rs|actor|ActorIdx| 不可空encode_to| |change_graph.rs|num_deps|usize| 不可空encode_to| |change_graph.rs|seq|usize| DeltaDeltaEncoder::encode_to| |change_graph.rs|max_op|usize| DeltaDeltaEncoder::encode_to| |change_graph.rs|deps|usize| DeltaDeltaEncoder::encode_to| |storage/bundle/builder.rs::BundleChangeWriter|actor|ActorIdx| 不可空经save_to_and_remap带 remap | |storage/bundle/builder.rs::BundleChangeWriter|message|OptionString| 经append_owned| |storage/bundle/builder.rs::BundleChangeWriter|dep_count|u32| 不可空 | |storage/bundle/builder.rs::BundleChangeWriter|extra_count|u32| 不可空 | |storage/bundle/builder.rs::BundleChangeWriter|seq|i64| Delta交叉验证过 | |storage/bundle/builder.rs::BundleChangeWriter|start_op|i64| Delta交叉验证过 | |storage/bundle/builder.rs::BundleChangeWriter|max_op|i64| Delta交叉验证过 | |storage/bundle/builder.rs::BundleChangeWriter|timestamp|i64| Delta交叉验证过 | |storage/bundle/builder.rs::BundleChangeWriter|deps|i64| Delta交叉验证过 | |storage/bundle/builder.rs::BundleOpWriter|obj_actor|OptionActorIdx| 经save_to_unless_and_remap带 remap | |storage/bundle/builder.rs::BundleOpWriter|key_actor|OptionActorIdx| 经save_to_unless_and_remap带 remap | |storage/bundle/builder.rs::BundleOpWriter|key_str|OptionString| 经append_owned| |storage/bundle/builder.rs::BundleOpWriter|id_actor|ActorIdx| 不可空经save_to_and_remap带 remap | |storage/bundle/builder.rs::BundleOpWriter|pred_actor|ActorIdx| 不可空经save_to_and_remap带 remap | |storage/bundle/builder.rs::BundleOpWriter|insert|bool| | |storage/bundle/builder.rs::BundleOpWriter|action|Action| | |storage/bundle/builder.rs::BundleOpWriter|value_meta|ValueMeta| | |storage/bundle/builder.rs::BundleOpWriter|pred_count|u32| | |storage/bundle/builder.rs::BundleOpWriter|obj_ctr|Optioni64| Delta交叉验证过 | |storage/bundle/builder.rs::BundleOpWriter|key_ctr|Optioni64| Delta交叉验证过 | |storage/bundle/builder.rs::BundleOpWriter|id_ctr|i64| Delta交叉验证过 | |storage/bundle/builder.rs::BundleOpWriter|pred_ctr|i64| Delta交叉验证过 | |storage/bundle/builder.rs::BundleOpWriter|expand|bool| | |storage/bundle/builder.rs::BundleOpWriter|mark_name|OptionString| 经append_owned|剩余 v0 编码器与迁移阻塞点| 文件 | 列 | 编码器 | 阻塞点 | |-|-|-|-| |op_set2/change.rs|value|RawCursor::encode_unless_empty|Raw 编码—— v1 的ColumnVecu8使用 RLE产生不同的线格式 | |change_graph.rs|timestamps、messages、extra_bytes_meta|ColumnData*::save_to_unless_empty| 这些是活跃的 v0 列而非流式编码器见列存储迁移一节 | |storage/bundle/builder.rs::BundleOpWriter|value|Encodera, RawCursor| Raw |需要完成的工作类别Raw 编码v1 需要一个RawEncoder无压缩的字节拼接。影响change.rs与bundle/builder.rs中的value列以及 change_graph 中本就是原始Vecu8拷贝的extra_bytes_raw。除此之外其余所有流式编码器类别RLE、Bool、UInt、Delta已全部迁移完成。迁移期间新增的可复用辅助工具hexane::Column::remap(|T| T)—— 遍历 run 并通过f重新发射每个值替换整列。要求T: ColumnValueRef。实现位于 rust/hexane/src/column.rs。hexane::RleEncoder::save_to_and_remap(out, |T| T)/save_to_unless_and_remap(out, unless, |T| T)—— 与save_to/save_to_unless类似但保存时直接应用f无需经过Column往返用RleDecoder直接遍历编码器自身的缓冲区并重发射进新编码器。实现见 rust/hexane/src/encoder.rs。hexane::RleEncoder::append_owned(T)—— 有主值的简写与append(T::Geta)互补让持有OptionString或String的调用点无需包装成append_n_owned(v, 1)即可追加。hexane::DeltaEncodera, T: DeltaValue—— 流式增量编码器镜像RleEncoder的接口append、append_n、extend、save、save_to、静态encode/encode_to。与 v0DeltaCursor及 v1DeltaColumn::from_values字节兼容。支持全部DeltaValue类型u32、u64、i32、i64、usize及其Option_变体。列存储迁移状态automerge 侧与上面的编码器迁移相互独立automerge 在内存中为 op set、change graph 及若干索引保留了活跃的ColumnData*列这些列通过 v0 列 APIsplice、iter、get等读写。已迁移列hexane::Column/hexane::PrefixColumn| 文件 | 字段 | 类型 | |-|-|-| |op_set2/columns.rs::Columns|key_str|hexane::ColumnOptionString| |op_set2/columns.rs::Columns|mark_name|hexane::ColumnOptionString| |op_set2/columns.rs::Columns|expand|hexane::Columnbool| |op_set2/columns.rs::Columns|insert|hexane::PrefixColumnbool| |op_set2/columns.rs::Indexes|text|hexane::PrefixColumnOptionu32| |op_set2/columns.rs::Indexes|top|hexane::PrefixColumnbool| |op_set2/columns.rs::Indexes|visible|hexane::Columnbool| |change_graph.rs::ChangeGraph|num_ops|hexane::Columnu64| |change_graph.rs::ChangeGraph|timestamps|hexane::DeltaColumni64| |change_graph.rs::ChangeGraph|messages|hexane::ColumnOptionString| |change_graph.rs::ChangeGraph|extra_bytes_meta|hexane::PrefixColumnValueMeta经PrefixValue for ValueMeta→ 字节长度前缀 | |op_set2/columns.rs::Columns|value|hexane::RawColumn—— 独立字节 arena自带 slab Fenwick BIT默认max_segments4 KiBsplice_slice([u8])splice(IntoIteratorItem impl AsRef[u8])|在 rust/automerge/src/op_set2/columns.rs 中可以验证这些字段的当前状态。剩余 v0ColumnData*列与迁移阻塞点| 文件 | 字段 | 类型 | 阻塞点 | |-|-|-|-| |op_set2/columns.rs::Columns|id_actor|ColumnDataActorCursor| 平凡 ——ActorCursor RleCursor64, ActorIdxRleValue for ActorIdx已存在只是还没做 | |op_set2/columns.rs::Columns|id_ctr|ColumnDataDeltaCursor|Delta 编码—— v1 有DeltaColumni64但形状不同无流式/splice 等价物。op set 在此列上使用splice、get_acc_delta、find_by_value等迁移还需让 v1 delta 列支持这些操作 | |op_set2/columns.rs::Columns|obj_actor|ColumnDataActorCursor| 与id_actor一样平凡。在iter_range、scope_to_value中大量使用后者要求值升序排列obj_actor满足 | |op_set2/columns.rs::Columns|obj_ctr|ColumnDataUIntCursor| 平凡。用于scope_to_value(obj.counter(), ..)—— 已写好的 v1ColumnOptionu64::scope_to_value已支持此模式 | |op_set2/columns.rs::Columns|key_actor|ColumnDataActorCursor| 平凡 | |op_set2/columns.rs::Columns|key_ctr|ColumnDataDeltaCursor| Delta —— 与id_ctr相同的阻塞点 | |op_set2/columns.rs::Columns|succ_count|ColumnDataUIntCursor| 使用iter().with_acc()与calculate_acc()实现前缀和语义text/list seek。v1 等价物是PrefixColumnu64—— 直接但调用方需改用get_prefix/advance_prefix而非 v0 acc API | |op_set2/columns.rs::Columns|succ_actor|ColumnDataActorCursor| 子列每个 succ 一条。布局与succ_count的前缀和耦合 | |op_set2/columns.rs::Columns|succ_ctr|ColumnDataDeltaCursor| Delta 子列耦合 | |op_set2/columns.rs::Columns|action|ColumnDataActionCursor| 平凡 ——Action是 RLE 且RleValue for Action已存在 | |op_set2/columns.rs::Columns|value_meta|ColumnDataMetaCursor| 平凡 ——RleValue for ValueMeta已存在。但MetaCursor携带Acc权重agg item.length()用于计算value列的字节偏移。v1PrefixColumnValueMeta需要同样的能力 —— 普通Column上没有聚合器钩子 | |op_set2/columns.rs::Indexes|inc|ColumnDataIntCursor| 有符号整数列。v1 已有RleValue for i64基本迁移是平凡的只是还没做 | |op_set2/columns.rs::Indexes|mark|MarkIndexColumn| 自定义列类型包装ColumnDataMarkIndex有自己的内部抽象start/end 标记、查找缓存。迁移是更大的重构不是简单换字段 | |op_set2/op_set/mark_index.rs|MarkIndexColumn::data|ColumnDataMarkIndex| 自定义MarkIndex游标类型需要 v1 编码还需验证 mark 区间查询使用的前缀和语义 |需要完成的工作类别平凡 RLE 迁移id_actor、obj_actor、obj_ctr、key_actor、action、inc、num_ops、messages与已完成的text/top/visible迁移同一模式 —— 改字段类型 更新所有调用方使用 v1 的迭代器/get/splice API。succ_count及其伙伴succ_count、succ_actor、succ_ctr子列耦合意味着这三列必须一起迁移。succ_count使用前缀和语义 → 变为PrefixColumnu32类似text子列由它的前缀和定位。Acc 权重列value_metav0 的MetaCursor提供agg(item) item.length()供列累加。extra_bytes_meta已通过PrefixValue for ValueMeta { to_prefix(v) v.length() as u128 }解决 —— 同样的实现可套用于value_meta。Delta 列id_ctr、key_ctr、succ_ctr、timestamps需要流式DeltaEncoder与DeltaColumn上匹配ColumnDataDeltaCursor的 splice/iter API。这是 v1 工作中最大的一块。MarkIndexColumn独立自定义列逻辑自成一体迁移本身就是一个小项目。迁移完成后的验证单元与模糊测试cargo test -p hexane约 525 个测试含模糊测试与 golden fixtures见 rust/hexane/README.md性能基准cargo bench -p hexanedivan 基准column_ops、load、remap 等格式稳定性hexane 的字节格式就是Automerge 文档格式被 golden fixtures 以字节精确的期望编码钉死对照原始参考实现捕获。如果某个改动触发了这些测试那不是测试失败而是与每一个现存文档的兼容性断裂rust/hexane/README.md。因此在迁移value这类 Raw 编码列之前务必确认RawEncoder的线格式与原RawCursor逐字节一致——这是迁移中唯一的格式级阻塞点其余类别RLE、Bool、UInt、Delta均已打通并得到 golden 测试的保护。赞分享后端【免费下载链接】automergeA JSON-like data structure (a CRDT) that can be modified concurrently by different users, and merged again automatically.项目地址https://gitcode.com/gh_mirrors/au/automerge点击查看免费下载相关推荐Next.js-Auth0 从 V0 到 V1 迁移指南Next.js Auth0 从 V0 到 V1 迁移指南 前言 Next.js Auth0 是一个用于 Next.js 应用的 Auth0 身份验证库。随着 V后端Great Expectations 从 V0 迁移到 V1 完全指南Great Expectations 从 V0 迁移到 V1 完全指南 前言为什么需要迁移 还在为数据质量验证的复杂性而头疼吗还在使用过时的 Great数据质检数据工程如何快速部署Qwen通义千问企业级AI大模型完整实践指南如何快速部署Qwen通义千问企业级AI大模型完整实践指南 通义千问Qwen是阿里巴巴云推出的开源大语言模型系列凭借其卓越的多语言理解能力、32K超长上下人工智能大模型微调LoRA模型量化本地部署模型推理服务上一篇想在电脑上玩Switch游戏却不知道从何开始Ryujinx模拟器为你提供完整解决方案下一篇如何快速掌握阿里巴巴Java编码指南(P3C)从零到精通的完整教程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考