tensorfs 0.3.39__tar.gz → 0.3.41__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. {tensorfs-0.3.39 → tensorfs-0.3.41}/Cargo.lock +2 -2
  2. {tensorfs-0.3.39 → tensorfs-0.3.41}/Cargo.toml +1 -1
  3. {tensorfs-0.3.39 → tensorfs-0.3.41}/PKG-INFO +1 -1
  4. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/derived/resume_tests.rs +150 -0
  5. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/derived.rs +121 -72
  6. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/meta.rs +74 -5
  7. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-py/src/store.rs +36 -1
  8. {tensorfs-0.3.39 → tensorfs-0.3.41}/pyproject.toml +1 -1
  9. {tensorfs-0.3.39 → tensorfs-0.3.41}/python/tensorfs/__init__.py +6 -0
  10. tensorfs-0.3.41/python/tensorfs/_derived_channel.py +502 -0
  11. {tensorfs-0.3.39 → tensorfs-0.3.41}/python/tensorfs/_ext.pyi +2 -0
  12. tensorfs-0.3.41/python/tensorfs/_source_channel.py +105 -0
  13. tensorfs-0.3.41/python/tensorfs/derived.py +233 -0
  14. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/Cargo.toml +0 -0
  15. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/build.rs +0 -0
  16. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
  17. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
  18. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
  19. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
  20. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/b64.rs +0 -0
  21. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
  22. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
  23. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/bin/tfs/fetch.rs +0 -0
  24. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
  25. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
  26. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
  27. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
  28. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
  29. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
  30. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/cache_roots.rs +0 -0
  31. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/canon.rs +0 -0
  32. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/capability.rs +0 -0
  33. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/catalog.rs +0 -0
  34. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/cbor.rs +0 -0
  35. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/checkpoint.rs +0 -0
  36. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/corpus.rs +0 -0
  37. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
  38. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/derived/estimate.rs +0 -0
  39. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
  40. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/derived/progress.rs +0 -0
  41. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/derived/retention.rs +0 -0
  42. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/derived/roots.rs +0 -0
  43. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/dtype.rs +0 -0
  44. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/durability.rs +0 -0
  45. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/err.rs +0 -0
  46. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/fetch.rs +0 -0
  47. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/fill.rs +0 -0
  48. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/fit.rs +0 -0
  49. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/gc.rs +0 -0
  50. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/header.rs +0 -0
  51. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/home.rs +0 -0
  52. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/ids.rs +0 -0
  53. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
  54. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
  55. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
  56. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
  57. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
  58. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
  59. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
  60. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/ingest/source.rs +0 -0
  61. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
  62. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
  63. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/jcs.rs +0 -0
  64. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/lib.rs +0 -0
  65. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/limits.rs +0 -0
  66. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/machine.rs +0 -0
  67. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/manifest.rs +0 -0
  68. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/project.rs +0 -0
  69. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/providers.rs +0 -0
  70. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/read.rs +0 -0
  71. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/receipt.rs +0 -0
  72. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/reclaim.rs +0 -0
  73. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/registry.rs +0 -0
  74. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/relation.rs +0 -0
  75. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/repo_cache.rs +0 -0
  76. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/repository.rs +0 -0
  77. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/sha256.rs +0 -0
  78. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/source_artifact.rs +0 -0
  79. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/spec.rs +0 -0
  80. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/staging.rs +0 -0
  81. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/storage.rs +0 -0
  82. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/store.rs +0 -0
  83. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/transport/api.rs +0 -0
  84. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/transport/http.rs +0 -0
  85. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/transport/hub.rs +0 -0
  86. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/transport/ledger.rs +0 -0
  87. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/transport/mod.rs +0 -0
  88. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/transport/policy.rs +0 -0
  89. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/transport/prefix.rs +0 -0
  90. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/transport/pull.rs +0 -0
  91. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/transport/push.rs +0 -0
  92. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
  93. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/transport/tests.rs +0 -0
  94. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/src/vectors.rs +0 -0
  95. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
  96. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/tests/fit.rs +0 -0
  97. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
  98. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
  99. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
  100. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
  101. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
  102. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
  103. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
  104. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
  105. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/tests/home.rs +0 -0
  106. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/tests/import_tree.rs +0 -0
  107. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
  108. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/tests/liveness.rs +0 -0
  109. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/tests/read_plan.rs +0 -0
  110. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/tests/reclaim.rs +0 -0
  111. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
  112. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
  113. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/tests/repo_store.rs +0 -0
  114. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
  115. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
  116. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
  117. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/tests/source_artifacts.rs +0 -0
  118. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/tests/source_plan.rs +0 -0
  119. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/tests/source_prefixes.rs +0 -0
  120. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
  121. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/tests/staging.rs +0 -0
  122. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
  123. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
  124. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/tests/version.rs +0 -0
  125. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-core/tests/write_through.rs +0 -0
  126. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-py/Cargo.toml +0 -0
  127. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-py/src/docs.rs +0 -0
  128. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-py/src/errors.rs +0 -0
  129. {tensorfs-0.3.39 → tensorfs-0.3.41}/crates/tensorfs-py/src/lib.rs +0 -0
  130. {tensorfs-0.3.39 → tensorfs-0.3.41}/python/tensorfs/errors.py +0 -0
  131. {tensorfs-0.3.39 → tensorfs-0.3.41}/python/tensorfs/py.typed +0 -0
