ducpy 3.6.1__tar.gz → 4.0.0__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 (76) hide show
  1. {ducpy-3.6.1 → ducpy-4.0.0}/Cargo.lock +62 -5
  2. {ducpy-3.6.1 → ducpy-4.0.0}/Cargo.toml +4 -1
  3. {ducpy-3.6.1 → ducpy-4.0.0}/PKG-INFO +3 -2
  4. {ducpy-3.6.1 → ducpy-4.0.0}/README.md +2 -2
  5. {ducpy-3.6.1 → ducpy-4.0.0}/packages/ducpy/crate/Cargo.toml +1 -1
  6. {ducpy-3.6.1 → ducpy-4.0.0}/packages/ducpy/crate/src/lib.rs +60 -34
  7. {ducpy-3.6.1 → ducpy-4.0.0}/packages/ducrs/Cargo.toml +2 -2
  8. {ducpy-3.6.1 → ducpy-4.0.0}/packages/ducrs/build.rs +11 -3
  9. {ducpy-3.6.1 → ducpy-4.0.0}/packages/ducrs/schema/duc.sql +13 -6
  10. {ducpy-3.6.1 → ducpy-4.0.0}/packages/ducrs/schema/migrations/3000001_to_3000002.sql +18 -6
  11. ducpy-4.0.0/packages/ducrs/schema/migrations/3000007_to_3000008.sql +59 -0
  12. ducpy-4.0.0/packages/ducrs/schema/migrations/3000008_to_3000009.sql +171 -0
  13. ducpy-4.0.0/packages/ducrs/schema/migrations/3000009_to_4000000.sql +6 -0
  14. {ducpy-3.6.1 → ducpy-4.0.0}/packages/ducrs/schema/search.sql +1 -1
  15. {ducpy-3.6.1 → ducpy-4.0.0}/packages/ducrs/schema/version_control.sql +28 -6
  16. ducpy-4.0.0/packages/ducrs/src/api/document.rs +330 -0
  17. {ducpy-3.6.1 → ducpy-4.0.0}/packages/ducrs/src/api/meta.rs +6 -7
  18. {ducpy-3.6.1 → ducpy-4.0.0}/packages/ducrs/src/api/version_control.rs +128 -101
  19. {ducpy-3.6.1 → ducpy-4.0.0}/packages/ducrs/src/db/bootstrap.rs +24 -14
  20. ducpy-4.0.0/packages/ducrs/src/db/bootstrap_tests.rs +297 -0
  21. {ducpy-3.6.1 → ducpy-4.0.0}/packages/ducrs/src/db/mod.rs +16 -0
  22. {ducpy-3.6.1 → ducpy-4.0.0}/packages/ducrs/src/db/wasm.rs +29 -2
  23. ducpy-4.0.0/packages/ducrs/src/external_file_chunks.rs +820 -0
  24. {ducpy-3.6.1 → ducpy-4.0.0}/packages/ducrs/src/lib.rs +5 -1
  25. {ducpy-3.6.1 → ducpy-4.0.0}/packages/ducrs/src/parse.rs +1050 -928
  26. {ducpy-3.6.1 → ducpy-4.0.0}/packages/ducrs/src/serialize.rs +546 -231
  27. ducpy-4.0.0/packages/ducrs/src/session.rs +706 -0
  28. {ducpy-3.6.1 → ducpy-4.0.0}/packages/ducrs/src/types.rs +156 -17
  29. ducpy-4.0.0/patches/sqlite-wasm-vfs/Cargo.lock +687 -0
  30. ducpy-4.0.0/patches/sqlite-wasm-vfs/Cargo.toml +88 -0
  31. ducpy-4.0.0/patches/sqlite-wasm-vfs/LICENSE +21 -0
  32. ducpy-4.0.0/patches/sqlite-wasm-vfs/README.md +3 -0
  33. ducpy-4.0.0/patches/sqlite-wasm-vfs/src/lib.rs +13 -0
  34. ducpy-4.0.0/patches/sqlite-wasm-vfs/src/relaxed_idb.rs +963 -0
  35. ducpy-4.0.0/patches/sqlite-wasm-vfs/src/sahpool.rs +1134 -0
  36. {ducpy-3.6.1 → ducpy-4.0.0}/pyproject.toml +1 -0
  37. {ducpy-3.6.1 → ducpy-4.0.0}/src/ducpy/__init__.py +4 -2
  38. {ducpy-3.6.1 → ducpy-4.0.0}/src/ducpy/parse.py +55 -43
  39. {ducpy-3.6.1 → ducpy-4.0.0}/src/ducpy/search/search_elements.py +2 -2
  40. {ducpy-3.6.1 → ducpy-4.0.0}/src/ducpy/search/search_external_files.py +83 -43
  41. {ducpy-3.6.1 → ducpy-4.0.0}/src/ducpy/serialize.py +17 -6
  42. {ducpy-3.6.1 → ducpy-4.0.0}/src/ducpy/utils/io.py +2 -3
  43. {ducpy-3.6.1 → ducpy-4.0.0}/src/ducpy_native/__init__.py +9 -6
  44. ducpy-3.6.1/packages/ducrs/src/api/document.rs +0 -82
  45. {ducpy-3.6.1 → ducpy-4.0.0}/LICENSE +0 -0
  46. {ducpy-3.6.1 → ducpy-4.0.0}/packages/ducrs/LICENSE +0 -0
  47. {ducpy-3.6.1 → ducpy-4.0.0}/packages/ducrs/README.md +0 -0
  48. {ducpy-3.6.1 → ducpy-4.0.0}/packages/ducrs/schema/migrations/3000000_to_3000001.sql +0 -0
  49. {ducpy-3.6.1 → ducpy-4.0.0}/packages/ducrs/schema/migrations/3000002_to_3000003.sql +0 -0
  50. {ducpy-3.6.1 → ducpy-4.0.0}/packages/ducrs/schema/migrations/3000003_to_3000004.sql +0 -0
  51. {ducpy-3.6.1 → ducpy-4.0.0}/packages/ducrs/schema/migrations/3000004_to_3000005.sql +0 -0
  52. {ducpy-3.6.1 → ducpy-4.0.0}/packages/ducrs/schema/migrations/3000005_to_3000006.sql +0 -0
  53. {ducpy-3.6.1 → ducpy-4.0.0}/packages/ducrs/schema/migrations/3000006_to_3000007.sql +0 -0
  54. {ducpy-3.6.1 → ducpy-4.0.0}/packages/ducrs/src/api/mod.rs +0 -0
  55. {ducpy-3.6.1 → ducpy-4.0.0}/packages/ducrs/src/db/native.rs +0 -0
  56. {ducpy-3.6.1 → ducpy-4.0.0}/packages/ducrs/src/serde_utils.rs +0 -0
  57. {ducpy-3.6.1 → ducpy-4.0.0}/src/ducpy/builders/__init__.py +0 -0
  58. {ducpy-3.6.1 → ducpy-4.0.0}/src/ducpy/builders/block_instance_builder.py +0 -0
  59. {ducpy-3.6.1 → ducpy-4.0.0}/src/ducpy/builders/block_utils.py +0 -0
  60. {ducpy-3.6.1 → ducpy-4.0.0}/src/ducpy/builders/element_builders.py +0 -0
  61. {ducpy-3.6.1 → ducpy-4.0.0}/src/ducpy/builders/mutate_builder.py +0 -0
  62. {ducpy-3.6.1 → ducpy-4.0.0}/src/ducpy/builders/sql_builder.py +0 -0
  63. {ducpy-3.6.1 → ducpy-4.0.0}/src/ducpy/builders/state_builders.py +0 -0
  64. {ducpy-3.6.1 → ducpy-4.0.0}/src/ducpy/builders/style_builders.py +0 -0
  65. {ducpy-3.6.1 → ducpy-4.0.0}/src/ducpy/classes/DataStateClass.py +0 -0
  66. {ducpy-3.6.1 → ducpy-4.0.0}/src/ducpy/classes/ElementsClass.py +0 -0
  67. {ducpy-3.6.1 → ducpy-4.0.0}/src/ducpy/classes/__init__.py +0 -0
  68. {ducpy-3.6.1 → ducpy-4.0.0}/src/ducpy/enums.py +0 -0
  69. {ducpy-3.6.1 → ducpy-4.0.0}/src/ducpy/search/__init__.py +0 -0
  70. {ducpy-3.6.1 → ducpy-4.0.0}/src/ducpy/search/image_ocr.py +0 -0
  71. {ducpy-3.6.1 → ducpy-4.0.0}/src/ducpy/search/search_pdf.py +0 -0
  72. {ducpy-3.6.1 → ducpy-4.0.0}/src/ducpy/utils/__init__.py +0 -0
  73. {ducpy-3.6.1 → ducpy-4.0.0}/src/ducpy/utils/constants.py +0 -0
  74. {ducpy-3.6.1 → ducpy-4.0.0}/src/ducpy/utils/convert.py +0 -0
  75. {ducpy-3.6.1 → ducpy-4.0.0}/src/ducpy/utils/mutate_utils.py +0 -0
  76. {ducpy-3.6.1 → ducpy-4.0.0}/src/ducpy/utils/rand_utils.py +0 -0
