pytrilogy 0.0.3.18__py3-none-any.whl → 0.0.3.20__py3-none-any.whl

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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: pytrilogy
3
- Version: 0.0.3.18
3
+ Version: 0.0.3.20
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -44,7 +44,7 @@ pytrilogy is an experimental implementation of the Trilogy language, a higher-le
44
44
  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.
45
45
 
46
46
  > [!TIP]
47
- > To get an overview of the language and run interactive examples, head to the [documentation](https://trilogydata.dev/).
47
+ > Try it online in a hosted [open-source studio](https://trilogydata.dev/trilogy-studio-core/). To get an overview of the language and run interactive examples, head to the [documentation](https://trilogydata.dev/).
48
48
 
49
49
  Installation: `pip install pytrilogy`
50
50
 
@@ -1,4 +1,4 @@
1
- trilogy/__init__.py,sha256=jrbWIot3RjwpdU7heWZocVfCD4BBtWw40jekbkQLvdg,303
1
+ trilogy/__init__.py,sha256=D0yQKpNnrX3HHQADMjE2XqRYe5vQ_m9_eUHfx-spBqs,303
2
2
  trilogy/compiler.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  trilogy/constants.py,sha256=qZ1d0hoKPPV2HHCoFwPYTVB7b6bXjpWvXd3lE-zEhy8,1494
4
4
  trilogy/engine.py,sha256=OK2RuqCIUId6yZ5hfF8J1nxGP0AJqHRZiafcowmW0xc,1728
@@ -10,7 +10,7 @@ trilogy/utility.py,sha256=euQccZLKoYBz0LNg5tzLlvv2YHvXh9HArnYp1V3uXsM,763
10
10
  trilogy/authoring/__init__.py,sha256=ohkYA3_LGYZh3fwzEYKTN6ofACDI5GYl3VCbGxVvlzY,2233
11
11
  trilogy/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
12
  trilogy/core/constants.py,sha256=7XaCpZn5mQmjTobbeBn56SzPWq9eMNDfzfsRU-fP0VE,171
13
- trilogy/core/enums.py,sha256=ND69oja7DOsZS2T8JlIuDW2-uKm74x9SJOWbAqNeopU,7137
13
+ trilogy/core/enums.py,sha256=wuW667WD3mhZnXmN2VXzohseHpdlmzrfLvPtQJNdhdw,7165
14
14
  trilogy/core/env_processor.py,sha256=pFsxnluKIusGKx1z7tTnfsd_xZcPy9pZDungkjkyvI0,3170
15
15
  trilogy/core/environment_helpers.py,sha256=oOpewPwMp8xOtx2ayeeyuLNUwr-cli7UanHKot5ebNY,7627
16
16
  trilogy/core/ergonomics.py,sha256=e-7gE29vPLFdg0_A1smQ7eOrUwKl5VYdxRSTddHweRA,1631
@@ -21,13 +21,13 @@ trilogy/core/internal.py,sha256=iicDBlC6nM8d7e7jqzf_ZOmpUsW8yrr2AA8AqEiLx-s,1577
21
21
  trilogy/core/optimization.py,sha256=xGO8piVsLrpqrx-Aid_Y56_5slSv4eZmlP64hCHRiEc,7957
22
22
  trilogy/core/query_processor.py,sha256=Do8YpdPBdsbKtl9n37hobzk8SORMGqH-e_zNNxd-BE4,19456
23
23
  trilogy/core/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
- trilogy/core/models/author.py,sha256=u2wS2rg3Qsqb5xNRUYvjgA7jOwQS2dMZzytlwJtglBs,70859
24
+ trilogy/core/models/author.py,sha256=gsn_vkm8gvnwTvzgqOL5v3X1Lx4n8xg32tRwk_9Mxnc,73494
25
25
  trilogy/core/models/build.py,sha256=bO1qYvuGl6LeNGgsfS6ZHAzZBR2lBPLg-QJymp9hgkU,57235
26
26
  trilogy/core/models/build_environment.py,sha256=8UggvlPU708GZWYPJMc_ou2r7M3TY2g69eqGvz03YX0,5528
27
27
  trilogy/core/models/core.py,sha256=nb4h1HHm5_qwmUkYth4zRhEttS1EtsMZCP4vT20EEAE,10326
28
28
  trilogy/core/models/datasource.py,sha256=6RjJUd2u4nYmEwFBpJlM9LbHVYDv8iHJxqiBMZqUrwI,9422
29
- trilogy/core/models/environment.py,sha256=4w1a9iHdRuOuSoZxz5Kzoy61Vjj4TZVXzKkCSP6c-Ow,26298
30
- trilogy/core/models/execute.py,sha256=kxkw14vgUudHHELXLo73AHiaMEpLfiv0qAvbxjJxn_k,33929
29
+ trilogy/core/models/environment.py,sha256=RlHNrRer4p1uSQM030iwGJL82M1hMyY5p8a550XTfUI,26606
30
+ trilogy/core/models/execute.py,sha256=4jbfwRt6Qv0kNzVU8b_z10Ln0Nk-CDvnbMEP1gCAbck,34204
31
31
  trilogy/core/optimizations/__init__.py,sha256=EBanqTXEzf1ZEYjAneIWoIcxtMDite5-n2dQ5xcfUtg,356
32
32
  trilogy/core/optimizations/base_optimization.py,sha256=gzDOKImoFn36k7XBD3ysEYDnbnb6vdVIztUfFQZsGnM,513
33
33
  trilogy/core/optimizations/inline_constant.py,sha256=lvNTIXaLNkw3HseJyXyDNk5R52doLU9sIg3pmU2_S08,1332
@@ -69,16 +69,16 @@ trilogy/core/statements/build.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hS
69
69
  trilogy/core/statements/common.py,sha256=KxEmz2ySySyZ6CTPzn0fJl5NX2KOk1RPyuUSwWhnK1g,759
70
70
  trilogy/core/statements/execute.py,sha256=cSlvpHFOqpiZ89pPZ5GDp9Hu6j6uj-5_h21FWm_L-KM,1248
71
71
  trilogy/dialect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
72
- trilogy/dialect/base.py,sha256=QnNK5fk8x2WBwOwnDqmWLD6cghLQg7e160G2Wtdrjkg,40338
72
+ trilogy/dialect/base.py,sha256=FE3JX0Q5c2ypJ-cweHFbwgHXYUN1zeDJ2rvZBr0hBPk,40415
73
73
  trilogy/dialect/bigquery.py,sha256=PkjFcNGZHYOe655PmJhb8a0afdFULuovqP0qQVO8m0I,2953
74
- trilogy/dialect/common.py,sha256=vYb-QPf_CnZ3mMLpOVjteWeLH1iaq2mn4WPx0XGoo20,4033
74
+ trilogy/dialect/common.py,sha256=oZr4EKYItbCeVA-vw-Q6Tv2-xy54s9vWaY6gevuQJIc,4619
75
75
  trilogy/dialect/config.py,sha256=EGYRQIbrkeMuud5Bkds7jSD5dCJR5hEYZUYcy-lYZl4,3308
76
76
  trilogy/dialect/dataframe.py,sha256=RUbNgReEa9g3pL6H7fP9lPTrAij5pkqedpZ99D8_5AE,1522
77
77
  trilogy/dialect/duckdb.py,sha256=TepCOhYWYw1oUuOT6ZGlB3l4X6S8rYcldWe3zZm3HoU,3710
78
78
  trilogy/dialect/enums.py,sha256=QYIcVr5RgpYMA1Wl0nWeojVVxJxy0V2_sn8uqSFNx20,4615
79
79
  trilogy/dialect/postgres.py,sha256=VH4EB4myjIeZTHeFU6vK00GxY9c53rCBjg2mLbdaCEE,3254
80
80
  trilogy/dialect/presto.py,sha256=Mw7_F8h19mWfuZHkHQJizQWbpu1lIHe6t2PA0r88gsY,3392
81
- trilogy/dialect/snowflake.py,sha256=wmao9p26jX5yIX5SC8sRAZTXkPGTvq6ixO693QTfhz8,2989
81
+ trilogy/dialect/snowflake.py,sha256=vc0374Og0O5OIB7-Z7jbwoJJg0iomjvnUqHlxM8B0rg,3120
82
82
  trilogy/dialect/sql_server.py,sha256=z2Vg7Qvw83rbGiEFIvHHLqVWJTWiz2xs76kpQj4HdTU,3131
83
83
  trilogy/hooks/__init__.py,sha256=T3SF3phuUDPLXKGRVE_Lf9mzuwoXWyaLolncR_1kY30,144
84
84
  trilogy/hooks/base_hook.py,sha256=I_l-NBMNC7hKTDx1JgHZPVOOCvLQ36m2oIGaR5EUMXY,1180
@@ -90,14 +90,14 @@ trilogy/parsing/common.py,sha256=99tDKrpQTk-SpyTXUqKFtm-lfmhjCOQIn25hxbQvRRg,214
90
90
  trilogy/parsing/config.py,sha256=Z-DaefdKhPDmSXLgg5V4pebhSB0h590vI0_VtHnlukI,111
91
91
  trilogy/parsing/exceptions.py,sha256=92E5i2frv5hj9wxObJZsZqj5T6bglvPzvdvco_vW1Zk,38
92
92
  trilogy/parsing/helpers.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
93
- trilogy/parsing/parse_engine.py,sha256=Hodpr2QjJNHSS5xGP-wPFjKK9gpygGG6loCikyRiwH8,60287
93
+ trilogy/parsing/parse_engine.py,sha256=kohD9ZTR8Atxd-Kctqsuu_QnLVJIz1xTs_6kmEFNa8U,59792
94
94
  trilogy/parsing/render.py,sha256=o_XuQWhcwx1lD9eGVqkqZEwkmQK0HdmWWokGBtdeH4I,17837
95
95
  trilogy/parsing/trilogy.lark,sha256=7libFS5HNiyHJYzr5_lEiY-Lpqit04_PgyIPHMZT7-w,12928
96
96
  trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
97
97
  trilogy/scripts/trilogy.py,sha256=1L0XrH4mVHRt1C9T1HnaDv2_kYEfbWTb5_-cBBke79w,3774
98
- pytrilogy-0.0.3.18.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
99
- pytrilogy-0.0.3.18.dist-info/METADATA,sha256=V6EpL5Z-9Xief5rqKaourYFLzmRMZH9jM31orwdgYAk,8984
100
- pytrilogy-0.0.3.18.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
101
- pytrilogy-0.0.3.18.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
102
- pytrilogy-0.0.3.18.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
103
- pytrilogy-0.0.3.18.dist-info/RECORD,,
98
+ pytrilogy-0.0.3.20.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
99
+ pytrilogy-0.0.3.20.dist-info/METADATA,sha256=LD5aYI8WQnZwSVATyt5cCjPv674PS1ca3TB2ZaS5jGE,9078
100
+ pytrilogy-0.0.3.20.dist-info/WHEEL,sha256=beeZ86-EfXScwlR_HKu4SllMC9wUEj_8Z_4FJ3egI2w,91
101
+ pytrilogy-0.0.3.20.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
102
+ pytrilogy-0.0.3.20.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
103
+ pytrilogy-0.0.3.20.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (76.0.0)
2
+ Generator: setuptools (76.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
trilogy/__init__.py CHANGED
@@ -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.18"
7
+ __version__ = "0.0.3.20"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
trilogy/core/enums.py CHANGED
@@ -8,6 +8,7 @@ class UnnestMode(Enum):
8
8
  CROSS_APPLY = "cross_apply"
9
9
  CROSS_JOIN = "cross_join"
10
10
  CROSS_JOIN_ALIAS = "cross_join_alias"
11
+ SNOWFLAKE = "snowflake"
11
12
 
12
13
 
13
14
  class ConceptSource(Enum):
@@ -318,6 +318,21 @@ class Conditional(Mergeable, ConceptArgs, Namespaced, BaseModel):
318
318
  operator=self.operator,
319
319
  )
320
320
 
321
+ def with_reference_replacement(self, source, target):
322
+ return self.__class__.model_construct(
323
+ left=(
324
+ self.left.with_reference_replacement(source, target)
325
+ if isinstance(self.left, Mergeable)
326
+ else self.left
327
+ ),
328
+ right=(
329
+ self.right.with_reference_replacement(source, target)
330
+ if isinstance(self.right, Mergeable)
331
+ else self.right
332
+ ),
333
+ operator=self.operator,
334
+ )
335
+
321
336
  @property
322
337
  def concept_arguments(self) -> Sequence[ConceptRef]:
323
338
  """Return concepts directly referenced in where clause"""
@@ -2135,6 +2150,52 @@ class AlignItem(Namespaced, BaseModel):
2135
2150
  )
2136
2151
 
2137
2152
 
2153
+ class CustomFunctionFactory:
2154
+ def __init__(
2155
+ self, function: Expr, namespace: str, function_arguments: list[ArgBinding]
2156
+ ):
2157
+ self.namespace = namespace
2158
+ self.function = function
2159
+ self.function_arguments = function_arguments
2160
+
2161
+ def with_namespace(self, namespace: str):
2162
+ self.namespace = namespace
2163
+ self.function = (
2164
+ self.function.with_namespace(namespace)
2165
+ if isinstance(self.function, Namespaced)
2166
+ else self.function
2167
+ )
2168
+ self.function_arguments = [
2169
+ x.with_namespace(namespace) for x in self.function_arguments
2170
+ ]
2171
+ return self
2172
+
2173
+ def __call__(self, *creation_args: list[Expr]):
2174
+ nout = (
2175
+ self.function.model_copy(deep=True)
2176
+ if isinstance(self.function, BaseModel)
2177
+ else self.function
2178
+ )
2179
+ creation_arg_list: list[Expr] = list(creation_args)
2180
+ if len(creation_args) < len(self.function_arguments):
2181
+ for binding in self.function_arguments[len(creation_arg_list) :]:
2182
+ if binding.default is None:
2183
+ raise ValueError(f"Missing argument {binding.name}")
2184
+ creation_arg_list.append(binding.default)
2185
+ if isinstance(nout, Mergeable):
2186
+ for idx, x in enumerate(creation_arg_list):
2187
+ if self.namespace == DEFAULT_NAMESPACE:
2188
+ target = f"{DEFAULT_NAMESPACE}.{self.function_arguments[idx].name}"
2189
+ else:
2190
+ target = self.function_arguments[idx].name
2191
+ nout = (
2192
+ nout.with_reference_replacement(target, x)
2193
+ if isinstance(nout, Mergeable)
2194
+ else nout
2195
+ )
2196
+ return nout
2197
+
2198
+
2138
2199
  class Metadata(BaseModel):
2139
2200
  """Metadata container object.
2140
2201
  TODO: support arbitrary tags"""
@@ -2164,10 +2225,20 @@ class Comment(BaseModel):
2164
2225
  text: str
2165
2226
 
2166
2227
 
2167
- class ArgBinding(BaseModel):
2228
+ class ArgBinding(Namespaced, BaseModel):
2168
2229
  name: str
2169
2230
  default: Expr | None = None
2170
2231
 
2232
+ def with_namespace(self, namespace):
2233
+ return ArgBinding(
2234
+ name=address_with_namespace(self.name, namespace),
2235
+ default=(
2236
+ self.default.with_namespace(namespace)
2237
+ if isinstance(self.default, Namespaced)
2238
+ else self.default
2239
+ ),
2240
+ )
2241
+
2171
2242
 
2172
2243
  class CustomType(BaseModel):
2173
2244
  name: str
@@ -9,7 +9,6 @@ from typing import (
9
9
  TYPE_CHECKING,
10
10
  Annotated,
11
11
  Any,
12
- Callable,
13
12
  Dict,
14
13
  ItemsView,
15
14
  List,
@@ -40,6 +39,7 @@ from trilogy.core.exceptions import (
40
39
  from trilogy.core.models.author import (
41
40
  Concept,
42
41
  ConceptRef,
42
+ CustomFunctionFactory,
43
43
  CustomType,
44
44
  Function,
45
45
  SelectLineage,
@@ -207,7 +207,7 @@ class Environment(BaseModel):
207
207
  datasources: Annotated[
208
208
  EnvironmentDatasourceDict, PlainValidator(validate_datasources)
209
209
  ] = Field(default_factory=EnvironmentDatasourceDict)
210
- functions: Dict[str, Callable[..., Any]] = Field(default_factory=dict)
210
+ functions: Dict[str, CustomFunctionFactory] = Field(default_factory=dict)
211
211
  data_types: Dict[str, CustomType] = Field(default_factory=dict)
212
212
  named_statements: Dict[str, SelectLineage] = Field(default_factory=dict)
213
213
  imports: Dict[str, list[Import]] = Field(
@@ -434,6 +434,14 @@ class Environment(BaseModel):
434
434
  self.alias_origin_lookup[address_with_namespace(key, alias)] = (
435
435
  val.with_namespace(alias)
436
436
  )
437
+
438
+ for key, function in source.functions.items():
439
+ if same_namespace:
440
+ self.functions[key] = function
441
+ else:
442
+ self.functions[address_with_namespace(key, alias)] = (
443
+ function.with_namespace(alias)
444
+ )
437
445
  return self
438
446
 
439
447
  def add_file_import(
@@ -59,6 +59,14 @@ class CTE(BaseModel):
59
59
  base_name_override: Optional[str] = None
60
60
  base_alias_override: Optional[str] = None
61
61
 
62
+ @field_validator("join_derived_concepts")
63
+ def validate_join_derived_concepts(cls, v):
64
+ if len(v) > 1:
65
+ raise NotImplementedError(
66
+ "Multiple join derived concepts not yet supported."
67
+ )
68
+ return unique(v, "address")
69
+
62
70
  @property
63
71
  def identifier(self):
64
72
  return self.name
trilogy/dialect/base.py CHANGED
@@ -694,6 +694,7 @@ class BaseDialect:
694
694
  UnnestMode.CROSS_APPLY,
695
695
  UnnestMode.CROSS_JOIN,
696
696
  UnnestMode.CROSS_JOIN_ALIAS,
697
+ UnnestMode.SNOWFLAKE,
697
698
  ):
698
699
  # for a cross apply, derivation happens in the join
699
700
  # so we only use the alias to select
@@ -722,7 +723,9 @@ class BaseDialect:
722
723
  UnnestMode.CROSS_JOIN_ALIAS,
723
724
  UnnestMode.CROSS_JOIN,
724
725
  UnnestMode.CROSS_APPLY,
726
+ UnnestMode.SNOWFLAKE,
725
727
  ):
728
+
726
729
  source = f"{render_unnest(self.UNNEST_MODE, self.QUOTE_CHARACTER, cte.join_derived_concepts[0], self.render_concept_sql, cte)}"
727
730
  # direct - eg DUCK DB - can be directly selected inline
728
731
  elif (
trilogy/dialect/common.py CHANGED
@@ -25,7 +25,12 @@ def render_unnest(
25
25
  ):
26
26
  if unnest_mode == UnnestMode.CROSS_JOIN:
27
27
  return f"{render_func(concept, cte, False)} as {quote_character}{concept.safe_address}{quote_character}"
28
- return f"{render_func(concept, cte, False)} as unnest_wrapper ({quote_character}{concept.safe_address}{quote_character})"
28
+ elif unnest_mode == UnnestMode.CROSS_JOIN_ALIAS:
29
+ return f"{render_func(concept, cte, False)} as unnest_wrapper ({quote_character}{concept.safe_address}{quote_character})"
30
+ elif unnest_mode == UnnestMode.SNOWFLAKE:
31
+
32
+ return f"flatten({render_func(concept, cte, False)}) as unnest_wrapper ( unnest1, unnest2, unnest3, unnest4, {quote_character}{cte.join_derived_concepts[0].safe_address}{quote_character})"
33
+ return f"{render_func(concept, cte, False)} as {quote_character}{concept.safe_address}{quote_character}"
29
34
 
30
35
 
31
36
  def render_join_concept(
@@ -67,6 +72,8 @@ def render_join(
67
72
  return f"CROSS JOIN {render_unnest(unnest_mode, quote_character, join.concept_to_unnest, render_func, cte)}"
68
73
  if unnest_mode == UnnestMode.CROSS_JOIN_ALIAS:
69
74
  return f"CROSS JOIN {render_unnest(unnest_mode, quote_character, join.concept_to_unnest, render_func, cte)}"
75
+ if unnest_mode == UnnestMode.SNOWFLAKE:
76
+ return f"LEFT JOIN LATERAL {render_unnest(unnest_mode, quote_character, join.concept_to_unnest, render_func, cte)}"
70
77
  return f"FULL JOIN {render_unnest(unnest_mode, quote_character, join.concept_to_unnest, render_func, cte)}"
71
78
  # left_name = join.left_name
72
79
  right_name = join.right_name
@@ -30,6 +30,8 @@ FUNCTION_MAP = {
30
30
  FunctionType.QUARTER: lambda x: f"EXTRACT(QUARTER from {x[0]})",
31
31
  # math
32
32
  FunctionType.DIVIDE: lambda x: f"DIV0({x[0]},{x[1]})",
33
+ FunctionType.UNNEST: lambda x: f"table(flatten({x[0]}))",
34
+ FunctionType.ARRAY: lambda x: f"ARRAY_CONSTRUCT({', '.join(x)})",
33
35
  }
34
36
 
35
37
  FUNCTION_GRAIN_MATCH_MAP = {
@@ -83,4 +85,4 @@ class SnowflakeDialect(BaseDialect):
83
85
  }
84
86
  QUOTE_CHARACTER = '"'
85
87
  SQL_TEMPLATE = BQ_SQL_TEMPLATE
86
- UNNEST_MODE = UnnestMode.CROSS_JOIN
88
+ UNNEST_MODE = UnnestMode.SNOWFLAKE
@@ -57,13 +57,13 @@ from trilogy.core.models.author import (
57
57
  Concept,
58
58
  ConceptRef,
59
59
  Conditional,
60
+ CustomFunctionFactory,
60
61
  CustomType,
61
62
  Expr,
62
63
  FilterItem,
63
64
  Function,
64
65
  Grain,
65
66
  HavingClause,
66
- Mergeable,
67
67
  Metadata,
68
68
  OrderBy,
69
69
  OrderItem,
@@ -983,7 +983,12 @@ class ParseToObjects(Transformer):
983
983
  text = self.resolve_import_address(target)
984
984
  self.text_lookup[token_lookup] = text
985
985
 
986
- raw_tokens = PARSER.parse(text)
986
+ try:
987
+ raw_tokens = PARSER.parse(text)
988
+ except Exception as e:
989
+ raise ImportError(
990
+ f"Unable to import '{target}', parsing error: {e}"
991
+ ) from e
987
992
  self.tokens[token_lookup] = raw_tokens
988
993
 
989
994
  if cache_lookup in self.parsed:
@@ -1198,23 +1203,11 @@ class ParseToObjects(Transformer):
1198
1203
  function_arguments: list[ArgBinding] = args[1]
1199
1204
  output = args[2]
1200
1205
 
1201
- def function_factory(*creation_args: list[Expr]):
1202
- nout = output.copy(deep=True)
1203
- creation_arg_list: list[Expr] = list(creation_args)
1204
- if len(creation_args) < len(function_arguments):
1205
- for binding in function_arguments[len(creation_arg_list) :]:
1206
- if binding.default is None:
1207
- raise ValueError(f"Missing argument {binding.name}")
1208
- creation_arg_list.append(binding.default)
1209
- if isinstance(nout, Mergeable):
1210
- for idx, x in enumerate(creation_arg_list):
1211
- # these will always be local namespace
1212
- nout = nout.with_reference_replacement(
1213
- f"{DEFAULT_NAMESPACE}.{function_arguments[idx].name}", x
1214
- )
1215
- return nout
1216
-
1217
- self.environment.functions[identity] = function_factory
1206
+ self.environment.functions[identity] = CustomFunctionFactory(
1207
+ function=output,
1208
+ namespace=self.environment.namespace,
1209
+ function_arguments=function_arguments,
1210
+ )
1218
1211
  return FunctionDeclaration(name=identity, args=function_arguments, expr=output)
1219
1212
 
1220
1213
  def custom_function(self, args):