@@ -421,7 +421,7 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
421
421
 
422
422
  [[package]]
423
423
  name = "tensorfs-core"
424
- version = "0.3.39"
424
+ version = "0.3.41"
425
425
  dependencies = [
426
426
  "fs2",
427
427
  "minicbor",
@@ -434,7 +434,7 @@ dependencies = [
434
434
 
435
435
  [[package]]
436
436
  name = "tensorfs-py"
437
- version = "0.3.39"
437
+ version = "0.3.41"
438
438
  dependencies = [
439
439
  "pyo3",
440
440
  "tensorfs-core",
@@ -3,5 +3,5 @@ resolver = "2"
3
3
  members = ["crates/tensorfs-core", "crates/tensorfs-py"]
4
4
 
5
5
  [workspace.package]
6
- version = "0.3.39"
6
+ version = "0.3.41"
7
7
  edition = "2021"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensorfs
3
- Version: 0.3.39
3
+ Version: 0.3.41
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Rust
@@ -918,3 +918,153 @@ fn source_free_restore_holds_survive_gc_until_begin_or_explicit_abandon() {
918
918
  release(&source_meta, writer);
919
919
  std::fs::remove_dir_all(source.root()).unwrap();
920
920
  }
921
+
922
+ /// Opt-in generated-data profile. Payloads are tiny so metadata scaling is visible.
923
+ /// Run in release mode; elapsed time is evidence, never a flaky unit-test threshold.
924
+ #[test]
925
+ #[ignore]
926
+ fn profile_derived_transaction_scaling() {
927
+ use std::time::Instant;
928
+ let scenarios = [
929
+ (219, 1, 1),
930
+ (219, 1, 10),
931
+ (219, 4, 10),
932
+ (219, 32, 10),
933
+ (219, 128, 10),
934
+ (3968, 1, 10),
935
+ (3968, 4, 10),
936
+ (219, 1, 219),
937
+ ];
938
+ for (keys, histories, writes) in scenarios {
939
+ let (store, meta, id, _) = fixture("row-profile");
940
+ let mut declaration = super::tests::created_declaration();
941
+ let prototype = declaration.components[0].add[0].clone();
942
+ declaration.components[0].add = (0..keys)
943
+ .map(|index| {
944
+ let mut tensor = prototype.clone();
945
+ tensor.key = format!("weight_{index:04}");
946
+ tensor.shape = vec![4];
947
+ tensor.parts[0].shape = vec![4];
948
+ tensor
949
+ })
950
+ .collect();
951
+ declaration.order = declaration.components[0]
952
+ .add
953
+ .iter()
954
+ .map(|tensor| ("model".into(), tensor.key.clone()))
955
+ .collect();
956
+ declaration.max_new_bytes = (keys * 16) as u64;
957
+ let writer = begin(&store, &meta, &id, 1, declaration, None).unwrap();
958
+ // Populate representative unrelated transactions without timing fixture creation.
959
+ let mut txn = meta.txn().unwrap();
960
+ let prototype = txn.rows[0].clone();
961
+ let row_bytes = crate::canon::write(&prototype.to_value()).len();
962
+ for index in 1..histories {
963
+ let mut row = prototype.clone();
964
+ row.id = ObjectRef::of(format!("history-{index}").as_bytes()).id();
965
+ txn.rows.push(row);
966
+ }
967
+ txn.commit().unwrap();
968
+ let started = Instant::now();
969
+ for _ in 0..20 {
970
+ check_writer(&meta, &id, 1).unwrap();
971
+ }
972
+ let fence_ms = started.elapsed().as_secs_f64() * 1000.0;
973
+ let mut part_ms = 0.0;
974
+ let mut checkpoint_ms = 0.0;
975
+ let mut prior = None;
976
+ for index in 0..writes {
977
+ let key = format!("weight_{index:04}");
978
+ let started = Instant::now();
979
+ add_part(
980
+ &store,
981
+ &meta,
982
+ &id,
983
+ 1,
984
+ ("model", &key, "value"),
985
+ &mut [index as u8; 16].as_slice(),
986
+ )
987
+ .unwrap();
988
+ part_ms += started.elapsed().as_secs_f64() * 1000.0;
989
+ let started = Instant::now();
990
+ let (_, head) =
991
+ checkpoint_progress(&store, &meta, &id, 1, "profile", "model", prior.as_ref())
992
+ .unwrap();
993
+ prior = head.head;
994
+ checkpoint_ms += started.elapsed().as_secs_f64() * 1000.0;
995
+ }
996
+ eprintln!("PROFILE {{\"keys\":{keys},\"histories\":{histories},\"writes\":{writes},\"row_bytes\":{row_bytes},\"fence20_ms\":{fence_ms:.3},\"add_ms\":{part_ms:.3},\"checkpoint_ms\":{checkpoint_ms:.3}}}");
997
+ fence(&meta, &id, 1).unwrap();
998
+ release(&meta, writer);
999
+ std::fs::remove_dir_all(store.root()).unwrap();
1000
+ }
1001
+ }
1002
+
1003
+ #[test]
1004
+ fn named_writer_operations_leave_other_rows_and_writer_lock_alone() {
1005
+ use rusqlite::{params, Connection};
1006
+ let (store, meta, first_id, declaration) = fixture("scoped-row");
1007
+ let second_id = ObjectRef::of(b"independent writer").id();
1008
+ let first = begin(&store, &meta, &first_id, 1, declaration.clone(), None).unwrap();
1009
+ let second = begin(&store, &meta, &second_id, 1, declaration.clone(), None).unwrap();
1010
+ let connection = Connection::open(crate::catalog::Catalog::path(store.root())).unwrap();
1011
+ let untouched: Vec<u8> = connection
1012
+ .query_row(
1013
+ "SELECT bytes FROM tensorfs_derived_transactions WHERE id=?1",
1014
+ [&second_id],
1015
+ |row| row.get(0),
1016
+ )
1017
+ .unwrap();
1018
+ // Corrupt unrelated history makes an accidental whole-table read fail, while
1019
+ // a scoped mutation must preserve it byte-for-byte and mutate only this writer.
1020
+ let damaged = ObjectRef::of(b"unrelated damaged history").id();
1021
+ connection
1022
+ .execute(
1023
+ "INSERT INTO tensorfs_derived_transactions(id,bytes) VALUES(?1,?2)",
1024
+ params![damaged, b"broken".as_slice()],
1025
+ )
1026
+ .unwrap();
1027
+ data(&store, &meta, &first_id, 1, 0x31);
1028
+ let after: Vec<u8> = connection
1029
+ .query_row(
1030
+ "SELECT bytes FROM tensorfs_derived_transactions WHERE id=?1",
1031
+ [&second_id],
1032
+ |row| row.get(0),
1033
+ )
1034
+ .unwrap();
1035
+ assert_eq!(untouched, after);
1036
+ // A read fence must succeed while an unrelated SQLite writer is active. The old
1037
+ // BEGIN IMMEDIATE observation instead hit the five-second busy timeout.
1038
+ connection.execute_batch("BEGIN IMMEDIATE").unwrap();
1039
+ check_writer(&meta, &second_id, 1).unwrap();
1040
+ assert!(completed(&meta, &second_id, 1).unwrap().0.is_empty());
1041
+ connection.execute_batch("ROLLBACK").unwrap();
1042
+ // A named operation does not parse unrelated history. Global inventory still
1043
+ // reports the damaged row rather than silently skipping it.
1044
+ check_writer(&meta, &first_id, 1).unwrap();
1045
+ assert!(meta.derived_rows().is_err());
1046
+ connection
1047
+ .execute(
1048
+ "DELETE FROM tensorfs_derived_transactions WHERE id=?1",
1049
+ [&damaged],
1050
+ )
1051
+ .unwrap();
1052
+ assert!(fence(&meta, &first_id, 1).unwrap());
1053
+ assert_eq!(
1054
+ check_writer(&meta, &first_id, 1).unwrap_err().code,
1055
+ Code::WRITER_FENCED
1056
+ );
1057
+ check_writer(&meta, &second_id, 1).unwrap();
1058
+ release(&meta, first);
1059
+ let resumed = begin(&store, &meta, &first_id, 2, declaration, None).unwrap();
1060
+ assert_eq!(completed(&meta, &first_id, 2).unwrap().0.len(), 1);
1061
+ assert_eq!(
1062
+ check_writer(&meta, &first_id, 1).unwrap_err().code,
1063
+ Code::WRITER_FENCED
1064
+ );
1065
+ fence(&meta, &first_id, 2).unwrap();
1066
+ fence(&meta, &second_id, 1).unwrap();
1067
+ release(&meta, resumed);
1068
+ release(&meta, second);
1069
+ std::fs::remove_dir_all(store.root()).unwrap();
1070
+ }
@@ -2005,6 +2005,19 @@ fn inspect_retained_source(
2005
2005
  code: Code::ROOT_ABSENT,
2006
2006
  detail: "source inspection resolved no source".into(),
2007
2007
  })?;
2008
+ for component in &component_names {
2009
+ if !source
2010
+ .header
2011
+ .components
2012
+ .iter()
2013
+ .any(|(name, _)| name == component)
2014
+ {
2015
+ return refuse(
2016
+ Code::MISSING_FIELD,
2017
+ format!("source has no component {component:?}"),
2018
+ );
2019
+ }
2020
+ }
2008
2021
  let configs = config_names
2009
2022
  .iter()
2010
2023
  .map(|name| {
@@ -2188,10 +2201,7 @@ pub fn begin(
2188
2201
  };
2189
2202
  // Byte verification can update the verification catalog. Keep it outside the SQL
2190
2203
  // writer transaction, then compare the observed row before assigning this epoch.
2191
- let initial = meta
2192
- .derived_rows()?
2193
- .into_iter()
2194
- .find(|row| row.id == transaction);
2204
+ let initial = meta.derived_row(&transaction)?;
2195
2205
  let mut row = match planned_row(
2196
2206
  initial.as_ref(),
2197
2207
  &transaction,
@@ -2220,7 +2230,7 @@ pub fn begin(
2220
2230
  return Err(error);
2221
2231
  }
2222
2232
  let committed = (|| {
2223
- let mut txn = meta.txn()?;
2233
+ let mut txn = meta.txn_for(&transaction)?;
2224
2234
  let current = txn.rows.iter_mut().find(|row| row.id == transaction);
2225
2235
  if current.as_deref() != initial.as_ref() {
2226
2236
  return refuse(
@@ -2430,8 +2440,8 @@ pub fn completed(
2430
2440
  transaction: &str,
2431
2441
  writer_session: u64,
2432
2442
  ) -> Result<(Vec<(String, String, String)>, Vec<String>)> {
2433
- let mut txn = meta.txn()?;
2434
- let row = open_row(&mut txn.rows, transaction, writer_session)?;
2443
+ let mut rows: Vec<_> = meta.derived_row(transaction)?.into_iter().collect();
2444
+ let row = open_row(&mut rows, transaction, writer_session)?;
2435
2445
  Ok((
2436
2446
  row.added_parts
2437
2447
  .iter()
@@ -2451,7 +2461,7 @@ pub fn checkpoint_progress(
2451
2461
  previous: Option<&ObjectRef>,
2452
2462
  ) -> Result<(String, crate::durability::Head)> {
2453
2463
  let progress = {
2454
- let mut txn = meta.txn()?;
2464
+ let mut txn = meta.txn_for(transaction)?;
2455
2465
  progress_row(open_row(&mut txn.rows, transaction, writer_session)?)
2456
2466
  };
2457
2467
  let plan = progress_plan(&progress);
@@ -2513,7 +2523,7 @@ pub fn checkpoint_progress(
2513
2523
  objects,
2514
2524
  None,
2515
2525
  )?;
2516
- let mut txn = meta.txn()?;
2526
+ let mut txn = meta.txn_for(transaction)?;
2517
2527
  let row = open_row(&mut txn.rows, transaction, writer_session)?;
2518
2528
  retain_progress(store, row, exported.head.head.as_ref())?;
2519
2529
  Ok((plan, exported.head))
@@ -2581,7 +2591,7 @@ pub(crate) fn retain_restore(
2581
2591
  if let Some(object) = object {
2582
2592
  require_payload_record(store, object, "restored checkpoint object")?;
2583
2593
  }
2584
- let txn = meta.txn()?;
2594
+ let txn = meta.txn_for(&transaction)?;
2585
2595
  let row = txn.rows.iter().find(|row| row.id == transaction);
2586
2596
  let active = if let Some(row) = row {
2587
2597
  if row.state != TransactionState::Open {
@@ -2704,8 +2714,8 @@ fn open_row<'a>(
2704
2714
 
2705
2715
  pub fn check_writer(meta: &Meta, transaction: &str, writer_session: u64) -> Result<()> {
2706
2716
  let transaction = transaction_id(transaction)?;
2707
- let mut txn = meta.txn()?;
2708
- open_row(&mut txn.rows, &transaction, writer_session)?;
2717
+ let mut rows: Vec<_> = meta.derived_row(&transaction)?.into_iter().collect();
2718
+ open_row(&mut rows, &transaction, writer_session)?;
2709
2719
  Ok(())
2710
2720
  }
2711
2721
 
@@ -2716,6 +2726,83 @@ fn declaration(row: &TransactionRow) -> Result<Declaration> {
2716
2726
  })?)
2717
2727
  }
2718
2728
 
2729
+ /// The exact native payload limit, checked against the current writer before
2730
+ /// an embedding owner admits storage or asks a producer to supply any bytes.
2731
+ pub fn part_write_bound(
2732
+ meta: &Meta,
2733
+ transaction: &str,
2734
+ writer_session: u64,
2735
+ value: (&str, &str, &str),
2736
+ ) -> Result<u64> {
2737
+ let transaction = transaction_id(transaction)?;
2738
+ let mut rows: Vec<_> = meta.derived_row(&transaction)?.into_iter().collect();
2739
+ let row = open_row(&mut rows, &transaction, writer_session)?;
2740
+ let part = declared_addition(&declaration(row)?, value)?;
2741
+ checked_bytes("derived part write", &part.shape, part.dtype)
2742
+ }
2743
+
2744
+ pub fn config_write_bound(
2745
+ meta: &Meta,
2746
+ transaction: &str,
2747
+ writer_session: u64,
2748
+ name: &str,
2749
+ ) -> Result<u64> {
2750
+ let transaction = transaction_id(transaction)?;
2751
+ let mut rows: Vec<_> = meta.derived_row(&transaction)?.into_iter().collect();
2752
+ let row = open_row(&mut rows, &transaction, writer_session)?;
2753
+ let declaration = declaration(row)?;
2754
+ require_added_config(&declaration, name)?;
2755
+ Ok(declaration.max_new_bytes.min(limits::DOC_MAX_BYTES as u64))
2756
+ }
2757
+
2758
+ fn declared_addition(
2759
+ declaration: &Declaration,
2760
+ (component, tensor_key, role): (&str, &str, &str),
2761
+ ) -> Result<PartDeclaration> {
2762
+ let part = declaration
2763
+ .components
2764
+ .iter()
2765
+ .find(|candidate| candidate.target == component)
2766
+ .and_then(|candidate| {
2767
+ candidate
2768
+ .add
2769
+ .iter()
2770
+ .find(|candidate| candidate.key == tensor_key)
2771
+ })
2772
+ .and_then(|candidate| {
2773
+ candidate
2774
+ .parts
2775
+ .iter()
2776
+ .find(|candidate| candidate.role == role)
2777
+ })
2778
+ .cloned()
2779
+ .ok_or_else(|| Refusal {
2780
+ code: Code::MISSING_FIELD,
2781
+ detail: format!("{component}/{tensor_key}#{role} is not a declared addition"),
2782
+ })?;
2783
+ if part.source.is_some() {
2784
+ return refuse(
2785
+ Code::TRANSACTION_CONFLICT,
2786
+ "grafted part cannot receive byte writes",
2787
+ );
2788
+ }
2789
+ Ok(part)
2790
+ }
2791
+
2792
+ fn require_added_config(declaration: &Declaration, name: &str) -> Result<()> {
2793
+ if declaration.configs.iter().any(|config| {
2794
+ matches!(config, ConfigDeclaration::Add { target }
2795
+ | ConfigDeclaration::Derive { target, .. } if target == name)
2796
+ }) {
2797
+ Ok(())
2798
+ } else {
2799
+ refuse(
2800
+ Code::MISSING_FIELD,
2801
+ format!("{name:?} is not a declared added config"),
2802
+ )
2803
+ }
2804
+ }
2805
+
2719
2806
  pub fn add_part<R: Read>(
2720
2807
  store: &Store,
2721
2808
  meta: &Meta,
@@ -2726,40 +2813,11 @@ pub fn add_part<R: Read>(
2726
2813
  ) -> Result<crate::checkpoint::Written> {
2727
2814
  let (component, tensor_key, role) = value;
2728
2815
  let transaction = transaction_id(transaction)?;
2729
- let (declaration, expected) = {
2730
- let mut txn = meta.txn()?;
2816
+ let expected = {
2817
+ let mut txn = meta.txn_for(&transaction)?;
2731
2818
  let row = open_row(&mut txn.rows, &transaction, writer_session)?;
2732
- let declaration = declaration(row)?;
2733
- let expected = declaration
2734
- .components
2735
- .iter()
2736
- .find(|candidate| candidate.target == component)
2737
- .and_then(|candidate| {
2738
- candidate
2739
- .add
2740
- .iter()
2741
- .find(|candidate| candidate.key == tensor_key)
2742
- })
2743
- .and_then(|candidate| {
2744
- candidate
2745
- .parts
2746
- .iter()
2747
- .find(|candidate| candidate.role == role)
2748
- })
2749
- .cloned()
2750
- .ok_or_else(|| Refusal {
2751
- code: Code::MISSING_FIELD,
2752
- detail: format!("{component}/{tensor_key}#{role} is not a declared addition"),
2753
- })?;
2754
- (declaration, expected)
2819
+ declared_addition(&declaration(row)?, value)?
2755
2820
  };
2756
- let _ = declaration;
2757
- if expected.source.is_some() {
2758
- return refuse(
2759
- Code::TRANSACTION_CONFLICT,
2760
- "grafted part cannot receive byte writes",
2761
- );
2762
- }
2763
2821
 
2764
2822
  let written = checkpoint::objectize(
2765
2823
  store,
@@ -2791,7 +2849,7 @@ pub fn add_part<R: Read>(
2791
2849
  }
2792
2850
  }
2793
2851
 
2794
- let mut txn = meta.txn()?;
2852
+ let mut txn = meta.txn_for(&transaction)?;
2795
2853
  let row = open_row(&mut txn.rows, &transaction, writer_session)?;
2796
2854
  if let Some(existing) = row
2797
2855
  .added_parts
@@ -2825,39 +2883,31 @@ pub fn add_config<R: Read>(
2825
2883
  reader: &mut R,
2826
2884
  ) -> Result<()> {
2827
2885
  let transaction = transaction_id(transaction)?;
2828
- {
2829
- let mut txn = meta.txn()?;
2886
+ let maximum = {
2887
+ let mut txn = meta.txn_for(&transaction)?;
2830
2888
  let row = open_row(&mut txn.rows, &transaction, writer_session)?;
2831
2889
  let declaration = declaration(row)?;
2832
- if !declaration.configs.iter().any(|config| {
2833
- matches!(
2834
- config,
2835
- ConfigDeclaration::Add { target }
2836
- | ConfigDeclaration::Derive { target, .. }
2837
- if target == config_name
2838
- )
2839
- }) {
2840
- return refuse(
2841
- Code::MISSING_FIELD,
2842
- format!("{config_name:?} is not a declared added config"),
2843
- );
2844
- }
2845
- }
2890
+ require_added_config(&declaration, config_name)?;
2891
+ declaration.max_new_bytes.min(limits::DOC_MAX_BYTES as u64)
2892
+ };
2846
2893
 
2847
2894
  let mut input = Vec::new();
2848
2895
  reader
2849
- .take(limits::DOC_MAX_BYTES as u64 + 1)
2896
+ .take(maximum + 1)
2850
2897
  .read_to_end(&mut input)
2851
2898
  .map_err(|error| Refusal {
2852
2899
  code: Code::IO_FAILED,
2853
2900
  detail: format!("read config: {error}"),
2854
2901
  })?;
2855
- if input.len() > limits::DOC_MAX_BYTES {
2856
- return refuse(Code::SIZE_CAP, "config exceeds the document byte cap");
2902
+ if input.len() as u64 > maximum {
2903
+ return refuse(
2904
+ Code::SIZE_CAP,
2905
+ "config exceeds the output or document byte cap",
2906
+ );
2857
2907
  }
2858
2908
  let config = crate::header::canonical_config(&format!("config {config_name:?}"), &input)?;
2859
2909
 
2860
- let mut txn = meta.txn()?;
2910
+ let mut txn = meta.txn_for(&transaction)?;
2861
2911
  let row = open_row(&mut txn.rows, &transaction, writer_session)?;
2862
2912
  if let Some(existing) = row
2863
2913
  .added_configs
@@ -3006,7 +3056,7 @@ pub fn commit(
3006
3056
  ) -> Result<ReceiptFacts> {
3007
3057
  let transaction = transaction_id(transaction)?;
3008
3058
  let row_before = {
3009
- let mut txn = meta.txn()?;
3059
+ let mut txn = meta.txn_for(&transaction)?;
3010
3060
  open_row(&mut txn.rows, &transaction, writer_session)?.clone()
3011
3061
  };
3012
3062
  let declaration = declaration(&row_before)?;
@@ -3028,7 +3078,7 @@ pub fn commit(
3028
3078
  let (inherited_payload_objects, inherited_payload_bytes) =
3029
3079
  inherit_observation(&header, &row_before)?;
3030
3080
 
3031
- let mut txn = meta.txn()?;
3081
+ let mut txn = meta.txn_for(&transaction)?;
3032
3082
  let row = open_row(&mut txn.rows, &transaction, writer_session)?;
3033
3083
  if row.added_parts != row_before.added_parts || row.added_configs != row_before.added_configs {
3034
3084
  return refuse(
@@ -3058,8 +3108,7 @@ pub enum Lookup {
3058
3108
 
3059
3109
  pub fn lookup(store: &Store, meta: &Meta, transaction: &str) -> Result<Lookup> {
3060
3110
  let transaction = transaction_id(transaction)?;
3061
- let rows = meta.derived_rows()?;
3062
- let Some(row) = rows.iter().find(|row| row.id == transaction) else {
3111
+ let Some(ref row) = meta.derived_row(&transaction)? else {
3063
3112
  return Ok(Lookup::Absent);
3064
3113
  };
3065
3114
  match row.state {
@@ -3079,7 +3128,7 @@ pub fn lookup(store: &Store, meta: &Meta, transaction: &str) -> Result<Lookup> {
3079
3128
 
3080
3129
  pub fn fence(meta: &Meta, transaction: &str, writer_session: u64) -> Result<bool> {
3081
3130
  let transaction = transaction_id(transaction)?;
3082
- let mut txn = meta.txn()?;
3131
+ let mut txn = meta.txn_for(&transaction)?;
3083
3132
  let row = txn
3084
3133
  .rows
3085
3134
  .iter_mut()
@@ -3098,7 +3147,7 @@ pub fn fence(meta: &Meta, transaction: &str, writer_session: u64) -> Result<bool
3098
3147
 
3099
3148
  pub fn abandon(store: &Store, meta: &Meta, transaction: &str) -> Result<Option<ReceiptFacts>> {
3100
3149
  let transaction = transaction_id(transaction)?;
3101
- let mut txn = meta.txn()?;
3150
+ let mut txn = meta.txn_for(&transaction)?;
3102
3151
  let result = match txn.rows.iter_mut().find(|row| row.id == transaction) {
3103
3152
  None => {
3104
3153
  txn.rows.push(TransactionRow {
@@ -3270,7 +3319,7 @@ fn effective_disposition(
3270
3319
 
3271
3320
  pub fn dispose(store: &Store, meta: &Meta, transaction: &str) -> Result<DispositionResult> {
3272
3321
  let transaction = transaction_id(transaction)?;
3273
- let mut txn = meta.txn()?;
3322
+ let mut txn = meta.txn_for(&transaction)?;
3274
3323
  let index = txn
3275
3324
  .rows
3276
3325
  .iter()
@@ -8,7 +8,7 @@ use std::path::PathBuf;
8
8
  use std::time::{SystemTime, UNIX_EPOCH};
9
9
 
10
10
  use fs2::FileExt;
11
- use rusqlite::{params, Connection};
11
+ use rusqlite::{params, Connection, OptionalExtension};
12
12
 
13
13
  use crate::err::{refuse, Code, Refusal, Result};
14
14
  use crate::ids::ascii_name;
@@ -92,7 +92,33 @@ impl Meta {
92
92
  .execute_batch("BEGIN IMMEDIATE")
93
93
  .map_err(|error| sql("begin derived transaction", error))?;
94
94
  let rows = read_derived(&connection)?;
95
- Ok(Txn { connection, rows })
95
+ Ok(Txn {
96
+ connection,
97
+ rows,
98
+ selected: None,
99
+ })
100
+ }
101
+
102
+ /// A writer mutates its own primary-key row; unrelated writers and retained history
103
+ /// must neither be decoded nor rewritten. The existing write lock still serializes
104
+ /// epoch changes, roots and accepted parts against global lifecycle operations.
105
+ pub(crate) fn txn_for(&self, id: &str) -> Result<Txn> {
106
+ let connection = self.connection()?;
107
+ connection
108
+ .execute_batch("BEGIN IMMEDIATE")
109
+ .map_err(|error| sql("begin derived transaction", error))?;
110
+ let rows = read_derived_row(&connection, id)?.into_iter().collect();
111
+ Ok(Txn {
112
+ connection,
113
+ rows,
114
+ selected: Some(id.to_string()),
115
+ })
116
+ }
117
+
118
+ /// Observations do not acquire SQLite's writer lock. Source reads check this both
119
+ /// before and after I/O, so a concurrent fence still invalidates the read.
120
+ pub(crate) fn derived_row(&self, id: &str) -> Result<Option<crate::derived::TransactionRow>> {
121
+ read_derived_row(&self.connection()?, id)
96
122
  }
97
123
 
98
124
  pub(crate) fn derived_rows(&self) -> Result<Vec<crate::derived::TransactionRow>> {
@@ -162,6 +188,7 @@ impl Meta {
162
188
  pub(crate) struct Txn {
163
189
  connection: Connection,
164
190
  pub(crate) rows: Vec<crate::derived::TransactionRow>,
191
+ selected: Option<String>,
165
192
  }
166
193
 
167
194
  impl Txn {
@@ -176,9 +203,24 @@ impl Txn {
176
203
  "duplicate derived transaction id",
177
204
  );
178
205
  }
179
- self.connection
180
- .execute("DELETE FROM tensorfs_derived_transactions", [])
181
- .map_err(|error| sql("clear derived transactions", error))?;
206
+ if let Some(id) = &self.selected {
207
+ if self.rows.len() > 1 || self.rows.iter().any(|row| &row.id != id) {
208
+ return refuse(
209
+ Code::TRANSACTION_CONFLICT,
210
+ "scoped transaction changed another writer",
211
+ );
212
+ }
213
+ self.connection
214
+ .execute(
215
+ "DELETE FROM tensorfs_derived_transactions WHERE id=?1",
216
+ [id],
217
+ )
218
+ .map_err(|error| sql("replace derived transaction", error))?;
219
+ } else {
220
+ self.connection
221
+ .execute("DELETE FROM tensorfs_derived_transactions", [])
222
+ .map_err(|error| sql("clear derived transactions", error))?;
223
+ }
182
224
  for row in &self.rows {
183
225
  let bytes = crate::canon::write(&row.to_value());
184
226
  self.connection
@@ -194,6 +236,33 @@ impl Txn {
194
236
  }
195
237
  }
196
238
 
239
+ fn read_derived_row(
240
+ connection: &Connection,
241
+ id: &str,
242
+ ) -> Result<Option<crate::derived::TransactionRow>> {
243
+ let bytes = connection
244
+ .query_row(
245
+ "SELECT bytes FROM tensorfs_derived_transactions WHERE id=?1",
246
+ [id],
247
+ |row| row.get::<_, Vec<u8>>(0),
248
+ )
249
+ .optional()
250
+ .map_err(|error| sql("read derived transaction", error))?;
251
+ bytes
252
+ .map(|bytes| {
253
+ let value = crate::canon::parse_canonical(&bytes, limits::DOC_MAX_BYTES)?;
254
+ let row = crate::derived::TransactionRow::from_value(&value)?;
255
+ if row.id != id {
256
+ return refuse(
257
+ Code::TRANSACTION_CONFLICT,
258
+ "derived row differs from its primary key",
259
+ );
260
+ }
261
+ Ok(row)
262
+ })
263
+ .transpose()
264
+ }
265
+
197
266
  fn read_derived(connection: &Connection) -> Result<Vec<crate::derived::TransactionRow>> {
198
267
  let mut statement = connection
199
268
  .prepare("SELECT bytes FROM tensorfs_derived_transactions ORDER BY id")
@@ -1864,7 +1864,12 @@ impl PyStore {
1864
1864
  let identity = source_facts(py, std::slice::from_ref(&inspection.view))?;
1865
1865
  out.set_item("source", identity.get_item(0)?)?;
1866
1866
  let component_rows = PyList::empty(py);
1867
- for component in &inspection.view.fact.components {
1867
+ // The selected fact is a normalized set. Author inspection must retain
1868
+ // the header's construction traversal, including nonlexical components.
1869
+ for (component, _) in &inspection.view.header.components {
1870
+ if !inspection.view.fact.components.contains(component) {
1871
+ continue;
1872
+ }
1868
1873
  let row = PyDict::new(py);
1869
1874
  row.set_item("name", component)?;
1870
1875
  row.set_item(
@@ -2338,6 +2343,36 @@ impl PyDerivedWriter {
2338
2343
 
2339
2344
  #[pymethods]
2340
2345
  impl PyDerivedWriter {
2346
+ fn part_write_bound(
2347
+ &self,
2348
+ py: Python<'_>,
2349
+ component: &str,
2350
+ key: &str,
2351
+ role: &str,
2352
+ ) -> PyResult<u64> {
2353
+ py.detach(|| {
2354
+ derived::part_write_bound(
2355
+ &self.meta,
2356
+ &self.transaction_id,
2357
+ self.writer_session_id,
2358
+ (component, key, role),
2359
+ )
2360
+ })
2361
+ .or_refuse(py)
2362
+ }
2363
+
2364
+ fn config_write_bound(&self, py: Python<'_>, name: &str) -> PyResult<u64> {
2365
+ py.detach(|| {
2366
+ derived::config_write_bound(
2367
+ &self.meta,
2368
+ &self.transaction_id,
2369
+ self.writer_session_id,
2370
+ name,
2371
+ )
2372
+ })
2373
+ .or_refuse(py)
2374
+ }
2375
+
2341
2376
  /// Effective exact sources after TensorFS has checked installed identity and closure.
2342
2377
  fn sources<'p>(&self, py: Python<'p>) -> PyResult<Bound<'p, PyList>> {
2343
2378
  source_facts(py, &self.source_views)