@@ -403,16 +403,20 @@ name = "ducjs-wasm"
403
403
  version = "0.0.0-development"
404
404
  dependencies = [
405
405
  "duc",
406
+ "flate2",
406
407
  "js-sys",
408
+ "rusqlite",
407
409
  "serde",
408
410
  "serde-wasm-bindgen",
409
411
  "serde_json",
412
+ "sqlite-wasm-vfs",
410
413
  "wasm-bindgen",
414
+ "wasm-bindgen-futures",
411
415
  ]
412
416
 
413
417
  [[package]]
414
418
  name = "ducpy-native"
415
- version = "3.6.1"
419
+ version = "4.0.0"
416
420
  dependencies = [
417
421
  "duc",
418
422
  "pyo3",
@@ -1127,6 +1131,16 @@ dependencies = [
1127
1131
  "autocfg",
1128
1132
  ]
1129
1133
 
1134
+ [[package]]
1135
+ name = "minicov"
1136
+ version = "0.3.8"
1137
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1138
+ checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d"
1139
+ dependencies = [
1140
+ "cc",
1141
+ "walkdir",
1142
+ ]
1143
+
1130
1144
  [[package]]
1131
1145
  name = "miniz_oxide"
1132
1146
  version = "0.8.9"
@@ -1587,9 +1601,9 @@ checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
1587
1601
 
1588
1602
  [[package]]
1589
1603
  name = "rsqlite-vfs"
1590
- version = "0.1.0"
1604
+ version = "0.1.1"
1591
1605
  source = "registry+https://github.com/rust-lang/crates.io-index"
1592
- checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d"
1606
+ checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c"
1593
1607
  dependencies = [
1594
1608
  "hashbrown",
1595
1609
  "thiserror",
@@ -1655,6 +1669,15 @@ version = "1.0.20"
1655
1669
  source = "registry+https://github.com/rust-lang/crates.io-index"
1656
1670
  checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
1657
1671
 
1672
+ [[package]]
1673
+ name = "same-file"
1674
+ version = "1.0.6"
1675
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1676
+ checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
1677
+ dependencies = [
1678
+ "winapi-util",
1679
+ ]
1680
+
1658
1681
  [[package]]
1659
1682
  name = "sealed"
1660
1683
  version = "0.6.0"
@@ -1831,16 +1854,16 @@ dependencies = [
1831
1854
  [[package]]
1832
1855
  name = "sqlite-wasm-vfs"
1833
1856
  version = "0.2.0"
1834
- source = "registry+https://github.com/rust-lang/crates.io-index"
1835
- checksum = "7f7a5c9ac229421d577bb5a9bb59048838509958b218dd4e0b3c1214a87c361e"
1836
1857
  dependencies = [
1837
1858
  "indexed_db_futures",
1838
1859
  "js-sys",
1839
1860
  "rsqlite-vfs",
1861
+ "sqlite-wasm-rs",
1840
1862
  "thiserror",
1841
1863
  "tokio",
1842
1864
  "wasm-bindgen",
1843
1865
  "wasm-bindgen-futures",
1866
+ "wasm-bindgen-test",
1844
1867
  "web-sys",
1845
1868
  ]
1846
1869
 
@@ -2183,6 +2206,16 @@ version = "0.9.5"
2183
2206
  source = "registry+https://github.com/rust-lang/crates.io-index"
2184
2207
  checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
2185
2208
 
2209
+ [[package]]
2210
+ name = "walkdir"
2211
+ version = "2.5.0"
2212
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2213
+ checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
2214
+ dependencies = [
2215
+ "same-file",
2216
+ "winapi-util",
2217
+ ]
2218
+
2186
2219
  [[package]]
2187
2220
  name = "wasip2"
2188
2221
  version = "1.0.2+wasi-0.2.9"
@@ -2264,6 +2297,30 @@ dependencies = [
2264
2297
  "unicode-ident",
2265
2298
  ]
2266
2299
 
2300
+ [[package]]
2301
+ name = "wasm-bindgen-test"
2302
+ version = "0.3.54"
2303
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2304
+ checksum = "4e381134e148c1062f965a42ed1f5ee933eef2927c3f70d1812158f711d39865"
2305
+ dependencies = [
2306
+ "js-sys",
2307
+ "minicov",
2308
+ "wasm-bindgen",
2309
+ "wasm-bindgen-futures",
2310
+ "wasm-bindgen-test-macro",
2311
+ ]
2312
+
2313
+ [[package]]
2314
+ name = "wasm-bindgen-test-macro"
2315
+ version = "0.3.54"
2316
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2317
+ checksum = "b673bca3298fe582aeef8352330ecbad91849f85090805582400850f8270a2e8"
2318
+ dependencies = [
2319
+ "proc-macro2",
2320
+ "quote",
2321
+ "syn",
2322
+ ]
2323
+
2267
2324
  [[package]]
2268
2325
  name = "web-sys"
2269
2326
  version = "0.3.81"
@@ -1,6 +1,6 @@
1
1
  [workspace]
2
2
  resolver = "2"
3
- members = ["packages/ducrs", "packages/ducpy/crate"]
3
+ members = ["packages/ducrs", "packages/ducpy/crate", "patches/sqlite-wasm-vfs"]
4
4
 
5
5
  [workspace.metadata.wasm-pack]
6
6
  "wasm-pack" = "0.14.0"
@@ -18,3 +18,6 @@ opt-level = "z"
18
18
  codegen-units = 1
19
19
  strip = true
20
20
  panic = "abort"
21
+
22
+ [patch.crates-io]
23
+ sqlite-wasm-vfs = { path = "patches/sqlite-wasm-vfs" }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ducpy
3
- Version: 3.6.1
3
+ Version: 4.0.0
4
4
  Classifier: License :: OSI Approved :: MIT License
5
5
  Requires-Dist: nanoid>=2.0.0
6
6
  Requires-Dist: pypdf>=4.2.0
@@ -85,7 +85,7 @@ Read and write `.duc` files using the `duc.parse` and `duc.serialize` modules.
85
85
 
86
86
  See the worked examples:
87
87
 
88
- - [Parsing](src/examples/parsing_demo.py) — `parse_duc` / `parse_duc_lazy` / `get_external_file` / `list_external_files`.
88
+ - [Parsing](src/examples/parsing_demo.py) — `parse_duc` / `list_external_files` / streaming revision helpers.
89
89
  - [Serialization](src/examples/serialization_demo.py) — `serialize_duc` from builder-created elements.
90
90
  - [External files](src/examples/external_files_demo.py) — attaching binary blobs (images, PDFs) to a `.duc` document.
91
91
 
@@ -108,3 +108,4 @@ Feel free to open an issue or contact us at [Ducflair Support](https://www.ducfl
108
108
  ## License
109
109
 
110
110
  This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for more details.
111
+
@@ -62,7 +62,7 @@ Read and write `.duc` files using the `duc.parse` and `duc.serialize` modules.
62
62
 
63
63
  See the worked examples:
64
64
 
65
- - [Parsing](src/examples/parsing_demo.py) — `parse_duc` / `parse_duc_lazy` / `get_external_file` / `list_external_files`.
65
+ - [Parsing](src/examples/parsing_demo.py) — `parse_duc` / `list_external_files` / streaming revision helpers.
66
66
  - [Serialization](src/examples/serialization_demo.py) — `serialize_duc` from builder-created elements.
67
67
  - [External files](src/examples/external_files_demo.py) — attaching binary blobs (images, PDFs) to a `.duc` document.
68
68
 
@@ -84,4 +84,4 @@ Feel free to open an issue or contact us at [Ducflair Support](https://www.ducfl
84
84
 
85
85
  ## License
86
86
 
87
- This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for more details.
87
+ This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for more details.
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "ducpy-native"
3
- version = "3.6.1"
3
+ version = "4.0.0"
4
4
  edition = "2021"
5
5
  description = "Python native extension for the duc file format (used by ducpy)"
6
6
  publish = false
@@ -1,60 +1,85 @@
1
1
  use pyo3::prelude::*;
2
- use pyo3::types::PyBytes;
2
+ use std::fs::File;
3
3
 
4
- /// Parse a `.duc` file (bytes) into a Python dict (ExportedDataState).
4
+ /// Parse a `.duc` file path into a Python dict (ExportedDataState).
5
5
  #[pyfunction]
6
- fn parse_duc(py: Python<'_>, buf: &[u8]) -> PyResult<PyObject> {
7
- let state = duc::parse::parse(buf)
6
+ fn parse_duc(py: Python<'_>, path: &str) -> PyResult<PyObject> {
7
+ let session = duc::session::DucSession::open_path(path)
8
+ .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
9
+ let state = session
10
+ .read_document_state()
8
11
  .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
9
12
  pythonize::pythonize(py, &state)
10
13
  .map(|b| b.unbind())
11
14
  .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))
12
15
  }
13
16
 
14
- /// Parse a `.duc` file lazily returns everything EXCEPT external file data blobs.
15
- ///
16
- /// Use `get_external_file()` or `list_external_files()` for on-demand access.
17
+ /// Serialize a Python dict (ExportedDataState) directly to a `.duc` output path.
17
18
  #[pyfunction]
18
- fn parse_duc_lazy(py: Python<'_>, buf: &[u8]) -> PyResult<PyObject> {
19
- let state = duc::parse::parse_lazy(buf)
19
+ fn serialize_duc(data: &Bound<'_, pyo3::types::PyAny>, output_path: &str) -> PyResult<()> {
20
+ let state: duc::types::ExportedDataState = pythonize::depythonize(data)
20
21
  .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
21
- pythonize::pythonize(py, &state)
22
- .map(|b| b.unbind())
22
+ let mut session = duc::session::DucSession::create_export_session()
23
+ .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
24
+ session
25
+ .write_document_state(&state)
26
+ .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
27
+ session
28
+ .finish_to_path(output_path)
23
29
  .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))
24
30
  }
25
31
 
26
- /// Serialize a Python dict (ExportedDataState) into `.duc` bytes.
32
+ /// List metadata for all external files without loading data blobs.
27
33
  #[pyfunction]
28
- fn serialize_duc(py: Python<'_>, data: &Bound<'_, pyo3::types::PyAny>) -> PyResult<PyObject> {
29
- let state: duc::types::ExportedDataState = pythonize::depythonize(data)
34
+ fn list_external_files(py: Python<'_>, path: &str) -> PyResult<PyObject> {
35
+ let session = duc::session::DucSession::open_path(path)
30
36
  .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
31
- let bytes = duc::serialize::serialize(&state)
37
+ let meta = session
38
+ .list_external_files()
39
+ .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
40
+ pythonize::pythonize(py, &meta)
41
+ .map(|b| b.unbind())
42
+ .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))
43
+ }
44
+
45
+ #[pyfunction]
46
+ fn stream_external_file_revision_to_path(
47
+ path: &str,
48
+ revision_id: &str,
49
+ output_path: &str,
50
+ ) -> PyResult<u64> {
51
+ let session = duc::session::DucSession::open_path(path)
32
52
  .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
33
- Ok(PyBytes::new(py, &bytes).into())
53
+ let mut out = File::create(output_path)
54
+ .map_err(|e| pyo3::exceptions::PyOSError::new_err(format!("{e}")))?;
55
+ session
56
+ .stream_external_file_revision_to_writer(revision_id, &mut out)
57
+ .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))
34
58
  }
35
59
 
36
- /// Fetch a single external file from a `.duc` buffer by file ID.
37
- ///
38
- /// Returns the file entry as a dict, or None if not found.
39
60
  #[pyfunction]
40
- fn get_external_file(py: Python<'_>, buf: &[u8], file_id: &str) -> PyResult<PyObject> {
41
- let entry = duc::parse::get_external_file(buf, file_id)
61
+ fn stream_checkpoint_data_to_path(
62
+ path: &str,
63
+ checkpoint_id: &str,
64
+ output_path: &str,
65
+ ) -> PyResult<u64> {
66
+ let session = duc::session::DucSession::open_path(path)
42
67
  .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
43
- match entry {
44
- Some(e) => pythonize::pythonize(py, &e)
45
- .map(|b| b.unbind())
46
- .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}"))),
47
- None => Ok(py.None()),
48
- }
68
+ let mut out = File::create(output_path)
69
+ .map_err(|e| pyo3::exceptions::PyOSError::new_err(format!("{e}")))?;
70
+ session
71
+ .stream_checkpoint_data_to_writer(checkpoint_id, &mut out)
72
+ .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))
49
73
  }
50
74
 
51
- /// List metadata for all external files (without loading the heavy data blobs).
52
75
  #[pyfunction]
53
- fn list_external_files(py: Python<'_>, buf: &[u8]) -> PyResult<PyObject> {
54
- let meta = duc::parse::list_external_files(buf)
76
+ fn stream_delta_changeset_to_path(path: &str, delta_id: &str, output_path: &str) -> PyResult<u64> {
77
+ let session = duc::session::DucSession::open_path(path)
55
78
  .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
56
- pythonize::pythonize(py, &meta)
57
- .map(|b| b.unbind())
79
+ let mut out = File::create(output_path)
80
+ .map_err(|e| pyo3::exceptions::PyOSError::new_err(format!("{e}")))?;
81
+ session
82
+ .stream_delta_changeset_to_writer(delta_id, &mut out)
58
83
  .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))
