duckdb-kql 0.0.1.dev3__tar.gz → 0.0.1.dev5__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.dev5}/PKG-INFO +3 -2
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/README.md +1 -1
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/demo/demo.ipynb +1 -1
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/api.md +1 -1
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/cli.md +24 -19
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/getting-started.md +1 -1
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/kusto-server.md +7 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/pyproject.toml +2 -1
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/_version.py +2 -2
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/cli.py +114 -84
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/server.py +35 -7
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_cli.py +92 -40
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_server.py +110 -8
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_workflows.py +4 -3
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/.gitignore +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/CODE_OF_CONDUCT.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/CONTRIBUTING.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/LICENSE +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/SECURITY.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/THIRD-PARTY-NOTICES.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/demo/demo.kql +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/TRANSLATION.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/ai-cost-strategy.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/azure-monitor-profile.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/code-review/README.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/code-review/kusto-client-compat.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/code-review/public-api-and-typing.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/code-review/review-2026-08-04.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/code-review/security-and-injection.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/code-review/testing-oracle-and-fixtures.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/code-review/tooling-packaging-ci-docs.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/code-review/translation-correctness.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/frequency-scan-results.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/implementation-options.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/implementation-plan.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/kql-on-duckdb-landscape.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/kql-support.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/kusto-client.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/lessons-from-bun-rewrite.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/licensing.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/m0-grammar-spike.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/oracle-harness.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/docs/test-plan.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/grammar/Kql.g4 +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/grammar/KqlTokens.g4 +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/grammar/UPSTREAM.md +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/__init__.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/__main__.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/_antlr/Kql.interp +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/_antlr/Kql.tokens +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/_antlr/KqlLexer.interp +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/_antlr/KqlLexer.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/_antlr/KqlLexer.tokens +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/_antlr/KqlListener.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/_antlr/KqlParser.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/_antlr/KqlVisitor.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/_antlr/__init__.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/comparison.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/control.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/engine.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/errors.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/fixtures.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/ir.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/kusto/__init__.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/kusto/_models.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/kusto/client.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/kusto/client_request_properties.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/kusto/exceptions.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/kusto/helpers.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/kusto/response.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/lower.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/oracle.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/params.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/parser.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/py.typed +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/schema.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/translate/__init__.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/translate/functions.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/src/duckdb_kql/types.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/conftest.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/profiles/azure-monitor.json +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_behavior.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_column_order_and_null_sort.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_comparison.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_control_commands.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_corpus.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_datetime_traps.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_demo_notebook.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_devcontainers.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_docs.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_dynamic.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_fixtures.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_getschema.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_join.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_kusto_client.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_let.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_null_semantics.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_parse.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_profile_azure_monitor.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_query_parameters.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_range_in_render.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_summarize.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_support_matrix.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tests/test_typing.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tools/check_profile.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tools/frequency_scan.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tools/gen_support_matrix.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tools/harvest_docs.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tools/make_fixtures.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/tools/regen_expectations.py +0 -0
- {duckdb_kql-0.0.1.dev3 → duckdb_kql-0.0.1.dev5}/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.dev5
|
|
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
|
|
@@ -44,6 +44,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
44
44
|
Classifier: Programming Language :: Python :: 3.11
|
|
45
45
|
Classifier: Programming Language :: Python :: 3.12
|
|
46
46
|
Classifier: Programming Language :: Python :: 3.13
|
|
47
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
47
48
|
Classifier: Topic :: Database
|
|
48
49
|
Classifier: Topic :: Database :: Front-Ends
|
|
49
50
|
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
@@ -133,7 +134,7 @@ Translate at build time and the output has no dependency on this package at all
|
|
|
133
134
|
— not even Python. Only your CI machine installs it.
|
|
134
135
|
|
|
135
136
|
```bash
|
|
136
|
-
duckdb-kql queries/ -o build/sql/ --check # fails the build if a .sql is stale
|
|
137
|
+
duckdb-kql translate queries/ -o build/sql/ --check # fails the build if a .sql is stale
|
|
137
138
|
```
|
|
138
139
|
|
|
139
140
|
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).
|
|
@@ -66,6 +66,13 @@ https://dataexplorer.azure.us
|
|
|
66
66
|
`--allow-origin` replaces that list. Widening it is a decision about who may read
|
|
67
67
|
this database from another browser tab, not a formatting preference.
|
|
68
68
|
|
|
69
|
+
The **origin** is the whole check. Request *headers* are not: a preflight echoes
|
|
70
|
+
back whatever `Access-Control-Request-Headers` asked for, because naming a header
|
|
71
|
+
authorises nothing and a hand-written list is only a second place to be wrong
|
|
72
|
+
about someone else's client. It was wrong once — the list said `x-ms-user` where
|
|
73
|
+
the web UI sends `x-ms-user-id`, and the browser answered by failing the real
|
|
74
|
+
POST with a bare `net::ERR_FAILED` after a preflight that returned 204.
|
|
75
|
+
|
|
69
76
|
**Nothing it serves can write.** The translated surface is read-only: no
|
|
70
77
|
supported KQL operator produces a statement that modifies the database, and
|
|
71
78
|
control commands that would administer a cluster are refused rather than
|
|
@@ -31,6 +31,7 @@ classifiers = [
|
|
|
31
31
|
"Programming Language :: Python :: 3.11",
|
|
32
32
|
"Programming Language :: Python :: 3.12",
|
|
33
33
|
"Programming Language :: Python :: 3.13",
|
|
34
|
+
"Programming Language :: Python :: 3.14",
|
|
34
35
|
"Topic :: Database",
|
|
35
36
|
"Topic :: Database :: Front-Ends",
|
|
36
37
|
"Topic :: Software Development :: Compilers",
|
|
@@ -167,7 +168,7 @@ files = ["src/duckdb_kql"]
|
|
|
167
168
|
# way, and the next dependency to adopt the syntax would do it again. Letting
|
|
168
169
|
# each interpreter check against itself is more robust *and* better coverage:
|
|
169
170
|
# CI runs the floor and the ceiling, so 3.10-incompatible code fails on the
|
|
170
|
-
# 3.10 lane while the newest semantics are exercised on the
|
|
171
|
+
# 3.10 lane while the newest semantics are exercised on the newest one.
|
|
171
172
|
disallow_untyped_defs = true
|
|
172
173
|
disallow_incomplete_defs = true
|
|
173
174
|
disallow_untyped_calls = true
|
|
@@ -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.dev5'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 0, 1, 'dev5')
|
|
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
|
|
|
@@ -41,6 +41,7 @@ import uuid
|
|
|
41
41
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
42
42
|
from typing import TYPE_CHECKING, Any, NamedTuple, cast
|
|
43
43
|
|
|
44
|
+
from . import __version__
|
|
44
45
|
from .control import SCHEMA, CommandColumn, is_control_command, split_command
|
|
45
46
|
from .errors import KqlError
|
|
46
47
|
from .types import kusto_type, rest_datatype
|
|
@@ -347,11 +348,18 @@ def error_response(message: str, *, code: str = "General_BadRequest") -> dict[st
|
|
|
347
348
|
|
|
348
349
|
_LOOPBACK = re.compile(r"^(127\.\d+\.\d+\.\d+|::1)$")
|
|
349
350
|
|
|
351
|
+
#: Only for a preflight that names no headers, which no browser sends — a
|
|
352
|
+
#: preflight exists *because* there is something non-simple to declare. Kept so
|
|
353
|
+
#: the header is never emitted empty.
|
|
354
|
+
_DEFAULT_ALLOW_HEADERS = "Authorization, Content-Type, Accept"
|
|
355
|
+
|
|
350
356
|
|
|
351
357
|
class _Handler(BaseHTTPRequestHandler):
|
|
352
358
|
"""One request. The connection and the allow-list come from the server."""
|
|
353
359
|
|
|
354
|
-
|
|
360
|
+
# `duckdb-kql/0.1.2`, not `duckdb-kql/duckdb-kql`: the slot after the slash
|
|
361
|
+
# is a version, and this used to interpolate the cluster name into it.
|
|
362
|
+
server_version = f"duckdb-kql/{__version__}"
|
|
355
363
|
# Announce HTTP/1.1 so the web UI's keep-alive works; every response below
|
|
356
364
|
# sets Content-Length, which is what makes that safe.
|
|
357
365
|
protocol_version = "HTTP/1.1"
|
|
@@ -406,17 +414,37 @@ class _Handler(BaseHTTPRequestHandler):
|
|
|
406
414
|
self.send_response(204)
|
|
407
415
|
self.send_header("Access-Control-Allow-Origin", origin)
|
|
408
416
|
self.send_header("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
|
|
409
|
-
self.send_header(
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
)
|
|
417
|
+
self.send_header("Access-Control-Allow-Headers", self._allow_headers())
|
|
418
|
+
# Only when asked. Chrome gates requests from a public page to a private
|
|
419
|
+
# address behind this; answering unprompted would claim a policy the
|
|
420
|
+
# browser did not ask about.
|
|
421
|
+
if self.headers.get("Access-Control-Request-Private-Network") == "true":
|
|
422
|
+
self.send_header("Access-Control-Allow-Private-Network", "true")
|
|
414
423
|
self.send_header("Access-Control-Allow-Credentials", "true")
|
|
415
424
|
self.send_header("Access-Control-Max-Age", "600")
|
|
416
|
-
self.send_header("Vary", "Origin")
|
|
425
|
+
self.send_header("Vary", "Origin, Access-Control-Request-Headers")
|
|
417
426
|
self.send_header("Content-Length", "0")
|
|
418
427
|
self.end_headers()
|
|
419
428
|
|
|
429
|
+
def _allow_headers(self) -> str:
|
|
430
|
+
"""The request headers the preflight permits: whatever was asked for.
|
|
431
|
+
|
|
432
|
+
Echoed rather than listed, because a hand-written list is a second place
|
|
433
|
+
to be wrong about someone else's client. It *was* wrong: the list said
|
|
434
|
+
`x-ms-user` where the Azure Data Explorer UI sends `x-ms-user-id`, CORS
|
|
435
|
+
matches header names exactly, and the browser answered by failing the
|
|
436
|
+
real POST with a bare `net::ERR_FAILED` — a preflight that returned 204
|
|
437
|
+
and still blocked the request. Every header the UI adds in future would
|
|
438
|
+
break it the same way.
|
|
439
|
+
|
|
440
|
+
This gives up nothing. Naming a header does not authorise anything; the
|
|
441
|
+
origin allow-list and the loopback bind are what decide who may talk to
|
|
442
|
+
this endpoint, and both are checked before we get here. `*` would be the
|
|
443
|
+
lazy version of this and is not equivalent — it is invalid alongside
|
|
444
|
+
`Allow-Credentials: true`, which is why the exact list is echoed back.
|
|
445
|
+
"""
|
|
446
|
+
return self.headers.get("Access-Control-Request-Headers", _DEFAULT_ALLOW_HEADERS)
|
|
447
|
+
|
|
420
448
|
def do_GET(self) -> None: # noqa: N802 - BaseHTTPRequestHandler's interface
|
|
421
449
|
"""A human landing page. Anything else 404s."""
|
|
422
450
|
if not self._is_local():
|