pytrilogy 0.0.2.53__tar.gz → 0.0.2.55__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 (114) hide show
  1. {pytrilogy-0.0.2.53/pytrilogy.egg-info → pytrilogy-0.0.2.55}/PKG-INFO +32 -12
  2. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/README.md +31 -11
  3. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55/pytrilogy.egg-info}/PKG-INFO +32 -12
  4. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/tests/test_parsing.py +5 -5
  5. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/tests/test_partial_handling.py +15 -5
  6. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/__init__.py +1 -1
  7. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/environment_helpers.py +9 -3
  8. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/models.py +32 -56
  9. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/processing/node_generators/common.py +4 -3
  10. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/processing/node_generators/filter_node.py +3 -2
  11. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/processing/node_generators/rowset_node.py +0 -1
  12. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/parsing/common.py +14 -12
  13. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/parsing/parse_engine.py +22 -19
  14. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/parsing/render.py +26 -7
  15. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/LICENSE.md +0 -0
  16. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/pyproject.toml +0 -0
  17. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/pytrilogy.egg-info/SOURCES.txt +0 -0
  18. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/pytrilogy.egg-info/dependency_links.txt +0 -0
  19. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/pytrilogy.egg-info/entry_points.txt +0 -0
  20. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/pytrilogy.egg-info/requires.txt +0 -0
  21. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/pytrilogy.egg-info/top_level.txt +0 -0
  22. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/setup.cfg +0 -0
  23. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/setup.py +0 -0
  24. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/tests/test_datatypes.py +0 -0
  25. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/tests/test_declarations.py +0 -0
  26. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/tests/test_derived_concepts.py +0 -0
  27. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/tests/test_discovery_nodes.py +0 -0
  28. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/tests/test_enums.py +0 -0
  29. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/tests/test_environment.py +0 -0
  30. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/tests/test_executor.py +0 -0
  31. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/tests/test_functions.py +0 -0
  32. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/tests/test_imports.py +0 -0
  33. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/tests/test_metadata.py +0 -0
  34. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/tests/test_models.py +0 -0
  35. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/tests/test_multi_join_assignments.py +0 -0
  36. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/tests/test_parse_engine.py +0 -0
  37. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/tests/test_query_processing.py +0 -0
  38. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/tests/test_select.py +0 -0
  39. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/tests/test_show.py +0 -0
  40. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/tests/test_statements.py +0 -0
  41. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/tests/test_undefined_concept.py +0 -0
  42. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/tests/test_where_clause.py +0 -0
  43. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/compiler.py +0 -0
  44. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/constants.py +0 -0
  45. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/__init__.py +0 -0
  46. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/constants.py +0 -0
  47. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/enums.py +0 -0
  48. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/env_processor.py +0 -0
  49. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/ergonomics.py +0 -0
  50. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/exceptions.py +0 -0
  51. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/functions.py +0 -0
  52. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/graph_models.py +0 -0
  53. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/internal.py +0 -0
  54. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/optimization.py +0 -0
  55. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/optimizations/__init__.py +0 -0
  56. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/optimizations/base_optimization.py +0 -0
  57. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/optimizations/inline_constant.py +0 -0
  58. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/optimizations/inline_datasource.py +0 -0
  59. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/optimizations/predicate_pushdown.py +0 -0
  60. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/processing/__init__.py +0 -0
  61. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/processing/concept_strategies_v3.py +0 -0
  62. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/processing/graph_utils.py +0 -0
  63. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/processing/node_generators/__init__.py +0 -0
  64. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/processing/node_generators/basic_node.py +0 -0
  65. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/processing/node_generators/group_node.py +0 -0
  66. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/processing/node_generators/group_to_node.py +0 -0
  67. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/processing/node_generators/multiselect_node.py +0 -0
  68. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/processing/node_generators/node_merge_node.py +0 -0
  69. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
  70. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +0 -0
  71. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/processing/node_generators/select_merge_node.py +0 -0
  72. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/processing/node_generators/select_node.py +0 -0
  73. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/processing/node_generators/union_node.py +0 -0
  74. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/processing/node_generators/unnest_node.py +0 -0
  75. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/processing/node_generators/window_node.py +0 -0
  76. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/processing/nodes/__init__.py +0 -0
  77. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/processing/nodes/base_node.py +0 -0
  78. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/processing/nodes/filter_node.py +0 -0
  79. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/processing/nodes/group_node.py +0 -0
  80. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/processing/nodes/merge_node.py +0 -0
  81. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/processing/nodes/select_node_v2.py +0 -0
  82. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/processing/nodes/union_node.py +0 -0
  83. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/processing/nodes/unnest_node.py +0 -0
  84. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/processing/nodes/window_node.py +0 -0
  85. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/processing/utility.py +0 -0
  86. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/core/query_processor.py +0 -0
  87. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/dialect/__init__.py +0 -0
  88. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/dialect/base.py +0 -0
  89. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/dialect/bigquery.py +0 -0
  90. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/dialect/common.py +0 -0
  91. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/dialect/config.py +0 -0
  92. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/dialect/duckdb.py +0 -0
  93. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/dialect/enums.py +0 -0
  94. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/dialect/postgres.py +0 -0
  95. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/dialect/presto.py +0 -0
  96. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/dialect/snowflake.py +0 -0
  97. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/dialect/sql_server.py +0 -0
  98. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/engine.py +0 -0
  99. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/executor.py +0 -0
  100. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/hooks/__init__.py +0 -0
  101. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/hooks/base_hook.py +0 -0
  102. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/hooks/graph_hook.py +0 -0
  103. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/hooks/query_debugger.py +0 -0
  104. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/metadata/__init__.py +0 -0
  105. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/parser.py +0 -0
  106. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/parsing/__init__.py +0 -0
  107. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/parsing/config.py +0 -0
  108. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/parsing/exceptions.py +0 -0
  109. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/parsing/helpers.py +0 -0
  110. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/parsing/trilogy.lark +0 -0
  111. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/py.typed +0 -0
  112. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/scripts/__init__.py +0 -0
  113. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/scripts/trilogy.py +0 -0
  114. {pytrilogy-0.0.2.53 → pytrilogy-0.0.2.55}/trilogy/utility.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pytrilogy
3
- Version: 0.0.2.53
3
+ Version: 0.0.2.55
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -34,7 +34,7 @@ Requires-Dist: snowflake-sqlalchemy; extra == "snowflake"
34
34
 
35
35
  pytrilogy is an experimental implementation of the Trilogy language, a higher-level SQL that replaces tables/joins with a lightweight semantic binding layer.
36
36
 
37
- Trilogy looks like SQL, but simpler. It's a modern SQL refresh targeted at SQL lovers who want reusability and simplicity with the power and iteratability of SQL. It compiles to SQL - making it easy to debug or integrate into existing workflows - and can be run against any supported SQL backend.
37
+ Trilogy looks like SQL, but simpler. It's a modern SQL refresh targeted at SQL lovers who want more reusability and composability without losing the expressiveness and iterative value of SQL. It compiles to SQL - making it easy to debug or integrate into existing workflows - and can be run against any supported SQL backend.
38
38
 
39
39
  > [!TIP]
40
40
  > To get an overview of the language and run interactive examples, head to the [documentation](https://trilogydata.dev/).
@@ -193,11 +193,12 @@ address `bigquery-public-data.usa_names.usa_1910_2013`;
193
193
  executor = Dialects.BIGQUERY.default_executor(environment=environment)
194
194
 
195
195
  results = executor.execute_text(
196
- '''SELECT
197
- name,
198
- sum(yearly_name_count) -> name_count
196
+ '''
199
197
  WHERE
200
198
  name = 'Elvis'
199
+ SELECT
200
+ name,
201
+ sum(yearly_name_count) -> name_count
201
202
  ORDER BY
202
203
  name_count desc
203
204
  LIMIT 10;
@@ -270,9 +271,11 @@ Please open an issue first to discuss what you would like to change, and then cr
270
271
  ## Similar in space
271
272
  Trilogy combines two aspects; a semantic layer and a query language. Examples of both are linked below:
272
273
 
273
- Python "semantic layers" are tools for defining data access to a warehouse in a more abstract way.
274
+ "semantic layers" are tools for defining an metadata layer above a SQL/warehouse base to enable higher level abstractions.
274
275
 
275
276
  - [metricsflow](https://github.com/dbt-labs/metricflow)
277
+ - [cube](https://github.com/cube-js/cube)
278
+ - [zillion](https://github.com/totalhack/zillion)
276
279
 
277
280
  "Better SQL" has been a popular space. We believe Trilogy takes a different approach then the following,
278
281
  but all are worth checking out. Please open PRs/comment for anything missed!
@@ -321,11 +324,14 @@ address <table>;
321
324
  Primary acces
322
325
 
323
326
  ```sql
324
- select
325
- <concept>,
326
- <concept>+1 -> <alias>
327
327
  WHERE
328
328
  <concept> = <value>
329
+ select
330
+ <concept>,
331
+ <concept>+1 -> <alias>,
332
+ ...
333
+ HAVING
334
+ <alias> = <value2>
329
335
  ORDER BY
330
336
  <concept> asc|desc
331
337
  ;
@@ -337,11 +343,13 @@ Reusable virtual set of rows. Useful for windows, filtering.
337
343
 
338
344
  ```sql
339
345
  with <alias> as
340
- select
341
- <concept>,
342
- <concept>+1 -> <alias>
343
346
  WHERE
344
347
  <concept> = <value>
348
+ select
349
+ <concept>,
350
+ <concept>+1 -> <alias>,
351
+ ...
352
+
345
353
 
346
354
  select <alias>.<concept>;
347
355
 
@@ -357,6 +365,18 @@ persist <alias> as <table_name> from
357
365
  <select>;
358
366
  ```
359
367
 
368
+ #### COPY
369
+
370
+ Currently supported target types are <CSV>, though backend support may vary.
371
+
372
+ ```sql
373
+ COPY INTO <TARGET_TYPE> '<target_path>' FROM SELECT
374
+ <concept>, ...
375
+ ORDER BY
376
+ <concept>, ...
377
+ ;
378
+ ```
379
+
360
380
  #### SHOW
361
381
 
362
382
  Return generated SQL without executing.
@@ -4,7 +4,7 @@
4
4
 
5
5
  pytrilogy is an experimental implementation of the Trilogy language, a higher-level SQL that replaces tables/joins with a lightweight semantic binding layer.
6
6
 
7
- Trilogy looks like SQL, but simpler. It's a modern SQL refresh targeted at SQL lovers who want reusability and simplicity with the power and iteratability of SQL. It compiles to SQL - making it easy to debug or integrate into existing workflows - and can be run against any supported SQL backend.
7
+ Trilogy looks like SQL, but simpler. It's a modern SQL refresh targeted at SQL lovers who want more reusability and composability without losing the expressiveness and iterative value of SQL. It compiles to SQL - making it easy to debug or integrate into existing workflows - and can be run against any supported SQL backend.
8
8
 
9
9
  > [!TIP]
10
10
  > To get an overview of the language and run interactive examples, head to the [documentation](https://trilogydata.dev/).
@@ -163,11 +163,12 @@ address `bigquery-public-data.usa_names.usa_1910_2013`;
163
163
  executor = Dialects.BIGQUERY.default_executor(environment=environment)
164
164
 
165
165
  results = executor.execute_text(
166
- '''SELECT
167
- name,
168
- sum(yearly_name_count) -> name_count
166
+ '''
169
167
  WHERE
170
168
  name = 'Elvis'
169
+ SELECT
170
+ name,
171
+ sum(yearly_name_count) -> name_count
171
172
  ORDER BY
172
173
  name_count desc
173
174
  LIMIT 10;
@@ -240,9 +241,11 @@ Please open an issue first to discuss what you would like to change, and then cr
240
241
  ## Similar in space
241
242
  Trilogy combines two aspects; a semantic layer and a query language. Examples of both are linked below:
242
243
 
243
- Python "semantic layers" are tools for defining data access to a warehouse in a more abstract way.
244
+ "semantic layers" are tools for defining an metadata layer above a SQL/warehouse base to enable higher level abstractions.
244
245
 
245
246
  - [metricsflow](https://github.com/dbt-labs/metricflow)
247
+ - [cube](https://github.com/cube-js/cube)
248
+ - [zillion](https://github.com/totalhack/zillion)
246
249
 
247
250
  "Better SQL" has been a popular space. We believe Trilogy takes a different approach then the following,
248
251
  but all are worth checking out. Please open PRs/comment for anything missed!
@@ -291,11 +294,14 @@ address <table>;
291
294
  Primary acces
292
295
 
293
296
  ```sql
294
- select
295
- <concept>,
296
- <concept>+1 -> <alias>
297
297
  WHERE
298
298
  <concept> = <value>
299
+ select
300
+ <concept>,
301
+ <concept>+1 -> <alias>,
302
+ ...
303
+ HAVING
304
+ <alias> = <value2>
299
305
  ORDER BY
300
306
  <concept> asc|desc
301
307
  ;
@@ -307,11 +313,13 @@ Reusable virtual set of rows. Useful for windows, filtering.
307
313
 
308
314
  ```sql
309
315
  with <alias> as
310
- select
311
- <concept>,
312
- <concept>+1 -> <alias>
313
316
  WHERE
314
317
  <concept> = <value>
318
+ select
319
+ <concept>,
320
+ <concept>+1 -> <alias>,
321
+ ...
322
+
315
323
 
316
324
  select <alias>.<concept>;
317
325
 
@@ -327,6 +335,18 @@ persist <alias> as <table_name> from
327
335
  <select>;
328
336
  ```
329
337
 
338
+ #### COPY
339
+
340
+ Currently supported target types are <CSV>, though backend support may vary.
341
+
342
+ ```sql
343
+ COPY INTO <TARGET_TYPE> '<target_path>' FROM SELECT
344
+ <concept>, ...
345
+ ORDER BY
346
+ <concept>, ...
347
+ ;
348
+ ```
349
+
330
350
  #### SHOW
331
351
 
332
352
  Return generated SQL without executing.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pytrilogy
3
- Version: 0.0.2.53
3
+ Version: 0.0.2.55
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -34,7 +34,7 @@ Requires-Dist: snowflake-sqlalchemy; extra == "snowflake"
34
34
 
35
35
  pytrilogy is an experimental implementation of the Trilogy language, a higher-level SQL that replaces tables/joins with a lightweight semantic binding layer.
36
36
 
37
- Trilogy looks like SQL, but simpler. It's a modern SQL refresh targeted at SQL lovers who want reusability and simplicity with the power and iteratability of SQL. It compiles to SQL - making it easy to debug or integrate into existing workflows - and can be run against any supported SQL backend.
37
+ Trilogy looks like SQL, but simpler. It's a modern SQL refresh targeted at SQL lovers who want more reusability and composability without losing the expressiveness and iterative value of SQL. It compiles to SQL - making it easy to debug or integrate into existing workflows - and can be run against any supported SQL backend.
38
38
 
39
39
  > [!TIP]
40
40
  > To get an overview of the language and run interactive examples, head to the [documentation](https://trilogydata.dev/).
@@ -193,11 +193,12 @@ address `bigquery-public-data.usa_names.usa_1910_2013`;
193
193
  executor = Dialects.BIGQUERY.default_executor(environment=environment)
194
194
 
195
195
  results = executor.execute_text(
196
- '''SELECT
197
- name,
198
- sum(yearly_name_count) -> name_count
196
+ '''
199
197
  WHERE
200
198
  name = 'Elvis'
199
+ SELECT
200
+ name,
201
+ sum(yearly_name_count) -> name_count
201
202
  ORDER BY
202
203
  name_count desc
203
204
  LIMIT 10;
@@ -270,9 +271,11 @@ Please open an issue first to discuss what you would like to change, and then cr
270
271
  ## Similar in space
271
272
  Trilogy combines two aspects; a semantic layer and a query language. Examples of both are linked below:
272
273
 
273
- Python "semantic layers" are tools for defining data access to a warehouse in a more abstract way.
274
+ "semantic layers" are tools for defining an metadata layer above a SQL/warehouse base to enable higher level abstractions.
274
275
 
275
276
  - [metricsflow](https://github.com/dbt-labs/metricflow)
277
+ - [cube](https://github.com/cube-js/cube)
278
+ - [zillion](https://github.com/totalhack/zillion)
276
279
 
277
280
  "Better SQL" has been a popular space. We believe Trilogy takes a different approach then the following,
278
281
  but all are worth checking out. Please open PRs/comment for anything missed!
@@ -321,11 +324,14 @@ address <table>;
321
324
  Primary acces
322
325
 
323
326
  ```sql
324
- select
325
- <concept>,
326
- <concept>+1 -> <alias>
327
327
  WHERE
328
328
  <concept> = <value>
329
+ select
330
+ <concept>,
331
+ <concept>+1 -> <alias>,
332
+ ...
333
+ HAVING
334
+ <alias> = <value2>
329
335
  ORDER BY
330
336
  <concept> asc|desc
331
337
  ;
@@ -337,11 +343,13 @@ Reusable virtual set of rows. Useful for windows, filtering.
337
343
 
338
344
  ```sql
339
345
  with <alias> as
340
- select
341
- <concept>,
342
- <concept>+1 -> <alias>
343
346
  WHERE
344
347
  <concept> = <value>
348
+ select
349
+ <concept>,
350
+ <concept>+1 -> <alias>,
351
+ ...
352
+
345
353
 
346
354
  select <alias>.<concept>;
347
355
 
@@ -357,6 +365,18 @@ persist <alias> as <table_name> from
357
365
  <select>;
358
366
  ```
359
367
 
368
+ #### COPY
369
+
370
+ Currently supported target types are <CSV>, though backend support may vary.
371
+
372
+ ```sql
373
+ COPY INTO <TARGET_TYPE> '<target_path>' FROM SELECT
374
+ <concept>, ...
375
+ ORDER BY
376
+ <concept>, ...
377
+ ;
378
+ ```
379
+
360
380
  #### SHOW
361
381
 
362
382
  Return generated SQL without executing.
@@ -182,7 +182,7 @@ select
182
182
  ]:
183
183
  assert f"local.{name}" in env.concepts
184
184
  assert env.concepts[name].purpose == Purpose.PROPERTY
185
- assert env.concepts[name].keys == (env.concepts["id"],)
185
+ assert env.concepts[name].keys == {env.concepts["id"].address}
186
186
 
187
187
 
188
188
  def test_purpose_and_derivation():
@@ -200,10 +200,10 @@ select
200
200
 
201
201
  for name in ["join_id"]:
202
202
  assert env.concepts[name].purpose == Purpose.PROPERTY
203
- assert env.concepts[name].keys == (
204
- env.concepts["id"],
205
- env.concepts["other_id"],
206
- )
203
+ assert env.concepts[name].keys == {
204
+ env.concepts["id"].address,
205
+ env.concepts["other_id"].address,
206
+ }
207
207
 
208
208
 
209
209
  def test_output_purpose():
@@ -35,7 +35,9 @@ def setup_titanic(env: Environment):
35
35
  namespace=namespace,
36
36
  datatype=DataType.INTEGER,
37
37
  purpose=Purpose.PROPERTY,
38
- keys=(id,),
38
+ keys={
39
+ id.address,
40
+ },
39
41
  )
40
42
 
41
43
  name = Concept(
@@ -43,7 +45,9 @@ def setup_titanic(env: Environment):
43
45
  namespace=namespace,
44
46
  datatype=DataType.STRING,
45
47
  purpose=Purpose.PROPERTY,
46
- keys=(id,),
48
+ keys={
49
+ id.address,
50
+ },
47
51
  )
48
52
 
49
53
  pclass = Concept(
@@ -51,21 +55,27 @@ def setup_titanic(env: Environment):
51
55
  namespace=namespace,
52
56
  purpose=Purpose.PROPERTY,
53
57
  datatype=DataType.INTEGER,
54
- keys=(id,),
58
+ keys={
59
+ id.address,
60
+ },
55
61
  )
56
62
  survived = Concept(
57
63
  name="survived",
58
64
  namespace=namespace,
59
65
  purpose=Purpose.PROPERTY,
60
66
  datatype=DataType.INTEGER,
61
- keys=(id,),
67
+ keys={
68
+ id.address,
69
+ },
62
70
  )
63
71
  fare = Concept(
64
72
  name="fare",
65
73
  namespace=namespace,
66
74
  purpose=Purpose.PROPERTY,
67
75
  datatype=DataType.FLOAT,
68
- keys=(id,),
76
+ keys={
77
+ id.address,
78
+ },
69
79
  )
70
80
  for x in [id, age, survived, name, pclass, fare]:
71
81
  env.add_concept(x)
@@ -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.2.53"
7
+ __version__ = "0.0.2.55"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -50,7 +50,9 @@ def generate_date_concepts(concept: Concept, environment: Environment):
50
50
  lineage=const_function,
51
51
  grain=const_function.output_grain,
52
52
  namespace=namespace,
53
- keys=(concept,),
53
+ keys=set(
54
+ concept.address,
55
+ ),
54
56
  metadata=Metadata(
55
57
  description=f"Auto-derived. Integer format. The {ftype.value} derived from {concept.name}, {base_description}",
56
58
  line_number=base_line_number,
@@ -99,7 +101,9 @@ def generate_datetime_concepts(concept: Concept, environment: Environment):
99
101
  lineage=const_function,
100
102
  grain=const_function.output_grain,
101
103
  namespace=namespace,
102
- keys=(concept,),
104
+ keys=set(
105
+ concept.address,
106
+ ),
103
107
  metadata=Metadata(
104
108
  description=f"Auto-derived. Integer format. The {ftype.value} derived from {concept.name}, {base_description}",
105
109
  line_number=base_line_number,
@@ -139,7 +143,9 @@ def generate_key_concepts(concept: Concept, environment: Environment):
139
143
  lineage=const_function,
140
144
  grain=const_function.output_grain,
141
145
  namespace=namespace,
142
- keys=(concept,),
146
+ keys={
147
+ concept.address,
148
+ },
143
149
  metadata=Metadata(
144
150
  description=f"Auto-derived. Integer format. The {ftype.value} derived from {concept.name}, {base_description}",
145
151
  line_number=base_line_number,
@@ -427,12 +427,15 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
427
427
  ]
428
428
  ] = None
429
429
  namespace: Optional[str] = Field(default=DEFAULT_NAMESPACE, validate_default=True)
430
- keys: Optional[Tuple["Concept", ...]] = None
430
+ keys: Optional[set[str]] = None
431
431
  grain: "Grain" = Field(default=None, validate_default=True) # type: ignore
432
432
  modifiers: List[Modifier] = Field(default_factory=list) # type: ignore
433
433
  pseudonyms: set[str] = Field(default_factory=set)
434
434
  _address_cache: str | None = None
435
435
 
436
+ def __init__(self, **data):
437
+ super().__init__(**data)
438
+
436
439
  def duplicate(self) -> Concept:
437
440
  return self.model_copy(deep=True)
438
441
 
@@ -480,7 +483,7 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
480
483
  grain=self.grain.with_merge(source, target, modifiers),
481
484
  namespace=self.namespace,
482
485
  keys=(
483
- tuple(x.with_merge(source, target, modifiers) for x in self.keys)
486
+ set(x if x != source.address else target.address for x in self.keys)
484
487
  if self.keys
485
488
  else None
486
489
  ),
@@ -488,17 +491,6 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
488
491
  pseudonyms=self.pseudonyms,
489
492
  )
490
493
 
491
- @field_validator("keys", mode="before")
492
- @classmethod
493
- def keys_validator(cls, v, info: ValidationInfo):
494
- if v is None:
495
- return v
496
- if not isinstance(v, (list, tuple)):
497
- raise ValueError(f"Keys must be a list or tuple, got {type(v)}")
498
- if isinstance(v, list):
499
- return tuple(v)
500
- return v
501
-
502
494
  @field_validator("namespace", mode="plain")
503
495
  @classmethod
504
496
  def namespace_validation(cls, v):
@@ -610,7 +602,7 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
610
602
  else namespace
611
603
  ),
612
604
  keys=(
613
- tuple([x.with_namespace(namespace) for x in self.keys])
605
+ set([address_with_namespace(x, namespace) for x in self.keys])
614
606
  if self.keys
615
607
  else None
616
608
  ),
@@ -627,25 +619,17 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
627
619
  local_concepts=local_concepts, grain=grain, environment=environment
628
620
  )
629
621
  final_grain = self.grain or grain
630
- keys = (
631
- tuple(
632
- [
633
- x.with_select_context(local_concepts, grain, environment)
634
- for x in self.keys
635
- ]
636
- )
637
- if self.keys
638
- else None
639
- )
622
+ keys = self.keys if self.keys else None
640
623
  if self.is_aggregate and isinstance(new_lineage, Function):
641
624
  grain_components = [environment.concepts[c] for c in grain.components]
642
625
  new_lineage = AggregateWrapper(function=new_lineage, by=grain_components)
643
626
  final_grain = grain
644
- keys = tuple(grain_components)
627
+ keys = set(grain.components)
645
628
  elif (
646
629
  self.is_aggregate and not keys and isinstance(new_lineage, AggregateWrapper)
647
630
  ):
648
- keys = tuple(new_lineage.by)
631
+ keys = set([x.address for x in new_lineage.by])
632
+
649
633
  return self.__class__(
650
634
  name=self.name,
651
635
  datatype=self.datatype,
@@ -687,13 +671,10 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
687
671
  if self.lineage:
688
672
  for item in self.lineage.arguments:
689
673
  if isinstance(item, Concept):
690
- if item.keys and not all(c in components for c in item.keys):
691
- components += item.sources
692
- else:
693
- components += item.sources
674
+ components += [x.address for x in item.sources]
694
675
  # TODO: set synonyms
695
676
  grain = Grain(
696
- components=set([x.address for x in components]),
677
+ components=set([x for x in components]),
697
678
  ) # synonym_set=generate_concept_synonyms(components))
698
679
  elif self.purpose == Purpose.METRIC:
699
680
  grain = Grain()
@@ -871,7 +852,7 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
871
852
 
872
853
  class ConceptRef(BaseModel):
873
854
  address: str
874
- line_no: int
855
+ line_no: int | None = None
875
856
 
876
857
  def hydrate(self, environment: Environment) -> Concept:
877
858
  return environment.concepts.__getitem__(self.address, self.line_no)
@@ -1058,13 +1039,19 @@ class EnvironmentConceptDict(dict):
1058
1039
  if DEFAULT_NAMESPACE + "." + key in self:
1059
1040
  return self.__getitem__(DEFAULT_NAMESPACE + "." + key, line_no)
1060
1041
  if not self.fail_on_missing:
1042
+ if "." in key:
1043
+ ns, rest = key.rsplit(".", 1)
1044
+ else:
1045
+ ns = DEFAULT_NAMESPACE
1046
+ rest = key
1061
1047
  if key in self.undefined:
1062
1048
  return self.undefined[key]
1063
1049
  undefined = UndefinedConcept(
1064
- name=key,
1050
+ name=rest,
1065
1051
  line_no=line_no,
1066
1052
  datatype=DataType.UNKNOWN,
1067
1053
  purpose=Purpose.UNKNOWN,
1054
+ namespace=ns,
1068
1055
  )
1069
1056
  self.undefined[key] = undefined
1070
1057
  return undefined
@@ -1365,18 +1352,6 @@ class Function(Mergeable, Namespaced, SelectContext, BaseModel):
1365
1352
  base_grain += input.grain
1366
1353
  return base_grain
1367
1354
 
1368
- @property
1369
- def output_keys(self) -> list[Concept]:
1370
- # aggregates have an abstract grain
1371
- components = []
1372
- # scalars have implicit grain of all arguments
1373
- for input in self.concept_arguments:
1374
- if input.purpose == Purpose.KEY:
1375
- components.append(input)
1376
- elif input.keys:
1377
- components += input.keys
1378
- return list(set(components))
1379
-
1380
1355
 
1381
1356
  class ConceptTransform(Namespaced, BaseModel):
1382
1357
  function: Function | FilterItem | WindowItem | AggregateWrapper
@@ -3203,7 +3178,9 @@ class UndefinedConcept(Concept, Mergeable, Namespaced):
3203
3178
  rval = local_concepts[self.address]
3204
3179
  rval = rval.with_select_context(local_concepts, grain, environment)
3205
3180
  return rval
3206
- environment.concepts.raise_undefined(self.address, line_no=self.line_no)
3181
+ if environment.concepts.fail_on_missing:
3182
+ environment.concepts.raise_undefined(self.address, line_no=self.line_no)
3183
+ return self
3207
3184
 
3208
3185
 
3209
3186
  class EnvironmentDatasourceDict(dict):
@@ -3336,10 +3313,16 @@ class Environment(BaseModel):
3336
3313
 
3337
3314
  @classmethod
3338
3315
  def from_file(cls, path: str | Path) -> "Environment":
3316
+ if isinstance(path, str):
3317
+ path = Path(path)
3339
3318
  with open(path, "r") as f:
3340
3319
  read = f.read()
3341
3320
  return Environment(working_path=Path(path).parent).parse(read)[0]
3342
3321
 
3322
+ @classmethod
3323
+ def from_string(cls, input: str) -> "Environment":
3324
+ return Environment().parse(input)[0]
3325
+
3343
3326
  @classmethod
3344
3327
  def from_cache(cls, path) -> Optional["Environment"]:
3345
3328
  with open(path, "r") as f:
@@ -3645,11 +3628,6 @@ class Environment(BaseModel):
3645
3628
  self.merge_concept(new_concept, current_concept, [])
3646
3629
  else:
3647
3630
  self.add_concept(current_concept, meta=meta, _ignore_cache=True)
3648
-
3649
- # else:
3650
- # self.add_concept(
3651
- # current_concept, meta=meta, _ignore_cache=True
3652
- # )
3653
3631
  if not _ignore_cache:
3654
3632
  self.gen_concept_list_caches()
3655
3633
  return datasource
@@ -4521,13 +4499,11 @@ class RowsetDerivationStatement(HasUUID, Namespaced, BaseModel):
4521
4499
  # remap everything to the properties of the rowset
4522
4500
  for x in output:
4523
4501
  if x.keys:
4524
- if all([k.address in orig for k in x.keys]):
4525
- x.keys = tuple(
4526
- [orig[k.address] if k.address in orig else k for k in x.keys]
4527
- )
4502
+ if all([k in orig for k in x.keys]):
4503
+ x.keys = set([orig[k].address if k in orig else k for k in x.keys])
4528
4504
  else:
4529
4505
  # TODO: fix this up
4530
- x.keys = tuple()
4506
+ x.keys = set()
4531
4507
  for x in output:
4532
4508
  if all([c in orig for c in x.grain.components]):
4533
4509
  x.grain = Grain(
@@ -26,6 +26,7 @@ def resolve_function_parent_concepts(
26
26
  if not isinstance(concept.lineage, (Function, AggregateWrapper)):
27
27
  raise ValueError(f"Concept {concept} lineage is not function or aggregate")
28
28
  if concept.derivation == PurposeLineage.AGGREGATE:
29
+ base: list[Concept] = []
29
30
  if not concept.grain.abstract:
30
31
  base = concept.lineage.concept_arguments + [
31
32
  environment.concepts[c] for c in concept.grain.components
@@ -41,7 +42,7 @@ def resolve_function_parent_concepts(
41
42
  extra_property_grain = concept.lineage.concept_arguments
42
43
  for x in extra_property_grain:
43
44
  if isinstance(x, Concept) and x.purpose == Purpose.PROPERTY and x.keys:
44
- base += x.keys
45
+ base += [environment.concepts[c] for c in x.keys]
45
46
  return unique(base, "address")
46
47
  # TODO: handle basic lineage chains?
47
48
  return unique(concept.lineage.concept_arguments, "address")
@@ -81,7 +82,7 @@ def resolve_filter_parent_concepts(
81
82
  and direct_parent.purpose == Purpose.PROPERTY
82
83
  and direct_parent.keys
83
84
  ):
84
- base_rows += direct_parent.keys
85
+ base_rows += [environment.concepts[c] for c in direct_parent.keys]
85
86
  if concept.lineage.where.existence_arguments:
86
87
  return (
87
88
  concept.lineage.content,
@@ -106,7 +107,7 @@ def gen_property_enrichment_node(
106
107
  for x in extra_properties:
107
108
  if not x.keys:
108
109
  raise SyntaxError(f"Property {x.address} missing keys in lookup")
109
- keys = "-".join([y.address for y in x.keys])
110
+ keys = "-".join([y for y in x.keys])
110
111
  required_keys[keys].add(x.address)
111
112
  final_nodes = []
112
113
  for _k, vs in required_keys.items():
@@ -138,7 +138,7 @@ def gen_filter_node(
138
138
  )
139
139
  parent.grain = Grain.from_concepts(
140
140
  (
141
- list(immediate_parent.keys)
141
+ [environment.concepts[k] for k in immediate_parent.keys]
142
142
  if immediate_parent.keys
143
143
  else [immediate_parent]
144
144
  )
@@ -146,7 +146,8 @@ def gen_filter_node(
146
146
  x
147
147
  for x in local_optional
148
148
  if x.address in [y.address for y in parent.output_concepts]
149
- ]
149
+ ],
150
+ environment=environment,
150
151
  )
151
152
  parent.rebuild_cache()
152
153
  filter_node = parent
@@ -97,7 +97,6 @@ def gen_rowset_node(
97
97
  possible_joins = concept_to_relevant_joins(
98
98
  [x for x in node.output_concepts if x.derivation != PurposeLineage.ROWSET]
99
99
  )
100
- logger.info({x.address: x.keys for x in possible_joins})
101
100
  if not possible_joins:
102
101
  logger.info(
103
102
  f"{padding(depth)}{LOGGER_PREFIX} no possible joins for rowset node to get {[x.address for x in local_optional]}; have {[x.address for x in node.output_concepts]}"