1use std::collections::HashMap;
4
5use serde_json::{json, Map, Value};
6
7use crate::types::{is_truthy, str_list, AGG_OPS};
8
9use super::definition::{normalize_fields, ComputeDef, FieldDef, Location, RelationDef, Schema};
10
11use crate::command::WriteLinkPolicy;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19pub enum Profile {
20 #[default]
22 Standard,
23 Text2Query,
25}
26
27impl Profile {
28 pub fn as_str(&self) -> &'static str {
30 match self {
31 Profile::Standard => "standard",
32 Profile::Text2Query => "text2query",
33 }
34 }
35
36 pub fn from_str_or_err(s: &str) -> Result<Profile, String> {
38 match s {
39 "standard" => Ok(Profile::Standard),
40 "text2query" => Ok(Profile::Text2Query),
41 other => Err(format!("未知 profile: {other}(仅 standard / text2query)")),
42 }
43 }
44}
45
46#[derive(Debug, Clone)]
48struct Entry {
49 schema: Schema,
50 links: Vec<Location>,
52 version: u64,
54}
55
56pub(super) struct BatchClass {
58 pub order: Vec<String>,
60 pub primaries: HashMap<String, (Value, Location, String)>,
62 pub replicas: HashMap<String, Vec<(Value, Location)>>,
64}
65
66pub(super) enum BatchError {
68 Parse(String),
70 PrimaryDuplicate { name: String, indices: Vec<usize> },
72 LocationConflict {
74 collection: String,
75 prev: String,
76 name: String,
77 source: String,
78 database: Option<String>,
79 schema: Option<String>,
80 },
81}
82
83pub(super) fn classify_batch(items: &[(Value, Location)]) -> Result<BatchClass, BatchError> {
89 let mut order: Vec<String> = Vec::new();
90 let mut primaries: HashMap<String, (Value, Location, String)> = HashMap::new();
91 let mut primary_idx: HashMap<String, usize> = HashMap::new();
92 let mut replicas: HashMap<String, Vec<(Value, Location)>> = HashMap::new();
93
94 for (idx, (defn, loc)) in items.iter().enumerate() {
95 let obj = defn
96 .as_object()
97 .ok_or_else(|| BatchError::Parse("schema 定义必须是对象".to_string()))?;
98 let name = obj
99 .get("name")
100 .and_then(|v| v.as_str())
101 .ok_or_else(|| BatchError::Parse("schema 缺少 name".to_string()))?
102 .to_string();
103 if !order.iter().any(|n| n == &name) {
104 order.push(name.clone());
105 }
106
107 if obj.get("replica").map(is_truthy).unwrap_or(false) {
108 replicas
109 .entry(name)
110 .or_default()
111 .push((defn.clone(), loc.clone()));
112 continue;
113 }
114
115 let collection = obj
116 .get("collection")
117 .and_then(|v| v.as_str())
118 .filter(|s| !s.is_empty())
119 .map(String::from)
120 .unwrap_or_else(|| name.clone());
121 if let Some(first) = primary_idx.get(&name) {
122 return Err(BatchError::PrimaryDuplicate {
124 name,
125 indices: vec![*first, idx],
126 });
127 }
128 primary_idx.insert(name.clone(), idx);
129 primaries.insert(name, (defn.clone(), loc.clone(), collection));
130 }
131
132 let mut quads: Vec<(String, String, Location)> = Vec::new();
134 for name in &order {
135 if let Some((_, loc, coll)) = primaries.get(name) {
136 quads.push((name.clone(), coll.clone(), loc.clone()));
137 }
138 if let Some(rs) = replicas.get(name) {
139 for (defn, loc) in rs {
140 let coll = defn
141 .get("name")
142 .and_then(|v| v.as_str())
143 .unwrap_or(name)
144 .to_string();
145 quads.push((name.clone(), coll, loc.clone()));
146 }
147 }
148 }
149 if let Some(c) = detect_location_conflict(&quads) {
150 return Err(BatchError::LocationConflict {
151 collection: c.collection,
152 prev: c.prev,
153 name: c.name,
154 source: c.source,
155 database: c.database,
156 schema: c.schema,
157 });
158 }
159
160 Ok(BatchClass {
161 order,
162 primaries,
163 replicas,
164 })
165}
166
167pub(super) struct LocationConflict {
169 pub collection: String,
170 pub prev: String,
171 pub name: String,
172 pub source: String,
173 pub database: Option<String>,
174 pub schema: Option<String>,
175}
176
177pub(super) fn detect_location_conflict(
181 entries: &[(String, String, Location)],
182) -> Option<LocationConflict> {
183 let mut seen: HashMap<(String, Option<String>, Option<String>, String), String> = HashMap::new();
184 for (name, collection, loc) in entries {
185 let key = (
186 loc.source.clone(),
187 loc.database.clone(),
188 loc.schema.clone(),
189 collection.clone(),
190 );
191 if let Some(prev) = seen.get(&key) {
192 if prev != name {
193 return Some(LocationConflict {
194 collection: collection.clone(),
195 prev: prev.clone(),
196 name: name.clone(),
197 source: loc.source.clone(),
198 database: loc.database.clone(),
199 schema: loc.schema.clone(),
200 });
201 }
202 } else {
203 seen.insert(key, name.clone());
204 }
205 }
206 None
207}
208
209fn batch_err_to_msg(e: BatchError) -> String {
211 match e {
212 BatchError::Parse(m) => m,
213 BatchError::PrimaryDuplicate { name, .. } => format!(
214 "同名主定义重复: {}(同一装载批次内 name 必须唯一,从定义请用 replica: true)",
215 name
216 ),
217 BatchError::LocationConflict {
218 collection,
219 prev,
220 source,
221 database,
222 schema,
223 ..
224 } => format!(
225 "定位冲突: ({}, {:?}, {:?}, {}) 已被 schema `{}` 占用",
226 source, database, schema, collection, prev
227 ),
228 }
229}
230
231#[derive(Debug, Clone, Default)]
232pub struct Registry {
233 entries: HashMap<String, Entry>,
234 order: Vec<String>,
235 require_context: bool,
239 profile: Profile,
241 rbac: Option<crate::rbac::RbacPolicy>,
243 role_rules: crate::permission::RoleRules,
245 meta_policy: crate::permission::MetaPolicy,
247 write_link_policy: WriteLinkPolicy,
249}
250
251impl Registry {
252 pub fn new() -> Self {
253 Self::default()
254 }
255
256 pub fn register(&mut self, defn: &Value) -> Result<(), String> {
262 self.register_with_ctx(defn, None)
263 }
264
265 pub fn register_with_ctx(
267 &mut self,
268 defn: &Value,
269 ctx: Option<&crate::permission::Context>,
270 ) -> Result<(), String> {
271 self.register_batch(&[(defn.clone(), Location::default())], ctx)
272 }
273
274 pub fn register_batch(
289 &mut self,
290 items: &[(Value, Location)],
291 ctx: Option<&crate::permission::Context>,
292 ) -> Result<(), String> {
293 let class = classify_batch(items).map_err(batch_err_to_msg)?;
295
296 for (defn, _loc) in items {
298 let name = defn.get("name").and_then(|v| v.as_str()).unwrap_or("");
299 if !crate::permission::can_register(&self.meta_policy, ctx) {
300 return Err(format!("ERR_PERMISSION: 无权注册或覆盖定义 {name}"));
301 }
302 }
303
304 struct Planned {
306 name: String,
307 schema: Schema,
308 links: Vec<Location>,
309 }
310 let mut planned: Vec<Planned> = Vec::new();
311 for name in &class.order {
312 let rs: Vec<Location> = class
313 .replicas
314 .get(name)
315 .map(|v| v.iter().map(|(_, l)| l.clone()).collect())
316 .unwrap_or_default();
317 if let Some((raw, loc, collection)) = class.primaries.get(name) {
318 let obj = raw.as_object().unwrap();
319 let schema = build_schema(obj, name.clone(), collection.clone(), loc)?;
320 let is_archive = obj.get("_isArchive").map(is_truthy).unwrap_or(false);
321 let mut links = vec![loc.clone()];
322 links.extend(rs);
323 planned.push(Planned {
324 name: name.clone(),
325 schema: schema.clone(),
326 links: links.clone(),
327 });
328 if !is_archive && !name.ends_with("Deleted") {
330 let aobj = archive_defn(obj, name, &schema.collection)
331 .as_object()
332 .cloned()
333 .unwrap();
334 let aname = format!("{}Deleted", name);
335 let acoll = format!("{}_deleted", schema.collection);
336 let aschema = build_schema(&aobj, aname.clone(), acoll, &links[0])?;
337 planned.push(Planned {
338 name: aname,
339 schema: aschema,
340 links,
341 });
342 }
343 } else {
344 let Some(existing) = self.entries.get(name) else {
346 return Err(format!(
347 "未找到主 schema: {}(从定义 `replica` 必须伴随同批主定义,或该 name 已注册)",
348 name
349 ));
350 };
351 let mut links = existing.links.clone();
352 links.extend(rs);
353 planned.push(Planned {
354 name: name.clone(),
355 schema: existing.schema.clone(),
356 links,
357 });
358 }
359 }
360
361 let batch_names: std::collections::HashSet<&str> =
363 planned.iter().map(|p| p.name.as_str()).collect();
364 let mut quads: Vec<(String, String, Location)> = Vec::new();
365 for (n, e) in &self.entries {
366 if batch_names.contains(n.as_str()) {
367 continue; }
369 for l in &e.links {
370 quads.push((n.clone(), e.schema.collection.clone(), l.clone()));
371 }
372 }
373 for p in &planned {
374 for l in &p.links {
375 quads.push((p.name.clone(), p.schema.collection.clone(), l.clone()));
376 }
377 }
378 if let Some(c) = detect_location_conflict(&quads) {
379 return Err(format!(
380 "定位冲突: ({}, {:?}, {:?}, {}) 已被 schema `{}` 占用",
381 c.source, c.database, c.schema, c.collection, c.prev
382 ));
383 }
384
385 for p in planned {
387 if let Some(e) = self.entries.get_mut(&p.name) {
388 e.schema = p.schema;
389 e.links = p.links;
390 e.version += 1;
391 } else {
392 self.entries.insert(
393 p.name.clone(),
394 Entry {
395 schema: p.schema,
396 links: p.links,
397 version: 1,
398 },
399 );
400 self.order.push(p.name);
401 }
402 }
403
404 Ok(())
405 }
406
407 pub fn links_of(&self, name: &str) -> Result<&[Location], String> {
409 Ok(&self.get_entry(name)?.links)
410 }
411
412 pub fn primary_location(&self, name: &str) -> Result<&Location, String> {
414 self.get_entry(name)?
415 .links
416 .first()
417 .ok_or_else(|| format!("Schema 无落点链路: {}", name))
418 }
419
420 pub fn version_of(&self, name: &str) -> Result<u64, String> {
422 Ok(self.get_entry(name)?.version)
423 }
424
425 fn get_entry(&self, name: &str) -> Result<&Entry, String> {
426 self.entries
427 .get(name)
428 .ok_or_else(|| format!("Schema 未注册: {}", name))
429 }
430
431 pub fn get(&self, name: &str) -> Result<&Schema, String> {
433 self.get_entry(name).map(|e| &e.schema)
434 }
435
436 pub fn has(&self, name: &str) -> bool {
437 self.entries.contains_key(name)
438 }
439
440 pub fn clear(&mut self) {
446 self.entries.clear();
447 self.order.clear();
448 }
449
450 pub fn set_require_context(&mut self, require: bool) {
454 self.require_context = require;
455 }
456
457 pub fn require_context(&self) -> bool {
459 self.require_context
460 }
461
462 pub fn set_profile(&mut self, profile: Profile) {
467 self.profile = profile;
468 }
469
470 pub fn profile(&self) -> Profile {
472 self.profile
473 }
474
475 pub fn set_exempt_roles(&mut self, roles: Vec<String>) {
477 self.role_rules.exempt_roles = roles;
478 }
479
480 pub fn set_deny_write_roles(&mut self, roles: Vec<String>) {
482 self.role_rules.deny_write_roles = roles;
483 }
484
485 pub fn set_unconfigured_policy(&mut self, policy: crate::permission::UnconfiguredPolicy) {
487 self.role_rules.unconfigured = policy;
488 }
489
490 pub fn role_rules(&self) -> &crate::permission::RoleRules {
492 &self.role_rules
493 }
494
495 pub fn set_meta_policy(&mut self, closed: bool, roles: Vec<String>) {
498 self.meta_policy = crate::permission::MetaPolicy { closed, roles };
499 }
500
501 pub fn meta_policy(&self) -> &crate::permission::MetaPolicy {
503 &self.meta_policy
504 }
505
506 pub fn set_write_link_policy(&mut self, policy: WriteLinkPolicy) {
511 self.write_link_policy = policy;
512 }
513
514 pub fn write_link_policy(&self) -> WriteLinkPolicy {
516 self.write_link_policy
517 }
518
519 pub fn can_register(&self, ctx: Option<&crate::permission::Context>) -> bool {
525 crate::permission::can_register(&self.meta_policy, ctx)
526 }
527
528 pub fn set_rbac(&mut self, policy: Option<&Value>) -> Result<(), String> {
531 match policy {
532 None => {
533 self.rbac = None;
534 Ok(())
535 }
536 Some(v) => {
537 let p = crate::rbac::RbacPolicy::from_json(v)?;
538 self.rbac = Some(p);
539 Ok(())
540 }
541 }
542 }
543
544 pub fn rbac(&self) -> Option<&crate::rbac::RbacPolicy> {
546 self.rbac.as_ref()
547 }
548
549 pub fn get_by_location(
554 &self,
555 source: &str,
556 database: Option<&str>,
557 schema: Option<&str>,
558 collection: &str,
559 ) -> Result<&Schema, String> {
560 let db = database.filter(|s| !s.is_empty());
561 let sc = schema.filter(|s| !s.is_empty());
562 self.entries
563 .values()
564 .find(|e| {
565 e.schema.collection == collection
566 && e.links.iter().any(|l| {
567 l.source == source
568 && l.database.as_deref().filter(|s| !s.is_empty()) == db
569 && l.schema.as_deref().filter(|s| !s.is_empty()) == sc
570 })
571 })
572 .map(|e| &e.schema)
573 .ok_or_else(|| {
574 format!(
575 "Schema 未定位(source = {}, database = {:?}, schema = {:?}, collection = {})",
576 source, db, sc, collection
577 )
578 })
579 }
580
581 pub fn get_for_command(
588 &self,
589 source: &str,
590 database: Option<&str>,
591 schema: Option<&str>,
592 collection: &str,
593 ) -> Result<&Schema, String> {
594 if let Ok(s) = self.get_by_location(source, database, schema, collection) {
595 return Ok(s);
596 }
597 let candidates: Vec<&Entry> = self
598 .entries
599 .values()
600 .filter(|e| {
601 e.schema.collection == collection && e.links.iter().any(|l| l.source == source)
602 })
603 .collect();
604 match candidates.len() {
605 0 => self.get_by_location(source, database, schema, collection),
606 1 => Ok(&candidates[0].schema),
607 _ => candidates
608 .iter()
609 .find(|e| e.schema.database().is_none() && e.schema.schema().is_none())
610 .map(|e| &e.schema)
611 .ok_or_else(|| {
612 format!(
613 "命令定位 ({}, {:?}, {:?}, {}) 存在多个结构 schema(多租户 override 需唯一的 (source, collection) 结构声明)",
614 source, database, schema, collection
615 )
616 }),
617 }
618 }
619
620 pub fn list(&self) -> Vec<String> {
621 self.order.clone()
622 }
623
624 pub fn schema_datasource(&self, name: &str) -> Result<Option<String>, String> {
626 Ok(self.get(name)?.source.clone())
627 }
628
629 pub fn resolve_datasource(
634 &self,
635 schema_name: &str,
636 config: &Value,
637 ) -> Result<crate::datasource::DataSource, String> {
638 let declared = self.get(schema_name)?.source.as_deref();
639 let cfg = crate::datasource::DataSourceConfig::from_json(config)?;
640 cfg.resolve(declared)
641 }
642}
643
644fn build_schema(
647 obj: &Map<String, Value>,
648 name: String,
649 collection: String,
650 loc: &Location,
651) -> Result<Schema, String> {
652 let timestamps = parse_timestamps(obj)?;
653 let mut fields = normalize_fields(obj.get("fields"))?;
654 if timestamps {
655 add_timestamp_fields(&mut fields);
656 }
657 let relations = parse_relations(obj);
658 let computes = parse_computes(obj, &relations)?;
660 let mut ids: Vec<&str> = Vec::new();
663 ids.extend(fields.keys().map(String::as_str));
664 ids.extend(relations.keys().map(String::as_str));
665 ids.extend(computes.iter().map(|(k, _)| k.as_str()));
666 crate::naming::detect_conflicts(ids)?;
667 Ok(Schema {
668 name,
669 collection,
670 id_prefix: obj
671 .get("idPrefix")
672 .and_then(|v| v.as_str())
673 .unwrap_or("")
674 .to_string(),
675 timestamps,
676 fields,
677 relations,
678 computes,
679 read: str_list(obj.get("read")),
680 write: str_list(obj.get("write")),
681 indexes: obj
682 .get("indexes")
683 .and_then(|v| v.as_array())
684 .cloned()
685 .unwrap_or_default(),
686 source: if loc.source == crate::datasource::DEFAULT_SOURCE {
687 None
688 } else {
689 Some(loc.source.clone())
690 },
691 database: loc.database.clone().filter(|s| !s.is_empty()),
692 schema: loc.schema.clone().filter(|s| !s.is_empty()),
693 })
694}
695
696fn parse_timestamps(obj: &Map<String, Value>) -> Result<bool, String> {
698 match obj.get("timestamps") {
699 None | Some(Value::Null) | Some(Value::Bool(true)) => Ok(true),
700 Some(Value::Bool(false)) => Ok(false),
701 Some(Value::String(s)) if s == "ms" || s == "s" => Ok(true),
702 Some(Value::String(s)) => Err(format!(
703 "timestamps 仅支持 true/false/'ms'/'s',实际 {:?}",
704 s
705 )),
706 Some(other) => Err(format!(
707 "timestamps 仅支持 true/false/'ms'/'s',实际 {}",
708 other
709 )),
710 }
711}
712
713fn add_timestamp_fields(fields: &mut HashMap<String, FieldDef>) {
715 for key in ["createdAt", "updatedAt"] {
716 fields.entry(key.to_string()).or_insert_with(|| FieldDef {
717 field_type: "number".to_string(),
718 required: false,
719 default: None,
720 read: None,
721 write: None,
722 fields: None,
723 strategy: None,
724 });
725 }
726}
727
728fn parse_relations(obj: &Map<String, Value>) -> HashMap<String, RelationDef> {
729 let mut relations = HashMap::new();
730 if let Some(Value::Object(rm)) = obj.get("relations") {
731 for (key, val) in rm {
732 let vo = val.as_object();
733 let get_str = |k: &str| -> Option<String> {
734 vo.and_then(|o| o.get(k))
735 .and_then(|v| v.as_str())
736 .filter(|s| !s.is_empty())
737 .map(String::from)
738 };
739 relations.insert(
740 key.clone(),
741 RelationDef {
742 model: get_str("model").unwrap_or_default(),
743 rel_type: get_str("type").unwrap_or_else(|| "many".to_string()),
744 local_field: get_str("localField").unwrap_or_else(|| "_id".to_string()),
745 foreign_field: get_str("foreignField").unwrap_or_else(|| key.clone()),
746 read: str_list(vo.and_then(|o| o.get("read"))),
747 },
748 );
749 }
750 }
751 relations
752}
753
754fn parse_computes(
755 obj: &Map<String, Value>,
756 relations: &HashMap<String, RelationDef>,
757) -> Result<Vec<(String, ComputeDef)>, String> {
758 let mut computes = Vec::new();
759 if let Some(Value::Object(cm)) = obj.get("computes") {
760 for (key, val) in cm {
761 let vo = val.as_object();
762 let get = |k: &str| vo.and_then(|o| o.get(k)).cloned();
763 let has_fn = get("fn").map(|v| is_truthy(&v)).unwrap_or(false);
764 let has_async_fn = get("asyncFn").map(|v| is_truthy(&v)).unwrap_or(false);
765 let agg = parse_agg(key, get("agg").as_ref(), relations, has_fn || has_async_fn)?;
766 computes.push((
767 key.clone(),
768 ComputeDef {
769 comp_type: get("type")
770 .and_then(|v| v.as_str().map(String::from))
771 .filter(|s| !s.is_empty())
772 .unwrap_or_else(|| "any".to_string()),
773 has_fn,
774 has_async_fn,
775 fn_ref: get("fnRef")
776 .and_then(|v| v.as_str().map(String::from))
777 .filter(|s| !s.is_empty()),
778 agg,
779 depends: str_list(get("depends").as_ref()).unwrap_or_default(),
780 read: str_list(get("read").as_ref()),
781 },
782 ));
783 }
784 }
785 Ok(computes)
786}
787
788fn parse_agg(
796 key: &str,
797 raw: Option<&Value>,
798 relations: &HashMap<String, RelationDef>,
799 has_callback: bool,
800) -> Result<Option<Value>, String> {
801 let Some(v) = raw.filter(|v| is_truthy(v)) else {
802 return Ok(None);
803 };
804 if has_callback {
805 return Err(format!(
806 "计算列 \"{key}\" 不能同时声明 agg 与 fn/asyncFn(二选一)"
807 ));
808 }
809 let obj = v.as_object().ok_or_else(|| {
810 format!("计算列 \"{key}\" 的 agg 必须是对象,如 {{\"$count\": \"lessons\"}}")
811 })?;
812 let mut it = obj.iter();
813 let (op, arg) = match (it.next(), it.next()) {
814 (Some(kv), None) => kv,
815 _ => return Err(format!("计算列 \"{key}\" 的 agg 必须且只能声明一个算子键")),
816 };
817 if !AGG_OPS.contains(&op.as_str()) {
818 return Err(format!(
819 "计算列 \"{key}\" 的 agg 算子 {op} 不在白名单($count/$sum/$avg/$min/$max)"
820 ));
821 }
822 let path = arg
823 .as_str()
824 .filter(|s| !s.is_empty() && *s != "*")
825 .ok_or_else(|| {
826 format!("计算列 \"{key}\" 的 agg 取值必须是关系路径字符串(如 \"lessons\" / \"lessons.duration\")")
827 })?;
828 let (rel, field) = match path.split_once('.') {
829 Some((r, f)) => (r, Some(f)),
830 None => (path, None),
831 };
832 if !relations.contains_key(rel) {
833 return Err(format!(
834 "计算列 \"{key}\" 的 agg 引用的关系 \"{rel}\" 未在 schema 中定义"
835 ));
836 }
837 if field
838 .map(|f| f.is_empty() || f.contains('.'))
839 .unwrap_or(false)
840 {
841 return Err(format!(
842 "计算列 \"{key}\" 的 agg 关系路径 \"{path}\" 非法:仅支持单级「关系.字段」(二级路径首批不支持)"
843 ));
844 }
845 match op.as_str() {
846 "$count" if field.is_some() => Err(format!(
847 "计算列 \"{key}\" 的 $count 仅支持关系整行计数(如 \"lessons\"),按字段计数待 P5"
848 )),
849 "$count" => Ok(Some(v.clone())),
850 _ if field.is_none() => Err(format!(
851 "计算列 \"{key}\" 的 agg 算子 {op} 必须引用关系字段(如 \"lessons.duration\")"
852 )),
853 _ => Ok(Some(v.clone())),
854 }
855}
856
857fn archive_defn(obj: &Map<String, Value>, name: &str, collection: &str) -> Value {
860 let mut arch_fields = obj
861 .get("fields")
862 .and_then(|v| v.as_object())
863 .cloned()
864 .unwrap_or_default();
865 arch_fields.insert("deletedAt".to_string(), json!({ "type": "number" }));
866 if let Some(idf) = arch_fields.get_mut("_id") {
869 if let Some(o) = idf.as_object_mut() {
870 o.remove("strategy");
871 }
872 }
873 let arch = json!({
874 "name": format!("{}Deleted", name),
875 "collection": format!("{}_deleted", collection),
876 "idPrefix": "",
877 "_isArchive": true,
878 "fields": Value::Object(arch_fields),
879 "indexes": obj.get("indexes").cloned().unwrap_or_else(|| json!([])),
880 });
881 arch
882}
883
884#[cfg(test)]
885mod tests {
886 use super::*;
887
888 #[test]
889 fn write_link_policy_defaults_to_reject() {
890 let r = Registry::new();
891 assert_eq!(r.write_link_policy(), WriteLinkPolicy::Reject);
892 let mut r2 = Registry::new();
893 assert_eq!(r2.write_link_policy(), WriteLinkPolicy::Reject);
894 r2.set_write_link_policy(WriteLinkPolicy::PrimaryOnly);
895 assert_eq!(r2.write_link_policy(), WriteLinkPolicy::PrimaryOnly);
896 }
897}