pytrilogy 0.0.3.96__tar.gz → 0.0.3.98__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.
Potentially problematic release.
This version of pytrilogy might be problematic. Click here for more details.
- {pytrilogy-0.0.3.96/pytrilogy.egg-info → pytrilogy-0.0.3.98}/PKG-INFO +16 -2
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/README.md +15 -1
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98/pytrilogy.egg-info}/PKG-INFO +16 -2
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/pytrilogy.egg-info/SOURCES.txt +2 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_parsing.py +49 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/__init__.py +1 -1
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/constants.py +1 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/enums.py +9 -0
- pytrilogy-0.0.3.98/trilogy/core/exceptions.py +105 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/functions.py +1 -1
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/models/execute.py +0 -1
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/node_generators/node_merge_node.py +109 -5
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/validation/common.py +53 -2
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/validation/concept.py +25 -4
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/validation/datasource.py +22 -20
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/validation/environment.py +3 -1
- pytrilogy-0.0.3.98/trilogy/core/validation/fix.py +106 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/dialect/base.py +2 -1
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/dialect/common.py +2 -10
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/dialect/metadata.py +1 -1
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/executor.py +7 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/parsing/parse_engine.py +10 -8
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/parsing/render.py +32 -5
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/parsing/trilogy.lark +7 -4
- pytrilogy-0.0.3.98/trilogy/std/metric.preql +15 -0
- pytrilogy-0.0.3.96/trilogy/core/exceptions.py +0 -51
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/LICENSE.md +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/pyproject.toml +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/pytrilogy.egg-info/dependency_links.txt +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/pytrilogy.egg-info/entry_points.txt +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/pytrilogy.egg-info/requires.txt +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/pytrilogy.egg-info/top_level.txt +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/setup.cfg +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/setup.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_datatypes.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_declarations.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_derived_concepts.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_discovery_nodes.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_enums.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_environment.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_execute_models.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_executor.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_failure.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_functions.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_imports.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_metadata.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_models.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_multi_join_assignments.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_parse_engine.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_parsing_failures.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_partial_handling.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_query_processing.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_query_render.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_select.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_show.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_statements.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_typing.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_undefined_concept.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_user_functions.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_where_clause.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/authoring/__init__.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/__init__.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/constants.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/env_processor.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/environment_helpers.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/ergonomics.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/graph_models.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/internal.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/models/__init__.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/models/author.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/models/build.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/models/build_environment.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/models/core.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/models/datasource.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/models/environment.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/optimization.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/optimizations/__init__.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/optimizations/base_optimization.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/optimizations/inline_datasource.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/optimizations/predicate_pushdown.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/__init__.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/concept_strategies_v3.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/discovery_loop.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/discovery_node_factory.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/discovery_utility.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/discovery_validation.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/graph_utils.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/node_generators/__init__.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/node_generators/basic_node.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/node_generators/common.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/node_generators/constant_node.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/node_generators/filter_node.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/node_generators/group_node.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/node_generators/group_to_node.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/node_generators/multiselect_node.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/node_generators/recursive_node.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/node_generators/rowset_node.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/node_generators/select_merge_node.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/node_generators/select_node.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/node_generators/synonym_node.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/node_generators/union_node.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/node_generators/unnest_node.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/node_generators/window_node.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/nodes/__init__.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/nodes/base_node.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/nodes/filter_node.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/nodes/group_node.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/nodes/merge_node.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/nodes/recursive_node.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/nodes/select_node_v2.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/nodes/union_node.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/nodes/unnest_node.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/nodes/window_node.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/utility.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/query_processor.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/statements/__init__.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/statements/author.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/statements/build.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/statements/common.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/statements/execute.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/utility.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/validation/__init__.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/dialect/__init__.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/dialect/bigquery.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/dialect/config.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/dialect/dataframe.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/dialect/duckdb.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/dialect/enums.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/dialect/postgres.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/dialect/presto.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/dialect/snowflake.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/dialect/sql_server.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/engine.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/hooks/__init__.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/hooks/base_hook.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/hooks/graph_hook.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/hooks/query_debugger.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/metadata/__init__.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/parser.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/parsing/__init__.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/parsing/common.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/parsing/config.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/parsing/exceptions.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/parsing/helpers.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/py.typed +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/render.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/scripts/__init__.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/scripts/trilogy.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/std/__init__.py +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/std/date.preql +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/std/display.preql +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/std/geography.preql +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/std/money.preql +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/std/net.preql +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/std/ranking.preql +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/std/report.preql +0 -0
- {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/utility.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytrilogy
|
|
3
|
-
Version: 0.0.3.
|
|
3
|
+
Version: 0.0.3.98
|
|
4
4
|
Summary: Declarative, typed query language that compiles to SQL.
|
|
5
5
|
Home-page:
|
|
6
6
|
Author:
|
|
@@ -334,7 +334,21 @@ from pytrilogy.authoring import Concept, Function, ...
|
|
|
334
334
|
|
|
335
335
|
Are likely to be unstable. Open an issue if you need to take dependencies on other modules outside those two paths.
|
|
336
336
|
|
|
337
|
-
##
|
|
337
|
+
## MCP/Server
|
|
338
|
+
|
|
339
|
+
Trilogy is straightforward to run as a server/MCP server; the former to generate SQL on demand and integrate into other tools, and MCP
|
|
340
|
+
for full interactive query loops.
|
|
341
|
+
|
|
342
|
+
This makes it easy to integrate Trilogy into existing tools or workflows.
|
|
343
|
+
|
|
344
|
+
You can see examples of both use cases in the trilogy-studio codebase [here](https://github.com/trilogy-data/trilogy-studio-core)
|
|
345
|
+
and install and run an MCP server directly with that codebase.
|
|
346
|
+
|
|
347
|
+
If you're interested in a more fleshed out standalone server or MCP server, please open an issue and we'll prioritize it!
|
|
348
|
+
|
|
349
|
+
## Trilogy Syntax Reference
|
|
350
|
+
|
|
351
|
+
Not exhaustive - see [documentation](https://trilogydata.dev/) for more details.
|
|
338
352
|
|
|
339
353
|
### Import
|
|
340
354
|
```sql
|
|
@@ -296,7 +296,21 @@ from pytrilogy.authoring import Concept, Function, ...
|
|
|
296
296
|
|
|
297
297
|
Are likely to be unstable. Open an issue if you need to take dependencies on other modules outside those two paths.
|
|
298
298
|
|
|
299
|
-
##
|
|
299
|
+
## MCP/Server
|
|
300
|
+
|
|
301
|
+
Trilogy is straightforward to run as a server/MCP server; the former to generate SQL on demand and integrate into other tools, and MCP
|
|
302
|
+
for full interactive query loops.
|
|
303
|
+
|
|
304
|
+
This makes it easy to integrate Trilogy into existing tools or workflows.
|
|
305
|
+
|
|
306
|
+
You can see examples of both use cases in the trilogy-studio codebase [here](https://github.com/trilogy-data/trilogy-studio-core)
|
|
307
|
+
and install and run an MCP server directly with that codebase.
|
|
308
|
+
|
|
309
|
+
If you're interested in a more fleshed out standalone server or MCP server, please open an issue and we'll prioritize it!
|
|
310
|
+
|
|
311
|
+
## Trilogy Syntax Reference
|
|
312
|
+
|
|
313
|
+
Not exhaustive - see [documentation](https://trilogydata.dev/) for more details.
|
|
300
314
|
|
|
301
315
|
### Import
|
|
302
316
|
```sql
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytrilogy
|
|
3
|
-
Version: 0.0.3.
|
|
3
|
+
Version: 0.0.3.98
|
|
4
4
|
Summary: Declarative, typed query language that compiles to SQL.
|
|
5
5
|
Home-page:
|
|
6
6
|
Author:
|
|
@@ -334,7 +334,21 @@ from pytrilogy.authoring import Concept, Function, ...
|
|
|
334
334
|
|
|
335
335
|
Are likely to be unstable. Open an issue if you need to take dependencies on other modules outside those two paths.
|
|
336
336
|
|
|
337
|
-
##
|
|
337
|
+
## MCP/Server
|
|
338
|
+
|
|
339
|
+
Trilogy is straightforward to run as a server/MCP server; the former to generate SQL on demand and integrate into other tools, and MCP
|
|
340
|
+
for full interactive query loops.
|
|
341
|
+
|
|
342
|
+
This makes it easy to integrate Trilogy into existing tools or workflows.
|
|
343
|
+
|
|
344
|
+
You can see examples of both use cases in the trilogy-studio codebase [here](https://github.com/trilogy-data/trilogy-studio-core)
|
|
345
|
+
and install and run an MCP server directly with that codebase.
|
|
346
|
+
|
|
347
|
+
If you're interested in a more fleshed out standalone server or MCP server, please open an issue and we'll prioritize it!
|
|
348
|
+
|
|
349
|
+
## Trilogy Syntax Reference
|
|
350
|
+
|
|
351
|
+
Not exhaustive - see [documentation](https://trilogydata.dev/) for more details.
|
|
338
352
|
|
|
339
353
|
### Import
|
|
340
354
|
```sql
|
|
@@ -116,6 +116,7 @@ trilogy/core/validation/common.py
|
|
|
116
116
|
trilogy/core/validation/concept.py
|
|
117
117
|
trilogy/core/validation/datasource.py
|
|
118
118
|
trilogy/core/validation/environment.py
|
|
119
|
+
trilogy/core/validation/fix.py
|
|
119
120
|
trilogy/dialect/__init__.py
|
|
120
121
|
trilogy/dialect/base.py
|
|
121
122
|
trilogy/dialect/bigquery.py
|
|
@@ -148,6 +149,7 @@ trilogy/std/__init__.py
|
|
|
148
149
|
trilogy/std/date.preql
|
|
149
150
|
trilogy/std/display.preql
|
|
150
151
|
trilogy/std/geography.preql
|
|
152
|
+
trilogy/std/metric.preql
|
|
151
153
|
trilogy/std/money.preql
|
|
152
154
|
trilogy/std/net.preql
|
|
153
155
|
trilogy/std/ranking.preql
|
|
@@ -321,6 +321,55 @@ def test_the_comments():
|
|
|
321
321
|
assert rendered == "null"
|
|
322
322
|
|
|
323
323
|
|
|
324
|
+
def test_the_comment_multiline():
|
|
325
|
+
env, parsed = parse_text(
|
|
326
|
+
"""const
|
|
327
|
+
# comment here?
|
|
328
|
+
order_id <- 4; # this is the order id
|
|
329
|
+
# order ids are important
|
|
330
|
+
|
|
331
|
+
SELECT
|
|
332
|
+
# TOOD - add in more columns?
|
|
333
|
+
order_id
|
|
334
|
+
WHERE
|
|
335
|
+
# order_id should not be null
|
|
336
|
+
order_id
|
|
337
|
+
# in this comp
|
|
338
|
+
is not
|
|
339
|
+
null; # nulls are the worst
|
|
340
|
+
|
|
341
|
+
"""
|
|
342
|
+
)
|
|
343
|
+
parsed[-1]
|
|
344
|
+
assert env.concepts["order_id"].metadata.description is not None
|
|
345
|
+
assert " this is the order id" in env.concepts["order_id"].metadata.description
|
|
346
|
+
assert " order ids are important" in env.concepts["order_id"].metadata.description
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def test_the_comment_multiline_enter():
|
|
350
|
+
# we should not associate it as a description if there is a newline
|
|
351
|
+
env, parsed = parse_text(
|
|
352
|
+
"""const
|
|
353
|
+
# comment here?
|
|
354
|
+
order_id <- 4;
|
|
355
|
+
# this is the order id
|
|
356
|
+
# order ids are important
|
|
357
|
+
|
|
358
|
+
SELECT
|
|
359
|
+
# TOOD - add in more columns?
|
|
360
|
+
order_id
|
|
361
|
+
WHERE
|
|
362
|
+
# order_id should not be null
|
|
363
|
+
order_id
|
|
364
|
+
# in this comp
|
|
365
|
+
is not
|
|
366
|
+
null; # nulls are the worst
|
|
367
|
+
|
|
368
|
+
"""
|
|
369
|
+
)
|
|
370
|
+
assert env.concepts["order_id"].metadata.description is None
|
|
371
|
+
|
|
372
|
+
|
|
324
373
|
def test_purpose_nesting():
|
|
325
374
|
env, parsed = parse_text(
|
|
326
375
|
"""key year int;
|
|
@@ -82,6 +82,15 @@ class Modifier(Enum):
|
|
|
82
82
|
return Modifier.NULLABLE
|
|
83
83
|
return super()._missing_(value=strval.capitalize())
|
|
84
84
|
|
|
85
|
+
def __lt__(self, other):
|
|
86
|
+
order = [
|
|
87
|
+
Modifier.HIDDEN,
|
|
88
|
+
Modifier.PARTIAL,
|
|
89
|
+
Modifier.NULLABLE,
|
|
90
|
+
Modifier.OPTIONAL,
|
|
91
|
+
]
|
|
92
|
+
return order.index(self) < order.index(other)
|
|
93
|
+
|
|
85
94
|
|
|
86
95
|
class JoinType(Enum):
|
|
87
96
|
INNER = "inner"
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any, List, Sequence
|
|
3
|
+
|
|
4
|
+
from trilogy.core.enums import Modifier
|
|
5
|
+
from trilogy.core.models.core import (
|
|
6
|
+
ArrayType,
|
|
7
|
+
DataType,
|
|
8
|
+
MapType,
|
|
9
|
+
NumericType,
|
|
10
|
+
StructType,
|
|
11
|
+
TraitDataType,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class UndefinedConceptException(Exception):
|
|
16
|
+
def __init__(self, message, suggestions: List[str]):
|
|
17
|
+
super().__init__(self, message)
|
|
18
|
+
self.message = message
|
|
19
|
+
self.suggestions = suggestions
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FrozenEnvironmentException(Exception):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class InvalidSyntaxException(Exception):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class UnresolvableQueryException(Exception):
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class NoDatasourceException(UnresolvableQueryException):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ModelValidationError(Exception):
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
message,
|
|
42
|
+
children: Sequence["ModelValidationError"] | None = None,
|
|
43
|
+
**kwargs,
|
|
44
|
+
):
|
|
45
|
+
super().__init__(self, message, **kwargs)
|
|
46
|
+
self.message = message
|
|
47
|
+
self.children = children
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class DatasourceModelValidationError(ModelValidationError):
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class DatasourceGrainValidationError(DatasourceModelValidationError):
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class DatasourceColumnBindingData:
|
|
60
|
+
address: str
|
|
61
|
+
value: Any
|
|
62
|
+
value_type: (
|
|
63
|
+
DataType | ArrayType | StructType | MapType | NumericType | TraitDataType
|
|
64
|
+
)
|
|
65
|
+
value_modifiers: List[Modifier]
|
|
66
|
+
actual_type: (
|
|
67
|
+
DataType | ArrayType | StructType | MapType | NumericType | TraitDataType
|
|
68
|
+
)
|
|
69
|
+
actual_modifiers: List[Modifier]
|
|
70
|
+
|
|
71
|
+
def format_failure(self):
|
|
72
|
+
return f"Concept {self.address} value '{self.value}' with type {self.value_modifiers} does not conform to expected type {str(self.actual_type)} with modifiers {self.actual_modifiers}"
|
|
73
|
+
|
|
74
|
+
def is_modifier_issue(self) -> bool:
|
|
75
|
+
return len(self.value_modifiers) > 0 and any(
|
|
76
|
+
[x not in self.actual_modifiers for x in self.value_modifiers]
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def is_type_issue(self) -> bool:
|
|
80
|
+
return self.value_type != self.actual_type
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class DatasourceColumnBindingError(DatasourceModelValidationError):
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
address: str,
|
|
87
|
+
errors: list[DatasourceColumnBindingData],
|
|
88
|
+
message: str | None = None,
|
|
89
|
+
):
|
|
90
|
+
if not message:
|
|
91
|
+
message = f"Datasource {address} failed validation. Found rows that do not conform to types: {[failure.format_failure() for failure in errors]}"
|
|
92
|
+
super().__init__(message)
|
|
93
|
+
self.errors = errors
|
|
94
|
+
self.dataset_address = address
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class ConceptModelValidationError(ModelValidationError):
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class AmbiguousRelationshipResolutionException(UnresolvableQueryException):
|
|
102
|
+
def __init__(self, message, parents: List[set[str]]):
|
|
103
|
+
super().__init__(self, message)
|
|
104
|
+
self.message = message
|
|
105
|
+
self.parents = parents
|
|
@@ -427,7 +427,7 @@ FUNCTION_REGISTRY: dict[FunctionType, FunctionConfig] = {
|
|
|
427
427
|
{DataType.STRING},
|
|
428
428
|
],
|
|
429
429
|
output_purpose=Purpose.PROPERTY,
|
|
430
|
-
output_type=DataType.
|
|
430
|
+
output_type=DataType.STRING,
|
|
431
431
|
arg_count=2,
|
|
432
432
|
),
|
|
433
433
|
FunctionType.SUBSTRING: FunctionConfig(
|
{pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/node_generators/node_merge_node.py
RENAMED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
from
|
|
1
|
+
from itertools import combinations
|
|
2
|
+
from typing import Callable, List, Optional
|
|
2
3
|
|
|
3
4
|
import networkx as nx
|
|
4
5
|
from networkx.algorithms import approximation as ax
|
|
@@ -11,7 +12,12 @@ from trilogy.core.graph_models import (
|
|
|
11
12
|
concept_to_node,
|
|
12
13
|
prune_sources_for_conditions,
|
|
13
14
|
)
|
|
14
|
-
from trilogy.core.models.build import
|
|
15
|
+
from trilogy.core.models.build import (
|
|
16
|
+
BuildConcept,
|
|
17
|
+
BuildConditional,
|
|
18
|
+
BuildGrain,
|
|
19
|
+
BuildWhereClause,
|
|
20
|
+
)
|
|
15
21
|
from trilogy.core.models.build_environment import BuildEnvironment
|
|
16
22
|
from trilogy.core.processing.nodes import History, MergeNode, StrategyNode
|
|
17
23
|
from trilogy.core.processing.utility import padding
|
|
@@ -29,7 +35,10 @@ def filter_pseudonyms_for_source(
|
|
|
29
35
|
if edge in pseudonyms:
|
|
30
36
|
lengths = {}
|
|
31
37
|
for n in edge:
|
|
32
|
-
|
|
38
|
+
try:
|
|
39
|
+
lengths[n] = nx.shortest_path_length(ds_graph, node, n)
|
|
40
|
+
except nx.NetworkXNoPath:
|
|
41
|
+
lengths[n] = 999
|
|
33
42
|
to_remove.add(max(lengths, key=lambda x: lengths.get(x, 0)))
|
|
34
43
|
for node in to_remove:
|
|
35
44
|
ds_graph.remove_node(node)
|
|
@@ -84,12 +93,104 @@ def extract_ds_components(
|
|
|
84
93
|
return graphs
|
|
85
94
|
|
|
86
95
|
|
|
96
|
+
def prune_and_merge(
|
|
97
|
+
G: nx.DiGraph,
|
|
98
|
+
keep_node_lambda: Callable[[str], bool],
|
|
99
|
+
) -> nx.DiGraph:
|
|
100
|
+
"""
|
|
101
|
+
Prune nodes of one type and create direct connections between remaining nodes.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
G: NetworkX graph
|
|
105
|
+
keep_node_type: The node type to keep
|
|
106
|
+
node_type_attr: Attribute name that stores node type
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
New graph with only keep_node_type nodes and merged connections
|
|
110
|
+
"""
|
|
111
|
+
# Get nodes to keep
|
|
112
|
+
nodes_to_keep = [n for n in G.nodes if keep_node_lambda(n)]
|
|
113
|
+
# Create new graph
|
|
114
|
+
new_graph = G.subgraph(nodes_to_keep).copy()
|
|
115
|
+
|
|
116
|
+
# Find paths between nodes to keep through removed nodes
|
|
117
|
+
nodes_to_remove = [n for n in G.nodes() if n not in nodes_to_keep]
|
|
118
|
+
|
|
119
|
+
for node_pair in combinations(nodes_to_keep, 2):
|
|
120
|
+
n1, n2 = node_pair
|
|
121
|
+
|
|
122
|
+
# Check if there's a path through removed nodes
|
|
123
|
+
try:
|
|
124
|
+
path = nx.shortest_path(G, n1, n2)
|
|
125
|
+
# If path exists and goes through nodes we're removing
|
|
126
|
+
if len(path) > 2 or any(node in nodes_to_remove for node in path[1:-1]):
|
|
127
|
+
new_graph.add_edge(n1, n2)
|
|
128
|
+
except nx.NetworkXNoPath:
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
return new_graph
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def reinject_common_join_keys_v2(
|
|
135
|
+
G: ReferenceGraph,
|
|
136
|
+
final: nx.DiGraph,
|
|
137
|
+
nodelist: list[str],
|
|
138
|
+
synonyms: set[str] = set(),
|
|
139
|
+
) -> bool:
|
|
140
|
+
# when we've discovered a concept join, for each pair of ds nodes
|
|
141
|
+
# check if they have more keys in common
|
|
142
|
+
# and inject those to discovery as join conditions
|
|
143
|
+
def is_ds_node(n: str) -> bool:
|
|
144
|
+
return n.startswith("ds~")
|
|
145
|
+
|
|
146
|
+
ds_graph = prune_and_merge(final, is_ds_node)
|
|
147
|
+
injected = False
|
|
148
|
+
for datasource in ds_graph.nodes:
|
|
149
|
+
node1 = G.datasources[datasource]
|
|
150
|
+
neighbors = nx.all_neighbors(ds_graph, datasource)
|
|
151
|
+
for neighbor in neighbors:
|
|
152
|
+
node2 = G.datasources[neighbor]
|
|
153
|
+
common_concepts = set(
|
|
154
|
+
x.concept.address for x in node1.columns
|
|
155
|
+
).intersection(set(x.concept.address for x in node2.columns))
|
|
156
|
+
concrete_concepts = [
|
|
157
|
+
x.concept for x in node1.columns if x.concept.address in common_concepts
|
|
158
|
+
]
|
|
159
|
+
reduced = BuildGrain.from_concepts(concrete_concepts).components
|
|
160
|
+
existing_addresses = set()
|
|
161
|
+
for concrete in concrete_concepts:
|
|
162
|
+
logger.info(
|
|
163
|
+
f"looking at column {concrete.address} with pseudonyms {concrete.pseudonyms}"
|
|
164
|
+
)
|
|
165
|
+
cnode = concept_to_node(concrete.with_default_grain())
|
|
166
|
+
if cnode in final.nodes:
|
|
167
|
+
existing_addresses.add(concrete.address)
|
|
168
|
+
continue
|
|
169
|
+
for concrete in concrete_concepts:
|
|
170
|
+
if concrete.address in synonyms:
|
|
171
|
+
continue
|
|
172
|
+
if concrete.address not in reduced:
|
|
173
|
+
continue
|
|
174
|
+
# skip anything that is already in the graph pseudonyms
|
|
175
|
+
if any(x in concrete.pseudonyms for x in existing_addresses):
|
|
176
|
+
continue
|
|
177
|
+
cnode = concept_to_node(concrete.with_default_grain())
|
|
178
|
+
final.add_edge(datasource, cnode)
|
|
179
|
+
final.add_edge(neighbor, cnode)
|
|
180
|
+
logger.info(
|
|
181
|
+
f"{LOGGER_PREFIX} reinjecting common join key {cnode} between {datasource} and {neighbor}"
|
|
182
|
+
)
|
|
183
|
+
injected = True
|
|
184
|
+
return injected
|
|
185
|
+
|
|
186
|
+
|
|
87
187
|
def determine_induced_minimal_nodes(
|
|
88
188
|
G: ReferenceGraph,
|
|
89
189
|
nodelist: list[str],
|
|
90
190
|
environment: BuildEnvironment,
|
|
91
191
|
filter_downstream: bool,
|
|
92
192
|
accept_partial: bool = False,
|
|
193
|
+
synonyms: set[str] = set(),
|
|
93
194
|
) -> nx.DiGraph | None:
|
|
94
195
|
H: nx.Graph = nx.to_undirected(G).copy()
|
|
95
196
|
nodes_to_remove = []
|
|
@@ -129,7 +230,7 @@ def determine_induced_minimal_nodes(
|
|
|
129
230
|
return None
|
|
130
231
|
path_removals = list(x for x in H.nodes if x not in paths)
|
|
131
232
|
if path_removals:
|
|
132
|
-
logger.debug(f"Removing paths {path_removals} from graph")
|
|
233
|
+
# logger.debug(f"Removing paths {path_removals} from graph")
|
|
133
234
|
H.remove_nodes_from(path_removals)
|
|
134
235
|
# logger.debug(f"Graph after path removal {H.nodes}")
|
|
135
236
|
sG: nx.Graph = ax.steinertree.steiner_tree(H, nodelist).copy()
|
|
@@ -148,8 +249,10 @@ def determine_induced_minimal_nodes(
|
|
|
148
249
|
if not accept_partial:
|
|
149
250
|
continue
|
|
150
251
|
final.add_edge(*edge)
|
|
151
|
-
# all concept nodes must have a parent
|
|
152
252
|
|
|
253
|
+
reinject_common_join_keys_v2(G, final, nodelist, synonyms)
|
|
254
|
+
|
|
255
|
+
# all concept nodes must have a parent
|
|
153
256
|
if not all(
|
|
154
257
|
[
|
|
155
258
|
final.in_degree(node) > 0
|
|
@@ -302,6 +405,7 @@ def resolve_weak_components(
|
|
|
302
405
|
filter_downstream=filter_downstream,
|
|
303
406
|
accept_partial=accept_partial,
|
|
304
407
|
environment=environment,
|
|
408
|
+
synonyms=synonyms,
|
|
305
409
|
)
|
|
306
410
|
|
|
307
411
|
if not g or not g.nodes:
|
|
@@ -2,13 +2,25 @@ from dataclasses import dataclass
|
|
|
2
2
|
from enum import Enum
|
|
3
3
|
|
|
4
4
|
from trilogy import Environment
|
|
5
|
-
from trilogy.authoring import
|
|
5
|
+
from trilogy.authoring import (
|
|
6
|
+
ConceptRef,
|
|
7
|
+
DataType,
|
|
8
|
+
Ordering,
|
|
9
|
+
Purpose,
|
|
10
|
+
)
|
|
11
|
+
from trilogy.constants import MagicConstants
|
|
12
|
+
from trilogy.core.enums import ComparisonOperator, FunctionType
|
|
6
13
|
from trilogy.core.exceptions import ModelValidationError
|
|
7
14
|
from trilogy.core.models.build import (
|
|
15
|
+
BuildCaseElse,
|
|
16
|
+
BuildCaseWhen,
|
|
8
17
|
BuildComparison,
|
|
9
18
|
BuildConcept,
|
|
10
19
|
BuildConditional,
|
|
11
20
|
BuildDatasource,
|
|
21
|
+
BuildFunction,
|
|
22
|
+
BuildOrderBy,
|
|
23
|
+
BuildOrderItem,
|
|
12
24
|
)
|
|
13
25
|
from trilogy.core.models.environment import EnvironmentConceptDict
|
|
14
26
|
from trilogy.core.models.execute import (
|
|
@@ -39,6 +51,32 @@ class ValidationType(Enum):
|
|
|
39
51
|
CONCEPTS = "concepts"
|
|
40
52
|
|
|
41
53
|
|
|
54
|
+
def build_order_args(concepts: list[BuildConcept]) -> list[BuildFunction]:
|
|
55
|
+
order_args = []
|
|
56
|
+
for concept in concepts:
|
|
57
|
+
order_args.append(
|
|
58
|
+
BuildFunction(
|
|
59
|
+
operator=FunctionType.CASE,
|
|
60
|
+
arguments=[
|
|
61
|
+
BuildCaseWhen(
|
|
62
|
+
comparison=BuildComparison(
|
|
63
|
+
left=concept,
|
|
64
|
+
operator=ComparisonOperator.IS,
|
|
65
|
+
right=MagicConstants.NULL,
|
|
66
|
+
),
|
|
67
|
+
expr=1,
|
|
68
|
+
),
|
|
69
|
+
BuildCaseElse(expr=0),
|
|
70
|
+
],
|
|
71
|
+
output_data_type=DataType.INTEGER,
|
|
72
|
+
output_purpose=Purpose.PROPERTY,
|
|
73
|
+
arg_count=2,
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return order_args
|
|
78
|
+
|
|
79
|
+
|
|
42
80
|
def easy_query(
|
|
43
81
|
concepts: list[BuildConcept],
|
|
44
82
|
datasource: BuildDatasource,
|
|
@@ -81,7 +119,6 @@ def easy_query(
|
|
|
81
119
|
group_to_grain=True,
|
|
82
120
|
base_alias_override=datasource.safe_identifier,
|
|
83
121
|
)
|
|
84
|
-
|
|
85
122
|
filter_cte = CTE(
|
|
86
123
|
name=f"datasource_{datasource.name}_filter",
|
|
87
124
|
source=QueryDatasource(
|
|
@@ -100,6 +137,20 @@ def easy_query(
|
|
|
100
137
|
grain=cte.grain,
|
|
101
138
|
condition=condition,
|
|
102
139
|
limit=limit,
|
|
140
|
+
order_by=BuildOrderBy(
|
|
141
|
+
items=[
|
|
142
|
+
BuildOrderItem(
|
|
143
|
+
expr=BuildFunction(
|
|
144
|
+
operator=FunctionType.SUM,
|
|
145
|
+
arguments=build_order_args(concepts),
|
|
146
|
+
output_data_type=DataType.INTEGER,
|
|
147
|
+
output_purpose=Purpose.PROPERTY,
|
|
148
|
+
arg_count=len(concepts),
|
|
149
|
+
),
|
|
150
|
+
order=Ordering.DESCENDING,
|
|
151
|
+
)
|
|
152
|
+
]
|
|
153
|
+
),
|
|
103
154
|
)
|
|
104
155
|
|
|
105
156
|
return ProcessedQuery(
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from trilogy import Environment, Executor
|
|
2
|
-
from trilogy.core.enums import Derivation, Purpose
|
|
2
|
+
from trilogy.core.enums import Derivation, Modifier, Purpose
|
|
3
3
|
from trilogy.core.exceptions import (
|
|
4
4
|
ConceptModelValidationError,
|
|
5
|
-
|
|
5
|
+
DatasourceColumnBindingData,
|
|
6
|
+
DatasourceColumnBindingError,
|
|
6
7
|
)
|
|
7
8
|
from trilogy.core.models.build import (
|
|
8
9
|
BuildConcept,
|
|
@@ -25,6 +26,15 @@ def validate_key_concept(
|
|
|
25
26
|
):
|
|
26
27
|
results: list[ValidationTest] = []
|
|
27
28
|
seen: dict[str, int] = {}
|
|
29
|
+
|
|
30
|
+
count = 0
|
|
31
|
+
for datasource in build_env.datasources.values():
|
|
32
|
+
if concept.address in [c.address for c in datasource.concepts]:
|
|
33
|
+
count += 1
|
|
34
|
+
# if it only has one source, it's a key
|
|
35
|
+
if count <= 1:
|
|
36
|
+
return results
|
|
37
|
+
|
|
28
38
|
for datasource in build_env.datasources.values():
|
|
29
39
|
if concept.address in [c.address for c in datasource.concepts]:
|
|
30
40
|
assignment = [
|
|
@@ -69,8 +79,19 @@ def validate_key_concept(
|
|
|
69
79
|
err = None
|
|
70
80
|
datasource_count: int = seen.get(datasource.name, 0)
|
|
71
81
|
if datasource_count < max_seen and assignment.is_complete:
|
|
72
|
-
err =
|
|
73
|
-
|
|
82
|
+
err = DatasourceColumnBindingError(
|
|
83
|
+
address=datasource.identifier,
|
|
84
|
+
errors=[
|
|
85
|
+
DatasourceColumnBindingData(
|
|
86
|
+
address=concept.address,
|
|
87
|
+
value=None,
|
|
88
|
+
value_type=concept.datatype,
|
|
89
|
+
value_modifiers=[Modifier.PARTIAL],
|
|
90
|
+
actual_type=concept.datatype,
|
|
91
|
+
actual_modifiers=concept.modifiers,
|
|
92
|
+
)
|
|
93
|
+
],
|
|
94
|
+
message=f"Key concept {concept.address} is missing values in datasource {datasource.name} (max cardinality in data {max_seen}, datasource has {seen[datasource.name]} values) but is not marked as partial.",
|
|
74
95
|
)
|
|
75
96
|
results.append(
|
|
76
97
|
ValidationTest(
|