duckdb-kql 0.0.1.dev3__tar.gz → 0.0.1.dev4__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.
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/PKG-INFO +2 -2
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/README.md +1 -1
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/demo/demo.ipynb +1 -1
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/docs/api.md +1 -1
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/docs/cli.md +24 -19
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/docs/getting-started.md +1 -1
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/src/duckdb_kql/_version.py +2 -2
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/src/duckdb_kql/cli.py +114 -84
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/tests/test_cli.py +92 -40
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/tests/test_server.py +10 -2
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/.gitignore +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/CODE_OF_CONDUCT.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/CONTRIBUTING.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/LICENSE +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/SECURITY.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/THIRD-PARTY-NOTICES.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/demo/demo.kql +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/docs/TRANSLATION.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/docs/ai-cost-strategy.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/docs/azure-monitor-profile.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/docs/code-review/README.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/docs/code-review/kusto-client-compat.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/docs/code-review/public-api-and-typing.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/docs/code-review/review-2026-08-04.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/docs/code-review/security-and-injection.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/docs/code-review/testing-oracle-and-fixtures.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/docs/code-review/tooling-packaging-ci-docs.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/docs/code-review/translation-correctness.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/docs/frequency-scan-results.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/docs/implementation-options.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/docs/implementation-plan.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/docs/kql-on-duckdb-landscape.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/docs/kql-support.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/docs/kusto-client.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/docs/kusto-server.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/docs/lessons-from-bun-rewrite.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/docs/licensing.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/docs/m0-grammar-spike.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/docs/oracle-harness.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/docs/test-plan.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/grammar/Kql.g4 +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/grammar/KqlTokens.g4 +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/grammar/UPSTREAM.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/pyproject.toml +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/src/duckdb_kql/__init__.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/src/duckdb_kql/__main__.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/src/duckdb_kql/_antlr/Kql.interp +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/src/duckdb_kql/_antlr/Kql.tokens +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/src/duckdb_kql/_antlr/KqlLexer.interp +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/src/duckdb_kql/_antlr/KqlLexer.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/src/duckdb_kql/_antlr/KqlLexer.tokens +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/src/duckdb_kql/_antlr/KqlListener.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/src/duckdb_kql/_antlr/KqlParser.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/src/duckdb_kql/_antlr/KqlVisitor.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/src/duckdb_kql/_antlr/__init__.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/src/duckdb_kql/comparison.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/src/duckdb_kql/control.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/src/duckdb_kql/engine.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/src/duckdb_kql/errors.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/src/duckdb_kql/fixtures.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/src/duckdb_kql/ir.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/src/duckdb_kql/kusto/__init__.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/src/duckdb_kql/kusto/_models.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/src/duckdb_kql/kusto/client.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/src/duckdb_kql/kusto/client_request_properties.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/src/duckdb_kql/kusto/exceptions.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/src/duckdb_kql/kusto/helpers.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/src/duckdb_kql/kusto/response.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/src/duckdb_kql/lower.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/src/duckdb_kql/oracle.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/src/duckdb_kql/params.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/src/duckdb_kql/parser.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/src/duckdb_kql/py.typed +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/src/duckdb_kql/schema.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/src/duckdb_kql/server.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/src/duckdb_kql/translate/__init__.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/src/duckdb_kql/translate/functions.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/src/duckdb_kql/types.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/tests/conftest.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/tests/profiles/azure-monitor.json +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/tests/test_behavior.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/tests/test_column_order_and_null_sort.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/tests/test_comparison.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/tests/test_control_commands.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/tests/test_corpus.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/tests/test_datetime_traps.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/tests/test_demo_notebook.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/tests/test_devcontainers.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/tests/test_docs.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/tests/test_dynamic.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/tests/test_fixtures.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/tests/test_getschema.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/tests/test_join.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/tests/test_kusto_client.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/tests/test_let.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/tests/test_null_semantics.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/tests/test_parse.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/tests/test_profile_azure_monitor.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/tests/test_query_parameters.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/tests/test_range_in_render.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/tests/test_summarize.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/tests/test_support_matrix.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/tests/test_typing.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/tests/test_workflows.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/tools/check_profile.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/tools/frequency_scan.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/tools/gen_support_matrix.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/tools/harvest_docs.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/tools/make_fixtures.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/tools/regen_expectations.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/tools/regen_parser.sh +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: duckdb-kql
|
|
3
|
-
Version: 0.0.1.
|
|
3
|
+
Version: 0.0.1.dev4
|
|
4
4
|
Summary: Run Kusto KQL queries on DuckDB, from Python
|
|
5
5
|
Project-URL: Homepage, https://github.com/mmaitre314/duckdb-kql
|
|
6
6
|
Project-URL: Documentation, https://github.com/mmaitre314/duckdb-kql/blob/main/docs/getting-started.md
|
|
@@ -133,7 +133,7 @@ Translate at build time and the output has no dependency on this package at all
|
|
|
133
133
|
— not even Python. Only your CI machine installs it.
|
|
134
134
|
|
|
135
135
|
```bash
|
|
136
|
-
duckdb-kql queries/ -o build/sql/ --check # fails the build if a .sql is stale
|
|
136
|
+
duckdb-kql translate queries/ -o build/sql/ --check # fails the build if a .sql is stale
|
|
137
137
|
```
|
|
138
138
|
|
|
139
139
|
See [Build-time translation](https://github.com/mmaitre314/duckdb-kql/blob/main/docs/cli.md).
|
|
@@ -52,7 +52,7 @@ Translate at build time and the output has no dependency on this package at all
|
|
|
52
52
|
— not even Python. Only your CI machine installs it.
|
|
53
53
|
|
|
54
54
|
```bash
|
|
55
|
-
duckdb-kql queries/ -o build/sql/ --check # fails the build if a .sql is stale
|
|
55
|
+
duckdb-kql translate queries/ -o build/sql/ --check # fails the build if a .sql is stale
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
See [Build-time translation](https://github.com/mmaitre314/duckdb-kql/blob/main/docs/cli.md).
|
|
@@ -277,7 +277,7 @@ translates `.kql` files to `.sql` so the output can be run without this package
|
|
|
277
277
|
installed at all. `--check` makes a stale generated file fail CI.
|
|
278
278
|
|
|
279
279
|
```bash
|
|
280
|
-
duckdb-kql queries/ -o build/sql/ --check
|
|
280
|
+
duckdb-kql translate queries/ -o build/sql/ --check
|
|
281
281
|
```
|
|
282
282
|
|
|
283
283
|
Full reference, including the generated header and how to bind the placeholders
|
|
@@ -1,19 +1,18 @@
|
|
|
1
|
-
# Build-time translation (`duckdb-kql`)
|
|
1
|
+
# Build-time translation (`duckdb-kql translate`)
|
|
2
2
|
|
|
3
3
|
Translate `.kql` to `.sql` once, in CI. What ships is plain SQL, and **nothing
|
|
4
4
|
that runs it needs this package** — not the transpiler, not Python.
|
|
5
5
|
|
|
6
|
-
> There is one subcommand, `duckdb-kql serve`, which does something else
|
|
7
|
-
> entirely: it runs a [local Kusto REST endpoint](kusto-server.md) over a DuckDB
|
|
8
|
-
> database. Everything that is not the literal word `serve` is a file to
|
|
9
|
-
> translate, so nothing on this page changes.
|
|
10
|
-
|
|
11
6
|
```bash
|
|
12
|
-
pip install duckdb-kql
|
|
13
|
-
duckdb-kql queries/ -o build/sql/ # translate
|
|
14
|
-
duckdb-kql queries/ -o build/sql/ --check # fail the build if stale
|
|
7
|
+
pip install duckdb-kql # antlr4 only; no database
|
|
8
|
+
duckdb-kql translate queries/ -o build/sql/ # translate
|
|
9
|
+
duckdb-kql translate queries/ -o build/sql/ --check # fail the build if stale
|
|
15
10
|
```
|
|
16
11
|
|
|
12
|
+
> `translate` is one of two subcommands. The other,
|
|
13
|
+
> [`duckdb-kql serve`](kusto-server.md), runs a local Kusto REST endpoint over a
|
|
14
|
+
> DuckDB database — a different job with different dependencies.
|
|
15
|
+
|
|
17
16
|
- [Why](#why)
|
|
18
17
|
- [Usage](#usage)
|
|
19
18
|
- [The generated header](#the-generated-header)
|
|
@@ -41,7 +40,7 @@ build time means:
|
|
|
41
40
|
## Usage
|
|
42
41
|
|
|
43
42
|
```
|
|
44
|
-
duckdb-kql FILE... [-o PATH] [--check] [--schema FILE] [--no-header] [-v]
|
|
43
|
+
duckdb-kql translate FILE... [-o PATH] [--check] [--schema FILE] [--no-header] [-v]
|
|
45
44
|
```
|
|
46
45
|
|
|
47
46
|
| | |
|
|
@@ -54,14 +53,20 @@ duckdb-kql FILE... [-o PATH] [--check] [--schema FILE] [--no-header] [-v]
|
|
|
54
53
|
| `-v, --verbose` | Report each file written, on stderr. |
|
|
55
54
|
|
|
56
55
|
```bash
|
|
57
|
-
duckdb-kql queries/errors.kql
|
|
58
|
-
duckdb-kql queries/errors.kql -o build/errors.sql
|
|
59
|
-
duckdb-kql queries/ -o build/
|
|
60
|
-
echo 'print x = 1' | duckdb-kql -
|
|
56
|
+
duckdb-kql translate queries/errors.kql # to stdout
|
|
57
|
+
duckdb-kql translate queries/errors.kql -o build/errors.sql # to one file
|
|
58
|
+
duckdb-kql translate queries/ -o build/ # a directory of each
|
|
59
|
+
echo 'print x = 1' | duckdb-kql translate - # from stdin
|
|
61
60
|
```
|
|
62
61
|
|
|
63
|
-
The command is also `python -m duckdb_kql`, for CI jobs where the
|
|
64
|
-
is not on `PATH`.
|
|
62
|
+
The command is also `python -m duckdb_kql translate`, for CI jobs where the
|
|
63
|
+
console script is not on `PATH`.
|
|
64
|
+
|
|
65
|
+
The verb is required: a bare `duckdb-kql queries/` is a usage error, not a
|
|
66
|
+
translation. It used to be the latter, and reading well was the whole argument
|
|
67
|
+
for it — but a filename in the verb slot is exactly the ambiguity a subcommand
|
|
68
|
+
removes, and the alternative is a second verb that silently collides with a file
|
|
69
|
+
of the same name.
|
|
65
70
|
|
|
66
71
|
## The generated header
|
|
67
72
|
|
|
@@ -128,7 +133,7 @@ Commit the `.sql` and let `--check` prove it matches:
|
|
|
128
133
|
|
|
129
134
|
```yaml
|
|
130
135
|
- run: pip install duckdb-kql
|
|
131
|
-
- run: duckdb-kql queries/ -o sql/ --check
|
|
136
|
+
- run: duckdb-kql translate queries/ -o sql/ --check
|
|
132
137
|
```
|
|
133
138
|
|
|
134
139
|
An edited `.kql` whose `.sql` was not regenerated fails with exit 3 and a list
|
|
@@ -140,7 +145,7 @@ If you would rather not commit the output, translate into the build directory
|
|
|
140
145
|
and skip `--check`:
|
|
141
146
|
|
|
142
147
|
```yaml
|
|
143
|
-
- run: pip install duckdb-kql && duckdb-kql queries/ -o build/sql/
|
|
148
|
+
- run: pip install duckdb-kql && duckdb-kql translate queries/ -o build/sql/
|
|
144
149
|
```
|
|
145
150
|
|
|
146
151
|
Errors are reported as `file:line:column: error: …`, which GitHub Actions and
|
|
@@ -160,7 +165,7 @@ time there is no connection, so pass a JSON file:
|
|
|
160
165
|
```
|
|
161
166
|
|
|
162
167
|
```bash
|
|
163
|
-
duckdb-kql queries/ -o build/sql/ --schema schema.json
|
|
168
|
+
duckdb-kql translate queries/ -o build/sql/ --schema schema.json
|
|
164
169
|
```
|
|
165
170
|
|
|
166
171
|
Everything else translates schema-free. A `join` without a schema fails with a
|
|
@@ -169,7 +169,7 @@ There is a command for exactly this. Translate your `.kql` files to `.sql` in
|
|
|
169
169
|
CI, ship the SQL, and nothing at runtime needs this package:
|
|
170
170
|
|
|
171
171
|
```bash
|
|
172
|
-
duckdb-kql queries/ -o build/sql/ --check
|
|
172
|
+
duckdb-kql translate queries/ -o build/sql/ --check
|
|
173
173
|
```
|
|
174
174
|
|
|
175
175
|
See [Build-time translation](cli.md).
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.0.1.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 0, 1, '
|
|
21
|
+
__version__ = version = '0.0.1.dev4'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 0, 1, 'dev4')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -1,30 +1,31 @@
|
|
|
1
|
-
"""``duckdb-kql`` —
|
|
2
|
-
|
|
3
|
-
The point of this command is that the *output* has no dependencies. Translate
|
|
4
|
-
your queries in CI, commit or ship the ``.sql``, and the thing that runs them
|
|
5
|
-
needs nothing from this package — not even DuckDB's Python bindings. A Go
|
|
6
|
-
service, a dbt model, a psql script and a notebook can all read the same file.
|
|
7
|
-
|
|
8
|
-
Translation is Layer 0 only: no database is opened and ``duckdb`` is never
|
|
9
|
-
imported, so ``pip install duckdb-kql`` alone is enough to run it.
|
|
1
|
+
"""``duckdb-kql`` — the command line, one subcommand per job.
|
|
10
2
|
|
|
11
3
|
::
|
|
12
4
|
|
|
13
|
-
duckdb-kql queries
|
|
14
|
-
duckdb-kql queries
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
5
|
+
duckdb-kql translate queries/ -o build/sql/ # KQL files -> SQL files
|
|
6
|
+
duckdb-kql translate queries/ -o build/sql/ --check # ... and fail if stale
|
|
7
|
+
duckdb-kql serve logs.duckdb # a local Kusto endpoint
|
|
8
|
+
|
|
9
|
+
**translate** is the build-time path, and the point of it is that the *output*
|
|
10
|
+
has no dependencies. Translate your queries in CI, commit or ship the ``.sql``,
|
|
11
|
+
and the thing that runs them needs nothing from this package — not even DuckDB's
|
|
12
|
+
Python bindings. A Go service, a dbt model, a psql script and a notebook can all
|
|
13
|
+
read the same file. ``--check`` is the mode that belongs in CI: it regenerates in
|
|
14
|
+
memory and compares, so a ``.kql`` edited without regenerating its ``.sql`` fails
|
|
15
|
+
the build instead of shipping a stale query.
|
|
16
|
+
|
|
17
|
+
It is Layer 0 only — no database is opened and ``duckdb`` is never imported, so
|
|
18
|
+
``pip install duckdb-kql`` alone is enough to run it.
|
|
19
|
+
|
|
20
|
+
**serve** is a different job entirely: a local Kusto-compatible HTTP endpoint
|
|
21
|
+
over a DuckDB database, so Kusto tools — including the Azure Data Explorer web
|
|
22
|
+
UI — can query it. It needs the ``duckdb`` extra.
|
|
23
|
+
|
|
24
|
+
Every subcommand is explicit. An earlier version took a bare list of files
|
|
25
|
+
(``duckdb-kql queries/ -o build/``), which read well while translation was the
|
|
26
|
+
only thing this command did, but leaves no room for a second verb: any new one
|
|
27
|
+
would be ambiguous with a file of the same name, and the ambiguity would be
|
|
28
|
+
silent. Naming the verb costs one word and keeps the space open.
|
|
28
29
|
"""
|
|
29
30
|
|
|
30
31
|
from __future__ import annotations
|
|
@@ -33,9 +34,9 @@ import argparse
|
|
|
33
34
|
import datetime as dt
|
|
34
35
|
import json
|
|
35
36
|
import sys
|
|
36
|
-
from collections.abc import Sequence
|
|
37
|
+
from collections.abc import Callable, Sequence
|
|
37
38
|
from pathlib import Path
|
|
38
|
-
from typing import Any
|
|
39
|
+
from typing import Any, cast
|
|
39
40
|
|
|
40
41
|
from . import __version__, to_sql
|
|
41
42
|
from .errors import KqlError, KqlSyntaxError
|
|
@@ -56,15 +57,17 @@ EXIT_STALE = 3
|
|
|
56
57
|
|
|
57
58
|
def main(argv: Sequence[str] | None = None) -> int:
|
|
58
59
|
"""Entry point. Returns a process exit code rather than raising."""
|
|
59
|
-
argv = list(sys.argv[1:] if argv is None else argv)
|
|
60
|
-
# Dispatched by hand rather than with argparse subparsers, because
|
|
61
|
-
# subparsers would make the existing form — a bare list of files — a
|
|
62
|
-
# subcommand too, and every documented invocation would have to change.
|
|
63
|
-
if argv and argv[0] == "serve":
|
|
64
|
-
return _serve(argv[1:])
|
|
65
|
-
|
|
66
60
|
args = _parser().parse_args(argv)
|
|
61
|
+
return cast("Callable[[argparse.Namespace], int]", args.run)(args)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
# translate
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
|
|
67
68
|
|
|
69
|
+
def _translate_command(args: argparse.Namespace) -> int:
|
|
70
|
+
"""``duckdb-kql translate`` — KQL files in, SQL files out."""
|
|
68
71
|
try:
|
|
69
72
|
schema = _load_schema(args.schema)
|
|
70
73
|
except (OSError, ValueError) as exc:
|
|
@@ -129,9 +132,10 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
129
132
|
# ---------------------------------------------------------------------------
|
|
130
133
|
|
|
131
134
|
|
|
132
|
-
def
|
|
135
|
+
def _serve_command(args: argparse.Namespace) -> int:
|
|
133
136
|
"""``duckdb-kql serve`` — a local Kusto endpoint over a DuckDB database."""
|
|
134
|
-
|
|
137
|
+
# Imported here, not at module scope: this is the only subcommand that needs
|
|
138
|
+
# a database, and `translate` is documented to run without one installed.
|
|
135
139
|
from .server import serve # noqa: PLC0415
|
|
136
140
|
|
|
137
141
|
origins = tuple(args.allow_origin) if args.allow_origin else ADX_ORIGINS
|
|
@@ -148,48 +152,6 @@ def _serve(argv: Sequence[str]) -> int:
|
|
|
148
152
|
return EXIT_OK
|
|
149
153
|
|
|
150
154
|
|
|
151
|
-
def _serve_parser() -> argparse.ArgumentParser:
|
|
152
|
-
parser = argparse.ArgumentParser(
|
|
153
|
-
prog="duckdb-kql serve",
|
|
154
|
-
description=(
|
|
155
|
-
"Serve a DuckDB database over the Kusto REST API, so Kusto tools "
|
|
156
|
-
"can query it. Open https://dataexplorer.azure.com, choose Add "
|
|
157
|
-
"connection, and give it the URL this prints."
|
|
158
|
-
),
|
|
159
|
-
epilog=(
|
|
160
|
-
"Listens on 127.0.0.1 only and cannot be made to listen anywhere "
|
|
161
|
-
"else: it answers unauthenticated queries, so reaching it has to "
|
|
162
|
-
"mean already being on this machine."
|
|
163
|
-
),
|
|
164
|
-
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
165
|
-
)
|
|
166
|
-
parser.add_argument(
|
|
167
|
-
"database",
|
|
168
|
-
nargs="?",
|
|
169
|
-
default=":memory:",
|
|
170
|
-
metavar="DATABASE",
|
|
171
|
-
help="DuckDB database file to serve. Omit for an empty in-memory one.",
|
|
172
|
-
)
|
|
173
|
-
parser.add_argument(
|
|
174
|
-
"-p",
|
|
175
|
-
"--port",
|
|
176
|
-
type=int,
|
|
177
|
-
default=DEFAULT_PORT,
|
|
178
|
-
metavar="PORT",
|
|
179
|
-
help=f"TCP port to listen on (default: {DEFAULT_PORT})",
|
|
180
|
-
)
|
|
181
|
-
parser.add_argument(
|
|
182
|
-
"--allow-origin",
|
|
183
|
-
action="append",
|
|
184
|
-
metavar="ORIGIN",
|
|
185
|
-
help=(
|
|
186
|
-
"additionally allow a browser origin to make cross-origin requests. "
|
|
187
|
-
"Repeatable. Replaces the Azure Data Explorer default list, and is a "
|
|
188
|
-
"decision about who may read this database from another browser tab."
|
|
189
|
-
),
|
|
190
|
-
)
|
|
191
|
-
parser.add_argument("--version", action="version", version=f"duckdb-kql {__version__}")
|
|
192
|
-
return parser
|
|
193
155
|
|
|
194
156
|
|
|
195
157
|
# ---------------------------------------------------------------------------
|
|
@@ -360,8 +322,34 @@ def _load_schema(path: str | None) -> dict[str, list[str]] | None:
|
|
|
360
322
|
|
|
361
323
|
|
|
362
324
|
def _parser() -> argparse.ArgumentParser:
|
|
325
|
+
"""The whole command line. Each subparser stores its handler in ``run``.
|
|
326
|
+
|
|
327
|
+
Dispatching through ``set_defaults(run=...)`` rather than a chain of
|
|
328
|
+
``if args.command == ...`` means a new subcommand is added in exactly one
|
|
329
|
+
place, and cannot be registered without being wired up.
|
|
330
|
+
"""
|
|
363
331
|
parser = argparse.ArgumentParser(
|
|
364
332
|
prog="duckdb-kql",
|
|
333
|
+
description=(
|
|
334
|
+
"Run KQL on DuckDB. `translate` turns .kql files into .sql at build "
|
|
335
|
+
"time; `serve` puts a local Kusto REST endpoint in front of a DuckDB "
|
|
336
|
+
"database."
|
|
337
|
+
),
|
|
338
|
+
epilog=(
|
|
339
|
+
"exit codes: 0 ok; 1 a query failed to translate, or the server "
|
|
340
|
+
"could not start; 2 bad usage; 3 --check found a missing or stale "
|
|
341
|
+
"output"
|
|
342
|
+
),
|
|
343
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
344
|
+
)
|
|
345
|
+
parser.add_argument("--version", action="version", version=f"duckdb-kql {__version__}")
|
|
346
|
+
# `required` so a bare `duckdb-kql` prints usage rather than a traceback
|
|
347
|
+
# about a missing `run` attribute.
|
|
348
|
+
subcommands = parser.add_subparsers(dest="command", metavar="COMMAND", required=True)
|
|
349
|
+
|
|
350
|
+
translate = subcommands.add_parser(
|
|
351
|
+
"translate",
|
|
352
|
+
help="translate .kql files to .sql",
|
|
365
353
|
description=(
|
|
366
354
|
"Translate KQL files to DuckDB SQL. The generated SQL has no "
|
|
367
355
|
"dependency on this package, so queries can be translated once at "
|
|
@@ -373,13 +361,14 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
373
361
|
),
|
|
374
362
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
375
363
|
)
|
|
376
|
-
|
|
364
|
+
translate.set_defaults(run=_translate_command)
|
|
365
|
+
translate.add_argument(
|
|
377
366
|
"files",
|
|
378
367
|
nargs="+",
|
|
379
368
|
metavar="FILE",
|
|
380
369
|
help="KQL files or directories to translate; '-' reads stdin",
|
|
381
370
|
)
|
|
382
|
-
|
|
371
|
+
translate.add_argument(
|
|
383
372
|
"-o",
|
|
384
373
|
"--output",
|
|
385
374
|
metavar="PATH",
|
|
@@ -388,7 +377,7 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
388
377
|
"stdout."
|
|
389
378
|
),
|
|
390
379
|
)
|
|
391
|
-
|
|
380
|
+
translate.add_argument(
|
|
392
381
|
"--check",
|
|
393
382
|
action="store_true",
|
|
394
383
|
help=(
|
|
@@ -396,7 +385,7 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
396
385
|
"in CI so an edited .kql cannot ship with a stale .sql."
|
|
397
386
|
),
|
|
398
387
|
)
|
|
399
|
-
|
|
388
|
+
translate.add_argument(
|
|
400
389
|
"--schema",
|
|
401
390
|
metavar="FILE",
|
|
402
391
|
help=(
|
|
@@ -404,18 +393,59 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
404
393
|
"Only `join` needs it."
|
|
405
394
|
),
|
|
406
395
|
)
|
|
407
|
-
|
|
396
|
+
translate.add_argument(
|
|
408
397
|
"--no-header",
|
|
409
398
|
action="store_true",
|
|
410
399
|
help="omit the generated-file comment block",
|
|
411
400
|
)
|
|
412
|
-
|
|
401
|
+
translate.add_argument(
|
|
413
402
|
"-v",
|
|
414
403
|
"--verbose",
|
|
415
404
|
action="store_true",
|
|
416
405
|
help="report each file written, on stderr",
|
|
417
406
|
)
|
|
418
|
-
|
|
407
|
+
|
|
408
|
+
serve = subcommands.add_parser(
|
|
409
|
+
"serve",
|
|
410
|
+
help="serve a DuckDB database over the Kusto REST API",
|
|
411
|
+
description=(
|
|
412
|
+
"Serve a DuckDB database over the Kusto REST API, so Kusto tools "
|
|
413
|
+
"can query it. Open https://dataexplorer.azure.com, choose Add "
|
|
414
|
+
"connection, and give it the URL this prints."
|
|
415
|
+
),
|
|
416
|
+
epilog=(
|
|
417
|
+
"Listens on 127.0.0.1 only and cannot be made to listen anywhere "
|
|
418
|
+
"else: it answers unauthenticated queries, so reaching it has to "
|
|
419
|
+
"mean already being on this machine."
|
|
420
|
+
),
|
|
421
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
422
|
+
)
|
|
423
|
+
serve.set_defaults(run=_serve_command)
|
|
424
|
+
serve.add_argument(
|
|
425
|
+
"database",
|
|
426
|
+
nargs="?",
|
|
427
|
+
default=":memory:",
|
|
428
|
+
metavar="DATABASE",
|
|
429
|
+
help="DuckDB database file to serve. Omit for an empty in-memory one.",
|
|
430
|
+
)
|
|
431
|
+
serve.add_argument(
|
|
432
|
+
"-p",
|
|
433
|
+
"--port",
|
|
434
|
+
type=int,
|
|
435
|
+
default=DEFAULT_PORT,
|
|
436
|
+
metavar="PORT",
|
|
437
|
+
help=f"TCP port to listen on (default: {DEFAULT_PORT})",
|
|
438
|
+
)
|
|
439
|
+
serve.add_argument(
|
|
440
|
+
"--allow-origin",
|
|
441
|
+
action="append",
|
|
442
|
+
metavar="ORIGIN",
|
|
443
|
+
help=(
|
|
444
|
+
"additionally allow a browser origin to make cross-origin requests. "
|
|
445
|
+
"Repeatable. Replaces the Azure Data Explorer default list, and is a "
|
|
446
|
+
"decision about who may read this database from another browser tab."
|
|
447
|
+
),
|
|
448
|
+
)
|
|
419
449
|
return parser
|
|
420
450
|
|
|
421
451
|
|
|
@@ -10,6 +10,7 @@ promised not to need.
|
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
+
import argparse
|
|
13
14
|
import re
|
|
14
15
|
import subprocess
|
|
15
16
|
import sys
|
|
@@ -45,7 +46,7 @@ def test_writes_to_stdout_by_default(
|
|
|
45
46
|
project: Path, capsys: pytest.CaptureFixture[str]
|
|
46
47
|
) -> None:
|
|
47
48
|
"""Writing files unasked would be a surprising default for a translator."""
|
|
48
|
-
assert main([str(project / "queries" / "count.kql")]) == EXIT_OK
|
|
49
|
+
assert main(["translate", str(project / "queries" / "count.kql")]) == EXIT_OK
|
|
49
50
|
out = capsys.readouterr().out
|
|
50
51
|
assert "SELECT" in out
|
|
51
52
|
assert not list(project.glob("**/*.sql"))
|
|
@@ -53,20 +54,20 @@ def test_writes_to_stdout_by_default(
|
|
|
53
54
|
|
|
54
55
|
def test_single_input_and_a_file_target(project: Path) -> None:
|
|
55
56
|
target = project / "out" / "count.sql"
|
|
56
|
-
assert main([str(project / "queries" / "count.kql"), "-o", str(target)]) == EXIT_OK
|
|
57
|
+
assert main(["translate", str(project / "queries" / "count.kql"), "-o", str(target)]) == EXIT_OK
|
|
57
58
|
assert "SELECT" in target.read_text(encoding="utf-8")
|
|
58
59
|
|
|
59
60
|
|
|
60
61
|
def test_a_directory_input_expands_to_its_kql_files(project: Path) -> None:
|
|
61
62
|
"""A build script has a directory, not a shell-expanded list."""
|
|
62
63
|
out = project / "build"
|
|
63
|
-
assert main([str(project / "queries"), "-o", str(out)]) == EXIT_OK
|
|
64
|
+
assert main(["translate", str(project / "queries"), "-o", str(out)]) == EXIT_OK
|
|
64
65
|
assert sorted(p.name for p in out.glob("*.sql")) == ["by_state.sql", "count.sql"]
|
|
65
66
|
|
|
66
67
|
|
|
67
68
|
def test_output_directory_is_created(project: Path) -> None:
|
|
68
69
|
out = project / "deep" / "nested" / "build"
|
|
69
|
-
assert main([str(project / "queries"), "-o", str(out)]) == EXIT_OK
|
|
70
|
+
assert main(["translate", str(project / "queries"), "-o", str(out)]) == EXIT_OK
|
|
70
71
|
assert (out / "count.sql").is_file()
|
|
71
72
|
|
|
72
73
|
|
|
@@ -76,7 +77,7 @@ def test_stdin_is_readable(
|
|
|
76
77
|
import io # noqa: PLC0415
|
|
77
78
|
|
|
78
79
|
monkeypatch.setattr("sys.stdin", io.StringIO("print x = 1"))
|
|
79
|
-
assert main(["-"]) == EXIT_OK
|
|
80
|
+
assert main(["translate", "-"]) == EXIT_OK
|
|
80
81
|
out = capsys.readouterr().out
|
|
81
82
|
assert "<stdin>" in out
|
|
82
83
|
assert 'AS "x"' in out
|
|
@@ -89,41 +90,41 @@ def test_stdin_is_readable(
|
|
|
89
90
|
|
|
90
91
|
def test_check_passes_on_freshly_generated_output(project: Path) -> None:
|
|
91
92
|
out = project / "build"
|
|
92
|
-
assert main([str(project / "queries"), "-o", str(out)]) == EXIT_OK
|
|
93
|
-
assert main([str(project / "queries"), "-o", str(out), "--check"]) == EXIT_OK
|
|
93
|
+
assert main(["translate", str(project / "queries"), "-o", str(out)]) == EXIT_OK
|
|
94
|
+
assert main(["translate", str(project / "queries"), "-o", str(out), "--check"]) == EXIT_OK
|
|
94
95
|
|
|
95
96
|
|
|
96
97
|
def test_check_fails_when_the_kql_changed(project: Path) -> None:
|
|
97
98
|
"""The point of the mode: an edited query cannot ship a stale .sql."""
|
|
98
99
|
out = project / "build"
|
|
99
|
-
main([str(project / "queries"), "-o", str(out)])
|
|
100
|
+
main(["translate", str(project / "queries"), "-o", str(out)])
|
|
100
101
|
(project / "queries" / "count.kql").write_text(SIMPLE + "| take 5\n", encoding="utf-8")
|
|
101
|
-
assert main([str(project / "queries"), "-o", str(out), "--check"]) == EXIT_STALE
|
|
102
|
+
assert main(["translate", str(project / "queries"), "-o", str(out), "--check"]) == EXIT_STALE
|
|
102
103
|
|
|
103
104
|
|
|
104
105
|
def test_check_fails_when_the_output_is_missing(project: Path) -> None:
|
|
105
106
|
assert (
|
|
106
|
-
main([str(project / "queries"), "-o", str(project / "nothing"), "--check"])
|
|
107
|
+
main(["translate", str(project / "queries"), "-o", str(project / "nothing"), "--check"])
|
|
107
108
|
== EXIT_STALE
|
|
108
109
|
)
|
|
109
110
|
|
|
110
111
|
|
|
111
112
|
def test_check_writes_nothing(project: Path) -> None:
|
|
112
113
|
out = project / "build"
|
|
113
|
-
main([str(project / "queries"), "-o", str(out), "--check"])
|
|
114
|
+
main(["translate", str(project / "queries"), "-o", str(out), "--check"])
|
|
114
115
|
assert not out.exists()
|
|
115
116
|
|
|
116
117
|
|
|
117
118
|
def test_check_without_an_output_is_a_usage_error(project: Path) -> None:
|
|
118
119
|
"""There is nothing to compare stdout against; saying so beats exiting 0."""
|
|
119
|
-
assert main([str(project / "queries"), "--check"]) == 2
|
|
120
|
+
assert main(["translate", str(project / "queries"), "--check"]) == 2
|
|
120
121
|
|
|
121
122
|
|
|
122
123
|
def test_check_names_the_stale_files(
|
|
123
124
|
project: Path, capsys: pytest.CaptureFixture[str]
|
|
124
125
|
) -> None:
|
|
125
126
|
out = project / "build"
|
|
126
|
-
main([str(project / "queries"), "-o", str(out), "--check"])
|
|
127
|
+
main(["translate", str(project / "queries"), "-o", str(out), "--check"])
|
|
127
128
|
assert "count.sql" in capsys.readouterr().err
|
|
128
129
|
|
|
129
130
|
|
|
@@ -139,7 +140,7 @@ def test_header_warns_about_the_time_zone(project: Path) -> None:
|
|
|
139
140
|
Someone running generated SQL has no library to set it for them.
|
|
140
141
|
"""
|
|
141
142
|
out = project / "build"
|
|
142
|
-
main([str(project / "queries"), "-o", str(out)])
|
|
143
|
+
main(["translate", str(project / "queries"), "-o", str(out)])
|
|
143
144
|
assert "SET TimeZone='UTC'" in (out / "count.sql").read_text(encoding="utf-8")
|
|
144
145
|
|
|
145
146
|
|
|
@@ -151,7 +152,7 @@ def test_header_carries_no_version_or_timestamp(project: Path) -> None:
|
|
|
151
152
|
from duckdb_kql import __version__ # noqa: PLC0415
|
|
152
153
|
|
|
153
154
|
out = project / "build"
|
|
154
|
-
main([str(project / "queries"), "-o", str(out)])
|
|
155
|
+
main(["translate", str(project / "queries"), "-o", str(out)])
|
|
155
156
|
header = (out / "count.sql").read_text(encoding="utf-8").split("\n\n")[0]
|
|
156
157
|
assert __version__ not in header
|
|
157
158
|
assert "20" not in header.replace("UTC", "") # no year, no date
|
|
@@ -160,7 +161,7 @@ def test_header_carries_no_version_or_timestamp(project: Path) -> None:
|
|
|
160
161
|
def test_header_maps_placeholders_back_to_parameter_names(project: Path) -> None:
|
|
161
162
|
"""``$kqlp0`` alone tells a caller nothing about what to bind."""
|
|
162
163
|
out = project / "build"
|
|
163
|
-
main([str(project / "queries"), "-o", str(out)])
|
|
164
|
+
main(["translate", str(project / "queries"), "-o", str(out)])
|
|
164
165
|
sql = (out / "by_state.sql").read_text(encoding="utf-8")
|
|
165
166
|
|
|
166
167
|
assert "$kqlp0" in sql and "state" in sql
|
|
@@ -172,13 +173,13 @@ def test_header_maps_placeholders_back_to_parameter_names(project: Path) -> None
|
|
|
172
173
|
|
|
173
174
|
def test_no_header_produces_bare_sql(project: Path) -> None:
|
|
174
175
|
out = project / "build"
|
|
175
|
-
main([str(project / "queries"), "-o", str(out), "--no-header"])
|
|
176
|
+
main(["translate", str(project / "queries"), "-o", str(out), "--no-header"])
|
|
176
177
|
assert (out / "count.sql").read_text(encoding="utf-8").startswith("WITH")
|
|
177
178
|
|
|
178
179
|
|
|
179
180
|
def test_a_query_without_parameters_gets_no_parameter_block(project: Path) -> None:
|
|
180
181
|
out = project / "build"
|
|
181
|
-
main([str(project / "queries"), "-o", str(out)])
|
|
182
|
+
main(["translate", str(project / "queries"), "-o", str(out)])
|
|
182
183
|
assert "Query parameters" not in (out / "count.sql").read_text(encoding="utf-8")
|
|
183
184
|
|
|
184
185
|
|
|
@@ -193,7 +194,7 @@ def test_a_syntax_error_reports_file_line_and_column(
|
|
|
193
194
|
"""``file:line:col:`` is what editors and CI annotators parse."""
|
|
194
195
|
bad = tmp_path / "bad.kql"
|
|
195
196
|
bad.write_text("StormEvents | where State ==\n", encoding="utf-8")
|
|
196
|
-
assert main([str(bad)]) == EXIT_TRANSLATION_ERROR
|
|
197
|
+
assert main(["translate", str(bad)]) == EXIT_TRANSLATION_ERROR
|
|
197
198
|
err = capsys.readouterr().err
|
|
198
199
|
# The shape is what matters, not which line the parser stopped on.
|
|
199
200
|
assert re.match(rf"^{re.escape(bad.as_posix())}:\d+:\d+: error: ", err), err
|
|
@@ -204,7 +205,7 @@ def test_an_unsupported_construct_is_an_error_not_a_guess(
|
|
|
204
205
|
) -> None:
|
|
205
206
|
bad = tmp_path / "unsupported.kql"
|
|
206
207
|
bad.write_text("StormEvents | parse State with * 'x' *\n", encoding="utf-8")
|
|
207
|
-
assert main([str(bad)]) == EXIT_TRANSLATION_ERROR
|
|
208
|
+
assert main(["translate", str(bad)]) == EXIT_TRANSLATION_ERROR
|
|
208
209
|
assert "error:" in capsys.readouterr().err
|
|
209
210
|
|
|
210
211
|
|
|
@@ -214,14 +215,14 @@ def test_one_bad_file_does_not_stop_the_others(
|
|
|
214
215
|
"""A build reporting one error at a time is a slow build."""
|
|
215
216
|
(project / "queries" / "bad.kql").write_text("| where", encoding="utf-8")
|
|
216
217
|
out = project / "build"
|
|
217
|
-
assert main([str(project / "queries"), "-o", str(out)]) == EXIT_TRANSLATION_ERROR
|
|
218
|
+
assert main(["translate", str(project / "queries"), "-o", str(out)]) == EXIT_TRANSLATION_ERROR
|
|
218
219
|
assert (out / "count.sql").is_file()
|
|
219
220
|
|
|
220
221
|
|
|
221
222
|
def test_a_missing_file_is_reported_not_raised(
|
|
222
223
|
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
223
224
|
) -> None:
|
|
224
|
-
assert main([str(tmp_path / "nope.kql")]) == EXIT_TRANSLATION_ERROR
|
|
225
|
+
assert main(["translate", str(tmp_path / "nope.kql")]) == EXIT_TRANSLATION_ERROR
|
|
225
226
|
assert "nope.kql" in capsys.readouterr().err
|
|
226
227
|
|
|
227
228
|
|
|
@@ -239,6 +240,7 @@ def test_schema_file_enables_join(tmp_path: Path) -> None:
|
|
|
239
240
|
assert (
|
|
240
241
|
main(
|
|
241
242
|
[
|
|
243
|
+
"translate",
|
|
242
244
|
str(tmp_path / "j.kql"),
|
|
243
245
|
"-o",
|
|
244
246
|
str(tmp_path / "j.sql"),
|
|
@@ -257,7 +259,7 @@ def test_a_malformed_schema_says_what_was_expected(
|
|
|
257
259
|
(tmp_path / "q.kql").write_text("print 1", encoding="utf-8")
|
|
258
260
|
(tmp_path / "schema.json").write_text('{"T": "not a list"}', encoding="utf-8")
|
|
259
261
|
assert (
|
|
260
|
-
main([str(tmp_path / "q.kql"), "--schema", str(tmp_path / "schema.json")])
|
|
262
|
+
main(["translate", str(tmp_path / "q.kql"), "--schema", str(tmp_path / "schema.json")])
|
|
261
263
|
== EXIT_TRANSLATION_ERROR
|
|
262
264
|
)
|
|
263
265
|
assert "list of column" in capsys.readouterr().err
|
|
@@ -280,7 +282,7 @@ def test_the_cli_does_not_import_duckdb(tmp_path: Path) -> None:
|
|
|
280
282
|
probe.write_text(
|
|
281
283
|
"import sys\n"
|
|
282
284
|
"from duckdb_kql.cli import main\n"
|
|
283
|
-
f"main([{str(query)!r}, '-o', {str(tmp_path / 'q.sql')!r}])\n"
|
|
285
|
+
f"main(['translate', {str(query)!r}, '-o', {str(tmp_path / 'q.sql')!r}])\n"
|
|
284
286
|
"assert 'duckdb' not in sys.modules, 'the CLI imported duckdb'\n"
|
|
285
287
|
"print('clean')\n",
|
|
286
288
|
encoding="utf-8",
|
|
@@ -300,7 +302,7 @@ def test_python_m_duckdb_kql_works(tmp_path: Path) -> None:
|
|
|
300
302
|
query = tmp_path / "q.kql"
|
|
301
303
|
query.write_text(SIMPLE, encoding="utf-8")
|
|
302
304
|
proc = subprocess.run( # noqa: S603 - fixed argv, no shell
|
|
303
|
-
[sys.executable, "-m", "duckdb_kql", str(query)],
|
|
305
|
+
[sys.executable, "-m", "duckdb_kql", "translate", str(query)],
|
|
304
306
|
capture_output=True,
|
|
305
307
|
text=True,
|
|
306
308
|
check=False,
|
|
@@ -317,28 +319,69 @@ def test_the_console_script_is_declared() -> None:
|
|
|
317
319
|
|
|
318
320
|
|
|
319
321
|
# ---------------------------------------------------------------------------
|
|
320
|
-
#
|
|
322
|
+
# Subcommands
|
|
321
323
|
# ---------------------------------------------------------------------------
|
|
322
324
|
|
|
325
|
+
#: Every verb this command answers to. A new one that is not listed here is a
|
|
326
|
+
#: new one nobody checked the wiring of.
|
|
327
|
+
SUBCOMMANDS = ["translate", "serve"]
|
|
323
328
|
|
|
324
|
-
def test_serve_is_the_only_word_that_is_not_a_filename(project: Path) -> None:
|
|
325
|
-
"""Adding a subcommand must not turn the documented form into one.
|
|
326
329
|
|
|
327
|
-
|
|
328
|
-
|
|
330
|
+
def test_the_subcommands_are_the_documented_ones() -> None:
|
|
331
|
+
from duckdb_kql.cli import _parser # noqa: PLC0415
|
|
332
|
+
|
|
333
|
+
(subparsers,) = [
|
|
334
|
+
action
|
|
335
|
+
for action in _parser()._actions
|
|
336
|
+
if isinstance(action, argparse._SubParsersAction)
|
|
337
|
+
]
|
|
338
|
+
assert sorted(subparsers.choices) == sorted(SUBCOMMANDS)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@pytest.mark.parametrize("command", SUBCOMMANDS)
|
|
342
|
+
def test_every_subcommand_is_wired_to_a_handler(command: str) -> None:
|
|
343
|
+
"""Registering a subparser and forgetting `set_defaults(run=...)` would
|
|
344
|
+
parse fine and then fail on the attribute, in front of the user."""
|
|
345
|
+
from duckdb_kql.cli import _parser # noqa: PLC0415
|
|
346
|
+
|
|
347
|
+
args = _parser().parse_args([command] + (["x.kql"] if command == "translate" else []))
|
|
348
|
+
assert callable(args.run)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def test_a_bare_invocation_is_a_usage_error_not_a_traceback() -> None:
|
|
352
|
+
"""`duckdb-kql` with no verb has nothing to do; 2 is argparse's usage code."""
|
|
353
|
+
with pytest.raises(SystemExit) as exit_code:
|
|
354
|
+
main([])
|
|
355
|
+
assert exit_code.value.code == 2
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def test_an_unknown_subcommand_lists_the_known_ones() -> None:
|
|
359
|
+
with pytest.raises(SystemExit) as exit_code:
|
|
360
|
+
main(["transalte", "q.kql"])
|
|
361
|
+
assert exit_code.value.code == 2
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def test_a_kql_file_is_no_longer_a_verb(tmp_path: Path) -> None:
|
|
365
|
+
"""The breaking change, stated as a test.
|
|
366
|
+
|
|
367
|
+
`duckdb-kql queries/` used to translate. It is now a usage error, because a
|
|
368
|
+
filename in the verb slot is exactly the ambiguity subcommands remove — and
|
|
369
|
+
a silent reinterpretation would be worse than a refusal.
|
|
329
370
|
"""
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
371
|
+
query = tmp_path / "q.kql"
|
|
372
|
+
query.write_text(SIMPLE, encoding="utf-8")
|
|
373
|
+
with pytest.raises(SystemExit) as exit_code:
|
|
374
|
+
main([str(query)])
|
|
375
|
+
assert exit_code.value.code == 2
|
|
333
376
|
|
|
334
377
|
|
|
335
378
|
def test_a_kql_file_called_serve_is_still_translated(
|
|
336
379
|
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
337
380
|
) -> None:
|
|
338
|
-
"""
|
|
381
|
+
"""The verb slot is the only place `serve` is a verb."""
|
|
339
382
|
query = tmp_path / "serve.kql"
|
|
340
383
|
query.write_text(SIMPLE, encoding="utf-8")
|
|
341
|
-
assert main([str(query)]) == EXIT_OK
|
|
384
|
+
assert main(["translate", str(query)]) == EXIT_OK
|
|
342
385
|
assert "SELECT" in capsys.readouterr().out
|
|
343
386
|
|
|
344
387
|
|
|
@@ -355,15 +398,24 @@ def test_serve_help_describes_the_local_only_guarantee(
|
|
|
355
398
|
|
|
356
399
|
|
|
357
400
|
def test_serve_defaults_to_an_in_memory_database() -> None:
|
|
358
|
-
from duckdb_kql.cli import
|
|
401
|
+
from duckdb_kql.cli import _parser # noqa: PLC0415
|
|
359
402
|
|
|
360
|
-
args =
|
|
403
|
+
args = _parser().parse_args(["serve"])
|
|
361
404
|
assert args.database == ":memory:"
|
|
362
405
|
assert args.port == 31415
|
|
363
406
|
|
|
364
407
|
|
|
365
408
|
def test_serve_takes_a_port_override() -> None:
|
|
366
|
-
from duckdb_kql.cli import
|
|
409
|
+
from duckdb_kql.cli import _parser # noqa: PLC0415
|
|
410
|
+
|
|
411
|
+
assert _parser().parse_args(["serve", "--port", "9000"]).port == 9000
|
|
412
|
+
assert _parser().parse_args(["serve", "-p", "9000"]).port == 9000
|
|
367
413
|
|
|
368
|
-
|
|
369
|
-
|
|
414
|
+
|
|
415
|
+
def test_the_top_level_help_names_both_jobs(capsys: pytest.CaptureFixture[str]) -> None:
|
|
416
|
+
"""Someone typing `duckdb-kql` blind should learn what it can do."""
|
|
417
|
+
with pytest.raises(SystemExit):
|
|
418
|
+
main(["--help"])
|
|
419
|
+
printed = capsys.readouterr().out
|
|
420
|
+
for command in SUBCOMMANDS:
|
|
421
|
+
assert command in printed
|
|
@@ -109,9 +109,17 @@ def test_it_binds_to_loopback_only(server) -> None:
|
|
|
109
109
|
|
|
110
110
|
def test_the_cli_cannot_ask_for_a_different_bind_address() -> None:
|
|
111
111
|
"""`--host` must not exist. A flag is all it would take to undo the above."""
|
|
112
|
-
|
|
112
|
+
import argparse # noqa: PLC0415
|
|
113
113
|
|
|
114
|
-
|
|
114
|
+
from duckdb_kql.cli import _parser # noqa: PLC0415
|
|
115
|
+
|
|
116
|
+
(subparsers,) = [
|
|
117
|
+
action
|
|
118
|
+
for action in _parser()._actions
|
|
119
|
+
if isinstance(action, argparse._SubParsersAction)
|
|
120
|
+
]
|
|
121
|
+
serve = subparsers.choices["serve"]
|
|
122
|
+
flags = {option for action in serve._actions for option in action.option_strings}
|
|
115
123
|
assert not flags & {"--host", "--bind", "--interface", "--address"}
|
|
116
124
|
|
|
117
125
|
|
|
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
|
{duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/docs/code-review/testing-oracle-and-fixtures.md
RENAMED
|
File without changes
|
{duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/docs/code-review/tooling-packaging-ci-docs.md
RENAMED
|
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
|
|
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
|
{duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev4}/src/duckdb_kql/kusto/client_request_properties.py
RENAMED
|
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
|
|
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
|