dirsql 0.3.34__tar.gz → 0.3.36__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 (130) hide show
  1. {dirsql-0.3.34 → dirsql-0.3.36}/Cargo.lock +1 -1
  2. {dirsql-0.3.34 → dirsql-0.3.36}/PKG-INFO +1 -1
  3. {dirsql-0.3.34 → dirsql-0.3.36}/dirsql/_async.py +23 -3
  4. {dirsql-0.3.34 → dirsql-0.3.36}/dirsql/_dirsql.pyi +4 -1
  5. {dirsql-0.3.34 → dirsql-0.3.36}/dirsql/resolve_config.py +5 -1
  6. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/Cargo.toml +1 -1
  7. {dirsql-0.3.34 → dirsql-0.3.36}/packages/rust/src/cli/mod.rs +9 -32
  8. {dirsql-0.3.34 → dirsql-0.3.36}/packages/rust/src/cli/native_config.rs +14 -215
  9. {dirsql-0.3.34 → dirsql-0.3.36}/packages/rust/src/cli/router.rs +9 -8
  10. {dirsql-0.3.34 → dirsql-0.3.36}/pyproject.toml +4 -3
  11. {dirsql-0.3.34 → dirsql-0.3.36}/Cargo.toml +0 -0
  12. {dirsql-0.3.34 → dirsql-0.3.36}/README.md +0 -0
  13. {dirsql-0.3.34 → dirsql-0.3.36}/dirsql/__init__.py +0 -0
  14. {dirsql-0.3.34 → dirsql-0.3.36}/dirsql/cli/__init__.py +0 -0
  15. {dirsql-0.3.34 → dirsql-0.3.36}/dirsql/cli/binary_path.py +0 -0
  16. {dirsql-0.3.34 → dirsql-0.3.36}/dirsql/cli/interpret/__init__.py +0 -0
  17. {dirsql-0.3.34 → dirsql-0.3.36}/dirsql/cli/interpret/dispatch_extract.py +0 -0
  18. {dirsql-0.3.34 → dirsql-0.3.36}/dirsql/cli/interpret/load_app.py +0 -0
  19. {dirsql-0.3.34 → dirsql-0.3.36}/dirsql/cli/interpret/run.py +0 -0
  20. {dirsql-0.3.34 → dirsql-0.3.36}/dirsql/cli/interpret/write_message.py +0 -0
  21. {dirsql-0.3.34 → dirsql-0.3.36}/dirsql/cli/is_windows.py +0 -0
  22. {dirsql-0.3.34 → dirsql-0.3.36}/dirsql/cli/main.py +0 -0
  23. {dirsql-0.3.34 → dirsql-0.3.36}/dirsql/py.typed +0 -0
  24. {dirsql-0.3.34 → dirsql-0.3.36}/docs/.claude/CLAUDE.md +0 -0
  25. {dirsql-0.3.34 → dirsql-0.3.36}/docs/.vitepress/config.ts +0 -0
  26. {dirsql-0.3.34 → dirsql-0.3.36}/docs/.vitepress/theme/index.ts +0 -0
  27. {dirsql-0.3.34 → dirsql-0.3.36}/docs/.vitepress/theme/lang.ts +0 -0
  28. {dirsql-0.3.34 → dirsql-0.3.36}/docs/AGENTS.md +0 -0
  29. {dirsql-0.3.34 → dirsql-0.3.36}/docs/api/index.md +0 -0
  30. {dirsql-0.3.34 → dirsql-0.3.36}/docs/cli/config.md +0 -0
  31. {dirsql-0.3.34 → dirsql-0.3.36}/docs/cli/http-api.md +0 -0
  32. {dirsql-0.3.34 → dirsql-0.3.36}/docs/cli/index.md +0 -0
  33. {dirsql-0.3.34 → dirsql-0.3.36}/docs/cli/init.md +0 -0
  34. {dirsql-0.3.34 → dirsql-0.3.36}/docs/cli/server.md +0 -0
  35. {dirsql-0.3.34 → dirsql-0.3.36}/docs/getting-started.md +0 -0
  36. {dirsql-0.3.34 → dirsql-0.3.36}/docs/guide/async.md +0 -0
  37. {dirsql-0.3.34 → dirsql-0.3.36}/docs/guide/crdt.md +0 -0
  38. {dirsql-0.3.34 → dirsql-0.3.36}/docs/guide/persistence.md +0 -0
  39. {dirsql-0.3.34 → dirsql-0.3.36}/docs/guide/querying.md +0 -0
  40. {dirsql-0.3.34 → dirsql-0.3.36}/docs/guide/tables.md +0 -0
  41. {dirsql-0.3.34 → dirsql-0.3.36}/docs/guide/watching.md +0 -0
  42. {dirsql-0.3.34 → dirsql-0.3.36}/docs/index.md +0 -0
  43. {dirsql-0.3.34 → dirsql-0.3.36}/docs/migrations.md +0 -0
  44. {dirsql-0.3.34 → dirsql-0.3.36}/docs/package.json +0 -0
  45. {dirsql-0.3.34 → dirsql-0.3.36}/docs/playwright.config.ts +0 -0
  46. {dirsql-0.3.34 → dirsql-0.3.36}/docs/pnpm-lock.yaml +0 -0
  47. {dirsql-0.3.34 → dirsql-0.3.36}/docs/pnpm-workspace.yaml +0 -0
  48. {dirsql-0.3.34 → dirsql-0.3.36}/docs/tests/integration/home.spec.ts +0 -0
  49. {dirsql-0.3.34 → dirsql-0.3.36}/docs/tests/integration/language-flag.spec.ts +0 -0
  50. {dirsql-0.3.34 → dirsql-0.3.36}/docs/tests/unit/config.test.ts +0 -0
  51. {dirsql-0.3.34 → dirsql-0.3.36}/docs/tests/unit/lang.test.ts +0 -0
  52. {dirsql-0.3.34 → dirsql-0.3.36}/docs/vitest.config.ts +0 -0
  53. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/README.md +0 -0
  54. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/conftest.py +0 -0
  55. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/docs/.claude/CLAUDE.md +0 -0
  56. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/docs/.vitepress/config.ts +0 -0
  57. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/docs/.vitepress/theme/index.ts +0 -0
  58. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
  59. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/docs/AGENTS.md +0 -0
  60. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/docs/api/index.md +0 -0
  61. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/docs/cli/config.md +0 -0
  62. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/docs/cli/http-api.md +0 -0
  63. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/docs/cli/index.md +0 -0
  64. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/docs/cli/init.md +0 -0
  65. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/docs/cli/server.md +0 -0
  66. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/docs/getting-started.md +0 -0
  67. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/docs/guide/async.md +0 -0
  68. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/docs/guide/crdt.md +0 -0
  69. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/docs/guide/persistence.md +0 -0
  70. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/docs/guide/querying.md +0 -0
  71. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/docs/guide/tables.md +0 -0
  72. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/docs/guide/watching.md +0 -0
  73. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/docs/index.md +0 -0
  74. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/docs/migrations.md +0 -0
  75. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/docs/package.json +0 -0
  76. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/docs/playwright.config.ts +0 -0
  77. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/docs/pnpm-lock.yaml +0 -0
  78. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/docs/pnpm-workspace.yaml +0 -0
  79. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/docs/tests/integration/home.spec.ts +0 -0
  80. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/docs/tests/integration/language-flag.spec.ts +0 -0
  81. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/docs/tests/unit/config.test.ts +0 -0
  82. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/docs/tests/unit/lang.test.ts +0 -0
  83. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/docs/vitest.config.ts +0 -0
  84. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/src/lib.rs +0 -0
  85. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/tests/__init__.py +0 -0
  86. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/tests/conftest.py +0 -0
  87. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/tests/e2e/__init__.py +0 -0
  88. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/tests/integration/__fixtures__/data/a/meta.json +0 -0
  89. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/tests/integration/__fixtures__/data/b/meta.json +0 -0
  90. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/tests/integration/__fixtures__/dirsql.config.py +0 -0
  91. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/tests/integration/__fixtures__/interpret/data/a/meta.json +0 -0
  92. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/tests/integration/__fixtures__/interpret/data/b/meta.json +0 -0
  93. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/tests/integration/__fixtures__/interpret/dirsql.config.py +0 -0
  94. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/tests/integration/__fixtures__/interpret/dirsql.config_no_app.py +0 -0
  95. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/tests/integration/__fixtures__/interpret/dirsql.config_raises.py +0 -0
  96. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/tests/integration/__init__.py +0 -0
  97. {dirsql-0.3.34 → dirsql-0.3.36}/packages/python/tests/integration/interpret_subprocess.py +0 -0
  98. {dirsql-0.3.34 → dirsql-0.3.36}/packages/rust/Cargo.toml +0 -0
  99. {dirsql-0.3.34 → dirsql-0.3.36}/packages/rust/README.md +0 -0
  100. {dirsql-0.3.34 → dirsql-0.3.36}/packages/rust/benches/db_bench.rs +0 -0
  101. {dirsql-0.3.34 → dirsql-0.3.36}/packages/rust/benches/differ_bench.rs +0 -0
  102. {dirsql-0.3.34 → dirsql-0.3.36}/packages/rust/benches/matcher_bench.rs +0 -0
  103. {dirsql-0.3.34 → dirsql-0.3.36}/packages/rust/benches/scanner_bench.rs +0 -0
  104. {dirsql-0.3.34 → dirsql-0.3.36}/packages/rust/docs/api/index.md +0 -0
  105. {dirsql-0.3.34 → dirsql-0.3.36}/packages/rust/docs/cli/config.md +0 -0
  106. {dirsql-0.3.34 → dirsql-0.3.36}/packages/rust/docs/cli/http-api.md +0 -0
  107. {dirsql-0.3.34 → dirsql-0.3.36}/packages/rust/docs/cli/index.md +0 -0
  108. {dirsql-0.3.34 → dirsql-0.3.36}/packages/rust/docs/cli/init.md +0 -0
  109. {dirsql-0.3.34 → dirsql-0.3.36}/packages/rust/docs/cli/server.md +0 -0
  110. {dirsql-0.3.34 → dirsql-0.3.36}/packages/rust/docs/getting-started.md +0 -0
  111. {dirsql-0.3.34 → dirsql-0.3.36}/packages/rust/docs/guide/async.md +0 -0
  112. {dirsql-0.3.34 → dirsql-0.3.36}/packages/rust/docs/guide/crdt.md +0 -0
  113. {dirsql-0.3.34 → dirsql-0.3.36}/packages/rust/docs/guide/persistence.md +0 -0
  114. {dirsql-0.3.34 → dirsql-0.3.36}/packages/rust/docs/guide/querying.md +0 -0
  115. {dirsql-0.3.34 → dirsql-0.3.36}/packages/rust/docs/guide/tables.md +0 -0
  116. {dirsql-0.3.34 → dirsql-0.3.36}/packages/rust/docs/guide/watching.md +0 -0
  117. {dirsql-0.3.34 → dirsql-0.3.36}/packages/rust/docs/index.md +0 -0
  118. {dirsql-0.3.34 → dirsql-0.3.36}/packages/rust/docs/migrations.md +0 -0
  119. {dirsql-0.3.34 → dirsql-0.3.36}/packages/rust/src/bin/dirsql.rs +0 -0
  120. {dirsql-0.3.34 → dirsql-0.3.36}/packages/rust/src/cli/init.rs +0 -0
  121. {dirsql-0.3.34 → dirsql-0.3.36}/packages/rust/src/cli/serialize.rs +0 -0
  122. {dirsql-0.3.34 → dirsql-0.3.36}/packages/rust/src/cli/server.rs +0 -0
  123. {dirsql-0.3.34 → dirsql-0.3.36}/packages/rust/src/config.rs +0 -0
  124. {dirsql-0.3.34 → dirsql-0.3.36}/packages/rust/src/db.rs +0 -0
  125. {dirsql-0.3.34 → dirsql-0.3.36}/packages/rust/src/differ.rs +0 -0
  126. {dirsql-0.3.34 → dirsql-0.3.36}/packages/rust/src/lib.rs +0 -0
  127. {dirsql-0.3.34 → dirsql-0.3.36}/packages/rust/src/matcher.rs +0 -0
  128. {dirsql-0.3.34 → dirsql-0.3.36}/packages/rust/src/persist.rs +0 -0
  129. {dirsql-0.3.34 → dirsql-0.3.36}/packages/rust/src/scanner.rs +0 -0
  130. {dirsql-0.3.34 → dirsql-0.3.36}/packages/rust/src/watcher.rs +0 -0
