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.

Files changed (159) hide show
  1. {pytrilogy-0.0.3.96/pytrilogy.egg-info → pytrilogy-0.0.3.98}/PKG-INFO +16 -2
  2. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/README.md +15 -1
  3. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98/pytrilogy.egg-info}/PKG-INFO +16 -2
  4. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/pytrilogy.egg-info/SOURCES.txt +2 -0
  5. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_parsing.py +49 -0
  6. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/__init__.py +1 -1
  7. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/constants.py +1 -0
  8. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/enums.py +9 -0
  9. pytrilogy-0.0.3.98/trilogy/core/exceptions.py +105 -0
  10. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/functions.py +1 -1
  11. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/models/execute.py +0 -1
  12. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/node_generators/node_merge_node.py +109 -5
  13. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/validation/common.py +53 -2
  14. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/validation/concept.py +25 -4
  15. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/validation/datasource.py +22 -20
  16. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/validation/environment.py +3 -1
  17. pytrilogy-0.0.3.98/trilogy/core/validation/fix.py +106 -0
  18. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/dialect/base.py +2 -1
  19. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/dialect/common.py +2 -10
  20. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/dialect/metadata.py +1 -1
  21. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/executor.py +7 -0
  22. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/parsing/parse_engine.py +10 -8
  23. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/parsing/render.py +32 -5
  24. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/parsing/trilogy.lark +7 -4
  25. pytrilogy-0.0.3.98/trilogy/std/metric.preql +15 -0
  26. pytrilogy-0.0.3.96/trilogy/core/exceptions.py +0 -51
  27. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/LICENSE.md +0 -0
  28. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/pyproject.toml +0 -0
  29. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/pytrilogy.egg-info/dependency_links.txt +0 -0
  30. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/pytrilogy.egg-info/entry_points.txt +0 -0
  31. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/pytrilogy.egg-info/requires.txt +0 -0
  32. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/pytrilogy.egg-info/top_level.txt +0 -0
  33. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/setup.cfg +0 -0
  34. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/setup.py +0 -0
  35. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_datatypes.py +0 -0
  36. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_declarations.py +0 -0
  37. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_derived_concepts.py +0 -0
  38. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_discovery_nodes.py +0 -0
  39. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_enums.py +0 -0
  40. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_environment.py +0 -0
  41. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_execute_models.py +0 -0
  42. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_executor.py +0 -0
  43. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_failure.py +0 -0
  44. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_functions.py +0 -0
  45. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_imports.py +0 -0
  46. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_metadata.py +0 -0
  47. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_models.py +0 -0
  48. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_multi_join_assignments.py +0 -0
  49. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_parse_engine.py +0 -0
  50. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_parsing_failures.py +0 -0
  51. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_partial_handling.py +0 -0
  52. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_query_processing.py +0 -0
  53. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_query_render.py +0 -0
  54. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_select.py +0 -0
  55. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_show.py +0 -0
  56. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_statements.py +0 -0
  57. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_typing.py +0 -0
  58. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_undefined_concept.py +0 -0
  59. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_user_functions.py +0 -0
  60. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/tests/test_where_clause.py +0 -0
  61. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/authoring/__init__.py +0 -0
  62. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/__init__.py +0 -0
  63. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/constants.py +0 -0
  64. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/env_processor.py +0 -0
  65. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/environment_helpers.py +0 -0
  66. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/ergonomics.py +0 -0
  67. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/graph_models.py +0 -0
  68. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/internal.py +0 -0
  69. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/models/__init__.py +0 -0
  70. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/models/author.py +0 -0
  71. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/models/build.py +0 -0
  72. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/models/build_environment.py +0 -0
  73. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/models/core.py +0 -0
  74. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/models/datasource.py +0 -0
  75. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/models/environment.py +0 -0
  76. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/optimization.py +0 -0
  77. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/optimizations/__init__.py +0 -0
  78. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/optimizations/base_optimization.py +0 -0
  79. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/optimizations/inline_datasource.py +0 -0
  80. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/optimizations/predicate_pushdown.py +0 -0
  81. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/__init__.py +0 -0
  82. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/concept_strategies_v3.py +0 -0
  83. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/discovery_loop.py +0 -0
  84. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/discovery_node_factory.py +0 -0
  85. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/discovery_utility.py +0 -0
  86. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/discovery_validation.py +0 -0
  87. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/graph_utils.py +0 -0
  88. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/node_generators/__init__.py +0 -0
  89. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/node_generators/basic_node.py +0 -0
  90. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/node_generators/common.py +0 -0
  91. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/node_generators/constant_node.py +0 -0
  92. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/node_generators/filter_node.py +0 -0
  93. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/node_generators/group_node.py +0 -0
  94. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/node_generators/group_to_node.py +0 -0
  95. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/node_generators/multiselect_node.py +0 -0
  96. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/node_generators/recursive_node.py +0 -0
  97. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/node_generators/rowset_node.py +0 -0
  98. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
  99. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +0 -0
  100. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/node_generators/select_merge_node.py +0 -0
  101. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/node_generators/select_node.py +0 -0
  102. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/node_generators/synonym_node.py +0 -0
  103. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/node_generators/union_node.py +0 -0
  104. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/node_generators/unnest_node.py +0 -0
  105. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/node_generators/window_node.py +0 -0
  106. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/nodes/__init__.py +0 -0
  107. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/nodes/base_node.py +0 -0
  108. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/nodes/filter_node.py +0 -0
  109. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/nodes/group_node.py +0 -0
  110. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/nodes/merge_node.py +0 -0
  111. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/nodes/recursive_node.py +0 -0
  112. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/nodes/select_node_v2.py +0 -0
  113. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/nodes/union_node.py +0 -0
  114. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/nodes/unnest_node.py +0 -0
  115. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/nodes/window_node.py +0 -0
  116. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/processing/utility.py +0 -0
  117. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/query_processor.py +0 -0
  118. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/statements/__init__.py +0 -0
  119. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/statements/author.py +0 -0
  120. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/statements/build.py +0 -0
  121. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/statements/common.py +0 -0
  122. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/statements/execute.py +0 -0
  123. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/utility.py +0 -0
  124. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/core/validation/__init__.py +0 -0
  125. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/dialect/__init__.py +0 -0
  126. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/dialect/bigquery.py +0 -0
  127. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/dialect/config.py +0 -0
  128. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/dialect/dataframe.py +0 -0
  129. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/dialect/duckdb.py +0 -0
  130. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/dialect/enums.py +0 -0
  131. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/dialect/postgres.py +0 -0
  132. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/dialect/presto.py +0 -0
  133. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/dialect/snowflake.py +0 -0
  134. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/dialect/sql_server.py +0 -0
  135. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/engine.py +0 -0
  136. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/hooks/__init__.py +0 -0
  137. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/hooks/base_hook.py +0 -0
  138. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/hooks/graph_hook.py +0 -0
  139. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/hooks/query_debugger.py +0 -0
  140. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/metadata/__init__.py +0 -0
  141. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/parser.py +0 -0
  142. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/parsing/__init__.py +0 -0
  143. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/parsing/common.py +0 -0
  144. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/parsing/config.py +0 -0
  145. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/parsing/exceptions.py +0 -0
  146. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/parsing/helpers.py +0 -0
  147. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/py.typed +0 -0
  148. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/render.py +0 -0
  149. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/scripts/__init__.py +0 -0
  150. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/scripts/trilogy.py +0 -0
  151. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/std/__init__.py +0 -0
  152. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/std/date.preql +0 -0
  153. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/std/display.preql +0 -0
  154. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/std/geography.preql +0 -0
  155. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/std/money.preql +0 -0
  156. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/std/net.preql +0 -0
  157. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/std/ranking.preql +0 -0
  158. {pytrilogy-0.0.3.96 → pytrilogy-0.0.3.98}/trilogy/std/report.preql +0 -0
  159. {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.96
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
- ## Trilogy Syntax Reference
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
- ## Trilogy Syntax Reference
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.96
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
- ## Trilogy Syntax Reference
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;
@@ -4,6 +4,6 @@ from trilogy.dialect.enums import Dialects
4
4
  from trilogy.executor import Executor
5
5
  from trilogy.parser import parse
6
6
 
7
- __version__ = "0.0.3.96"
7
+ __version__ = "0.0.3.98"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -16,6 +16,7 @@ ENV_CACHE_NAME = ".preql_cache.json"
16
16
 
17
17
  class MagicConstants(Enum):
18
18
  NULL = "null"
19
+ LINE_SEPARATOR = "\n"
19
20
 
20
21
 
21
22
  NULL_VALUE = MagicConstants.NULL
@@ -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.BOOL,
430
+ output_type=DataType.STRING,
431
431
  arg_count=2,
432
432
  ),
433
433
  FunctionType.SUBSTRING: FunctionConfig(
@@ -151,7 +151,6 @@ class CTE(BaseModel):
151
151
  ]
152
152
  ):
153
153
  return False
154
-
155
154
  self.source.datasources = [
156
155
  ds_being_inlined,
157
156
  *[
@@ -1,4 +1,5 @@
1
- from typing import List, Optional
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 BuildConcept, BuildConditional, BuildWhereClause
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
- lengths[n] = nx.shortest_path_length(ds_graph, node, n)
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 ConceptRef
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
- DatasourceModelValidationError,
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 = DatasourceModelValidationError(
73
- 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."
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(