uqal-core 0.1.0__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.
- uqal_core-0.1.0/.env.example +12 -0
- uqal_core-0.1.0/.github/ISSUE_TEMPLATE/bug_report.yml +52 -0
- uqal_core-0.1.0/.github/ISSUE_TEMPLATE/feature_request.yml +38 -0
- uqal_core-0.1.0/.github/PULL_REQUEST_TEMPLATE.md +13 -0
- uqal_core-0.1.0/.github/workflows/publish.yml +29 -0
- uqal_core-0.1.0/.gitignore +27 -0
- uqal_core-0.1.0/.python-version +1 -0
- uqal_core-0.1.0/LICENSE +21 -0
- uqal_core-0.1.0/PKG-INFO +106 -0
- uqal_core-0.1.0/README.md +91 -0
- uqal_core-0.1.0/SECURITY.md +23 -0
- uqal_core-0.1.0/hr_schema.uqal +1 -0
- uqal_core-0.1.0/pyproject.toml +46 -0
- uqal_core-0.1.0/scripts/dev/check_query.py +102 -0
- uqal_core-0.1.0/scripts/dev/inspect_grammar.py +80 -0
- uqal_core-0.1.0/scripts/dev/validate_module.py +326 -0
- uqal_core-0.1.0/src/uqal_core/ast/__init__.py +0 -0
- uqal_core-0.1.0/src/uqal_core/ast/condition_registry.py +42 -0
- uqal_core-0.1.0/src/uqal_core/ast/module_nodes.py +33 -0
- uqal_core-0.1.0/src/uqal_core/ast/nodes.py +424 -0
- uqal_core-0.1.0/src/uqal_core/ast/transformer.py +616 -0
- uqal_core-0.1.0/src/uqal_core/cache/cache_manager.py +226 -0
- uqal_core-0.1.0/src/uqal_core/cli/__init__.py +0 -0
- uqal_core-0.1.0/src/uqal_core/cli/commands/__init__.py +0 -0
- uqal_core-0.1.0/src/uqal_core/cli/commands/add_connection.py +446 -0
- uqal_core-0.1.0/src/uqal_core/cli/commands/add_module.py +169 -0
- uqal_core-0.1.0/src/uqal_core/cli/commands/cache.py +189 -0
- uqal_core-0.1.0/src/uqal_core/cli/commands/list_modules.py +36 -0
- uqal_core-0.1.0/src/uqal_core/cli/commands/repl.py +578 -0
- uqal_core-0.1.0/src/uqal_core/cli/commands/run.py +145 -0
- uqal_core-0.1.0/src/uqal_core/cli/commands/script.py +278 -0
- uqal_core-0.1.0/src/uqal_core/cli/main.py +51 -0
- uqal_core-0.1.0/src/uqal_core/config/__init__.py +0 -0
- uqal_core-0.1.0/src/uqal_core/config/config_manager.py +248 -0
- uqal_core-0.1.0/src/uqal_core/config/connection_schema.py +114 -0
- uqal_core-0.1.0/src/uqal_core/config/loader.py +162 -0
- uqal_core-0.1.0/src/uqal_core/engine.py +261 -0
- uqal_core-0.1.0/src/uqal_core/execution/__init__.py +0 -0
- uqal_core-0.1.0/src/uqal_core/execution/executor.py +637 -0
- uqal_core-0.1.0/src/uqal_core/execution/result_set.py +155 -0
- uqal_core-0.1.0/src/uqal_core/module_interface.py +221 -0
- uqal_core-0.1.0/src/uqal_core/module_loader.py +255 -0
- uqal_core-0.1.0/src/uqal_core/modules/standard/dummy/__init__.py +0 -0
- uqal_core-0.1.0/src/uqal_core/modules/standard/dummy/connection_schema.py +30 -0
- uqal_core-0.1.0/src/uqal_core/modules/standard/dummy/module.py +134 -0
- uqal_core-0.1.0/src/uqal_core/modules/standard/mongodb/__init__.py +0 -0
- uqal_core-0.1.0/src/uqal_core/modules/standard/mongodb/capabilities.py +24 -0
- uqal_core-0.1.0/src/uqal_core/modules/standard/mongodb/connection_schema.py +96 -0
- uqal_core-0.1.0/src/uqal_core/modules/standard/mongodb/grammar_extension.lark +5 -0
- uqal_core-0.1.0/src/uqal_core/modules/standard/mongodb/module.py +525 -0
- uqal_core-0.1.0/src/uqal_core/modules/standard/mongodb/native_validator.py +40 -0
- uqal_core-0.1.0/src/uqal_core/modules/standard/mongodb/schema_sync.py +258 -0
- uqal_core-0.1.0/src/uqal_core/modules/standard/mongodb/translator.py +235 -0
- uqal_core-0.1.0/src/uqal_core/modules/standard/mongodb/type_mapping.py +58 -0
- uqal_core-0.1.0/src/uqal_core/modules/standard/neo4j/__init__.py +0 -0
- uqal_core-0.1.0/src/uqal_core/modules/standard/neo4j/capabilities.py +25 -0
- uqal_core-0.1.0/src/uqal_core/modules/standard/neo4j/connection_schema.py +68 -0
- uqal_core-0.1.0/src/uqal_core/modules/standard/neo4j/grammar_extension.lark +9 -0
- uqal_core-0.1.0/src/uqal_core/modules/standard/neo4j/module.py +282 -0
- uqal_core-0.1.0/src/uqal_core/modules/standard/neo4j/native_validator.py +47 -0
- uqal_core-0.1.0/src/uqal_core/modules/standard/neo4j/schema_sync.py +203 -0
- uqal_core-0.1.0/src/uqal_core/modules/standard/neo4j/translator.py +476 -0
- uqal_core-0.1.0/src/uqal_core/modules/standard/neo4j/type_mapping.py +70 -0
- uqal_core-0.1.0/src/uqal_core/modules/standard/postgresql/__init__.py +0 -0
- uqal_core-0.1.0/src/uqal_core/modules/standard/postgresql/capabilities.py +30 -0
- uqal_core-0.1.0/src/uqal_core/modules/standard/postgresql/complexity.py +85 -0
- uqal_core-0.1.0/src/uqal_core/modules/standard/postgresql/connection_schema.py +98 -0
- uqal_core-0.1.0/src/uqal_core/modules/standard/postgresql/grammar_extension.lark +5 -0
- uqal_core-0.1.0/src/uqal_core/modules/standard/postgresql/module.py +370 -0
- uqal_core-0.1.0/src/uqal_core/modules/standard/postgresql/native_validator.py +86 -0
- uqal_core-0.1.0/src/uqal_core/modules/standard/postgresql/schema_sync.py +182 -0
- uqal_core-0.1.0/src/uqal_core/modules/standard/postgresql/translator.py +446 -0
- uqal_core-0.1.0/src/uqal_core/modules/standard/postgresql/type_mapping.py +68 -0
- uqal_core-0.1.0/src/uqal_core/parser/__init__.py +0 -0
- uqal_core-0.1.0/src/uqal_core/parser/base_grammar.lark +198 -0
- uqal_core-0.1.0/src/uqal_core/parser/fragments/.gitkeep +0 -0
- uqal_core-0.1.0/src/uqal_core/parser/fragments/conditions.lark +14 -0
- uqal_core-0.1.0/src/uqal_core/parser/fragments/expressions.lark +23 -0
- uqal_core-0.1.0/src/uqal_core/parser/fragments/params.lark +19 -0
- uqal_core-0.1.0/src/uqal_core/parser/fragments/types.lark +19 -0
- uqal_core-0.1.0/src/uqal_core/parser/grammar_builder.py +179 -0
- uqal_core-0.1.0/src/uqal_core/planner/__init__.py +0 -0
- uqal_core-0.1.0/src/uqal_core/planner/query_planner.py +394 -0
- uqal_core-0.1.0/src/uqal_core/registry/__init__.py +0 -0
- uqal_core-0.1.0/src/uqal_core/registry/connection_registry.py +99 -0
- uqal_core-0.1.0/src/uqal_core/registry/module_registry.py +114 -0
- uqal_core-0.1.0/src/uqal_core/schema/__init__.py +0 -0
- uqal_core-0.1.0/src/uqal_core/schema/schema_store.py +134 -0
- uqal_core-0.1.0/src/uqal_core/scripts/editor.py +211 -0
- uqal_core-0.1.0/src/uqal_core/scripts/script_manager.py +93 -0
- uqal_core-0.1.0/src/uqal_core/typecheck/__init__.py +0 -0
- uqal_core-0.1.0/src/uqal_core/typecheck/checker.py +493 -0
- uqal_core-0.1.0/src/uqal_core/types.py +132 -0
- uqal_core-0.1.0/tests/__init__.py +0 -0
- uqal_core-0.1.0/tests/conftest.py +139 -0
- uqal_core-0.1.0/tests/e2e/__init__.py +0 -0
- uqal_core-0.1.0/tests/e2e/test_full_workflow.py +119 -0
- uqal_core-0.1.0/tests/integration/__init__.py +0 -0
- uqal_core-0.1.0/tests/integration/test_cli.py +127 -0
- uqal_core-0.1.0/tests/integration/test_module_compliance.py +238 -0
- uqal_core-0.1.0/tests/integration/test_module_loader.py +57 -0
- uqal_core-0.1.0/tests/unit/__init__.py +0 -0
- uqal_core-0.1.0/tests/unit/test_config_manager.py +200 -0
- uqal_core-0.1.0/tests/unit/test_create_view.py +216 -0
- uqal_core-0.1.0/tests/unit/test_executor.py +214 -0
- uqal_core-0.1.0/tests/unit/test_executor_output.py +288 -0
- uqal_core-0.1.0/tests/unit/test_module_nodes.py +85 -0
- uqal_core-0.1.0/tests/unit/test_mongodb_module.py +322 -0
- uqal_core-0.1.0/tests/unit/test_neo4j_module.py +282 -0
- uqal_core-0.1.0/tests/unit/test_neo4j_rel_traversal.py +457 -0
- uqal_core-0.1.0/tests/unit/test_neo4j_translator.py +285 -0
- uqal_core-0.1.0/tests/unit/test_output_format.py +190 -0
- uqal_core-0.1.0/tests/unit/test_planner.py +188 -0
- uqal_core-0.1.0/tests/unit/test_result_set.py +69 -0
- uqal_core-0.1.0/tests/unit/test_schema_store.py +82 -0
- uqal_core-0.1.0/tests/unit/test_transformer.py +164 -0
- uqal_core-0.1.0/tests/unit/test_typecheck.py +161 -0
- uqal_core-0.1.0/tests/unit/test_types.py +56 -0
- uqal_core-0.1.0/uqal_config.example.json +17 -0
- uqal_core-0.1.0/uv.lock +455 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# UQAL secrets template
|
|
2
|
+
# Copy this file to .env and fill in your real values
|
|
3
|
+
# Never commit .env to git
|
|
4
|
+
|
|
5
|
+
# Example for a connection named "db1":
|
|
6
|
+
# UQAL_DB1_USER=
|
|
7
|
+
# UQAL_DB1_PASSWORD=
|
|
8
|
+
# UQAL_DB1_SSL_CERT=
|
|
9
|
+
|
|
10
|
+
# Example for a connection named "graph":
|
|
11
|
+
# UQAL_GRAPH_USER=
|
|
12
|
+
# UQAL_GRAPH_PASSWORD=
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
name: Bug Report
|
|
2
|
+
description: Something is not working as expected in uqal-core.
|
|
3
|
+
title: "bug: "
|
|
4
|
+
labels: ["bug"]
|
|
5
|
+
body:
|
|
6
|
+
- type: input
|
|
7
|
+
id: version
|
|
8
|
+
attributes:
|
|
9
|
+
label: uqal-core version
|
|
10
|
+
placeholder: "e.g. 0.3.1"
|
|
11
|
+
validations:
|
|
12
|
+
required: true
|
|
13
|
+
|
|
14
|
+
- type: input
|
|
15
|
+
id: python
|
|
16
|
+
attributes:
|
|
17
|
+
label: Python version
|
|
18
|
+
placeholder: "e.g. 3.12.2"
|
|
19
|
+
validations:
|
|
20
|
+
required: true
|
|
21
|
+
|
|
22
|
+
- type: textarea
|
|
23
|
+
id: description
|
|
24
|
+
attributes:
|
|
25
|
+
label: What happened?
|
|
26
|
+
description: A clear description of the bug.
|
|
27
|
+
validations:
|
|
28
|
+
required: true
|
|
29
|
+
|
|
30
|
+
- type: textarea
|
|
31
|
+
id: reproduction
|
|
32
|
+
attributes:
|
|
33
|
+
label: Steps to reproduce
|
|
34
|
+
placeholder: |
|
|
35
|
+
1. Load module ...
|
|
36
|
+
2. Run query ...
|
|
37
|
+
3. See error
|
|
38
|
+
validations:
|
|
39
|
+
required: true
|
|
40
|
+
|
|
41
|
+
- type: textarea
|
|
42
|
+
id: expected
|
|
43
|
+
attributes:
|
|
44
|
+
label: Expected behaviour
|
|
45
|
+
validations:
|
|
46
|
+
required: true
|
|
47
|
+
|
|
48
|
+
- type: textarea
|
|
49
|
+
id: logs
|
|
50
|
+
attributes:
|
|
51
|
+
label: Error output / stack trace
|
|
52
|
+
render: shell
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: Feature Request
|
|
2
|
+
description: Suggest an improvement or new capability for uqal-core.
|
|
3
|
+
title: "feat: "
|
|
4
|
+
labels: ["enhancement"]
|
|
5
|
+
body:
|
|
6
|
+
- type: textarea
|
|
7
|
+
id: problem
|
|
8
|
+
attributes:
|
|
9
|
+
label: What problem does this solve?
|
|
10
|
+
description: Describe the limitation or gap you are running into.
|
|
11
|
+
validations:
|
|
12
|
+
required: true
|
|
13
|
+
|
|
14
|
+
- type: textarea
|
|
15
|
+
id: solution
|
|
16
|
+
attributes:
|
|
17
|
+
label: Proposed solution
|
|
18
|
+
description: How should it work? Include example syntax or API if relevant.
|
|
19
|
+
validations:
|
|
20
|
+
required: true
|
|
21
|
+
|
|
22
|
+
- type: textarea
|
|
23
|
+
id: alternatives
|
|
24
|
+
attributes:
|
|
25
|
+
label: Alternatives considered
|
|
26
|
+
description: Other approaches you thought about and why you ruled them out.
|
|
27
|
+
|
|
28
|
+
- type: checkboxes
|
|
29
|
+
id: scope
|
|
30
|
+
attributes:
|
|
31
|
+
label: Affected area
|
|
32
|
+
options:
|
|
33
|
+
- label: Parser / Grammar
|
|
34
|
+
- label: AST / Transformer
|
|
35
|
+
- label: Module interface
|
|
36
|
+
- label: Module loader
|
|
37
|
+
- label: CLI
|
|
38
|
+
- label: Dev tooling
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
## What
|
|
2
|
+
|
|
3
|
+
<!-- What does this PR change? -->
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
<!-- Why is this change needed? Link an issue if one exists. Closes #... -->
|
|
8
|
+
|
|
9
|
+
## Checklist
|
|
10
|
+
|
|
11
|
+
- [ ] Tests added or updated
|
|
12
|
+
- [ ] `uv run pytest` passes locally
|
|
13
|
+
- [ ] No debug code or temporary files included
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
name: Build and publish
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
environment: release
|
|
12
|
+
permissions:
|
|
13
|
+
id-token: write # required for PyPI Trusted Publishing
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: "3.12"
|
|
21
|
+
|
|
22
|
+
- name: Install uv
|
|
23
|
+
uses: astral-sh/setup-uv@v4
|
|
24
|
+
|
|
25
|
+
- name: Build
|
|
26
|
+
run: uv build
|
|
27
|
+
|
|
28
|
+
- name: Publish to PyPI
|
|
29
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Python-generated files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[oc]
|
|
4
|
+
build/
|
|
5
|
+
dist/
|
|
6
|
+
wheels/
|
|
7
|
+
*.egg-info
|
|
8
|
+
|
|
9
|
+
# Virtual environments
|
|
10
|
+
.venv
|
|
11
|
+
|
|
12
|
+
test_grammar.py
|
|
13
|
+
test_grammar_files.py
|
|
14
|
+
|
|
15
|
+
test_grammar_builder.py
|
|
16
|
+
|
|
17
|
+
tests/logs/
|
|
18
|
+
tests/coverage_html/
|
|
19
|
+
|
|
20
|
+
.env
|
|
21
|
+
|
|
22
|
+
uqal_config.json
|
|
23
|
+
|
|
24
|
+
.coverage
|
|
25
|
+
tests/coverage_html/
|
|
26
|
+
|
|
27
|
+
.uqal/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.14.0
|
uqal_core-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 UQAL
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
uqal_core-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: uqal-core
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Requires-Python: >=3.12
|
|
7
|
+
Requires-Dist: click>=8.4.2
|
|
8
|
+
Requires-Dist: lark>=1.3.1
|
|
9
|
+
Requires-Dist: neo4j>=6.2.0
|
|
10
|
+
Requires-Dist: prompt-toolkit>=3.0.52
|
|
11
|
+
Requires-Dist: psycopg2-binary>=2.9.12
|
|
12
|
+
Requires-Dist: pymongo>=4.17.0
|
|
13
|
+
Requires-Dist: textual>=8.2.8
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# uqal-core
|
|
17
|
+
|
|
18
|
+
The core engine of [UQAL](https://uqal-lang.github.io/uqal-docs) — a unified query abstraction layer that lets you write one query syntax and run it against any supported database.
|
|
19
|
+
|
|
20
|
+
`uqal-core` contains the parser, grammar builder, AST, module loader, and CLI. It is the runtime that all UQAL modules plug into.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## What is UQAL?
|
|
25
|
+
|
|
26
|
+
UQAL is a database-agnostic query language. Instead of writing SQL for PostgreSQL, Cypher for Neo4j, and MQL for MongoDB separately, you write UQAL once and the appropriate module translates it into the native query for the connected database.
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
mydb.users.get_table(where active = true, fields id, name)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
uv add uqal-core
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Requires Python 3.12+.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Quick Start
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Start the interactive UQAL shell
|
|
48
|
+
uqal shell
|
|
49
|
+
|
|
50
|
+
# Run a query file
|
|
51
|
+
uqal run query.uqal
|
|
52
|
+
|
|
53
|
+
# List loaded modules
|
|
54
|
+
uqal list-modules
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Modules
|
|
60
|
+
|
|
61
|
+
Database support comes from modules. Standard modules ship with `uqal-core`. Community modules are installed separately:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
uv add uqal-postgis
|
|
65
|
+
uqal add-module community.postgis
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Browse available modules: [uqal-lang/uqal-modules](https://github.com/uqal-lang/uqal-modules)
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Development Setup
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
git clone https://github.com/uqal-lang/uqal-core
|
|
76
|
+
cd uqal-core
|
|
77
|
+
uv sync
|
|
78
|
+
uv run pytest
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Dev scripts are in `scripts/dev/`:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# Parse and translate a query
|
|
85
|
+
uv run python scripts/dev/check_query.py "mydb.users.get_table()"
|
|
86
|
+
|
|
87
|
+
# Inspect the combined grammar
|
|
88
|
+
uv run python scripts/dev/inspect_grammar.py
|
|
89
|
+
|
|
90
|
+
# Validate a module implementation
|
|
91
|
+
uv run python scripts/dev/validate_module.py standard.postgresql
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Contributing
|
|
97
|
+
|
|
98
|
+
`uqal-core` is maintained by the core team. See [CONTRIBUTING](https://uqal-lang.github.io/uqal-docs/contributing) for the development workflow.
|
|
99
|
+
|
|
100
|
+
To contribute a new database module, head to [uqal-lang/uqal-modules](https://github.com/uqal-lang/uqal-modules).
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# uqal-core
|
|
2
|
+
|
|
3
|
+
The core engine of [UQAL](https://uqal-lang.github.io/uqal-docs) — a unified query abstraction layer that lets you write one query syntax and run it against any supported database.
|
|
4
|
+
|
|
5
|
+
`uqal-core` contains the parser, grammar builder, AST, module loader, and CLI. It is the runtime that all UQAL modules plug into.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## What is UQAL?
|
|
10
|
+
|
|
11
|
+
UQAL is a database-agnostic query language. Instead of writing SQL for PostgreSQL, Cypher for Neo4j, and MQL for MongoDB separately, you write UQAL once and the appropriate module translates it into the native query for the connected database.
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
mydb.users.get_table(where active = true, fields id, name)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
uv add uqal-core
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Requires Python 3.12+.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Start the interactive UQAL shell
|
|
33
|
+
uqal shell
|
|
34
|
+
|
|
35
|
+
# Run a query file
|
|
36
|
+
uqal run query.uqal
|
|
37
|
+
|
|
38
|
+
# List loaded modules
|
|
39
|
+
uqal list-modules
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Modules
|
|
45
|
+
|
|
46
|
+
Database support comes from modules. Standard modules ship with `uqal-core`. Community modules are installed separately:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
uv add uqal-postgis
|
|
50
|
+
uqal add-module community.postgis
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Browse available modules: [uqal-lang/uqal-modules](https://github.com/uqal-lang/uqal-modules)
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Development Setup
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
git clone https://github.com/uqal-lang/uqal-core
|
|
61
|
+
cd uqal-core
|
|
62
|
+
uv sync
|
|
63
|
+
uv run pytest
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Dev scripts are in `scripts/dev/`:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# Parse and translate a query
|
|
70
|
+
uv run python scripts/dev/check_query.py "mydb.users.get_table()"
|
|
71
|
+
|
|
72
|
+
# Inspect the combined grammar
|
|
73
|
+
uv run python scripts/dev/inspect_grammar.py
|
|
74
|
+
|
|
75
|
+
# Validate a module implementation
|
|
76
|
+
uv run python scripts/dev/validate_module.py standard.postgresql
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Contributing
|
|
82
|
+
|
|
83
|
+
`uqal-core` is maintained by the core team. See [CONTRIBUTING](https://uqal-lang.github.io/uqal-docs/contributing) for the development workflow.
|
|
84
|
+
|
|
85
|
+
To contribute a new database module, head to [uqal-lang/uqal-modules](https://github.com/uqal-lang/uqal-modules).
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
| Version | Supported |
|
|
6
|
+
|---------|-----------|
|
|
7
|
+
| latest (`main`) | ✓ |
|
|
8
|
+
| older releases | ✗ |
|
|
9
|
+
|
|
10
|
+
## Reporting a Vulnerability
|
|
11
|
+
|
|
12
|
+
**Do not open a public GitHub issue for security vulnerabilities.**
|
|
13
|
+
|
|
14
|
+
Use GitHub's private vulnerability reporting instead:
|
|
15
|
+
**[Report a vulnerability](https://github.com/uqal-lang/uqal-core/security/advisories/new)**
|
|
16
|
+
|
|
17
|
+
Please include:
|
|
18
|
+
- A description of the vulnerability
|
|
19
|
+
- Steps to reproduce
|
|
20
|
+
- Potential impact
|
|
21
|
+
- A suggested fix if you have one
|
|
22
|
+
|
|
23
|
+
We will respond within **72 hours** and aim to release a fix within **14 days** depending on severity.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uqal_hr.sql("SELECT table_name, column_name, data_type FROM information_schema.columns WHERE table_schema = 'public' ORDER BY table_name, ordinal_position")
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "uqal-core"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"click>=8.4.2",
|
|
9
|
+
"lark>=1.3.1",
|
|
10
|
+
"neo4j>=6.2.0",
|
|
11
|
+
"prompt-toolkit>=3.0.52",
|
|
12
|
+
"psycopg2-binary>=2.9.12",
|
|
13
|
+
"pymongo>=4.17.0",
|
|
14
|
+
"textual>=8.2.8",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[dependency-groups]
|
|
18
|
+
dev = [
|
|
19
|
+
"pytest>=9.1.1",
|
|
20
|
+
"pytest-cov>=6.0.0",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[build-system]
|
|
24
|
+
requires = ["hatchling"]
|
|
25
|
+
build-backend = "hatchling.build"
|
|
26
|
+
|
|
27
|
+
[tool.hatch.build.targets.wheel]
|
|
28
|
+
packages = ["src/uqal_core"]
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
uqal = "uqal_core.cli.main:cli"
|
|
32
|
+
|
|
33
|
+
[tool.pytest.ini_options]
|
|
34
|
+
addopts = "-v --tb=short --cov=src/uqal_core --cov-report=term-missing --cov-report=html:tests/coverage_html"
|
|
35
|
+
testpaths = ["tests"]
|
|
36
|
+
log_cli = true
|
|
37
|
+
log_cli_level = "INFO"
|
|
38
|
+
log_file = "tests/logs/test_results.log"
|
|
39
|
+
log_file_level = "INFO"
|
|
40
|
+
log_file_format = "%(asctime)s %(levelname)s %(message)s"
|
|
41
|
+
log_file_date_format = "%Y-%m-%d %H:%M:%S"
|
|
42
|
+
markers = [
|
|
43
|
+
"unit: fast isolated tests with no external dependencies",
|
|
44
|
+
"integration: tests combining multiple components",
|
|
45
|
+
"e2e: full end-to-end workflow tests",
|
|
46
|
+
]
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Parse and translate a UQAL query — shows the parse tree, AST nodes,
|
|
3
|
+
and the translated native query output.
|
|
4
|
+
|
|
5
|
+
Useful for testing module translation during development without
|
|
6
|
+
needing a real database connection.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
uv run python scripts/dev/check_query.py "mydb.users.get_table()"
|
|
10
|
+
uv run python scripts/dev/check_query.py "mydb.users.get_table(where active = true, fields id, name)"
|
|
11
|
+
uv run python scripts/dev/check_query.py "let x = 1 + 2 output x"
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def header(title: str) -> None:
|
|
23
|
+
print(f"\n\033[1;34m{'─' * 60}\033[0m")
|
|
24
|
+
print(f"\033[1;34m {title}\033[0m")
|
|
25
|
+
print(f"\033[1;34m{'─' * 60}\033[0m")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def check_query(query: str, module_names: list[str] | None = None) -> None:
|
|
29
|
+
from uqal_core.module_loader import ModuleLoader
|
|
30
|
+
from uqal_core.registry.module_registry import ModuleRegistry
|
|
31
|
+
from uqal_core.parser.grammar_builder import GrammarBuilder
|
|
32
|
+
from uqal_core.ast.transformer import UQALTransformer
|
|
33
|
+
|
|
34
|
+
modules_to_load = module_names or ["standard.postgresql", "standard.mongodb", "standard.neo4j"]
|
|
35
|
+
|
|
36
|
+
registry = ModuleRegistry()
|
|
37
|
+
loader = ModuleLoader(registry=registry)
|
|
38
|
+
loader.load(modules_to_load)
|
|
39
|
+
|
|
40
|
+
# Use all registered modules (includes transitive dependencies)
|
|
41
|
+
loaded_modules = [registry.get_module(m) for m in registry.list_modules()]
|
|
42
|
+
|
|
43
|
+
builder = GrammarBuilder()
|
|
44
|
+
parser = builder.build(loaded_modules)
|
|
45
|
+
transformer = UQALTransformer()
|
|
46
|
+
|
|
47
|
+
header("Input query")
|
|
48
|
+
print(f" {query}")
|
|
49
|
+
|
|
50
|
+
header("Parse tree")
|
|
51
|
+
try:
|
|
52
|
+
tree = parser.parse(query)
|
|
53
|
+
print(tree.pretty())
|
|
54
|
+
except Exception as e:
|
|
55
|
+
print(f"\033[91m ✗ Parse failed: {e}\033[0m")
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
header("AST nodes")
|
|
59
|
+
try:
|
|
60
|
+
program = transformer.transform(tree)
|
|
61
|
+
for i, stmt in enumerate(program.statements):
|
|
62
|
+
print(f" [{i}] {type(stmt).__name__}: {stmt}")
|
|
63
|
+
except Exception as e:
|
|
64
|
+
print(f"\033[91m ✗ Transform failed: {e}\033[0m")
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
header("Translation")
|
|
68
|
+
try:
|
|
69
|
+
for i, stmt in enumerate(program.statements):
|
|
70
|
+
# Find the module that handles this statement
|
|
71
|
+
for module in loaded_modules:
|
|
72
|
+
try:
|
|
73
|
+
result = module.translate(stmt)
|
|
74
|
+
if result is not None:
|
|
75
|
+
query_str, params = result
|
|
76
|
+
print(f" Module: {module.get_manifest().name}")
|
|
77
|
+
print(f" Query: {query_str}")
|
|
78
|
+
if params:
|
|
79
|
+
print(f" Params: {params}")
|
|
80
|
+
break
|
|
81
|
+
except (NotImplementedError, AttributeError):
|
|
82
|
+
continue
|
|
83
|
+
except Exception as e:
|
|
84
|
+
print(f" (translation not available for this statement type: {e})")
|
|
85
|
+
|
|
86
|
+
print()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
if __name__ == "__main__":
|
|
90
|
+
if len(sys.argv) < 2:
|
|
91
|
+
print("Usage: uv run python scripts/dev/check_query.py \"<query>\"")
|
|
92
|
+
print(" uv run python scripts/dev/check_query.py \"<query>\" --module standard.neo4j")
|
|
93
|
+
sys.exit(1)
|
|
94
|
+
|
|
95
|
+
query = sys.argv[1]
|
|
96
|
+
modules = None
|
|
97
|
+
|
|
98
|
+
if "--module" in sys.argv:
|
|
99
|
+
idx = sys.argv.index("--module")
|
|
100
|
+
modules = sys.argv[idx + 1:]
|
|
101
|
+
|
|
102
|
+
check_query(query, modules)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Print the complete combined UQAL grammar for all (or selected) modules.
|
|
3
|
+
Useful for debugging grammar rules, extension points, and conflicts.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
uv run python scripts/dev/inspect_grammar.py
|
|
7
|
+
uv run python scripts/dev/inspect_grammar.py standard.neo4j
|
|
8
|
+
uv run python scripts/dev/inspect_grammar.py standard.postgresql standard.neo4j
|
|
9
|
+
uv run python scripts/dev/inspect_grammar.py --extension-points
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
|
|
18
|
+
|
|
19
|
+
_EXTENSION_POINTS = [
|
|
20
|
+
"condition",
|
|
21
|
+
"expr",
|
|
22
|
+
"statement",
|
|
23
|
+
"db_call",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def inspect_grammar(module_names: list[str], show_extension_points: bool = False) -> None:
|
|
28
|
+
from uqal_core.module_loader import ModuleLoader
|
|
29
|
+
from uqal_core.registry.module_registry import ModuleRegistry
|
|
30
|
+
from uqal_core.parser.grammar_builder import GrammarBuilder
|
|
31
|
+
|
|
32
|
+
registry = ModuleRegistry()
|
|
33
|
+
loader = ModuleLoader(registry=registry)
|
|
34
|
+
loader.load(module_names)
|
|
35
|
+
loaded = [registry.get_module(m) for m in module_names if registry.get_module(m)]
|
|
36
|
+
|
|
37
|
+
builder = GrammarBuilder()
|
|
38
|
+
|
|
39
|
+
if show_extension_points:
|
|
40
|
+
print("\n\033[1;34mExtension points and their contributors:\033[0m\n")
|
|
41
|
+
for point in _EXTENSION_POINTS:
|
|
42
|
+
contributors = []
|
|
43
|
+
for module in loaded:
|
|
44
|
+
caps = module.get_capabilities()
|
|
45
|
+
exts = getattr(caps, "grammar_extensions", {})
|
|
46
|
+
rules = exts.get(point, [])
|
|
47
|
+
if rules:
|
|
48
|
+
contributors.append(f"{module.get_manifest().name}: {rules}")
|
|
49
|
+
if contributors:
|
|
50
|
+
print(f" \033[33m{point}\033[0m")
|
|
51
|
+
for c in contributors:
|
|
52
|
+
print(f" → {c}")
|
|
53
|
+
else:
|
|
54
|
+
print(f" \033[90m{point} (no extensions)\033[0m")
|
|
55
|
+
print()
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
print(f"\n\033[1;34mLoaded modules: {[m.get_manifest().name for m in loaded]}\033[0m\n")
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
grammar_text = builder.get_combined_grammar(loaded)
|
|
62
|
+
print(grammar_text)
|
|
63
|
+
except AttributeError:
|
|
64
|
+
# Fallback: build and show what we can
|
|
65
|
+
parser = builder.build(loaded)
|
|
66
|
+
print("(Grammar built successfully — source text not directly accessible)")
|
|
67
|
+
print(f"\nParser type: {type(parser)}")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
if __name__ == "__main__":
|
|
71
|
+
args = sys.argv[1:]
|
|
72
|
+
|
|
73
|
+
show_ext_points = "--extension-points" in args
|
|
74
|
+
if show_ext_points:
|
|
75
|
+
args = [a for a in args if a != "--extension-points"]
|
|
76
|
+
|
|
77
|
+
if not args:
|
|
78
|
+
args = ["standard.postgresql", "standard.mongodb", "standard.neo4j"]
|
|
79
|
+
|
|
80
|
+
inspect_grammar(args, show_extension_points=show_ext_points)
|