@@ -499,7 +499,7 @@ dependencies = [
499
499
 
500
500
  [[package]]
501
501
  name = "dirsql-py-ext"
502
- version = "0.3.34"
502
+ version = "0.3.36"
503
503
  dependencies = [
504
504
  "dirsql",
505
505
  "pyo3",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dirsql
3
- Version: 0.3.34
3
+ Version: 0.3.36
4
4
  Requires-Dist: pytest>=8 ; extra == 'dev'
5
5
  Requires-Dist: pytest-describe>=2 ; extra == 'dev'
6
6
  Requires-Dist: pytest-asyncio>=0.23 ; extra == 'dev'
@@ -1,10 +1,21 @@
1
1
  """Async-by-default DirSQL wrapper."""
2
2
 
3
3
  import asyncio
4
+ from typing import TYPE_CHECKING
4
5
 
5
6
  from dirsql._dirsql import DirSQL as _RustDirSQL
6
7
  from dirsql.resolve_config import resolve_config
7
8
 
9
+ if TYPE_CHECKING:
10
+ # `typing.override` is 3.12+; the package supports 3.11, so source it from
11
+ # `typing_extensions` for the type checker only. At runtime `@override` is
12
+ # a pure marker, so a no-op identity avoids the runtime dependency.
13
+ from typing_extensions import override
14
+ else:
15
+
16
+ def override(func):
17
+ return func
18
+
8
19
 
9
20
  class _WatchStream:
10
21
  """Async iterator that polls for file events."""
@@ -111,15 +122,24 @@ class DirSQL:
111
122
  raise self._init_error
112
123
 
113
124
  async def query(self, sql):
114
- """Execute a SQL query asynchronously."""
115
- return await asyncio.to_thread(self._db.query, sql) # ty:ignore[unresolved-attribute]
125
+ """Execute a SQL query asynchronously.
126
+
127
+ Awaits :meth:`ready` first, so calling ``query`` before an explicit
128
+ ``await db.ready()`` waits for the background scan (and re-raises any
129
+ initialization error) instead of failing on a still-``None`` ``_db``.
130
+ """
131
+ await self.ready()
132
+ db = self._db
133
+ assert db is not None # ready() returned, so _init_bg set _db
134
+ return await asyncio.to_thread(db.query, sql)
116
135
 
117
136
  def watch(self):
118
137
  """Start watching for file changes. Returns an async iterable of RowEvent."""
119
138
  return _WatchStream(self._db)
120
139
 
121
140
  @property
122
- def __dict__(self): # ty:ignore[missing-override-decorator]
141
+ @override
142
+ def __dict__(self):
123
143
  """Resolved construction state as a JSON-serializable dict.
124
144
 
125
145
  Recomputed on each access; reads the ``.dirsql.toml`` if ``config=``
@@ -13,6 +13,8 @@ from collections.abc import Callable
13
13
  from os import PathLike
14
14
  from typing import Any, NotRequired, TypedDict
15
15
 
16
+ from typing_extensions import override
17
+
16
18
  __version__: str
17
19
 
18
20
  Row = dict[str, Any]
@@ -51,7 +53,8 @@ class RowEvent:
51
53
  error: str | None
52
54
  file_path: str | None
53
55
 
54
- def __repr__(self) -> str: ... # ty:ignore[missing-override-decorator]
56
+ @override
57
+ def __repr__(self) -> str: ...
55
58
 
56
59
  class DirSQL:
57
60
  """Synchronous binding class. ``dirsql._async.DirSQL`` wraps it."""
@@ -23,7 +23,11 @@ def resolve_config(
23
23
  cfg_dir = os.path.dirname(os.path.abspath(config))
24
24
 
25
25
  def _abs(p):
26
- return p if os.path.isabs(p) else os.path.join(cfg_dir, p) # ty:ignore[no-matching-overload]
26
+ # Only reached from the `"<key>" in cfg` branches below, and cfg is
27
+ # non-empty only after a config file was loaded -- which sets cfg_dir.
28
+ base = cfg_dir
29
+ assert base is not None
30
+ return p if os.path.isabs(p) else os.path.join(base, p)
27
31
 
28
32
  return {
29
33
  "root": root or (_abs(cfg["root"]) if "root" in cfg else cfg_dir),
@@ -4,7 +4,7 @@ name = "dirsql-py-ext"
4
4
  # pypi/maturin handler can rewrite it via `write-version` before
5
5
  # `maturin build`. `pyproject.toml` declares `dynamic = ["version"]`
6
6
  # and maturin reads this field. Mirrors `packages/rust/Cargo.toml`.
7
- version = "0.3.34"
7
+ version = "0.3.36"
8
8
  edition.workspace = true
9
9
  publish = false
10
10
  readme = "README.md"
@@ -160,7 +160,15 @@ pub enum ServerError {
160
160
  #[cfg(test)]
161
161
  mod tests {
162
162
  use super::*;
163
- use tempfile::TempDir;
163
+
164
+ // `From<DirSQL> for AppState` produces the `Ready` arm -- this is
165
+ // verified at the integration tier by `from_dirsql_yields_ready_state`
166
+ // in `tests/cli_integration.rs`, which builds a real `DirSQL` over a
167
+ // temp directory (so the initial scan runs) and asserts the public
168
+ // `AppState::Ready` variant. It lived here once but needed
169
+ // `std::fs::write` to populate the scanned directory, which the
170
+ // `testing-conventions` `unit lint` isolation rule forbids in a unit
171
+ // test (effectful std). The pure config-default test below stays inline.
164
172
 
165
173
  #[test]
166
174
  fn default_config_binds_localhost_7117_with_30s_timeout() {
@@ -169,35 +177,4 @@ mod tests {
169
177
  assert_eq!(cfg.port, 7117);
170
178
  assert_eq!(cfg.query_timeout, Duration::from_secs(30));
171
179
  }
172
-
173
- #[test]
174
- fn app_state_from_dirsql_is_ready() {
175
- // `AppState: !Debug` (it wraps a `DirSQL`). Assert the `Ready` variant
176
- // without a dead match arm by routing through `require_ready`, which
177
- // returns `Ok` only for `Ready` -- a branch-free `.is_ok()` check.
178
- let dir = TempDir::new().unwrap();
179
- // Write a matching file so the extract closure runs during the initial
180
- // scan (otherwise the closure body would be a dead coverage region).
181
- std::fs::write(dir.path().join("a.txt"), b"").unwrap();
182
- let db = DirSQL::with_ignore(
183
- dir.path(),
184
- vec![crate::Table::new(
185
- "CREATE TABLE t (name TEXT)",
186
- "*.txt",
187
- |_| {
188
- vec![crate::Row::from_iter([(
189
- "name".to_string(),
190
- crate::Value::Text("x".into()),
191
- )])]
192
- },
193
- )],
194
- Vec::<String>::new(),
195
- )
196
- .unwrap();
197
- let state: AppState = db.into();
198
- assert!(
199
- router::require_ready(&state).is_ok(),
200
- "From<DirSQL> must produce AppState::Ready",
201
- );
202
- }
203
180
  }
@@ -296,7 +296,16 @@ fn json_to_value(v: serde_json::Value) -> Value {
296
296
  mod tests {
297
297
  use super::*;
298
298
  use std::io::Cursor;
299
- use std::process::{Command, Stdio};
299
+
300
+ // The subprocess-driven tests (`InterpretHelper::from_child` against a
301
+ // real `bash`/`true` child, and the `build_dirsql` round-trips that
302
+ // spawn a fake helper and write real fixture files) live in
303
+ // `tests/native_config.rs`. They exercise effectful std
304
+ // (`std::process::Command`, `std::fs::write`) and so belong at the
305
+ // integration tier per the `testing-conventions` `unit lint` isolation
306
+ // rule. The pure wire-format tests below operate on in-memory
307
+ // `Cursor`/`Vec` streams and stay inline next to the private functions
308
+ // they cover.
300
309
 
301
310
  // -- parse_handshake -----------------------------------------------
302
311
 
@@ -511,160 +520,10 @@ mod tests {
511
520
  }
512
521
 
513
522
  // -- build_dirsql --------------------------------------------------
514
-
515
- // -- InterpretHelper end-to-end (subprocess via bash) -------------
516
-
517
- /// Spawn a fake helper that prints `handshake` once on stdout then
518
- /// emits one canned response per line received on stdin (an infinite
519
- /// loop until stdin closes). Returns the helper paired with the
520
- /// parsed [`NativeConfig`]. Drives the production
521
- /// `InterpretHelper::from_child` constructor so the post-spawn
522
- /// plumbing is exercised by these tests.
523
- fn spawn_fake_helper(
524
- handshake: &str,
525
- response_per_request: &str,
526
- ) -> (Arc<InterpretHelper>, NativeConfig) {
527
- let child = Command::new("bash")
528
- .arg("-c")
529
- .arg(format!(
530
- "printf '%s\\n' '{handshake}'; while IFS= read -r line; do printf '%s\\n' '{response_per_request}'; done",
531
- ))
532
- .stdin(Stdio::piped())
533
- .stdout(Stdio::piped())
534
- .spawn()
535
- .unwrap();
536
- InterpretHelper::from_child(child).unwrap()
537
- }
538
-
539
- #[test]
540
- fn build_dirsql_threads_persist_and_persist_path_into_the_builder() {
541
- // Cover the `if config.persist` and `if let Some(p) = config.persist_path`
542
- // branches in `build_dirsql`. A real persist build needs the cache file
543
- // to live under root, so point persist_path at a path inside the
544
- // tempdir and pass persist=true.
545
- let tmp = tempfile::TempDir::new().unwrap();
546
- std::fs::write(tmp.path().join("a.json"), b"{}").unwrap();
547
- let cache_path = tmp.path().join(".dirsql/cache.db");
548
-
549
- let handshake = format!(
550
- r#"{{"type":"config","state":{{"root":"{}","tables":[{{"ddl":"CREATE TABLE papers (title TEXT)","glob":"*.json"}}],"persist":true,"persist_path":"{}"}}}}"#,
551
- tmp.path().display(),
552
- cache_path.display(),
553
- );
554
- let (helper, config) = spawn_fake_helper(
555
- &handshake,
556
- r#"{"type":"result","id":1,"ok":true,"rows":[{"title":"y"}]}"#,
557
- );
558
- assert!(config.persist);
559
- assert_eq!(config.persist_path.as_ref().unwrap(), &cache_path);
560
-
561
- let db = build_dirsql(helper, config).unwrap();
562
- let rows = db.query("SELECT COUNT(*) AS n FROM papers").unwrap();
563
- assert_eq!(rows[0].get("n"), Some(&Value::Integer(1)));
564
- assert!(cache_path.exists(), "persist build should create the cache");
565
- }
566
-
567
- #[test]
568
- fn build_dirsql_threads_extensions_into_the_builder() {
569
- // A handshake whose `extensions` names a missing shared library must
570
- // fail the build, proving the parsed extensions reach the core's
571
- // load-at-startup path (enable -> load -> disable). (#229)
572
- let tmp = tempfile::TempDir::new().unwrap();
573
- std::fs::write(tmp.path().join("a.json"), b"{}").unwrap();
574
-
575
- let handshake = format!(
576
- r#"{{"type":"config","state":{{"root":"{}","tables":[{{"ddl":"CREATE TABLE papers (title TEXT)","glob":"*.json"}}],"extensions":[{{"path":"/nonexistent/dirsql-no-such-ext.so"}}]}}}}"#,
577
- tmp.path().display(),
578
- );
579
- let (helper, config) = spawn_fake_helper(
580
- &handshake,
581
- r#"{"type":"result","id":1,"ok":true,"rows":[{"title":"y"}]}"#,
582
- );
583
- assert_eq!(config.extensions.len(), 1);
584
- assert_eq!(
585
- config.extensions[0].path,
586
- PathBuf::from("/nonexistent/dirsql-no-such-ext.so"),
587
- );
588
-
589
- let err = match build_dirsql(helper, config) {
590
- Ok(_) => panic!("expected build to fail on a missing extension"),
591
- Err(e) => e,
592
- };
593
- assert!(err.contains("failed to load extension"), "got: {err}");
594
- }
595
-
596
- #[test]
597
- fn build_dirsql_round_trip_with_fake_helper_invokes_extract_per_matched_file() {
598
- // Create a tempdir with two matching files so the scan invokes
599
- // `extract` twice — exercising the closure created inside
600
- // `build_dirsql`, the `dispatch_extract` path through the
601
- // helper's IO mutex, and end-to-end DDL/glob wiring.
602
- let tmp = tempfile::TempDir::new().unwrap();
603
- std::fs::write(tmp.path().join("a.json"), b"{}").unwrap();
604
- std::fs::write(tmp.path().join("b.json"), b"{}").unwrap();
605
-
606
- let handshake = format!(
607
- r#"{{"type":"config","state":{{"root":"{}","tables":[{{"ddl":"CREATE TABLE papers (title TEXT)","glob":"*.json"}}]}}}}"#,
608
- tmp.path().display(),
609
- );
610
- let (helper, config) = spawn_fake_helper(
611
- &handshake,
612
- r#"{"type":"result","id":1,"ok":true,"rows":[{"title":"x"}]}"#,
613
- );
614
-
615
- let db = build_dirsql(helper, config).unwrap();
616
- let rows = db.query("SELECT COUNT(*) AS n FROM papers").unwrap();
617
- assert_eq!(rows.len(), 1);
618
- assert_eq!(rows[0].get("n"), Some(&Value::Integer(2)));
619
- }
620
-
621
- #[test]
622
- fn helper_extract_round_trip_succeeds_against_a_fake_subprocess() {
623
- let (helper, _config) = spawn_fake_helper(
624
- r#"{"type":"config","state":{"root":"/tmp","tables":[{"ddl":"CREATE TABLE papers (title TEXT)","glob":"*"}]}}"#,
625
- r#"{"type":"result","id":1,"ok":true,"rows":[{"title":"Alpha"}]}"#,
626
- );
627
-
628
- let rows = helper.extract("papers", "/x/a.json").unwrap();
629
- assert_eq!(rows.len(), 1);
630
- assert_eq!(rows[0].get("title"), Some(&Value::Text("Alpha".into())));
631
-
632
- // Second request increments the id counter and still works against
633
- // the canned response (the fake echoes the same line per request).
634
- let rows2 = helper.extract("papers", "/x/b.json").unwrap();
635
- assert_eq!(rows2.len(), 1);
636
- }
637
-
638
- #[test]
639
- fn helper_from_child_errors_when_stdin_was_not_piped() {
640
- // `Command::spawn` without `.stdin(Stdio::piped())` inherits the
641
- // parent's stdin — `child.stdin.take()` returns `None`, driving
642
- // the first `.ok_or_else` arm in `from_child`.
643
- let child = Command::new("true").spawn().unwrap();
644
- let err = match InterpretHelper::from_child(child) {
645
- Ok(_) => panic!("expected error when stdin is not piped"),
646
- Err(e) => e,
647
- };
648
- assert!(
649
- err.contains("failed to capture interpret stdin"),
650
- "got: {err}",
651
- );
652
- }
653
-
654
- #[test]
655
- fn helper_from_child_errors_when_stdout_was_not_piped() {
656
- // Pipe stdin but leave stdout inherited — `child.stdout.take()`
657
- // returns `None`, driving the second `.ok_or_else` arm.
658
- let child = Command::new("true").stdin(Stdio::piped()).spawn().unwrap();
659
- let err = match InterpretHelper::from_child(child) {
660
- Ok(_) => panic!("expected error when stdout is not piped"),
661
- Err(e) => e,
662
- };
663
- assert!(
664
- err.contains("failed to capture interpret stdout"),
665
- "got: {err}",
666
- );
667
- }
523
+ //
524
+ // The `build_dirsql` round-trips and the `InterpretHelper::from_child`
525
+ // success/error paths spawn a real subprocess (`bash`/`true`) and write
526
+ // real fixture files, so they live in `tests/native_config.rs`.
668
527
 
669
528
  /// A writer that fails every write with the given `io::ErrorKind`,
670
529
  /// used to exercise `dispatch_extract`'s IO-error arms.
@@ -720,64 +579,4 @@ mod tests {
720
579
  let err = dispatch_extract(&mut stdout, &mut stdin, 1, "t", "p").unwrap_err();
721
580
  assert!(err.contains("read from interpret"), "got: {err}");
722
581
  }
723
-
724
- #[test]
725
- fn helper_from_child_errors_when_subprocess_emits_no_handshake() {
726
- // A subprocess that exits without writing anything drives
727
- // `from_child` through to `parse_handshake`'s empty-line arm.
728
- let child = Command::new("true")
729
- .stdin(Stdio::piped())
730
- .stdout(Stdio::piped())
731
- .spawn()
732
- .unwrap();
733
- let err = match InterpretHelper::from_child(child) {
734
- Ok(_) => panic!("expected error from empty-stdout subprocess"),
735
- Err(e) => e,
736
- };
737
- assert!(
738
- err.contains("exited before sending handshake"),
739
- "got: {err}"
740
- );
741
- }
742
-
743
- #[test]
744
- fn build_dirsql_errors_when_ddl_has_no_table_name() {
745
- // The DDL is rejected by `parse_table_name` before any extract
746
- // closure is created, so the helper's IO is never touched. Use a
747
- // real child (`true` exits cleanly) just to satisfy the
748
- // `InterpretHelper` shape.
749
- let mut child = Command::new("true")
750
- .stdin(Stdio::piped())
751
- .stdout(Stdio::piped())
752
- .spawn()
753
- .unwrap();
754
- let stdin = child.stdin.take().unwrap();
755
- let stdout = child.stdout.take().unwrap();
756
- let helper = Arc::new(InterpretHelper {
757
- _child: child,
758
- io: Arc::new(Mutex::new(HelperIo {
759
- stdin: BufWriter::new(stdin),
760
- stdout: BufReader::new(stdout),
761
- })),
762
- next_id: AtomicU64::new(1),
763
- });
764
- let config = NativeConfig {
765
- root: PathBuf::from("/tmp"),
766
- tables: vec![HandshakeTable {
767
- ddl: "NOT VALID DDL".into(),
768
- glob: "*.json".into(),
769
- strict: false,
770
- }],
771
- ignore: Vec::new(),
772
- persist: false,
773
- persist_path: None,
774
- extensions: Vec::new(),
775
- };
776
- // `DirSQL` doesn't impl Debug, so we can't use `unwrap_err` directly.
777
- let err = match build_dirsql(helper, config) {
778
- Ok(_) => panic!("expected build_dirsql to fail on invalid DDL"),
779
- Err(e) => e,
780
- };
781
- assert!(err.contains("could not parse table name"), "got: {err}");
782
- }
783
582
  }
@@ -167,15 +167,16 @@ pub(super) fn error_response(status: StatusCode, message: impl Into<String>) ->
167
167
  #[cfg(test)]
168
168
  mod tests {
169
169
  use super::*;
170
- use crate::db::DbError;
171
170
 
172
- #[test]
173
- fn classify_core_error_is_bad_request() {
174
- // A SQLite-level failure is the client's fault (bad SQL), so it maps
175
- // to 400 rather than a server error.
176
- let err = DirSqlError::Core(DbError::SchemaMismatch("nope".into()));
177
- assert_eq!(classify_query_error(&err), StatusCode::BAD_REQUEST);
178
- }
171
+ // The `DirSqlError::Core => 400` arm of `classify_query_error` is
172
+ // exercised end-to-end at the integration tier by
173
+ // `post_query_malformed_sql_returns_400_not_500` in
174
+ // `tests/cli_integration.rs`, which posts malformed SQL to `/query` and
175
+ // asserts the 400. Constructing a `Core` value inline would require
176
+ // importing the first-party `crate::db::DbError`, which the
177
+ // `testing-conventions` `unit lint` isolation rule forbids (a unit test
178
+ // may reach only `super::` and pure `std`). The non-Core arm below is
179
+ // pure -- it builds a `super::DirSqlError::Lock` -- so it stays inline.
179
180
 
180
181
  #[test]
181
182
  fn classify_non_core_error_is_internal_server_error() {
@@ -65,9 +65,10 @@ include = [
65
65
  ]
66
66
  manifest-path = "packages/python/Cargo.toml"
67
67
 
68
- # Type checking. Migration model: existing untyped code is frozen behind
69
- # `# ty: ignore[...]` baseline comments inserted by `ty check --add-ignore`;
70
- # any new error in new or touched code fails CI. Every rule is promoted to
68
+ # Type checking. The source tree currently type-checks clean: the initial
69
+ # `# ty: ignore[...]` baseline (seeded by `ty check --add-ignore`) has been
70
+ # driven to zero, so any new error in new or touched code fails CI with no
71
+ # pre-existing suppressions to hide behind. Every rule is promoted to
71
72
  # `"error"`, matching TypeScript-strict discipline. Test files are excluded
72
73
  # because they monkeypatch fakes onto production module attributes
73
74
  # (`monkeypatch.setattr(async_mod, "_RustDirSQL", ...)`) -- AGENTS.md flags
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes