dirsql 0.2.2__tar.gz → 0.2.4__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 (55) hide show
  1. {dirsql-0.2.2 → dirsql-0.2.4}/PKG-INFO +8 -9
  2. {dirsql-0.2.2/packages/python → dirsql-0.2.4}/README.md +7 -8
  3. {dirsql-0.2.2 → dirsql-0.2.4/packages/python}/README.md +7 -8
  4. {dirsql-0.2.2 → dirsql-0.2.4}/packages/python/src/lib.rs +25 -21
  5. {dirsql-0.2.2 → dirsql-0.2.4}/packages/python/tests/integration/test_binding.py +23 -33
  6. {dirsql-0.2.2 → dirsql-0.2.4}/packages/python/tests/integration/test_docs_gaps.py +7 -7
  7. {dirsql-0.2.2 → dirsql-0.2.4}/packages/python/tests/integration/test_from_config.py +15 -15
  8. {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/src/config.rs +55 -3
  9. {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/src/db.rs +4 -4
  10. {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/src/lib.rs +313 -61
  11. {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/src/parser.rs +2 -2
  12. {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/src/watcher.rs +15 -15
  13. {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/tests/sdk.rs +230 -0
  14. {dirsql-0.2.2 → dirsql-0.2.4}/pyproject.toml +1 -1
  15. {dirsql-0.2.2 → dirsql-0.2.4}/python/dirsql/_async.py +17 -27
  16. {dirsql-0.2.2 → dirsql-0.2.4}/python/dirsql/test_async.py +2 -1
  17. {dirsql-0.2.2 → dirsql-0.2.4}/Cargo.lock +0 -0
  18. {dirsql-0.2.2 → dirsql-0.2.4}/Cargo.toml +0 -0
  19. {dirsql-0.2.2 → dirsql-0.2.4}/packages/python/Cargo.toml +0 -0
  20. {dirsql-0.2.2 → dirsql-0.2.4}/packages/python/python/conftest.py +0 -0
  21. {dirsql-0.2.2 → dirsql-0.2.4}/packages/python/tests/__init__.py +0 -0
  22. {dirsql-0.2.2 → dirsql-0.2.4}/packages/python/tests/conftest.py +0 -0
  23. {dirsql-0.2.2 → dirsql-0.2.4}/packages/python/tests/integration/__init__.py +0 -0
  24. {dirsql-0.2.2 → dirsql-0.2.4}/packages/python/tests/integration/test_async_dirsql.py +0 -0
  25. {dirsql-0.2.2 → dirsql-0.2.4}/packages/python/tests/integration/test_dirsql.py +0 -0
  26. {dirsql-0.2.2 → dirsql-0.2.4}/packages/python/tests/integration/test_docs_examples.py +0 -0
  27. {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/Cargo.toml +0 -0
  28. {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/README.md +0 -0
  29. {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/benches/db_bench.rs +0 -0
  30. {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/benches/differ_bench.rs +0 -0
  31. {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/benches/matcher_bench.rs +0 -0
  32. {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/benches/scanner_bench.rs +0 -0
  33. {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/src/bin/dirsql.rs +0 -0
  34. {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/src/cli/mod.rs +0 -0
  35. {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/src/cli/router.rs +0 -0
  36. {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/src/cli/serialize.rs +0 -0
  37. {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/src/cli/server.rs +0 -0
  38. {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/src/differ.rs +0 -0
  39. {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/src/matcher.rs +0 -0
  40. {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/src/scanner.rs +0 -0
  41. {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/tests/async_sdk.rs +0 -0
  42. {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/tests/cli_e2e.rs +0 -0
  43. {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/tests/cli_integration.rs +0 -0
  44. {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/tests/docs_examples.rs +0 -0
  45. {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/tests/docs_gaps.rs +0 -0
  46. {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/tests/from_config.rs +0 -0
  47. {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/tests/readonly_query.rs +0 -0
  48. {dirsql-0.2.2 → dirsql-0.2.4}/python/dirsql/__init__.py +0 -0
  49. {dirsql-0.2.2 → dirsql-0.2.4}/python/dirsql/_cli/__init__.py +0 -0
  50. {dirsql-0.2.2 → dirsql-0.2.4}/python/dirsql/_cli/binary_path.py +0 -0
  51. {dirsql-0.2.2 → dirsql-0.2.4}/python/dirsql/_cli/binary_path_test.py +0 -0
  52. {dirsql-0.2.2 → dirsql-0.2.4}/python/dirsql/_cli/is_windows.py +0 -0
  53. {dirsql-0.2.2 → dirsql-0.2.4}/python/dirsql/_cli/is_windows_test.py +0 -0
  54. {dirsql-0.2.2 → dirsql-0.2.4}/python/dirsql/_cli/main.py +0 -0
  55. {dirsql-0.2.2 → dirsql-0.2.4}/python/dirsql/_cli/main_test.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dirsql
3
- Version: 0.2.2
3
+ Version: 0.2.4
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'
@@ -167,13 +167,16 @@ Defines how files map to a SQL table.
167
167
  - **`glob`** (`str`): A glob pattern matched against file paths relative to root.
168
168
  - **`extract`** (`Callable[[str, str], list[dict]]`): A function receiving `(relative_path, file_content)` and returning a list of row dicts. Each dict's keys must match the DDL column names.
169
169
 
170
- ### `DirSQL(root, *, tables, ignore=None)`
170
+ ### `DirSQL(root=None, *, tables=None, ignore=None, config=None)`
171
171
 
172
172
  Creates an in-memory SQLite database indexed from the directory at `root`. The constructor is sync and returns immediately; scanning runs in a background thread.
173
173
 
174
- - **`root`** (`str`): Path to the directory to index.
175
- - **`tables`** (`list[Table]`): Table definitions.
176
- - **`ignore`** (`list[str] | None`): Glob patterns for paths to skip.
174
+ At least one of `root` or `config` must be supplied. When both `root` and `config` are passed (or `config` declares `[dirsql].root`), the explicit `root` wins and a warning is emitted on stderr.
175
+
176
+ - **`root`** (`str | None`): Path to the directory to index. Optional when `config` supplies one.
177
+ - **`tables`** (`list[Table] | None`): Programmatic table definitions. Appended to any tables in the config file.
178
+ - **`ignore`** (`list[str] | None`): Glob patterns for paths to skip. Appended to any `[dirsql].ignore` patterns in the config file.
179
+ - **`config`** (`str | None`): Optional path to a `.dirsql.toml` file. Its `[[table]]` entries, `[dirsql].ignore`, and optional `[dirsql].root` are merged into the constructor's inputs.
177
180
 
178
181
  #### `await DirSQL.ready()`
179
182
 
@@ -187,10 +190,6 @@ Execute a SQL query. Returns a list of dicts keyed by column name. Internal trac
187
190
 
188
191
  Returns an async iterator that yields `RowEvent` objects as files change on disk. Starts the filesystem watcher on first iteration.
189
192
 
190
- #### `DirSQL.from_config(path) -> DirSQL`
191
-
192
- Create a `DirSQL` instance from a `.dirsql.toml` config file. Returns immediately; scanning runs in the background. Call `await db.ready()` before querying.
193
-
194
193
  ### `RowEvent`
195
194
 
196
195
  Emitted by `watch()` when a file change produces row-level diffs.
@@ -150,13 +150,16 @@ Defines how files map to a SQL table.
150
150
  - **`glob`** (`str`): A glob pattern matched against file paths relative to root.
151
151
  - **`extract`** (`Callable[[str, str], list[dict]]`): A function receiving `(relative_path, file_content)` and returning a list of row dicts. Each dict's keys must match the DDL column names.
152
152
 
153
- ### `DirSQL(root, *, tables, ignore=None)`
153
+ ### `DirSQL(root=None, *, tables=None, ignore=None, config=None)`
154
154
 
155
155
  Creates an in-memory SQLite database indexed from the directory at `root`. The constructor is sync and returns immediately; scanning runs in a background thread.
156
156
 
157
- - **`root`** (`str`): Path to the directory to index.
158
- - **`tables`** (`list[Table]`): Table definitions.
159
- - **`ignore`** (`list[str] | None`): Glob patterns for paths to skip.
157
+ At least one of `root` or `config` must be supplied. When both `root` and `config` are passed (or `config` declares `[dirsql].root`), the explicit `root` wins and a warning is emitted on stderr.
158
+
159
+ - **`root`** (`str | None`): Path to the directory to index. Optional when `config` supplies one.
160
+ - **`tables`** (`list[Table] | None`): Programmatic table definitions. Appended to any tables in the config file.
161
+ - **`ignore`** (`list[str] | None`): Glob patterns for paths to skip. Appended to any `[dirsql].ignore` patterns in the config file.
162
+ - **`config`** (`str | None`): Optional path to a `.dirsql.toml` file. Its `[[table]]` entries, `[dirsql].ignore`, and optional `[dirsql].root` are merged into the constructor's inputs.
160
163
 
161
164
  #### `await DirSQL.ready()`
162
165
 
@@ -170,10 +173,6 @@ Execute a SQL query. Returns a list of dicts keyed by column name. Internal trac
170
173
 
171
174
  Returns an async iterator that yields `RowEvent` objects as files change on disk. Starts the filesystem watcher on first iteration.
172
175
 
173
- #### `DirSQL.from_config(path) -> DirSQL`
174
-
175
- Create a `DirSQL` instance from a `.dirsql.toml` config file. Returns immediately; scanning runs in the background. Call `await db.ready()` before querying.
176
-
177
176
  ### `RowEvent`
178
177
 
179
178
  Emitted by `watch()` when a file change produces row-level diffs.
@@ -150,13 +150,16 @@ Defines how files map to a SQL table.
150
150
  - **`glob`** (`str`): A glob pattern matched against file paths relative to root.
151
151
  - **`extract`** (`Callable[[str, str], list[dict]]`): A function receiving `(relative_path, file_content)` and returning a list of row dicts. Each dict's keys must match the DDL column names.
152
152
 
153
- ### `DirSQL(root, *, tables, ignore=None)`
153
+ ### `DirSQL(root=None, *, tables=None, ignore=None, config=None)`
154
154
 
155
155
  Creates an in-memory SQLite database indexed from the directory at `root`. The constructor is sync and returns immediately; scanning runs in a background thread.
156
156
 
157
- - **`root`** (`str`): Path to the directory to index.
158
- - **`tables`** (`list[Table]`): Table definitions.
159
- - **`ignore`** (`list[str] | None`): Glob patterns for paths to skip.
157
+ At least one of `root` or `config` must be supplied. When both `root` and `config` are passed (or `config` declares `[dirsql].root`), the explicit `root` wins and a warning is emitted on stderr.
158
+
159
+ - **`root`** (`str | None`): Path to the directory to index. Optional when `config` supplies one.
160
+ - **`tables`** (`list[Table] | None`): Programmatic table definitions. Appended to any tables in the config file.
161
+ - **`ignore`** (`list[str] | None`): Glob patterns for paths to skip. Appended to any `[dirsql].ignore` patterns in the config file.
162
+ - **`config`** (`str | None`): Optional path to a `.dirsql.toml` file. Its `[[table]]` entries, `[dirsql].ignore`, and optional `[dirsql].root` are merged into the constructor's inputs.
160
163
 
161
164
  #### `await DirSQL.ready()`
162
165
 
@@ -170,10 +173,6 @@ Execute a SQL query. Returns a list of dicts keyed by column name. Internal trac
170
173
 
171
174
  Returns an async iterator that yields `RowEvent` objects as files change on disk. Starts the filesystem watcher on first iteration.
172
175
 
173
- #### `DirSQL.from_config(path) -> DirSQL`
174
-
175
- Create a `DirSQL` instance from a `.dirsql.toml` config file. Returns immediately; scanning runs in the background. Call `await db.ready()` before querying.
176
-
177
176
  ### `RowEvent`
178
177
 
179
178
  Emitted by `watch()` when a file change produces row-level diffs.
@@ -4,8 +4,8 @@
4
4
  //! - wraps a Python `extract` callable in a Rust closure (acquiring the GIL as
5
5
  //! needed) so it can be handed to [`dirsql::Table`]
6
6
  //! - converts row dicts, values, and events between Python and Rust
7
- //! - forwards `new` / `from_config` / `query` / `_start_watcher` /
8
- //! `_poll_events` to the corresponding `DirSQL` methods
7
+ //! - forwards `new` / `query` / `_start_watcher` / `_poll_events` to the
8
+ //! corresponding `DirSQL` methods
9
9
  //!
10
10
  //! The Python-side async wrapper (`dirsql._async.DirSQL`) drives this binding
11
11
  //! via `asyncio.to_thread`.
@@ -86,37 +86,41 @@ mod python {
86
86
  #[pymethods]
87
87
  impl PyDirSQL {
88
88
  #[new]
89
- #[pyo3(signature = (root, *, tables, ignore=None))]
89
+ #[pyo3(signature = (root=None, *, tables=None, ignore=None, config=None))]
90
90
  fn new(
91
91
  py: Python<'_>,
92
- root: String,
93
- tables: Vec<PyRef<'_, PyTable>>,
92
+ root: Option<String>,
93
+ tables: Option<Vec<PyRef<'_, PyTable>>>,
94
94
  ignore: Option<Vec<String>>,
95
+ config: Option<String>,
95
96
  ) -> PyResult<Self> {
96
- let rust_tables: Vec<Table> = tables.iter().map(|t| build_table(py, t)).collect();
97
+ let rust_tables: Vec<Table> = tables
98
+ .as_deref()
99
+ .map(|ts| ts.iter().map(|t| build_table(py, t)).collect())
100
+ .unwrap_or_default();
97
101
 
98
102
  let inner = py
99
- .detach(move || match ignore {
100
- Some(ig) => DirSQL::with_ignore(root, rust_tables, ig),
101
- None => DirSQL::new(root, rust_tables),
103
+ .detach(move || {
104
+ let mut builder = DirSQL::builder();
105
+ if let Some(r) = root {
106
+ builder = builder.root(r);
107
+ }
108
+ if !rust_tables.is_empty() {
109
+ builder = builder.tables(rust_tables);
110
+ }
111
+ if let Some(ig) = ignore {
112
+ builder = builder.ignore(ig);
113
+ }
114
+ if let Some(c) = config {
115
+ builder = builder.config(c);
116
+ }
117
+ builder.build()
102
118
  })
103
119
  .map_err(to_py_err)?;
104
120
 
105
121
  Ok(Self { inner })
106
122
  }
107
123
 
108
- #[classmethod]
109
- fn from_config(
110
- _cls: &Bound<'_, pyo3::types::PyType>,
111
- py: Python<'_>,
112
- path: String,
113
- ) -> PyResult<Self> {
114
- let inner = py
115
- .detach(move || DirSQL::from_config_path(&path))
116
- .map_err(to_py_err)?;
117
- Ok(Self { inner })
118
- }
119
-
120
124
  fn query(&self, py: Python<'_>, sql: String) -> PyResult<Py<PyList>> {
121
125
  let db = self.inner.clone();
122
126
  let rows = py.detach(move || db.query(&sql)).map_err(to_py_err)?;
@@ -1,11 +1,11 @@
1
1
  """Integration tests for the Python SDK binding layer.
2
2
 
3
3
  These tests exercise the async Python wrapper in ``dirsql._async`` in
4
- isolation by mocking the Rust core (``dirsql._dirsql.DirSQL`` / its
5
- ``from_config`` classmethod). They verify the SDK's binding glue --
6
- offloading to threads, ready()/error propagation, lazy watcher startup,
7
- event iteration, config-based construction, and kwarg forwarding --
8
- without touching the real PyO3-backed engine.
4
+ isolation by mocking the Rust core (``dirsql._dirsql.DirSQL``). They
5
+ verify the SDK's binding glue -- offloading to threads, ready()/error
6
+ propagation, lazy watcher startup, event iteration, config-based
7
+ construction, and kwarg forwarding -- without touching the real
8
+ PyO3-backed engine.
9
9
 
10
10
  Core behaviour (SQL semantics, scanning, diffing, watching) is covered
11
11
  by the Rust core's own unit tests and by the local-only e2e suite.
@@ -27,35 +27,21 @@ class _FakeRustDirSQL:
27
27
 
28
28
  instances: list = []
29
29
 
30
- def __init__(self, root, *, tables, ignore=None):
30
+ def __init__(self, root=None, *, tables=None, ignore=None, config=None):
31
31
  self.root = root
32
32
  self.tables = tables
33
33
  self.ignore = ignore
34
+ self.config = config
34
35
  self.queries: list[str] = []
35
- self.query_results: list = [{"ok": 1}]
36
+ self.query_results: list = (
37
+ [{"from_config": config}] if config is not None else [{"ok": 1}]
38
+ )
36
39
  self.started = False
37
40
  self.poll_calls: list[int] = []
38
41
  # Scripted event batches; each poll returns the next batch.
39
42
  self.poll_batches: list[list] = []
40
43
  _FakeRustDirSQL.instances.append(self)
41
44
 
42
- # Class-level from_config so we can swap it with a callable that
43
- # returns a fresh instance (mirrors the real classmethod shape).
44
- @classmethod
45
- def from_config(cls, path):
46
- inst = object.__new__(cls)
47
- inst.root = None
48
- inst.tables = None
49
- inst.ignore = None
50
- inst.queries = []
51
- inst.query_results = [{"from_config": path}]
52
- inst.started = False
53
- inst.poll_calls = []
54
- inst.poll_batches = []
55
- inst.config_path = path
56
- cls.instances.append(inst)
57
- return inst
58
-
59
45
  def query(self, sql):
60
46
  self.queries.append(sql)
61
47
  return self.query_results
@@ -219,16 +205,17 @@ def describe_binding_layer():
219
205
  assert event == "late"
220
206
  assert len(fake.poll_calls) == 3
221
207
 
222
- def describe_from_config():
223
- # Feature: DirSQL.from_config(path) classmethod. See
208
+ def describe_config_kwarg():
209
+ # Feature: DirSQL(config=path) forwards to the Rust core. See
224
210
  # docs/guide/config.md and packages/python/README.md.
225
211
  @pytest.mark.asyncio
226
- async def it_delegates_to_rust_from_config(mock_core):
227
- db = async_mod.DirSQL.from_config("/some/.dirsql.toml")
212
+ async def it_forwards_config_path_to_core(mock_core):
213
+ db = async_mod.DirSQL(config="/some/.dirsql.toml")
228
214
  await db.ready()
229
215
 
230
216
  inst = _FakeRustDirSQL.instances[-1]
231
- assert inst.config_path == "/some/.dirsql.toml"
217
+ assert inst.config == "/some/.dirsql.toml"
218
+ assert inst.root is None
232
219
 
233
220
  result = await db.query("SELECT 1")
234
221
  assert result == [{"from_config": "/some/.dirsql.toml"}]
@@ -236,15 +223,18 @@ def describe_binding_layer():
236
223
  @pytest.mark.asyncio
237
224
  async def it_surfaces_config_load_errors(monkeypatch):
238
225
  class Boom(_FakeRustDirSQL):
239
- @classmethod
240
- def from_config(cls, path):
241
- raise FileNotFoundError(path)
226
+ def __init__(self, root=None, **kwargs):
227
+ raise FileNotFoundError(kwargs.get("config") or root)
242
228
 
243
229
  monkeypatch.setattr(async_mod, "_RustDirSQL", Boom)
244
- db = async_mod.DirSQL.from_config("/missing.toml")
230
+ db = async_mod.DirSQL(config="/missing.toml")
245
231
  with pytest.raises(FileNotFoundError):
246
232
  await db.ready()
247
233
 
234
+ def it_rejects_construction_without_root_or_config():
235
+ with pytest.raises(TypeError):
236
+ async_mod.DirSQL()
237
+
248
238
  def describe_ignore_kwarg():
249
239
  # Feature: ignore patterns. See docs/guide/tables.md and
250
240
  # packages/python/README.md (ignore= kwarg on DirSQL).
@@ -84,7 +84,7 @@ ddl = "CREATE TABLE produce (name TEXT, count TEXT)"
84
84
  glob = "*.tsv"
85
85
  """,
86
86
  )
87
- db = DirSQL.from_config(os.path.join(config_dir, ".dirsql.toml"))
87
+ db = DirSQL(config=os.path.join(config_dir, ".dirsql.toml"))
88
88
  await db.ready()
89
89
  results = await db.query("SELECT * FROM produce ORDER BY name")
90
90
  assert len(results) == 2
@@ -110,7 +110,7 @@ ddl = "CREATE TABLE events (type TEXT, count INTEGER)"
110
110
  glob = "*.ndjson"
111
111
  """,
112
112
  )
113
- db = DirSQL.from_config(os.path.join(config_dir, ".dirsql.toml"))
113
+ db = DirSQL(config=os.path.join(config_dir, ".dirsql.toml"))
114
114
  await db.ready()
115
115
  results = await db.query("SELECT * FROM events ORDER BY type")
116
116
  assert len(results) == 2
@@ -132,7 +132,7 @@ ddl = "CREATE TABLE app (name TEXT, version TEXT)"
132
132
  glob = "config/*.toml"
133
133
  """,
134
134
  )
135
- db = DirSQL.from_config(os.path.join(config_dir, ".dirsql.toml"))
135
+ db = DirSQL(config=os.path.join(config_dir, ".dirsql.toml"))
136
136
  await db.ready()
137
137
  results = await db.query("SELECT * FROM app")
138
138
  assert len(results) == 1
@@ -155,7 +155,7 @@ ddl = "CREATE TABLE items (name TEXT, price REAL)"
155
155
  glob = "*.{ext}"
156
156
  """,
157
157
  )
158
- db = DirSQL.from_config(os.path.join(config_dir, ".dirsql.toml"))
158
+ db = DirSQL(config=os.path.join(config_dir, ".dirsql.toml"))
159
159
  await db.ready()
160
160
  results = await db.query("SELECT * FROM items")
161
161
  assert len(results) == 1
@@ -177,7 +177,7 @@ ddl = "CREATE TABLE posts (title TEXT, author TEXT, body TEXT)"
177
177
  glob = "posts/*.md"
178
178
  """,
179
179
  )
180
- db = DirSQL.from_config(os.path.join(config_dir, ".dirsql.toml"))
180
+ db = DirSQL(config=os.path.join(config_dir, ".dirsql.toml"))
181
181
  await db.ready()
182
182
  results = await db.query("SELECT * FROM posts")
183
183
  assert len(results) == 1
@@ -203,7 +203,7 @@ glob = "items/*.json"
203
203
  strict = true
204
204
  """,
205
205
  )
206
- db = DirSQL.from_config(os.path.join(config_dir, ".dirsql.toml"))
206
+ db = DirSQL(config=os.path.join(config_dir, ".dirsql.toml"))
207
207
  with pytest.raises(Exception):
208
208
  await db.ready()
209
209
 
@@ -223,7 +223,7 @@ glob = "items/*.json"
223
223
  strict = true
224
224
  """,
225
225
  )
226
- db = DirSQL.from_config(os.path.join(config_dir, ".dirsql.toml"))
226
+ db = DirSQL(config=os.path.join(config_dir, ".dirsql.toml"))
227
227
  await db.ready()
228
228
  results = await db.query("SELECT * FROM items")
229
229
  assert len(results) == 1
@@ -1,4 +1,4 @@
1
- """Integration tests for DirSQL.from_config()."""
1
+ """Integration tests for DirSQL(config=)."""
2
2
 
3
3
  import json
4
4
  import os
@@ -44,7 +44,7 @@ glob = "items/*.json"
44
44
  """,
45
45
  )
46
46
 
47
- db = DirSQL.from_config(os.path.join(config_dir, ".dirsql.toml"))
47
+ db = DirSQL(config=os.path.join(config_dir, ".dirsql.toml"))
48
48
  await db.ready()
49
49
  results = await db.query("SELECT * FROM items ORDER BY name")
50
50
  assert len(results) == 2
@@ -68,7 +68,7 @@ glob = "*.csv"
68
68
  """,
69
69
  )
70
70
 
71
- db = DirSQL.from_config(os.path.join(config_dir, ".dirsql.toml"))
71
+ db = DirSQL(config=os.path.join(config_dir, ".dirsql.toml"))
72
72
  await db.ready()
73
73
  results = await db.query("SELECT * FROM produce ORDER BY name")
74
74
  assert len(results) == 2
@@ -93,7 +93,7 @@ glob = "*.jsonl"
93
93
  """,
94
94
  )
95
95
 
96
- db = DirSQL.from_config(os.path.join(config_dir, ".dirsql.toml"))
96
+ db = DirSQL(config=os.path.join(config_dir, ".dirsql.toml"))
97
97
  await db.ready()
98
98
  results = await db.query("SELECT * FROM events ORDER BY type")
99
99
  assert len(results) == 2
@@ -121,7 +121,7 @@ glob = "comments/{thread_id}/index.jsonl"
121
121
  """,
122
122
  )
123
123
 
124
- db = DirSQL.from_config(os.path.join(config_dir, ".dirsql.toml"))
124
+ db = DirSQL(config=os.path.join(config_dir, ".dirsql.toml"))
125
125
  await db.ready()
126
126
  results = await db.query("SELECT * FROM comments ORDER BY thread_id")
127
127
  assert len(results) == 2
@@ -150,7 +150,7 @@ age = "age"
150
150
  """,
151
151
  )
152
152
 
153
- db = DirSQL.from_config(os.path.join(config_dir, ".dirsql.toml"))
153
+ db = DirSQL(config=os.path.join(config_dir, ".dirsql.toml"))
154
154
  await db.ready()
155
155
  results = await db.query("SELECT * FROM people")
156
156
  assert len(results) == 1
@@ -184,7 +184,7 @@ each = "data.items"
184
184
  """,
185
185
  )
186
186
 
187
- db = DirSQL.from_config(os.path.join(config_dir, ".dirsql.toml"))
187
+ db = DirSQL(config=os.path.join(config_dir, ".dirsql.toml"))
188
188
  await db.ready()
189
189
  results = await db.query("SELECT * FROM items ORDER BY name")
190
190
  assert len(results) == 2
@@ -215,7 +215,7 @@ glob = "data/**/*.json"
215
215
  """,
216
216
  )
217
217
 
218
- db = DirSQL.from_config(os.path.join(config_dir, ".dirsql.toml"))
218
+ db = DirSQL(config=os.path.join(config_dir, ".dirsql.toml"))
219
219
  await db.ready()
220
220
  results = await db.query("SELECT * FROM items")
221
221
  assert len(results) == 1
@@ -246,7 +246,7 @@ glob = "authors/*.json"
246
246
  """,
247
247
  )
248
248
 
249
- db = DirSQL.from_config(os.path.join(config_dir, ".dirsql.toml"))
249
+ db = DirSQL(config=os.path.join(config_dir, ".dirsql.toml"))
250
250
  await db.ready()
251
251
  posts = await db.query("SELECT * FROM posts")
252
252
  authors = await db.query("SELECT * FROM authors")
@@ -259,7 +259,7 @@ glob = "authors/*.json"
259
259
  @pytest.mark.asyncio
260
260
  async def it_raises_on_missing_config_file(config_dir):
261
261
  """from_config raises when the config file doesn't exist."""
262
- db = DirSQL.from_config(os.path.join(config_dir, "nonexistent.toml"))
262
+ db = DirSQL(config=os.path.join(config_dir, "nonexistent.toml"))
263
263
  with pytest.raises(Exception):
264
264
  await db.ready()
265
265
 
@@ -270,7 +270,7 @@ glob = "authors/*.json"
270
270
  os.path.join(config_dir, ".dirsql.toml"),
271
271
  "this is not valid [[[",
272
272
  )
273
- db = DirSQL.from_config(os.path.join(config_dir, ".dirsql.toml"))
273
+ db = DirSQL(config=os.path.join(config_dir, ".dirsql.toml"))
274
274
  with pytest.raises(Exception):
275
275
  await db.ready()
276
276
 
@@ -284,7 +284,7 @@ glob = "authors/*.json"
284
284
  glob = "*.json"
285
285
  """,
286
286
  )
287
- db = DirSQL.from_config(os.path.join(config_dir, ".dirsql.toml"))
287
+ db = DirSQL(config=os.path.join(config_dir, ".dirsql.toml"))
288
288
  with pytest.raises(Exception):
289
289
  await db.ready()
290
290
 
@@ -303,7 +303,7 @@ ddl = "CREATE TABLE t (x TEXT)"
303
303
  glob = "*.dat"
304
304
  """,
305
305
  )
306
- db = DirSQL.from_config(os.path.join(config_dir, ".dirsql.toml"))
306
+ db = DirSQL(config=os.path.join(config_dir, ".dirsql.toml"))
307
307
  with pytest.raises(Exception, match="[Ff]ormat|[Uu]nsupported"):
308
308
  await db.ready()
309
309
 
@@ -326,7 +326,7 @@ format = "csv"
326
326
  """,
327
327
  )
328
328
 
329
- db = DirSQL.from_config(os.path.join(config_dir, ".dirsql.toml"))
329
+ db = DirSQL(config=os.path.join(config_dir, ".dirsql.toml"))
330
330
  await db.ready()
331
331
  results = await db.query("SELECT * FROM t")
332
332
  assert len(results) == 1
@@ -349,7 +349,7 @@ glob = "items/*.json"
349
349
  """,
350
350
  )
351
351
 
352
- db = DirSQL.from_config(os.path.join(config_dir, ".dirsql.toml"))
352
+ db = DirSQL(config=os.path.join(config_dir, ".dirsql.toml"))
353
353
  await db.ready()
354
354
  results = await db.query("SELECT name FROM items WHERE price > 1.0")
355
355
  assert len(results) == 1
@@ -1,5 +1,5 @@
1
1
  use std::collections::HashMap;
2
- use std::path::Path;
2
+ use std::path::{Path, PathBuf};
3
3
 
4
4
  use serde::Deserialize;
5
5
 
@@ -26,6 +26,11 @@ pub type Result<T> = std::result::Result<T, ConfigError>;
26
26
  /// Parsed configuration from a `.dirsql.toml` file.
27
27
  #[derive(Debug, Clone)]
28
28
  pub struct Config {
29
+ /// Optional root directory override. When absent, callers derive the
30
+ /// root from the config file's own location. When present, it is taken
31
+ /// relative to the config file's parent (so a config at
32
+ /// `/proj/.dirsql.toml` with `root = "docs"` scans `/proj/docs`).
33
+ pub root: Option<PathBuf>,
29
34
  pub ignore: Vec<String>,
30
35
  pub tables: Vec<TableConfig>,
31
36
  }
@@ -51,6 +56,7 @@ struct RawConfig {
51
56
 
52
57
  #[derive(Deserialize)]
53
58
  struct RawDirsql {
59
+ root: Option<PathBuf>,
54
60
  ignore: Option<Vec<String>>,
55
61
  }
56
62
 
@@ -87,7 +93,10 @@ pub fn load_config(path: &Path) -> Result<Config> {
87
93
  pub fn load_config_str(content: &str) -> Result<Config> {
88
94
  let raw: RawConfig = toml::from_str(content)?;
89
95
 
90
- let ignore = raw.dirsql.and_then(|d| d.ignore).unwrap_or_default();
96
+ let (root, ignore) = match raw.dirsql {
97
+ Some(d) => (d.root, d.ignore.unwrap_or_default()),
98
+ None => (None, Vec::new()),
99
+ };
91
100
 
92
101
  let raw_tables = raw.table.unwrap_or_default();
93
102
  let mut tables = Vec::with_capacity(raw_tables.len());
@@ -111,7 +120,11 @@ pub fn load_config_str(content: &str) -> Result<Config> {
111
120
  });
112
121
  }
113
122
 
114
- Ok(Config { ignore, tables })
123
+ Ok(Config {
124
+ root,
125
+ ignore,
126
+ tables,
127
+ })
115
128
  }
116
129
 
117
130
  #[cfg(test)]
@@ -367,6 +380,45 @@ glob = "*.dat"
367
380
  assert_eq!(config.tables[0].format, None);
368
381
  }
369
382
 
383
+ #[test]
384
+ fn optional_root_parses_when_present() {
385
+ let toml = r#"
386
+ [dirsql]
387
+ root = "docs"
388
+
389
+ [[table]]
390
+ ddl = "CREATE TABLE t (x TEXT)"
391
+ glob = "*.json"
392
+ "#;
393
+ let config = load_config_str(toml).unwrap();
394
+ assert_eq!(config.root.as_deref(), Some(Path::new("docs")));
395
+ }
396
+
397
+ #[test]
398
+ fn root_absent_by_default() {
399
+ let toml = r#"
400
+ [[table]]
401
+ ddl = "CREATE TABLE t (x TEXT)"
402
+ glob = "*.json"
403
+ "#;
404
+ let config = load_config_str(toml).unwrap();
405
+ assert!(config.root.is_none());
406
+ }
407
+
408
+ #[test]
409
+ fn root_can_be_absolute() {
410
+ let toml = r#"
411
+ [dirsql]
412
+ root = "/tmp/data"
413
+
414
+ [[table]]
415
+ ddl = "CREATE TABLE t (x TEXT)"
416
+ glob = "*.json"
417
+ "#;
418
+ let config = load_config_str(toml).unwrap();
419
+ assert_eq!(config.root.as_deref(), Some(Path::new("/tmp/data")));
420
+ }
421
+
370
422
  #[test]
371
423
  fn multiple_tables_preserve_order() {
372
424
  let toml = r#"
@@ -541,11 +541,11 @@ mod tests {
541
541
 
542
542
  #[test]
543
543
  fn value_to_sql_real() {
544
- let v = Value::Real(3.14);
544
+ let v = Value::Real(1.5);
545
545
  let result = v.to_sql().unwrap();
546
546
  match result {
547
547
  rusqlite::types::ToSqlOutput::Owned(rusqlite::types::Value::Real(f)) => {
548
- assert!((f - 3.14).abs() < f64::EPSILON);
548
+ assert!((f - 1.5).abs() < f64::EPSILON);
549
549
  }
550
550
  _ => panic!("expected Real"),
551
551
  }
@@ -587,8 +587,8 @@ mod tests {
587
587
 
588
588
  #[test]
589
589
  fn value_from_sqlite_real() {
590
- let v = Value::from(rusqlite::types::Value::Real(2.718));
591
- assert_eq!(v, Value::Real(2.718));
590
+ let v = Value::from(rusqlite::types::Value::Real(1.25));
591
+ assert_eq!(v, Value::Real(1.25));
592
592
  }
593
593
 
594
594
  #[test]