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.
- {dirsql-0.2.2 → dirsql-0.2.4}/PKG-INFO +8 -9
- {dirsql-0.2.2/packages/python → dirsql-0.2.4}/README.md +7 -8
- {dirsql-0.2.2 → dirsql-0.2.4/packages/python}/README.md +7 -8
- {dirsql-0.2.2 → dirsql-0.2.4}/packages/python/src/lib.rs +25 -21
- {dirsql-0.2.2 → dirsql-0.2.4}/packages/python/tests/integration/test_binding.py +23 -33
- {dirsql-0.2.2 → dirsql-0.2.4}/packages/python/tests/integration/test_docs_gaps.py +7 -7
- {dirsql-0.2.2 → dirsql-0.2.4}/packages/python/tests/integration/test_from_config.py +15 -15
- {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/src/config.rs +55 -3
- {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/src/db.rs +4 -4
- {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/src/lib.rs +313 -61
- {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/src/parser.rs +2 -2
- {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/src/watcher.rs +15 -15
- {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/tests/sdk.rs +230 -0
- {dirsql-0.2.2 → dirsql-0.2.4}/pyproject.toml +1 -1
- {dirsql-0.2.2 → dirsql-0.2.4}/python/dirsql/_async.py +17 -27
- {dirsql-0.2.2 → dirsql-0.2.4}/python/dirsql/test_async.py +2 -1
- {dirsql-0.2.2 → dirsql-0.2.4}/Cargo.lock +0 -0
- {dirsql-0.2.2 → dirsql-0.2.4}/Cargo.toml +0 -0
- {dirsql-0.2.2 → dirsql-0.2.4}/packages/python/Cargo.toml +0 -0
- {dirsql-0.2.2 → dirsql-0.2.4}/packages/python/python/conftest.py +0 -0
- {dirsql-0.2.2 → dirsql-0.2.4}/packages/python/tests/__init__.py +0 -0
- {dirsql-0.2.2 → dirsql-0.2.4}/packages/python/tests/conftest.py +0 -0
- {dirsql-0.2.2 → dirsql-0.2.4}/packages/python/tests/integration/__init__.py +0 -0
- {dirsql-0.2.2 → dirsql-0.2.4}/packages/python/tests/integration/test_async_dirsql.py +0 -0
- {dirsql-0.2.2 → dirsql-0.2.4}/packages/python/tests/integration/test_dirsql.py +0 -0
- {dirsql-0.2.2 → dirsql-0.2.4}/packages/python/tests/integration/test_docs_examples.py +0 -0
- {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/Cargo.toml +0 -0
- {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/README.md +0 -0
- {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/benches/db_bench.rs +0 -0
- {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/benches/differ_bench.rs +0 -0
- {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/benches/matcher_bench.rs +0 -0
- {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/benches/scanner_bench.rs +0 -0
- {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/src/bin/dirsql.rs +0 -0
- {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/src/cli/mod.rs +0 -0
- {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/src/cli/router.rs +0 -0
- {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/src/cli/serialize.rs +0 -0
- {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/src/cli/server.rs +0 -0
- {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/src/differ.rs +0 -0
- {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/src/matcher.rs +0 -0
- {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/src/scanner.rs +0 -0
- {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/tests/async_sdk.rs +0 -0
- {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/tests/cli_e2e.rs +0 -0
- {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/tests/cli_integration.rs +0 -0
- {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/tests/docs_examples.rs +0 -0
- {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/tests/docs_gaps.rs +0 -0
- {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/tests/from_config.rs +0 -0
- {dirsql-0.2.2 → dirsql-0.2.4}/packages/rust/tests/readonly_query.rs +0 -0
- {dirsql-0.2.2 → dirsql-0.2.4}/python/dirsql/__init__.py +0 -0
- {dirsql-0.2.2 → dirsql-0.2.4}/python/dirsql/_cli/__init__.py +0 -0
- {dirsql-0.2.2 → dirsql-0.2.4}/python/dirsql/_cli/binary_path.py +0 -0
- {dirsql-0.2.2 → dirsql-0.2.4}/python/dirsql/_cli/binary_path_test.py +0 -0
- {dirsql-0.2.2 → dirsql-0.2.4}/python/dirsql/_cli/is_windows.py +0 -0
- {dirsql-0.2.2 → dirsql-0.2.4}/python/dirsql/_cli/is_windows_test.py +0 -0
- {dirsql-0.2.2 → dirsql-0.2.4}/python/dirsql/_cli/main.py +0 -0
- {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.
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
- **`
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
- **`
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
- **`
|
|
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` / `
|
|
8
|
-
//!
|
|
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
|
|
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 ||
|
|
100
|
-
|
|
101
|
-
|
|
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``
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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 =
|
|
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
|
|
223
|
-
# Feature: DirSQL
|
|
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
|
|
227
|
-
db = async_mod.DirSQL
|
|
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.
|
|
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
|
-
|
|
240
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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(
|
|
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 -
|
|
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(
|
|
591
|
-
assert_eq!(v, Value::Real(
|
|
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]
|