59
84
  }
60
85
 
@@ -101,10 +126,11 @@ fn get_migrations(_py: Python<'_>) -> PyResult<Vec<(i64, i64, String)>> {
101
126
  #[pymodule]
102
127
  fn ducpy_native(m: &Bound<'_, PyModule>) -> PyResult<()> {
103
128
  m.add_function(wrap_pyfunction!(parse_duc, m)?)?;
104
- m.add_function(wrap_pyfunction!(parse_duc_lazy, m)?)?;
105
129
  m.add_function(wrap_pyfunction!(serialize_duc, m)?)?;
106
- m.add_function(wrap_pyfunction!(get_external_file, m)?)?;
107
130
  m.add_function(wrap_pyfunction!(list_external_files, m)?)?;
131
+ m.add_function(wrap_pyfunction!(stream_external_file_revision_to_path, m)?)?;
132
+ m.add_function(wrap_pyfunction!(stream_checkpoint_data_to_path, m)?)?;
133
+ m.add_function(wrap_pyfunction!(stream_delta_changeset_to_path, m)?)?;
108
134
  m.add_function(wrap_pyfunction!(get_schema_version, m)?)?;
109
135
  m.add_function(wrap_pyfunction!(get_schema_version_int, m)?)?;
110
136
  m.add_function(wrap_pyfunction!(get_duc_schema_sql, m)?)?;
@@ -28,11 +28,11 @@ serde_json = "1.0"
28
28
  serde_repr = "0.1"
29
29
  serde_bytes = "0.11"
30
30
  lazy_static = "1.4"
31
- rusqlite = { version = "0.38", features = ["bundled"] }
31
+ rusqlite = { version = "0.38", features = ["bundled", "serialize"] }
32
32
  thiserror = "2"
33
33
 
34
34
  [target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies]
35
- sqlite-wasm-vfs = { version = "0.2", optional = true }
35
+ sqlite-wasm-vfs = { path = "../../patches/sqlite-wasm-vfs", version = "0.2", optional = true }
36
36
  wasm-bindgen = "0.2"
37
37
 
38
38
  [dev-dependencies]
@@ -7,7 +7,12 @@ fn schema_user_version_from_sql(sql: &str) -> u32 {
7
7
  for line in sql.lines() {
8
8
  let trimmed = line.trim();
9
9
  if let Some(rest) = trimmed.strip_prefix("PRAGMA user_version") {
10
- let rest = rest.trim().trim_start_matches('=').trim().trim_end_matches(';').trim();
10
+ let rest = rest
11
+ .trim()
12
+ .trim_start_matches('=')
13
+ .trim()
14
+ .trim_end_matches(';')
15
+ .trim();
11
16
  if let Ok(v) = rest.parse::<u32>() {
12
17
  return v;
13
18
  }
@@ -133,7 +138,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
133
138
  (uv, decode_user_version_to_semver(uv))
134
139
  }
135
140
  Err(e) => {
136
- eprintln!("cargo:warning=Could not read {:?}: {e}. Defaulting to 0.0.0.", sql_path);
141
+ eprintln!(
142
+ "cargo:warning=Could not read {:?}: {e}. Defaulting to 0.0.0.",
143
+ sql_path
144
+ );
137
145
  (0, "0.0.0".to_string())
138
146
  }
139
147
  };
@@ -153,4 +161,4 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
153
161
  println!("cargo:rerun-if-changed=build.rs");
154
162
 
155
163
  Ok(())
156
- }
164
+ }
@@ -1,7 +1,7 @@
1
1
  -- "DUC_" in ASCII
2
2
  -- Apply in order: duc.sql → version_control.sql → search.sql
3
3
  PRAGMA application_id = 1146569567;
4
- PRAGMA user_version = 3000007;
4
+ PRAGMA user_version = 4000000;
5
5
  PRAGMA journal_mode = WAL;
6
6
  PRAGMA foreign_keys = ON;
7
7
  PRAGMA synchronous = NORMAL;
@@ -996,8 +996,15 @@ CREATE TABLE external_file_revisions (
996
996
  CREATE INDEX idx_external_file_revisions_file ON external_file_revisions(file_id);
997
997
 
998
998
  -- Actual binary content for each revision, separated from metadata
999
- -- so that metadata can be read without loading heavy blobs.
1000
- CREATE TABLE external_file_revision_data (
1001
- revision_id TEXT PRIMARY KEY REFERENCES external_file_revisions(id) ON DELETE CASCADE,
1002
- data BLOB NOT NULL
1003
- ) WITHOUT ROWID;
999
+ -- and chunked so large files can be streamed without loading one BLOB.
1000
+ CREATE TABLE external_file_revision_chunks (
1001
+ revision_id TEXT NOT NULL REFERENCES external_file_revisions(id) ON DELETE CASCADE,
1002
+ chunk_index INTEGER NOT NULL CHECK (chunk_index >= 0),
1003
+ offset_bytes INTEGER NOT NULL CHECK (offset_bytes >= 0),
1004
+ size_bytes INTEGER NOT NULL CHECK (size_bytes >= 0),
1005
+ data BLOB NOT NULL,
1006
+ PRIMARY KEY (revision_id, chunk_index)
1007
+ ) WITHOUT ROWID;
1008
+
1009
+ CREATE INDEX idx_external_file_revision_chunks_revision_offset
1010
+ ON external_file_revision_chunks(revision_id, offset_bytes);
@@ -3,17 +3,18 @@
3
3
  -- `external_file_revision_data` table so metadata can be read without
4
4
  -- loading heavy blobs.
5
5
 
6
- -- 1. Create the new data table
7
- CREATE TABLE external_file_revision_data (
8
- revision_id TEXT PRIMARY KEY REFERENCES external_file_revisions(id) ON DELETE CASCADE,
6
+ PRAGMA foreign_keys = OFF;
7
+
8
+ -- 1. Stage existing data blobs before the parent table is renamed/recreated.
9
+ CREATE TABLE _external_file_revision_data_v3000001 (
10
+ revision_id TEXT PRIMARY KEY,
9
11
  data BLOB NOT NULL
10
12
  ) WITHOUT ROWID;
11
13
 
12
- -- 2. Move existing data blobs into the new table
13
- INSERT INTO external_file_revision_data (revision_id, data)
14
+ INSERT INTO _external_file_revision_data_v3000001 (revision_id, data)
14
15
  SELECT id, data FROM external_file_revisions;
15
16
 
16
- -- 3. Recreate external_file_revisions without the data column
17
+ -- 2. Recreate external_file_revisions without the data column.
17
18
  ALTER TABLE external_file_revisions RENAME TO _ext_revisions_old_v3000001;
18
19
 
19
20
  CREATE TABLE external_file_revisions (
@@ -35,6 +36,17 @@ INSERT INTO external_file_revisions (id, file_id, size_bytes, checksum, source_n
35
36
  SELECT id, file_id, size_bytes, checksum, source_name, mime_type, message, created, last_retrieved
36
37
  FROM _ext_revisions_old_v3000001;
37
38
 
39
+ -- 3. Create the final data table after the new parent table exists.
40
+ CREATE TABLE external_file_revision_data (
41
+ revision_id TEXT PRIMARY KEY REFERENCES external_file_revisions(id) ON DELETE CASCADE,
42
+ data BLOB NOT NULL
43
+ ) WITHOUT ROWID;
44
+
45
+ INSERT INTO external_file_revision_data (revision_id, data)
46
+ SELECT revision_id, data FROM _external_file_revision_data_v3000001;
47
+
38
48
  DROP TABLE _ext_revisions_old_v3000001;
49
+ DROP TABLE _external_file_revision_data_v3000001;
39
50
 
51
+ PRAGMA foreign_keys = ON;
40
52
  PRAGMA user_version = 3000002;
@@ -0,0 +1,59 @@
1
+ -- Migration: 3000007 -> 3000008
2
+ -- Store external file revision bytes in ordered chunks instead of one BLOB row.
3
+
4
+ BEGIN IMMEDIATE;
5
+ PRAGMA defer_foreign_keys = ON;
6
+
7
+ CREATE TABLE external_file_revision_chunks (
8
+ revision_id TEXT NOT NULL REFERENCES external_file_revisions(id) ON DELETE CASCADE,
9
+ chunk_index INTEGER NOT NULL CHECK (chunk_index >= 0),
10
+ offset_bytes INTEGER NOT NULL CHECK (offset_bytes >= 0),
11
+ size_bytes INTEGER NOT NULL CHECK (size_bytes >= 0),
12
+ data BLOB NOT NULL,
13
+ PRIMARY KEY (revision_id, chunk_index)
14
+ ) WITHOUT ROWID;
15
+
16
+ CREATE INDEX idx_external_file_revision_chunks_revision_offset
17
+ ON external_file_revision_chunks(revision_id, offset_bytes);
18
+
19
+ WITH RECURSIVE chunk_offsets(offset_bytes) AS (
20
+ VALUES(0)
21
+ UNION ALL
22
+ SELECT offset_bytes + 8388608
23
+ FROM chunk_offsets
24
+ WHERE EXISTS (
25
+ SELECT 1
26
+ FROM external_file_revision_data
27
+ WHERE length(data) > offset_bytes + 8388608
28
+ )
29
+ )
30
+ INSERT INTO external_file_revision_chunks (
31
+ revision_id, chunk_index, offset_bytes, size_bytes, data
32
+ )
33
+ SELECT
34
+ source.revision_id,
35
+ chunk_offsets.offset_bytes / 8388608,
36
+ chunk_offsets.offset_bytes,
37
+ length(substr(source.data, chunk_offsets.offset_bytes + 1, 8388608)),
38
+ substr(source.data, chunk_offsets.offset_bytes + 1, 8388608)
39
+ FROM external_file_revision_data AS source
40
+ JOIN chunk_offsets
41
+ ON chunk_offsets.offset_bytes = 0
42
+ OR chunk_offsets.offset_bytes < length(source.data);
43
+
44
+ UPDATE external_file_revisions
45
+ SET size_bytes = (
46
+ SELECT length(source.data)
47
+ FROM external_file_revision_data AS source
48
+ WHERE source.revision_id = external_file_revisions.id
49
+ )
50
+ WHERE EXISTS (
51
+ SELECT 1
52
+ FROM external_file_revision_data AS source
53
+ WHERE source.revision_id = external_file_revisions.id
54
+ );
55
+
56
+ DROP TABLE external_file_revision_data;
57
+
58
+ PRAGMA user_version = 3000008;
59
+ COMMIT;