relationalai 0.13.0.dev0__py3-none-any.whl → 0.13.2__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.
Files changed (838) hide show
  1. frontend/debugger/dist/.gitignore +2 -0
  2. frontend/debugger/dist/assets/favicon-Dy0ZgA6N.png +0 -0
  3. frontend/debugger/dist/assets/index-Cssla-O7.js +208 -0
  4. frontend/debugger/dist/assets/index-DlHsYx1V.css +9 -0
  5. frontend/debugger/dist/index.html +17 -0
  6. relationalai/__init__.py +256 -1
  7. relationalai/clients/__init__.py +18 -0
  8. relationalai/clients/client.py +947 -0
  9. relationalai/clients/config.py +673 -0
  10. relationalai/clients/direct_access_client.py +118 -0
  11. relationalai/clients/exec_txn_poller.py +91 -0
  12. relationalai/clients/hash_util.py +31 -0
  13. relationalai/clients/local.py +586 -0
  14. relationalai/clients/profile_polling.py +73 -0
  15. relationalai/clients/resources/__init__.py +8 -0
  16. relationalai/clients/resources/azure/azure.py +502 -0
  17. relationalai/clients/resources/snowflake/__init__.py +20 -0
  18. relationalai/clients/resources/snowflake/cli_resources.py +98 -0
  19. relationalai/clients/resources/snowflake/direct_access_resources.py +734 -0
  20. relationalai/clients/resources/snowflake/engine_service.py +381 -0
  21. relationalai/clients/resources/snowflake/engine_state_handlers.py +315 -0
  22. relationalai/clients/resources/snowflake/error_handlers.py +240 -0
  23. relationalai/clients/resources/snowflake/export_procedure.py.jinja +249 -0
  24. relationalai/clients/resources/snowflake/resources_factory.py +99 -0
  25. relationalai/clients/resources/snowflake/snowflake.py +3185 -0
  26. relationalai/clients/resources/snowflake/use_index_poller.py +1019 -0
  27. relationalai/clients/resources/snowflake/use_index_resources.py +188 -0
  28. relationalai/clients/resources/snowflake/util.py +387 -0
  29. relationalai/clients/result_helpers.py +420 -0
  30. relationalai/clients/types.py +118 -0
  31. relationalai/clients/util.py +356 -0
  32. relationalai/debugging.py +389 -0
  33. relationalai/dsl.py +1749 -0
  34. relationalai/early_access/builder/__init__.py +30 -0
  35. relationalai/early_access/builder/builder/__init__.py +35 -0
  36. relationalai/early_access/builder/snowflake/__init__.py +12 -0
  37. relationalai/early_access/builder/std/__init__.py +25 -0
  38. relationalai/early_access/builder/std/decimals/__init__.py +12 -0
  39. relationalai/early_access/builder/std/integers/__init__.py +12 -0
  40. relationalai/early_access/builder/std/math/__init__.py +12 -0
  41. relationalai/early_access/builder/std/strings/__init__.py +14 -0
  42. relationalai/early_access/devtools/__init__.py +12 -0
  43. relationalai/early_access/devtools/benchmark_lqp/__init__.py +12 -0
  44. relationalai/early_access/devtools/extract_lqp/__init__.py +12 -0
  45. relationalai/early_access/dsl/adapters/orm/adapter_qb.py +427 -0
  46. relationalai/early_access/dsl/adapters/orm/parser.py +636 -0
  47. relationalai/early_access/dsl/adapters/owl/adapter.py +176 -0
  48. relationalai/early_access/dsl/adapters/owl/parser.py +160 -0
  49. relationalai/early_access/dsl/bindings/common.py +402 -0
  50. relationalai/early_access/dsl/bindings/csv.py +170 -0
  51. relationalai/early_access/dsl/bindings/legacy/binding_models.py +143 -0
  52. relationalai/early_access/dsl/bindings/snowflake.py +64 -0
  53. relationalai/early_access/dsl/codegen/binder.py +411 -0
  54. relationalai/early_access/dsl/codegen/common.py +79 -0
  55. relationalai/early_access/dsl/codegen/helpers.py +23 -0
  56. relationalai/early_access/dsl/codegen/relations.py +700 -0
  57. relationalai/early_access/dsl/codegen/weaver.py +417 -0
  58. relationalai/early_access/dsl/core/builders/__init__.py +47 -0
  59. relationalai/early_access/dsl/core/builders/logic.py +19 -0
  60. relationalai/early_access/dsl/core/builders/scalar_constraint.py +11 -0
  61. relationalai/early_access/dsl/core/constraints/predicate/atomic.py +455 -0
  62. relationalai/early_access/dsl/core/constraints/predicate/universal.py +73 -0
  63. relationalai/early_access/dsl/core/constraints/scalar.py +310 -0
  64. relationalai/early_access/dsl/core/context.py +13 -0
  65. relationalai/early_access/dsl/core/cset.py +132 -0
  66. relationalai/early_access/dsl/core/exprs/__init__.py +116 -0
  67. relationalai/early_access/dsl/core/exprs/relational.py +18 -0
  68. relationalai/early_access/dsl/core/exprs/scalar.py +412 -0
  69. relationalai/early_access/dsl/core/instances.py +44 -0
  70. relationalai/early_access/dsl/core/logic/__init__.py +193 -0
  71. relationalai/early_access/dsl/core/logic/aggregation.py +98 -0
  72. relationalai/early_access/dsl/core/logic/exists.py +223 -0
  73. relationalai/early_access/dsl/core/logic/helper.py +163 -0
  74. relationalai/early_access/dsl/core/namespaces.py +32 -0
  75. relationalai/early_access/dsl/core/relations.py +276 -0
  76. relationalai/early_access/dsl/core/rules.py +112 -0
  77. relationalai/early_access/dsl/core/std/__init__.py +45 -0
  78. relationalai/early_access/dsl/core/temporal/recall.py +6 -0
  79. relationalai/early_access/dsl/core/types/__init__.py +270 -0
  80. relationalai/early_access/dsl/core/types/concepts.py +128 -0
  81. relationalai/early_access/dsl/core/types/constrained/__init__.py +267 -0
  82. relationalai/early_access/dsl/core/types/constrained/nominal.py +143 -0
  83. relationalai/early_access/dsl/core/types/constrained/subtype.py +124 -0
  84. relationalai/early_access/dsl/core/types/standard.py +92 -0
  85. relationalai/early_access/dsl/core/types/unconstrained.py +50 -0
  86. relationalai/early_access/dsl/core/types/variables.py +203 -0
  87. relationalai/early_access/dsl/ir/compiler.py +318 -0
  88. relationalai/early_access/dsl/ir/executor.py +260 -0
  89. relationalai/early_access/dsl/ontologies/constraints.py +88 -0
  90. relationalai/early_access/dsl/ontologies/export.py +30 -0
  91. relationalai/early_access/dsl/ontologies/models.py +453 -0
  92. relationalai/early_access/dsl/ontologies/python_printer.py +303 -0
  93. relationalai/early_access/dsl/ontologies/readings.py +60 -0
  94. relationalai/early_access/dsl/ontologies/relationships.py +322 -0
  95. relationalai/early_access/dsl/ontologies/roles.py +87 -0
  96. relationalai/early_access/dsl/ontologies/subtyping.py +55 -0
  97. relationalai/early_access/dsl/orm/constraints.py +438 -0
  98. relationalai/early_access/dsl/orm/measures/dimensions.py +200 -0
  99. relationalai/early_access/dsl/orm/measures/initializer.py +16 -0
  100. relationalai/early_access/dsl/orm/measures/measure_rules.py +275 -0
  101. relationalai/early_access/dsl/orm/measures/measures.py +299 -0
  102. relationalai/early_access/dsl/orm/measures/role_exprs.py +268 -0
  103. relationalai/early_access/dsl/orm/models.py +256 -0
  104. relationalai/early_access/dsl/orm/object_oriented_printer.py +344 -0
  105. relationalai/early_access/dsl/orm/printer.py +469 -0
  106. relationalai/early_access/dsl/orm/reasoners.py +480 -0
  107. relationalai/early_access/dsl/orm/relations.py +19 -0
  108. relationalai/early_access/dsl/orm/relationships.py +251 -0
  109. relationalai/early_access/dsl/orm/types.py +42 -0
  110. relationalai/early_access/dsl/orm/utils.py +79 -0
  111. relationalai/early_access/dsl/orm/verb.py +204 -0
  112. relationalai/early_access/dsl/physical_metadata/tables.py +133 -0
  113. relationalai/early_access/dsl/relations.py +170 -0
  114. relationalai/early_access/dsl/rulesets.py +69 -0
  115. relationalai/early_access/dsl/schemas/__init__.py +450 -0
  116. relationalai/early_access/dsl/schemas/builder.py +48 -0
  117. relationalai/early_access/dsl/schemas/comp_names.py +51 -0
  118. relationalai/early_access/dsl/schemas/components.py +203 -0
  119. relationalai/early_access/dsl/schemas/contexts.py +156 -0
  120. relationalai/early_access/dsl/schemas/exprs.py +89 -0
  121. relationalai/early_access/dsl/schemas/fragments.py +464 -0
  122. relationalai/early_access/dsl/serialization.py +79 -0
  123. relationalai/early_access/dsl/serialize/exporter.py +163 -0
  124. relationalai/early_access/dsl/snow/api.py +105 -0
  125. relationalai/early_access/dsl/snow/common.py +76 -0
  126. relationalai/early_access/dsl/state_mgmt/__init__.py +129 -0
  127. relationalai/early_access/dsl/state_mgmt/state_charts.py +125 -0
  128. relationalai/early_access/dsl/state_mgmt/transitions.py +130 -0
  129. relationalai/early_access/dsl/types/__init__.py +40 -0
  130. relationalai/early_access/dsl/types/concepts.py +12 -0
  131. relationalai/early_access/dsl/types/entities.py +135 -0
  132. relationalai/early_access/dsl/types/values.py +17 -0
  133. relationalai/early_access/dsl/utils.py +102 -0
  134. relationalai/early_access/graphs/__init__.py +13 -0
  135. relationalai/early_access/lqp/__init__.py +12 -0
  136. relationalai/early_access/lqp/compiler/__init__.py +12 -0
  137. relationalai/early_access/lqp/constructors/__init__.py +18 -0
  138. relationalai/early_access/lqp/executor/__init__.py +12 -0
  139. relationalai/early_access/lqp/ir/__init__.py +12 -0
  140. relationalai/early_access/lqp/passes/__init__.py +12 -0
  141. relationalai/early_access/lqp/pragmas/__init__.py +12 -0
  142. relationalai/early_access/lqp/primitives/__init__.py +12 -0
  143. relationalai/early_access/lqp/types/__init__.py +12 -0
  144. relationalai/early_access/lqp/utils/__init__.py +12 -0
  145. relationalai/early_access/lqp/validators/__init__.py +12 -0
  146. relationalai/early_access/metamodel/__init__.py +58 -0
  147. relationalai/early_access/metamodel/builtins/__init__.py +12 -0
  148. relationalai/early_access/metamodel/compiler/__init__.py +12 -0
  149. relationalai/early_access/metamodel/dependency/__init__.py +12 -0
  150. relationalai/early_access/metamodel/factory/__init__.py +17 -0
  151. relationalai/early_access/metamodel/helpers/__init__.py +12 -0
  152. relationalai/early_access/metamodel/ir/__init__.py +14 -0
  153. relationalai/early_access/metamodel/rewrite/__init__.py +7 -0
  154. relationalai/early_access/metamodel/typer/__init__.py +3 -0
  155. relationalai/early_access/metamodel/typer/typer/__init__.py +12 -0
  156. relationalai/early_access/metamodel/types/__init__.py +15 -0
  157. relationalai/early_access/metamodel/util/__init__.py +15 -0
  158. relationalai/early_access/metamodel/visitor/__init__.py +12 -0
  159. relationalai/early_access/rel/__init__.py +12 -0
  160. relationalai/early_access/rel/executor/__init__.py +12 -0
  161. relationalai/early_access/rel/rel_utils/__init__.py +12 -0
  162. relationalai/early_access/rel/rewrite/__init__.py +7 -0
  163. relationalai/early_access/solvers/__init__.py +19 -0
  164. relationalai/early_access/sql/__init__.py +11 -0
  165. relationalai/early_access/sql/executor/__init__.py +3 -0
  166. relationalai/early_access/sql/rewrite/__init__.py +3 -0
  167. relationalai/early_access/tests/logging/__init__.py +12 -0
  168. relationalai/early_access/tests/test_snapshot_base/__init__.py +12 -0
  169. relationalai/early_access/tests/utils/__init__.py +12 -0
  170. relationalai/environments/__init__.py +35 -0
  171. relationalai/environments/base.py +381 -0
  172. relationalai/environments/colab.py +14 -0
  173. relationalai/environments/generic.py +71 -0
  174. relationalai/environments/ipython.py +68 -0
  175. relationalai/environments/jupyter.py +9 -0
  176. relationalai/environments/snowbook.py +169 -0
  177. relationalai/errors.py +2496 -0
  178. relationalai/experimental/SF.py +38 -0
  179. relationalai/experimental/inspect.py +47 -0
  180. relationalai/experimental/pathfinder/__init__.py +158 -0
  181. relationalai/experimental/pathfinder/api.py +160 -0
  182. relationalai/experimental/pathfinder/automaton.py +584 -0
  183. relationalai/experimental/pathfinder/bridge.py +226 -0
  184. relationalai/experimental/pathfinder/compiler.py +416 -0
  185. relationalai/experimental/pathfinder/datalog.py +214 -0
  186. relationalai/experimental/pathfinder/diagnostics.py +56 -0
  187. relationalai/experimental/pathfinder/filter.py +236 -0
  188. relationalai/experimental/pathfinder/glushkov.py +439 -0
  189. relationalai/experimental/pathfinder/options.py +265 -0
  190. relationalai/experimental/pathfinder/pathfinder-v0.7.0.rel +1951 -0
  191. relationalai/experimental/pathfinder/rpq.py +344 -0
  192. relationalai/experimental/pathfinder/transition.py +200 -0
  193. relationalai/experimental/pathfinder/utils.py +26 -0
  194. relationalai/experimental/paths/README.md +107 -0
  195. relationalai/experimental/paths/api.py +143 -0
  196. relationalai/experimental/paths/benchmarks/grid_graph.py +37 -0
  197. relationalai/experimental/paths/code_organization.md +2 -0
  198. relationalai/experimental/paths/examples/Movies.ipynb +16328 -0
  199. relationalai/experimental/paths/examples/basic_example.py +40 -0
  200. relationalai/experimental/paths/examples/minimal_engine_warmup.py +3 -0
  201. relationalai/experimental/paths/examples/movie_example.py +77 -0
  202. relationalai/experimental/paths/examples/movies_data/actedin.csv +193 -0
  203. relationalai/experimental/paths/examples/movies_data/directed.csv +45 -0
  204. relationalai/experimental/paths/examples/movies_data/follows.csv +7 -0
  205. relationalai/experimental/paths/examples/movies_data/movies.csv +39 -0
  206. relationalai/experimental/paths/examples/movies_data/person.csv +134 -0
  207. relationalai/experimental/paths/examples/movies_data/produced.csv +16 -0
  208. relationalai/experimental/paths/examples/movies_data/ratings.csv +10 -0
  209. relationalai/experimental/paths/examples/movies_data/wrote.csv +11 -0
  210. relationalai/experimental/paths/examples/paths_benchmark.py +115 -0
  211. relationalai/experimental/paths/examples/paths_example.py +116 -0
  212. relationalai/experimental/paths/examples/pattern_to_automaton.py +28 -0
  213. relationalai/experimental/paths/find_paths_via_automaton.py +85 -0
  214. relationalai/experimental/paths/graph.py +185 -0
  215. relationalai/experimental/paths/path_algorithms/find_paths.py +280 -0
  216. relationalai/experimental/paths/path_algorithms/one_sided_ball_repetition.py +26 -0
  217. relationalai/experimental/paths/path_algorithms/one_sided_ball_upto.py +111 -0
  218. relationalai/experimental/paths/path_algorithms/single.py +59 -0
  219. relationalai/experimental/paths/path_algorithms/two_sided_balls_repetition.py +39 -0
  220. relationalai/experimental/paths/path_algorithms/two_sided_balls_upto.py +103 -0
  221. relationalai/experimental/paths/path_algorithms/usp-old.py +130 -0
  222. relationalai/experimental/paths/path_algorithms/usp-tuple.py +183 -0
  223. relationalai/experimental/paths/path_algorithms/usp.py +150 -0
  224. relationalai/experimental/paths/product_graph.py +93 -0
  225. relationalai/experimental/paths/rpq/automaton.py +584 -0
  226. relationalai/experimental/paths/rpq/diagnostics.py +56 -0
  227. relationalai/experimental/paths/rpq/rpq.py +378 -0
  228. relationalai/experimental/paths/tests/tests_limit_sp_max_length.py +90 -0
  229. relationalai/experimental/paths/tests/tests_limit_sp_multiple.py +119 -0
  230. relationalai/experimental/paths/tests/tests_limit_sp_single.py +104 -0
  231. relationalai/experimental/paths/tests/tests_limit_walks_multiple.py +113 -0
  232. relationalai/experimental/paths/tests/tests_limit_walks_single.py +149 -0
  233. relationalai/experimental/paths/tests/tests_one_sided_ball_repetition_multiple.py +70 -0
  234. relationalai/experimental/paths/tests/tests_one_sided_ball_repetition_single.py +64 -0
  235. relationalai/experimental/paths/tests/tests_one_sided_ball_upto_multiple.py +115 -0
  236. relationalai/experimental/paths/tests/tests_one_sided_ball_upto_single.py +75 -0
  237. relationalai/experimental/paths/tests/tests_single_paths.py +152 -0
  238. relationalai/experimental/paths/tests/tests_single_walks.py +208 -0
  239. relationalai/experimental/paths/tests/tests_single_walks_undirected.py +297 -0
  240. relationalai/experimental/paths/tests/tests_two_sided_balls_repetition_multiple.py +107 -0
  241. relationalai/experimental/paths/tests/tests_two_sided_balls_repetition_single.py +76 -0
  242. relationalai/experimental/paths/tests/tests_two_sided_balls_upto_multiple.py +76 -0
  243. relationalai/experimental/paths/tests/tests_two_sided_balls_upto_single.py +110 -0
  244. relationalai/experimental/paths/tests/tests_usp_nsp_multiple.py +229 -0
  245. relationalai/experimental/paths/tests/tests_usp_nsp_single.py +108 -0
  246. relationalai/experimental/paths/tree_agg.py +168 -0
  247. relationalai/experimental/paths/utilities/iterators.py +27 -0
  248. relationalai/experimental/paths/utilities/prefix_sum.py +91 -0
  249. relationalai/experimental/solvers.py +1087 -0
  250. relationalai/loaders/csv.py +195 -0
  251. relationalai/loaders/loader.py +177 -0
  252. relationalai/loaders/types.py +23 -0
  253. relationalai/rel_emitter.py +373 -0
  254. relationalai/rel_utils.py +185 -0
  255. relationalai/semantics/__init__.py +22 -146
  256. relationalai/semantics/designs/query_builder/identify_by.md +106 -0
  257. relationalai/semantics/devtools/benchmark_lqp.py +535 -0
  258. relationalai/semantics/devtools/compilation_manager.py +294 -0
  259. relationalai/semantics/devtools/extract_lqp.py +110 -0
  260. relationalai/semantics/internal/internal.py +3785 -0
  261. relationalai/semantics/internal/snowflake.py +325 -0
  262. relationalai/semantics/lqp/README.md +34 -0
  263. relationalai/semantics/lqp/builtins.py +16 -0
  264. relationalai/semantics/lqp/compiler.py +22 -0
  265. relationalai/semantics/lqp/constructors.py +68 -0
  266. relationalai/semantics/lqp/executor.py +469 -0
  267. relationalai/semantics/lqp/intrinsics.py +24 -0
  268. relationalai/semantics/lqp/model2lqp.py +877 -0
  269. relationalai/semantics/lqp/passes.py +680 -0
  270. relationalai/semantics/lqp/primitives.py +252 -0
  271. relationalai/semantics/lqp/result_helpers.py +202 -0
  272. relationalai/semantics/lqp/rewrite/annotate_constraints.py +57 -0
  273. relationalai/semantics/lqp/rewrite/cdc.py +216 -0
  274. relationalai/semantics/lqp/rewrite/extract_common.py +338 -0
  275. relationalai/semantics/lqp/rewrite/extract_keys.py +512 -0
  276. relationalai/semantics/lqp/rewrite/function_annotations.py +114 -0
  277. relationalai/semantics/lqp/rewrite/functional_dependencies.py +314 -0
  278. relationalai/semantics/lqp/rewrite/quantify_vars.py +296 -0
  279. relationalai/semantics/lqp/rewrite/splinter.py +76 -0
  280. relationalai/semantics/lqp/types.py +101 -0
  281. relationalai/semantics/lqp/utils.py +160 -0
  282. relationalai/semantics/lqp/validators.py +57 -0
  283. relationalai/semantics/metamodel/__init__.py +40 -6
  284. relationalai/semantics/metamodel/builtins.py +771 -205
  285. relationalai/semantics/metamodel/compiler.py +133 -0
  286. relationalai/semantics/metamodel/dependency.py +862 -0
  287. relationalai/semantics/metamodel/executor.py +61 -0
  288. relationalai/semantics/metamodel/factory.py +287 -0
  289. relationalai/semantics/metamodel/helpers.py +361 -0
  290. relationalai/semantics/metamodel/rewrite/discharge_constraints.py +39 -0
  291. relationalai/semantics/metamodel/rewrite/dnf_union_splitter.py +210 -0
  292. relationalai/semantics/metamodel/rewrite/extract_nested_logicals.py +78 -0
  293. relationalai/semantics/metamodel/rewrite/flatten.py +554 -0
  294. relationalai/semantics/metamodel/rewrite/format_outputs.py +165 -0
  295. relationalai/semantics/metamodel/typer/checker.py +353 -0
  296. relationalai/semantics/metamodel/typer/typer.py +1399 -0
  297. relationalai/semantics/metamodel/util.py +506 -0
  298. relationalai/semantics/reasoners/__init__.py +10 -0
  299. relationalai/semantics/reasoners/graph/README.md +620 -0
  300. relationalai/semantics/reasoners/graph/__init__.py +37 -0
  301. relationalai/semantics/reasoners/graph/core.py +9019 -0
  302. relationalai/semantics/reasoners/graph/design/beyond_demand_transform.md +797 -0
  303. relationalai/semantics/reasoners/graph/tests/README.md +21 -0
  304. relationalai/semantics/reasoners/optimization/__init__.py +68 -0
  305. relationalai/semantics/reasoners/optimization/common.py +88 -0
  306. relationalai/semantics/reasoners/optimization/solvers_dev.py +568 -0
  307. relationalai/semantics/reasoners/optimization/solvers_pb.py +1414 -0
  308. relationalai/semantics/rel/builtins.py +40 -0
  309. relationalai/semantics/rel/compiler.py +989 -0
  310. relationalai/semantics/rel/executor.py +362 -0
  311. relationalai/semantics/rel/rel.py +482 -0
  312. relationalai/semantics/rel/rel_utils.py +276 -0
  313. relationalai/semantics/snowflake/__init__.py +3 -0
  314. relationalai/semantics/sql/compiler.py +2503 -0
  315. relationalai/semantics/sql/executor/duck_db.py +52 -0
  316. relationalai/semantics/sql/executor/result_helpers.py +64 -0
  317. relationalai/semantics/sql/executor/snowflake.py +149 -0
  318. relationalai/semantics/sql/rewrite/denormalize.py +222 -0
  319. relationalai/semantics/sql/rewrite/double_negation.py +49 -0
  320. relationalai/semantics/sql/rewrite/recursive_union.py +127 -0
  321. relationalai/semantics/sql/rewrite/sort_output_query.py +246 -0
  322. relationalai/semantics/sql/sql.py +504 -0
  323. relationalai/semantics/std/__init__.py +40 -60
  324. relationalai/semantics/std/constraints.py +43 -37
  325. relationalai/semantics/std/datetime.py +135 -246
  326. relationalai/semantics/std/decimals.py +52 -45
  327. relationalai/semantics/std/floats.py +5 -13
  328. relationalai/semantics/std/integers.py +11 -26
  329. relationalai/semantics/std/math.py +112 -183
  330. relationalai/semantics/std/pragmas.py +11 -0
  331. relationalai/semantics/std/re.py +62 -80
  332. relationalai/semantics/std/std.py +14 -0
  333. relationalai/semantics/std/strings.py +60 -117
  334. relationalai/semantics/tests/test_snapshot_abstract.py +143 -0
  335. relationalai/semantics/tests/test_snapshot_base.py +9 -0
  336. relationalai/semantics/tests/utils.py +46 -0
  337. relationalai/std/__init__.py +70 -0
  338. relationalai/tools/cli.py +2089 -0
  339. relationalai/tools/cli_controls.py +1826 -0
  340. relationalai/tools/cli_helpers.py +802 -0
  341. relationalai/tools/debugger.py +183 -289
  342. relationalai/tools/debugger_client.py +109 -0
  343. relationalai/tools/debugger_server.py +302 -0
  344. relationalai/tools/dev.py +685 -0
  345. relationalai/tools/notes +7 -0
  346. relationalai/tools/qb_debugger.py +425 -0
  347. relationalai/util/clean_up_databases.py +95 -0
  348. relationalai/util/format.py +106 -48
  349. relationalai/util/list_databases.py +9 -0
  350. relationalai/util/otel_configuration.py +26 -0
  351. relationalai/util/otel_handler.py +484 -0
  352. relationalai/util/snowflake_handler.py +88 -0
  353. relationalai/util/span_format_test.py +43 -0
  354. relationalai/util/span_tracker.py +207 -0
  355. relationalai/util/spans_file_handler.py +72 -0
  356. relationalai/util/tracing_handler.py +34 -0
  357. relationalai-0.13.2.dist-info/METADATA +74 -0
  358. relationalai-0.13.2.dist-info/RECORD +460 -0
  359. relationalai-0.13.2.dist-info/WHEEL +4 -0
  360. relationalai-0.13.2.dist-info/entry_points.txt +3 -0
  361. relationalai-0.13.2.dist-info/licenses/LICENSE +202 -0
  362. relationalai_test_util/__init__.py +4 -0
  363. relationalai_test_util/fixtures.py +233 -0
  364. relationalai_test_util/snapshot.py +252 -0
  365. relationalai_test_util/traceback.py +118 -0
  366. relationalai/config/__init__.py +0 -56
  367. relationalai/config/config.py +0 -289
  368. relationalai/config/config_fields.py +0 -86
  369. relationalai/config/connections/__init__.py +0 -46
  370. relationalai/config/connections/base.py +0 -23
  371. relationalai/config/connections/duckdb.py +0 -29
  372. relationalai/config/connections/snowflake.py +0 -243
  373. relationalai/config/external/__init__.py +0 -17
  374. relationalai/config/external/dbt_converter.py +0 -101
  375. relationalai/config/external/dbt_models.py +0 -93
  376. relationalai/config/external/snowflake_converter.py +0 -41
  377. relationalai/config/external/snowflake_models.py +0 -85
  378. relationalai/config/external/utils.py +0 -19
  379. relationalai/semantics/backends/lqp/annotations.py +0 -11
  380. relationalai/semantics/backends/sql/sql_compiler.py +0 -327
  381. relationalai/semantics/frontend/base.py +0 -1707
  382. relationalai/semantics/frontend/core.py +0 -179
  383. relationalai/semantics/frontend/front_compiler.py +0 -1313
  384. relationalai/semantics/frontend/pprint.py +0 -408
  385. relationalai/semantics/metamodel/metamodel.py +0 -437
  386. relationalai/semantics/metamodel/metamodel_analyzer.py +0 -519
  387. relationalai/semantics/metamodel/metamodel_compiler.py +0 -0
  388. relationalai/semantics/metamodel/pprint.py +0 -412
  389. relationalai/semantics/metamodel/rewriter.py +0 -266
  390. relationalai/semantics/metamodel/typer.py +0 -1378
  391. relationalai/semantics/std/aggregates.py +0 -149
  392. relationalai/semantics/std/common.py +0 -44
  393. relationalai/semantics/std/numbers.py +0 -86
  394. relationalai/shims/executor.py +0 -147
  395. relationalai/shims/helpers.py +0 -126
  396. relationalai/shims/hoister.py +0 -221
  397. relationalai/shims/mm2v0.py +0 -1290
  398. relationalai/tools/cli/__init__.py +0 -6
  399. relationalai/tools/cli/cli.py +0 -90
  400. relationalai/tools/cli/components/__init__.py +0 -5
  401. relationalai/tools/cli/components/progress_reader.py +0 -1524
  402. relationalai/tools/cli/components/utils.py +0 -58
  403. relationalai/tools/cli/config_template.py +0 -45
  404. relationalai/tools/cli/dev.py +0 -19
  405. relationalai/tools/typer_debugger.py +0 -93
  406. relationalai/util/dataclasses.py +0 -43
  407. relationalai/util/docutils.py +0 -40
  408. relationalai/util/error.py +0 -199
  409. relationalai/util/naming.py +0 -145
  410. relationalai/util/python.py +0 -35
  411. relationalai/util/runtime.py +0 -156
  412. relationalai/util/schema.py +0 -197
  413. relationalai/util/source.py +0 -185
  414. relationalai/util/structures.py +0 -163
  415. relationalai/util/tracing.py +0 -261
  416. relationalai-0.13.0.dev0.dist-info/METADATA +0 -46
  417. relationalai-0.13.0.dev0.dist-info/RECORD +0 -488
  418. relationalai-0.13.0.dev0.dist-info/WHEEL +0 -5
  419. relationalai-0.13.0.dev0.dist-info/entry_points.txt +0 -3
  420. relationalai-0.13.0.dev0.dist-info/top_level.txt +0 -2
  421. v0/relationalai/__init__.py +0 -216
  422. v0/relationalai/clients/__init__.py +0 -5
  423. v0/relationalai/clients/azure.py +0 -477
  424. v0/relationalai/clients/client.py +0 -912
  425. v0/relationalai/clients/config.py +0 -673
  426. v0/relationalai/clients/direct_access_client.py +0 -118
  427. v0/relationalai/clients/hash_util.py +0 -31
  428. v0/relationalai/clients/local.py +0 -571
  429. v0/relationalai/clients/profile_polling.py +0 -73
  430. v0/relationalai/clients/result_helpers.py +0 -420
  431. v0/relationalai/clients/snowflake.py +0 -3869
  432. v0/relationalai/clients/types.py +0 -113
  433. v0/relationalai/clients/use_index_poller.py +0 -980
  434. v0/relationalai/clients/util.py +0 -356
  435. v0/relationalai/debugging.py +0 -389
  436. v0/relationalai/dsl.py +0 -1749
  437. v0/relationalai/early_access/builder/__init__.py +0 -30
  438. v0/relationalai/early_access/builder/builder/__init__.py +0 -35
  439. v0/relationalai/early_access/builder/snowflake/__init__.py +0 -12
  440. v0/relationalai/early_access/builder/std/__init__.py +0 -25
  441. v0/relationalai/early_access/builder/std/decimals/__init__.py +0 -12
  442. v0/relationalai/early_access/builder/std/integers/__init__.py +0 -12
  443. v0/relationalai/early_access/builder/std/math/__init__.py +0 -12
  444. v0/relationalai/early_access/builder/std/strings/__init__.py +0 -14
  445. v0/relationalai/early_access/devtools/__init__.py +0 -12
  446. v0/relationalai/early_access/devtools/benchmark_lqp/__init__.py +0 -12
  447. v0/relationalai/early_access/devtools/extract_lqp/__init__.py +0 -12
  448. v0/relationalai/early_access/dsl/adapters/orm/adapter_qb.py +0 -427
  449. v0/relationalai/early_access/dsl/adapters/orm/parser.py +0 -636
  450. v0/relationalai/early_access/dsl/adapters/owl/adapter.py +0 -176
  451. v0/relationalai/early_access/dsl/adapters/owl/parser.py +0 -160
  452. v0/relationalai/early_access/dsl/bindings/common.py +0 -402
  453. v0/relationalai/early_access/dsl/bindings/csv.py +0 -170
  454. v0/relationalai/early_access/dsl/bindings/legacy/binding_models.py +0 -143
  455. v0/relationalai/early_access/dsl/bindings/snowflake.py +0 -64
  456. v0/relationalai/early_access/dsl/codegen/binder.py +0 -411
  457. v0/relationalai/early_access/dsl/codegen/common.py +0 -79
  458. v0/relationalai/early_access/dsl/codegen/helpers.py +0 -23
  459. v0/relationalai/early_access/dsl/codegen/relations.py +0 -700
  460. v0/relationalai/early_access/dsl/codegen/weaver.py +0 -417
  461. v0/relationalai/early_access/dsl/core/builders/__init__.py +0 -47
  462. v0/relationalai/early_access/dsl/core/builders/logic.py +0 -19
  463. v0/relationalai/early_access/dsl/core/builders/scalar_constraint.py +0 -11
  464. v0/relationalai/early_access/dsl/core/constraints/predicate/atomic.py +0 -455
  465. v0/relationalai/early_access/dsl/core/constraints/predicate/universal.py +0 -73
  466. v0/relationalai/early_access/dsl/core/constraints/scalar.py +0 -310
  467. v0/relationalai/early_access/dsl/core/context.py +0 -13
  468. v0/relationalai/early_access/dsl/core/cset.py +0 -132
  469. v0/relationalai/early_access/dsl/core/exprs/__init__.py +0 -116
  470. v0/relationalai/early_access/dsl/core/exprs/relational.py +0 -18
  471. v0/relationalai/early_access/dsl/core/exprs/scalar.py +0 -412
  472. v0/relationalai/early_access/dsl/core/instances.py +0 -44
  473. v0/relationalai/early_access/dsl/core/logic/__init__.py +0 -193
  474. v0/relationalai/early_access/dsl/core/logic/aggregation.py +0 -98
  475. v0/relationalai/early_access/dsl/core/logic/exists.py +0 -223
  476. v0/relationalai/early_access/dsl/core/logic/helper.py +0 -163
  477. v0/relationalai/early_access/dsl/core/namespaces.py +0 -32
  478. v0/relationalai/early_access/dsl/core/relations.py +0 -276
  479. v0/relationalai/early_access/dsl/core/rules.py +0 -112
  480. v0/relationalai/early_access/dsl/core/std/__init__.py +0 -45
  481. v0/relationalai/early_access/dsl/core/temporal/recall.py +0 -6
  482. v0/relationalai/early_access/dsl/core/types/__init__.py +0 -270
  483. v0/relationalai/early_access/dsl/core/types/concepts.py +0 -128
  484. v0/relationalai/early_access/dsl/core/types/constrained/__init__.py +0 -267
  485. v0/relationalai/early_access/dsl/core/types/constrained/nominal.py +0 -143
  486. v0/relationalai/early_access/dsl/core/types/constrained/subtype.py +0 -124
  487. v0/relationalai/early_access/dsl/core/types/standard.py +0 -92
  488. v0/relationalai/early_access/dsl/core/types/unconstrained.py +0 -50
  489. v0/relationalai/early_access/dsl/core/types/variables.py +0 -203
  490. v0/relationalai/early_access/dsl/ir/compiler.py +0 -318
  491. v0/relationalai/early_access/dsl/ir/executor.py +0 -260
  492. v0/relationalai/early_access/dsl/ontologies/constraints.py +0 -88
  493. v0/relationalai/early_access/dsl/ontologies/export.py +0 -30
  494. v0/relationalai/early_access/dsl/ontologies/models.py +0 -453
  495. v0/relationalai/early_access/dsl/ontologies/python_printer.py +0 -303
  496. v0/relationalai/early_access/dsl/ontologies/readings.py +0 -60
  497. v0/relationalai/early_access/dsl/ontologies/relationships.py +0 -322
  498. v0/relationalai/early_access/dsl/ontologies/roles.py +0 -87
  499. v0/relationalai/early_access/dsl/ontologies/subtyping.py +0 -55
  500. v0/relationalai/early_access/dsl/orm/constraints.py +0 -438
  501. v0/relationalai/early_access/dsl/orm/measures/dimensions.py +0 -200
  502. v0/relationalai/early_access/dsl/orm/measures/initializer.py +0 -16
  503. v0/relationalai/early_access/dsl/orm/measures/measure_rules.py +0 -275
  504. v0/relationalai/early_access/dsl/orm/measures/measures.py +0 -299
  505. v0/relationalai/early_access/dsl/orm/measures/role_exprs.py +0 -268
  506. v0/relationalai/early_access/dsl/orm/models.py +0 -256
  507. v0/relationalai/early_access/dsl/orm/object_oriented_printer.py +0 -344
  508. v0/relationalai/early_access/dsl/orm/printer.py +0 -469
  509. v0/relationalai/early_access/dsl/orm/reasoners.py +0 -480
  510. v0/relationalai/early_access/dsl/orm/relations.py +0 -19
  511. v0/relationalai/early_access/dsl/orm/relationships.py +0 -251
  512. v0/relationalai/early_access/dsl/orm/types.py +0 -42
  513. v0/relationalai/early_access/dsl/orm/utils.py +0 -79
  514. v0/relationalai/early_access/dsl/orm/verb.py +0 -204
  515. v0/relationalai/early_access/dsl/physical_metadata/tables.py +0 -133
  516. v0/relationalai/early_access/dsl/relations.py +0 -170
  517. v0/relationalai/early_access/dsl/rulesets.py +0 -69
  518. v0/relationalai/early_access/dsl/schemas/__init__.py +0 -450
  519. v0/relationalai/early_access/dsl/schemas/builder.py +0 -48
  520. v0/relationalai/early_access/dsl/schemas/comp_names.py +0 -51
  521. v0/relationalai/early_access/dsl/schemas/components.py +0 -203
  522. v0/relationalai/early_access/dsl/schemas/contexts.py +0 -156
  523. v0/relationalai/early_access/dsl/schemas/exprs.py +0 -89
  524. v0/relationalai/early_access/dsl/schemas/fragments.py +0 -464
  525. v0/relationalai/early_access/dsl/serialization.py +0 -79
  526. v0/relationalai/early_access/dsl/serialize/exporter.py +0 -163
  527. v0/relationalai/early_access/dsl/snow/api.py +0 -104
  528. v0/relationalai/early_access/dsl/snow/common.py +0 -76
  529. v0/relationalai/early_access/dsl/state_mgmt/__init__.py +0 -129
  530. v0/relationalai/early_access/dsl/state_mgmt/state_charts.py +0 -125
  531. v0/relationalai/early_access/dsl/state_mgmt/transitions.py +0 -130
  532. v0/relationalai/early_access/dsl/types/__init__.py +0 -40
  533. v0/relationalai/early_access/dsl/types/concepts.py +0 -12
  534. v0/relationalai/early_access/dsl/types/entities.py +0 -135
  535. v0/relationalai/early_access/dsl/types/values.py +0 -17
  536. v0/relationalai/early_access/dsl/utils.py +0 -102
  537. v0/relationalai/early_access/graphs/__init__.py +0 -13
  538. v0/relationalai/early_access/lqp/__init__.py +0 -12
  539. v0/relationalai/early_access/lqp/compiler/__init__.py +0 -12
  540. v0/relationalai/early_access/lqp/constructors/__init__.py +0 -18
  541. v0/relationalai/early_access/lqp/executor/__init__.py +0 -12
  542. v0/relationalai/early_access/lqp/ir/__init__.py +0 -12
  543. v0/relationalai/early_access/lqp/passes/__init__.py +0 -12
  544. v0/relationalai/early_access/lqp/pragmas/__init__.py +0 -12
  545. v0/relationalai/early_access/lqp/primitives/__init__.py +0 -12
  546. v0/relationalai/early_access/lqp/types/__init__.py +0 -12
  547. v0/relationalai/early_access/lqp/utils/__init__.py +0 -12
  548. v0/relationalai/early_access/lqp/validators/__init__.py +0 -12
  549. v0/relationalai/early_access/metamodel/__init__.py +0 -58
  550. v0/relationalai/early_access/metamodel/builtins/__init__.py +0 -12
  551. v0/relationalai/early_access/metamodel/compiler/__init__.py +0 -12
  552. v0/relationalai/early_access/metamodel/dependency/__init__.py +0 -12
  553. v0/relationalai/early_access/metamodel/factory/__init__.py +0 -17
  554. v0/relationalai/early_access/metamodel/helpers/__init__.py +0 -12
  555. v0/relationalai/early_access/metamodel/ir/__init__.py +0 -14
  556. v0/relationalai/early_access/metamodel/rewrite/__init__.py +0 -7
  557. v0/relationalai/early_access/metamodel/typer/__init__.py +0 -3
  558. v0/relationalai/early_access/metamodel/typer/typer/__init__.py +0 -12
  559. v0/relationalai/early_access/metamodel/types/__init__.py +0 -15
  560. v0/relationalai/early_access/metamodel/util/__init__.py +0 -15
  561. v0/relationalai/early_access/metamodel/visitor/__init__.py +0 -12
  562. v0/relationalai/early_access/rel/__init__.py +0 -12
  563. v0/relationalai/early_access/rel/executor/__init__.py +0 -12
  564. v0/relationalai/early_access/rel/rel_utils/__init__.py +0 -12
  565. v0/relationalai/early_access/rel/rewrite/__init__.py +0 -7
  566. v0/relationalai/early_access/solvers/__init__.py +0 -19
  567. v0/relationalai/early_access/sql/__init__.py +0 -11
  568. v0/relationalai/early_access/sql/executor/__init__.py +0 -3
  569. v0/relationalai/early_access/sql/rewrite/__init__.py +0 -3
  570. v0/relationalai/early_access/tests/logging/__init__.py +0 -12
  571. v0/relationalai/early_access/tests/test_snapshot_base/__init__.py +0 -12
  572. v0/relationalai/early_access/tests/utils/__init__.py +0 -12
  573. v0/relationalai/environments/__init__.py +0 -35
  574. v0/relationalai/environments/base.py +0 -381
  575. v0/relationalai/environments/colab.py +0 -14
  576. v0/relationalai/environments/generic.py +0 -71
  577. v0/relationalai/environments/ipython.py +0 -68
  578. v0/relationalai/environments/jupyter.py +0 -9
  579. v0/relationalai/environments/snowbook.py +0 -169
  580. v0/relationalai/errors.py +0 -2455
  581. v0/relationalai/experimental/SF.py +0 -38
  582. v0/relationalai/experimental/inspect.py +0 -47
  583. v0/relationalai/experimental/pathfinder/__init__.py +0 -158
  584. v0/relationalai/experimental/pathfinder/api.py +0 -160
  585. v0/relationalai/experimental/pathfinder/automaton.py +0 -584
  586. v0/relationalai/experimental/pathfinder/bridge.py +0 -226
  587. v0/relationalai/experimental/pathfinder/compiler.py +0 -416
  588. v0/relationalai/experimental/pathfinder/datalog.py +0 -214
  589. v0/relationalai/experimental/pathfinder/diagnostics.py +0 -56
  590. v0/relationalai/experimental/pathfinder/filter.py +0 -236
  591. v0/relationalai/experimental/pathfinder/glushkov.py +0 -439
  592. v0/relationalai/experimental/pathfinder/options.py +0 -265
  593. v0/relationalai/experimental/pathfinder/rpq.py +0 -344
  594. v0/relationalai/experimental/pathfinder/transition.py +0 -200
  595. v0/relationalai/experimental/pathfinder/utils.py +0 -26
  596. v0/relationalai/experimental/paths/api.py +0 -143
  597. v0/relationalai/experimental/paths/benchmarks/grid_graph.py +0 -37
  598. v0/relationalai/experimental/paths/examples/basic_example.py +0 -40
  599. v0/relationalai/experimental/paths/examples/minimal_engine_warmup.py +0 -3
  600. v0/relationalai/experimental/paths/examples/movie_example.py +0 -77
  601. v0/relationalai/experimental/paths/examples/paths_benchmark.py +0 -115
  602. v0/relationalai/experimental/paths/examples/paths_example.py +0 -116
  603. v0/relationalai/experimental/paths/examples/pattern_to_automaton.py +0 -28
  604. v0/relationalai/experimental/paths/find_paths_via_automaton.py +0 -85
  605. v0/relationalai/experimental/paths/graph.py +0 -185
  606. v0/relationalai/experimental/paths/path_algorithms/find_paths.py +0 -280
  607. v0/relationalai/experimental/paths/path_algorithms/one_sided_ball_repetition.py +0 -26
  608. v0/relationalai/experimental/paths/path_algorithms/one_sided_ball_upto.py +0 -111
  609. v0/relationalai/experimental/paths/path_algorithms/single.py +0 -59
  610. v0/relationalai/experimental/paths/path_algorithms/two_sided_balls_repetition.py +0 -39
  611. v0/relationalai/experimental/paths/path_algorithms/two_sided_balls_upto.py +0 -103
  612. v0/relationalai/experimental/paths/path_algorithms/usp-old.py +0 -130
  613. v0/relationalai/experimental/paths/path_algorithms/usp-tuple.py +0 -183
  614. v0/relationalai/experimental/paths/path_algorithms/usp.py +0 -150
  615. v0/relationalai/experimental/paths/product_graph.py +0 -93
  616. v0/relationalai/experimental/paths/rpq/automaton.py +0 -584
  617. v0/relationalai/experimental/paths/rpq/diagnostics.py +0 -56
  618. v0/relationalai/experimental/paths/rpq/rpq.py +0 -378
  619. v0/relationalai/experimental/paths/tests/tests_limit_sp_max_length.py +0 -90
  620. v0/relationalai/experimental/paths/tests/tests_limit_sp_multiple.py +0 -119
  621. v0/relationalai/experimental/paths/tests/tests_limit_sp_single.py +0 -104
  622. v0/relationalai/experimental/paths/tests/tests_limit_walks_multiple.py +0 -113
  623. v0/relationalai/experimental/paths/tests/tests_limit_walks_single.py +0 -149
  624. v0/relationalai/experimental/paths/tests/tests_one_sided_ball_repetition_multiple.py +0 -70
  625. v0/relationalai/experimental/paths/tests/tests_one_sided_ball_repetition_single.py +0 -64
  626. v0/relationalai/experimental/paths/tests/tests_one_sided_ball_upto_multiple.py +0 -115
  627. v0/relationalai/experimental/paths/tests/tests_one_sided_ball_upto_single.py +0 -75
  628. v0/relationalai/experimental/paths/tests/tests_single_paths.py +0 -152
  629. v0/relationalai/experimental/paths/tests/tests_single_walks.py +0 -208
  630. v0/relationalai/experimental/paths/tests/tests_single_walks_undirected.py +0 -297
  631. v0/relationalai/experimental/paths/tests/tests_two_sided_balls_repetition_multiple.py +0 -107
  632. v0/relationalai/experimental/paths/tests/tests_two_sided_balls_repetition_single.py +0 -76
  633. v0/relationalai/experimental/paths/tests/tests_two_sided_balls_upto_multiple.py +0 -76
  634. v0/relationalai/experimental/paths/tests/tests_two_sided_balls_upto_single.py +0 -110
  635. v0/relationalai/experimental/paths/tests/tests_usp_nsp_multiple.py +0 -229
  636. v0/relationalai/experimental/paths/tests/tests_usp_nsp_single.py +0 -108
  637. v0/relationalai/experimental/paths/tree_agg.py +0 -168
  638. v0/relationalai/experimental/paths/utilities/iterators.py +0 -27
  639. v0/relationalai/experimental/paths/utilities/prefix_sum.py +0 -91
  640. v0/relationalai/experimental/solvers.py +0 -1087
  641. v0/relationalai/loaders/csv.py +0 -195
  642. v0/relationalai/loaders/loader.py +0 -177
  643. v0/relationalai/loaders/types.py +0 -23
  644. v0/relationalai/rel_emitter.py +0 -373
  645. v0/relationalai/rel_utils.py +0 -185
  646. v0/relationalai/semantics/__init__.py +0 -29
  647. v0/relationalai/semantics/devtools/benchmark_lqp.py +0 -536
  648. v0/relationalai/semantics/devtools/compilation_manager.py +0 -294
  649. v0/relationalai/semantics/devtools/extract_lqp.py +0 -110
  650. v0/relationalai/semantics/internal/internal.py +0 -3785
  651. v0/relationalai/semantics/internal/snowflake.py +0 -324
  652. v0/relationalai/semantics/lqp/builtins.py +0 -16
  653. v0/relationalai/semantics/lqp/compiler.py +0 -22
  654. v0/relationalai/semantics/lqp/constructors.py +0 -68
  655. v0/relationalai/semantics/lqp/executor.py +0 -469
  656. v0/relationalai/semantics/lqp/intrinsics.py +0 -24
  657. v0/relationalai/semantics/lqp/model2lqp.py +0 -839
  658. v0/relationalai/semantics/lqp/passes.py +0 -680
  659. v0/relationalai/semantics/lqp/primitives.py +0 -252
  660. v0/relationalai/semantics/lqp/result_helpers.py +0 -202
  661. v0/relationalai/semantics/lqp/rewrite/annotate_constraints.py +0 -57
  662. v0/relationalai/semantics/lqp/rewrite/cdc.py +0 -216
  663. v0/relationalai/semantics/lqp/rewrite/extract_common.py +0 -338
  664. v0/relationalai/semantics/lqp/rewrite/extract_keys.py +0 -449
  665. v0/relationalai/semantics/lqp/rewrite/function_annotations.py +0 -114
  666. v0/relationalai/semantics/lqp/rewrite/functional_dependencies.py +0 -314
  667. v0/relationalai/semantics/lqp/rewrite/quantify_vars.py +0 -296
  668. v0/relationalai/semantics/lqp/rewrite/splinter.py +0 -76
  669. v0/relationalai/semantics/lqp/types.py +0 -101
  670. v0/relationalai/semantics/lqp/utils.py +0 -160
  671. v0/relationalai/semantics/lqp/validators.py +0 -57
  672. v0/relationalai/semantics/metamodel/__init__.py +0 -40
  673. v0/relationalai/semantics/metamodel/builtins.py +0 -774
  674. v0/relationalai/semantics/metamodel/compiler.py +0 -133
  675. v0/relationalai/semantics/metamodel/dependency.py +0 -862
  676. v0/relationalai/semantics/metamodel/executor.py +0 -61
  677. v0/relationalai/semantics/metamodel/factory.py +0 -287
  678. v0/relationalai/semantics/metamodel/helpers.py +0 -361
  679. v0/relationalai/semantics/metamodel/rewrite/discharge_constraints.py +0 -39
  680. v0/relationalai/semantics/metamodel/rewrite/dnf_union_splitter.py +0 -210
  681. v0/relationalai/semantics/metamodel/rewrite/extract_nested_logicals.py +0 -78
  682. v0/relationalai/semantics/metamodel/rewrite/flatten.py +0 -549
  683. v0/relationalai/semantics/metamodel/rewrite/format_outputs.py +0 -165
  684. v0/relationalai/semantics/metamodel/typer/checker.py +0 -353
  685. v0/relationalai/semantics/metamodel/typer/typer.py +0 -1395
  686. v0/relationalai/semantics/metamodel/util.py +0 -505
  687. v0/relationalai/semantics/reasoners/__init__.py +0 -10
  688. v0/relationalai/semantics/reasoners/graph/__init__.py +0 -37
  689. v0/relationalai/semantics/reasoners/graph/core.py +0 -9020
  690. v0/relationalai/semantics/reasoners/optimization/__init__.py +0 -68
  691. v0/relationalai/semantics/reasoners/optimization/common.py +0 -88
  692. v0/relationalai/semantics/reasoners/optimization/solvers_dev.py +0 -568
  693. v0/relationalai/semantics/reasoners/optimization/solvers_pb.py +0 -1163
  694. v0/relationalai/semantics/rel/builtins.py +0 -40
  695. v0/relationalai/semantics/rel/compiler.py +0 -989
  696. v0/relationalai/semantics/rel/executor.py +0 -359
  697. v0/relationalai/semantics/rel/rel.py +0 -482
  698. v0/relationalai/semantics/rel/rel_utils.py +0 -276
  699. v0/relationalai/semantics/snowflake/__init__.py +0 -3
  700. v0/relationalai/semantics/sql/compiler.py +0 -2503
  701. v0/relationalai/semantics/sql/executor/duck_db.py +0 -52
  702. v0/relationalai/semantics/sql/executor/result_helpers.py +0 -64
  703. v0/relationalai/semantics/sql/executor/snowflake.py +0 -145
  704. v0/relationalai/semantics/sql/rewrite/denormalize.py +0 -222
  705. v0/relationalai/semantics/sql/rewrite/double_negation.py +0 -49
  706. v0/relationalai/semantics/sql/rewrite/recursive_union.py +0 -127
  707. v0/relationalai/semantics/sql/rewrite/sort_output_query.py +0 -246
  708. v0/relationalai/semantics/sql/sql.py +0 -504
  709. v0/relationalai/semantics/std/__init__.py +0 -54
  710. v0/relationalai/semantics/std/constraints.py +0 -43
  711. v0/relationalai/semantics/std/datetime.py +0 -363
  712. v0/relationalai/semantics/std/decimals.py +0 -62
  713. v0/relationalai/semantics/std/floats.py +0 -7
  714. v0/relationalai/semantics/std/integers.py +0 -22
  715. v0/relationalai/semantics/std/math.py +0 -141
  716. v0/relationalai/semantics/std/pragmas.py +0 -11
  717. v0/relationalai/semantics/std/re.py +0 -83
  718. v0/relationalai/semantics/std/std.py +0 -14
  719. v0/relationalai/semantics/std/strings.py +0 -63
  720. v0/relationalai/semantics/tests/__init__.py +0 -0
  721. v0/relationalai/semantics/tests/test_snapshot_abstract.py +0 -143
  722. v0/relationalai/semantics/tests/test_snapshot_base.py +0 -9
  723. v0/relationalai/semantics/tests/utils.py +0 -46
  724. v0/relationalai/std/__init__.py +0 -70
  725. v0/relationalai/tools/__init__.py +0 -0
  726. v0/relationalai/tools/cli.py +0 -1940
  727. v0/relationalai/tools/cli_controls.py +0 -1826
  728. v0/relationalai/tools/cli_helpers.py +0 -390
  729. v0/relationalai/tools/debugger.py +0 -183
  730. v0/relationalai/tools/debugger_client.py +0 -109
  731. v0/relationalai/tools/debugger_server.py +0 -302
  732. v0/relationalai/tools/dev.py +0 -685
  733. v0/relationalai/tools/qb_debugger.py +0 -425
  734. v0/relationalai/util/clean_up_databases.py +0 -95
  735. v0/relationalai/util/format.py +0 -123
  736. v0/relationalai/util/list_databases.py +0 -9
  737. v0/relationalai/util/otel_configuration.py +0 -25
  738. v0/relationalai/util/otel_handler.py +0 -484
  739. v0/relationalai/util/snowflake_handler.py +0 -88
  740. v0/relationalai/util/span_format_test.py +0 -43
  741. v0/relationalai/util/span_tracker.py +0 -207
  742. v0/relationalai/util/spans_file_handler.py +0 -72
  743. v0/relationalai/util/tracing_handler.py +0 -34
  744. /relationalai/{semantics/frontend → analysis}/__init__.py +0 -0
  745. {v0/relationalai → relationalai}/analysis/mechanistic.py +0 -0
  746. {v0/relationalai → relationalai}/analysis/whynot.py +0 -0
  747. /relationalai/{shims → auth}/__init__.py +0 -0
  748. {v0/relationalai → relationalai}/auth/jwt_generator.py +0 -0
  749. {v0/relationalai → relationalai}/auth/oauth_callback_server.py +0 -0
  750. {v0/relationalai → relationalai}/auth/token_handler.py +0 -0
  751. {v0/relationalai → relationalai}/auth/util.py +0 -0
  752. {v0/relationalai/clients → relationalai/clients/resources/snowflake}/cache_store.py +0 -0
  753. {v0/relationalai → relationalai}/compiler.py +0 -0
  754. {v0/relationalai → relationalai}/dependencies.py +0 -0
  755. {v0/relationalai → relationalai}/docutils.py +0 -0
  756. {v0/relationalai/analysis → relationalai/early_access}/__init__.py +0 -0
  757. {v0/relationalai → relationalai}/early_access/dsl/__init__.py +0 -0
  758. {v0/relationalai/auth → relationalai/early_access/dsl/adapters}/__init__.py +0 -0
  759. {v0/relationalai/early_access → relationalai/early_access/dsl/adapters/orm}/__init__.py +0 -0
  760. {v0/relationalai → relationalai}/early_access/dsl/adapters/orm/model.py +0 -0
  761. {v0/relationalai/early_access/dsl/adapters → relationalai/early_access/dsl/adapters/owl}/__init__.py +0 -0
  762. {v0/relationalai → relationalai}/early_access/dsl/adapters/owl/model.py +0 -0
  763. {v0/relationalai/early_access/dsl/adapters/orm → relationalai/early_access/dsl/bindings}/__init__.py +0 -0
  764. {v0/relationalai/early_access/dsl/adapters/owl → relationalai/early_access/dsl/bindings/legacy}/__init__.py +0 -0
  765. {v0/relationalai/early_access/dsl/bindings → relationalai/early_access/dsl/codegen}/__init__.py +0 -0
  766. {v0/relationalai → relationalai}/early_access/dsl/constants.py +0 -0
  767. {v0/relationalai → relationalai}/early_access/dsl/core/__init__.py +0 -0
  768. {v0/relationalai → relationalai}/early_access/dsl/core/constraints/__init__.py +0 -0
  769. {v0/relationalai → relationalai}/early_access/dsl/core/constraints/predicate/__init__.py +0 -0
  770. {v0/relationalai → relationalai}/early_access/dsl/core/stack.py +0 -0
  771. {v0/relationalai/early_access/dsl/bindings/legacy → relationalai/early_access/dsl/core/temporal}/__init__.py +0 -0
  772. {v0/relationalai → relationalai}/early_access/dsl/core/utils.py +0 -0
  773. {v0/relationalai/early_access/dsl/codegen → relationalai/early_access/dsl/ir}/__init__.py +0 -0
  774. {v0/relationalai/early_access/dsl/core/temporal → relationalai/early_access/dsl/ontologies}/__init__.py +0 -0
  775. {v0/relationalai → relationalai}/early_access/dsl/ontologies/raw_source.py +0 -0
  776. {v0/relationalai/early_access/dsl/ir → relationalai/early_access/dsl/orm}/__init__.py +0 -0
  777. {v0/relationalai/early_access/dsl/ontologies → relationalai/early_access/dsl/orm/measures}/__init__.py +0 -0
  778. {v0/relationalai → relationalai}/early_access/dsl/orm/reasoner_errors.py +0 -0
  779. {v0/relationalai/early_access/dsl/orm → relationalai/early_access/dsl/physical_metadata}/__init__.py +0 -0
  780. {v0/relationalai/early_access/dsl/orm/measures → relationalai/early_access/dsl/serialize}/__init__.py +0 -0
  781. {v0/relationalai → relationalai}/early_access/dsl/serialize/binding_model.py +0 -0
  782. {v0/relationalai → relationalai}/early_access/dsl/serialize/model.py +0 -0
  783. {v0/relationalai/early_access/dsl/physical_metadata → relationalai/early_access/dsl/snow}/__init__.py +0 -0
  784. {v0/relationalai → relationalai}/early_access/tests/__init__.py +0 -0
  785. {v0/relationalai → relationalai}/environments/ci.py +0 -0
  786. {v0/relationalai → relationalai}/environments/hex.py +0 -0
  787. {v0/relationalai → relationalai}/environments/terminal.py +0 -0
  788. {v0/relationalai → relationalai}/experimental/__init__.py +0 -0
  789. {v0/relationalai → relationalai}/experimental/graphs.py +0 -0
  790. {v0/relationalai → relationalai}/experimental/paths/__init__.py +0 -0
  791. {v0/relationalai → relationalai}/experimental/paths/benchmarks/__init__.py +0 -0
  792. {v0/relationalai → relationalai}/experimental/paths/path_algorithms/__init__.py +0 -0
  793. {v0/relationalai → relationalai}/experimental/paths/rpq/__init__.py +0 -0
  794. {v0/relationalai → relationalai}/experimental/paths/rpq/filter.py +0 -0
  795. {v0/relationalai → relationalai}/experimental/paths/rpq/glushkov.py +0 -0
  796. {v0/relationalai → relationalai}/experimental/paths/rpq/transition.py +0 -0
  797. {v0/relationalai → relationalai}/experimental/paths/utilities/__init__.py +0 -0
  798. {v0/relationalai → relationalai}/experimental/paths/utilities/utilities.py +0 -0
  799. {v0/relationalai/early_access/dsl/serialize → relationalai/loaders}/__init__.py +0 -0
  800. {v0/relationalai → relationalai}/metagen.py +0 -0
  801. {v0/relationalai → relationalai}/metamodel.py +0 -0
  802. {v0/relationalai → relationalai}/rel.py +0 -0
  803. {v0/relationalai → relationalai}/semantics/devtools/__init__.py +0 -0
  804. {v0/relationalai → relationalai}/semantics/internal/__init__.py +0 -0
  805. {v0/relationalai → relationalai}/semantics/internal/annotations.py +0 -0
  806. {v0/relationalai → relationalai}/semantics/lqp/__init__.py +0 -0
  807. {v0/relationalai → relationalai}/semantics/lqp/ir.py +0 -0
  808. {v0/relationalai → relationalai}/semantics/lqp/pragmas.py +0 -0
  809. {v0/relationalai → relationalai}/semantics/lqp/rewrite/__init__.py +0 -0
  810. {v0/relationalai → relationalai}/semantics/metamodel/dataflow.py +0 -0
  811. {v0/relationalai → relationalai}/semantics/metamodel/ir.py +0 -0
  812. {v0/relationalai → relationalai}/semantics/metamodel/rewrite/__init__.py +0 -0
  813. {v0/relationalai → relationalai}/semantics/metamodel/typer/__init__.py +0 -0
  814. {v0/relationalai → relationalai}/semantics/metamodel/types.py +0 -0
  815. {v0/relationalai → relationalai}/semantics/metamodel/visitor.py +0 -0
  816. {v0/relationalai → relationalai}/semantics/reasoners/experimental/__init__.py +0 -0
  817. {v0/relationalai → relationalai}/semantics/rel/__init__.py +0 -0
  818. {v0/relationalai → relationalai}/semantics/sql/__init__.py +0 -0
  819. {v0/relationalai → relationalai}/semantics/sql/executor/__init__.py +0 -0
  820. {v0/relationalai → relationalai}/semantics/sql/rewrite/__init__.py +0 -0
  821. {v0/relationalai/early_access/dsl/snow → relationalai/semantics/tests}/__init__.py +0 -0
  822. {v0/relationalai → relationalai}/semantics/tests/logging.py +0 -0
  823. {v0/relationalai → relationalai}/std/aggregates.py +0 -0
  824. {v0/relationalai → relationalai}/std/dates.py +0 -0
  825. {v0/relationalai → relationalai}/std/graphs.py +0 -0
  826. {v0/relationalai → relationalai}/std/inspect.py +0 -0
  827. {v0/relationalai → relationalai}/std/math.py +0 -0
  828. {v0/relationalai → relationalai}/std/re.py +0 -0
  829. {v0/relationalai → relationalai}/std/strings.py +0 -0
  830. {v0/relationalai/loaders → relationalai/tools}/__init__.py +0 -0
  831. {v0/relationalai → relationalai}/tools/cleanup_snapshots.py +0 -0
  832. {v0/relationalai → relationalai}/tools/constants.py +0 -0
  833. {v0/relationalai → relationalai}/tools/query_utils.py +0 -0
  834. {v0/relationalai → relationalai}/tools/snapshot_viewer.py +0 -0
  835. {v0/relationalai → relationalai}/util/__init__.py +0 -0
  836. {v0/relationalai → relationalai}/util/constants.py +0 -0
  837. {v0/relationalai → relationalai}/util/graph.py +0 -0
  838. {v0/relationalai → relationalai}/util/timeout.py +0 -0
@@ -0,0 +1,3785 @@
1
+ from __future__ import annotations
2
+ from contextlib import contextmanager
3
+ from contextvars import ContextVar
4
+ from dataclasses import dataclass
5
+ from enum import Enum, EnumMeta
6
+ import re
7
+ from typing import Any, Optional, Sequence as PySequence, Type, TypedDict, cast
8
+ import itertools
9
+ import rich
10
+ import sys
11
+
12
+ from pandas import DataFrame
13
+ import numpy as np
14
+ import pandas as pd
15
+ from more_itertools import peekable
16
+
17
+ from relationalai import debugging, errors
18
+ from relationalai.environments.base import find_external_frame
19
+ from relationalai.clients.config import Config
20
+ from relationalai.util.otel_configuration import configure_otel
21
+ from relationalai.clients.result_helpers import Int128Dtype
22
+ from relationalai.semantics.metamodel import factory as f, helpers, ir, builtins, types
23
+ from relationalai.semantics.metamodel.typer import typer
24
+ from relationalai.semantics.metamodel.util import NameCache, OrderedSet, ordered_set, FrozenOrderedSet
25
+ from relationalai.semantics.rel.executor import RelExecutor
26
+ from relationalai.semantics.lqp.executor import LQPExecutor
27
+ from relationalai.semantics.sql.executor import SnowflakeExecutor
28
+ from relationalai.environments import runtime_env, SessionEnvironment
29
+ from collections import Counter, defaultdict
30
+ from snowflake.snowpark import Session, DataFrame as SnowparkDataFrame
31
+
32
+ from datetime import date, datetime
33
+ from decimal import Decimal as PyDecimal
34
+
35
+ #--------------------------------------------------
36
+ # Globals
37
+ #--------------------------------------------------
38
+
39
+ _global_id = peekable(itertools.count(0))
40
+
41
+ # Single context variable with default values
42
+ _overrides = ContextVar("overrides", default = {})
43
+ def overrides(key: str, default: bool | str | dict | datetime | None):
44
+ return _overrides.get().get(key, default)
45
+
46
+ # Flag that users set in the config or directly on the model, but that can still be
47
+ # overridden globally. Precedence is overrides > model argument > config.
48
+ def overridable_flag(key: str, config: Config, user_pref: bool | None, default: bool):
49
+ if user_pref is not None:
50
+ preferred = cast(bool, user_pref)
51
+ else:
52
+ preferred = cast(bool, config.get(key, default))
53
+ return overrides(key, preferred)
54
+
55
+ @contextmanager
56
+ def with_overrides(**kwargs):
57
+ token = _overrides.set({**_overrides.get(), **kwargs})
58
+ try:
59
+ yield
60
+ finally:
61
+ _overrides.reset(token)
62
+
63
+ # Intrinsic values to override for stable snapshots.
64
+ def get_intrinsic_overrides() -> dict[str, Any]:
65
+ datetime_now = overrides('datetime_now', None)
66
+ if datetime_now is not None:
67
+ return {'datetime_now': datetime_now}
68
+ return {}
69
+
70
+ #--------------------------------------------------
71
+ # Root tracking
72
+ #--------------------------------------------------
73
+
74
+ _track_default = True
75
+ _track_roots = ContextVar('track_roots', default=_track_default)
76
+ _global_roots = ordered_set()
77
+
78
+ def _add_root(root):
79
+ if _track_roots.get():
80
+ _global_roots.add(root)
81
+
82
+ def _remove_roots(items: PySequence[Producer|Fragment]):
83
+ for item in items:
84
+ if hasattr(item, "__hash__") and item.__hash__ and item in _global_roots:
85
+ _global_roots.remove(item)
86
+
87
+ # decorator
88
+ def roots(enabled=_track_default):
89
+ def decorator(func):
90
+ def wrapper(*args, **kwargs):
91
+ token = _track_roots.set(enabled)
92
+ try:
93
+ return func(*args, **kwargs)
94
+ finally:
95
+ _track_roots.reset(token)
96
+ return wrapper
97
+ return decorator
98
+
99
+ # with root_tracking(enabled=False): ...
100
+ @contextmanager
101
+ def root_tracking(enabled=_track_default):
102
+ token = _track_roots.set(enabled)
103
+ try:
104
+ yield
105
+ finally:
106
+ _track_roots.reset(token)
107
+
108
+ #--------------------------------------------------
109
+ # Helpers
110
+ #--------------------------------------------------
111
+
112
+ def default_dir(obj):
113
+ """
114
+ This function returns the names of attributes from `__dict__` and relevant
115
+ parent's attributes. It's a simplification of what `dir()` does by default.
116
+ The intention is for `default_dir(obj)` to return what dir(obj) would
117
+ return if `obj.__dir__` was not defined.
118
+
119
+ There are some corner cases that are not handled because we don't need them,
120
+ e.g. obj being `None` or a type, or corner cases of `__slots__`.
121
+ """
122
+
123
+ attributes = set()
124
+
125
+ # Add attributes from the object's __dict__ if it exists
126
+ if hasattr(obj, '__dict__'):
127
+ attributes.update(obj.__dict__.keys())
128
+
129
+ # Add attributes from class and base classes
130
+ if hasattr(obj, '__class__'):
131
+ for cls in obj.__class__.__mro__:
132
+ attributes.update(cls.__dict__.keys())
133
+
134
+ return attributes
135
+
136
+
137
+ def unwrap_list(item:Any) -> Any:
138
+ if isinstance(item, (list, tuple)) and len(item) == 1:
139
+ return item[0]
140
+ elif isinstance(item, (list, tuple)) and len(item) > 1:
141
+ raise ValueError(f"Expected a single item, got {len(item)}")
142
+ return item
143
+
144
+ def flatten(items:PySequence[Any], flatten_tuples=False) -> list[Any]:
145
+ flat = []
146
+ for item in items:
147
+ if isinstance(item, (list, tuple)) and (flatten_tuples or not isinstance(item, TupleArg)):
148
+ flat.extend(flatten(item, flatten_tuples=flatten_tuples))
149
+ else:
150
+ flat.append(item)
151
+ return flat
152
+
153
+ def find_subjects(items: PySequence[Producer]) -> set[Concept|Ref]:
154
+ subjects = set()
155
+ for item in items:
156
+ if isinstance(item, Concept):
157
+ subjects.add(item)
158
+ elif isinstance(item, ConceptExpression):
159
+ subjects.add(item._op)
160
+ elif isinstance(item, Expression):
161
+ subjects.update(find_subjects(item._params))
162
+ elif isinstance(item, Ref):
163
+ subjects.add(item)
164
+ elif isinstance(item, Relationship) and item._parent:
165
+ subjects.update(find_subjects([item._parent]))
166
+ return subjects
167
+
168
+ def to_type(item: Any) -> Concept|None:
169
+ if isinstance(item, Concept):
170
+ return item
171
+ elif isinstance(item, (Ref, Alias, TypeRef)):
172
+ return to_type(item._thing)
173
+ elif isinstance(item, ConceptExpression):
174
+ return to_type(item._op)
175
+ elif isinstance(item, Expression):
176
+ return to_type(item._params[-1])
177
+
178
+ def find_local(name:str) -> Any:
179
+ frame = find_external_frame()
180
+ if frame and name in frame.f_locals:
181
+ return frame.f_locals[name]
182
+ return None
183
+
184
+ def field_to_type(model:Model|None, field: Field) -> Concept:
185
+ if field.type is not None:
186
+ return field.type
187
+ type_str = field.type_str
188
+ if type_str in python_types_str_to_concepts:
189
+ return python_types_str_to_concepts[type_str]
190
+ elif model and type_str in model.concepts:
191
+ concepts = model.concepts[type_str]
192
+ if len(concepts) > 1:
193
+ # this can be expensive, but is only done if the type_str is ambiguous
194
+ found = find_local(type_str)
195
+ if found in set(concepts):
196
+ return found
197
+ if not found:
198
+ raise ValueError(f"Ambiguous reference to Concept '{type_str}'")
199
+ raise ValueError(f"Reference '{type_str}' is not a valid Concept")
200
+ return concepts[0]
201
+ elif type_str in Concept.builtins:
202
+ return Concept.builtins[type_str]
203
+ elif type_str.lower() in Concept.globals:
204
+ return Concept.globals[type_str.lower()]
205
+ elif found := find_local(type_str):
206
+ if isinstance(found, Concept):
207
+ return found
208
+ elif hasattr(found, "_to_type") and callable(found._to_type):
209
+ return cast(Concept, found._to_type())
210
+ raise ValueError(f"Reference '{type_str}' is not a valid Concept")
211
+ elif type_str.startswith("Decimal"):
212
+ return decimal_concept_by_name(type_str)
213
+ else:
214
+ return Concept.builtins["Any"]
215
+
216
+ def to_name(item:Any) -> str:
217
+ if isinstance(item, Relationship) and isinstance(item._parent, Concept):
218
+ return f"{item._parent._name}_{item._name}"
219
+ elif isinstance(item, (Ref, Alias)):
220
+ return item._name or to_name(item._thing)
221
+ elif isinstance(item, RelationshipRef):
222
+ return item._relationship._name
223
+ elif isinstance(item, ConceptExpression):
224
+ return item._op._name.lower()
225
+ elif isinstance(item, Concept):
226
+ return item._name.lower()
227
+ return getattr(item, "_name", "v")
228
+
229
+ def find_model(items: Any) -> Model|None:
230
+ if isinstance(items, (list, tuple)):
231
+ for item in items:
232
+ model = find_model(item)
233
+ if model:
234
+ return model
235
+ elif isinstance(items, dict):
236
+ for item in items.values():
237
+ model = find_model(item)
238
+ if model:
239
+ return model
240
+ else:
241
+ if hasattr(items, "_model") and items._model:
242
+ return items._model
243
+ return None
244
+
245
+ def with_source(item:Any):
246
+ if not hasattr(item, "_source"):
247
+ raise ValueError(f"Item {item} has no source")
248
+ elif item._source is None:
249
+ return {}
250
+ elif debugging.DEBUG:
251
+ source = item._source.to_source_info()
252
+ if source:
253
+ return { "file": source.file, "line": source.line, "source": source.source }
254
+ else:
255
+ return {"file":item._source.file, "line":item._source.line}
256
+ else:
257
+ return {"file":item._source.file, "line":item._source.line}
258
+
259
+ def has_keys(item: Any) -> bool:
260
+ try:
261
+ return bool(len(find_keys(item)))
262
+ except Exception:
263
+ return False
264
+
265
+ def find_keys(item: Any, keys:OrderedSet[Any]|None = None) -> OrderedSet[Any]:
266
+ if keys is None:
267
+ keys = ordered_set()
268
+
269
+ if isinstance(item, (list, tuple)):
270
+ for it in item:
271
+ find_keys(it, keys)
272
+
273
+ elif isinstance(item, (Relationship, RelationshipReading)) and item._parent:
274
+ find_keys(item._parent, keys)
275
+ if item.is_many():
276
+ keys.add(item._field_refs[-1])
277
+
278
+ elif isinstance(item, RelationshipRef):
279
+ find_keys(item._parent, keys)
280
+ if item._relationship.is_many():
281
+ keys.add(item._field_refs[-1])
282
+
283
+ elif isinstance(item, (Relationship, RelationshipReading, Property)):
284
+ if item.is_many():
285
+ keys.update(item._field_refs)
286
+ else:
287
+ keys.add(item._field_refs[0])
288
+
289
+ elif isinstance(item, Concept):
290
+ if not item._is_primitive():
291
+ keys.add(item)
292
+
293
+ elif isinstance(item, ConceptExpression):
294
+ for it in item._params[1].values():
295
+ find_keys(it, keys)
296
+
297
+ elif isinstance(item, Ref):
298
+ if isinstance(item._thing, Concept):
299
+ if not item._thing._is_primitive():
300
+ keys.add(item)
301
+ else:
302
+ find_keys(item._thing, keys)
303
+
304
+ elif isinstance(item, RelationshipFieldRef):
305
+ find_keys(item._relationship, keys)
306
+
307
+ elif isinstance(item, ArgumentRef):
308
+ find_keys(item._arg, keys)
309
+
310
+ elif isinstance(item, TypeRef):
311
+ find_keys(item._thing, keys)
312
+
313
+ elif isinstance(item, Alias):
314
+ find_keys(item._thing, keys)
315
+
316
+ elif isinstance(item, Aggregate):
317
+ keys.update(item._group)
318
+
319
+ elif isinstance(item, Expression):
320
+ find_keys(item._params, keys)
321
+
322
+ elif isinstance(item, Data):
323
+ keys.add(item._row_id)
324
+
325
+ elif isinstance(item, DataColumn):
326
+ keys.add(item._data)
327
+
328
+ elif isinstance(item, BranchRef):
329
+ find_keys(item._match, keys)
330
+
331
+ elif isinstance(item, Match):
332
+ pass
333
+ elif isinstance(item, Distinct):
334
+ pass
335
+ elif isinstance(item, PY_LITERAL_TYPES):
336
+ pass
337
+ elif hasattr(item, "_to_keys"):
338
+ keys.update(item._to_keys())
339
+ else:
340
+ raise ValueError(f"Cannot find keys for {item}")
341
+
342
+ return keys
343
+
344
+
345
+ class Key:
346
+ def __init__(self, val:Any, is_group:bool = False):
347
+ self.val = val
348
+ self.is_group = is_group
349
+
350
+ def find_select_keys(item: Any, keys:OrderedSet[Key]|None = None, enable_primitive_key:bool = False) -> OrderedSet[Key]:
351
+ if keys is None:
352
+ keys = ordered_set()
353
+
354
+ if isinstance(item, (list, tuple)):
355
+ for it in item:
356
+ find_select_keys(it, keys, enable_primitive_key=enable_primitive_key)
357
+
358
+ elif isinstance(item, (Relationship, RelationshipReading)) and item._parent:
359
+ find_select_keys(item._parent, keys)
360
+ if item.is_many():
361
+ keys.add( Key(item._field_refs[-1]) )
362
+
363
+ elif isinstance(item, RelationshipRef):
364
+ find_select_keys(item._parent, keys)
365
+ if item._relationship.is_many():
366
+ keys.add( Key(item._field_refs[-1]) )
367
+
368
+ elif isinstance(item, (Relationship, RelationshipReading)):
369
+ if item.is_many():
370
+ for fld in item._field_refs:
371
+ keys.add( Key(fld) )
372
+ else:
373
+ keys.add( Key(item._field_refs[0]) )
374
+
375
+ elif isinstance(item, Concept):
376
+ if not item._is_primitive() or enable_primitive_key:
377
+ keys.add( Key(item) )
378
+
379
+ elif isinstance(item, ConceptExpression):
380
+ for it in item._params[1].values():
381
+ find_select_keys(it, keys)
382
+
383
+ elif isinstance(item, Ref):
384
+ if isinstance(item._thing, Concept):
385
+ if not item._thing._is_primitive() or enable_primitive_key:
386
+ keys.add( Key(item) )
387
+ else:
388
+ find_select_keys(item._thing, keys)
389
+
390
+ elif isinstance(item, TypeRef):
391
+ pass
392
+
393
+ elif isinstance(item, RelationshipFieldRef):
394
+ find_select_keys(item._relationship, keys)
395
+
396
+ elif isinstance(item, ArgumentRef):
397
+ find_select_keys(item._arg, keys)
398
+
399
+ elif isinstance(item, Alias):
400
+ find_select_keys(item._thing, keys, enable_primitive_key=enable_primitive_key)
401
+
402
+ elif isinstance(item, Aggregate):
403
+ keys.update( Key(k, True) for k in item._group )
404
+
405
+ elif isinstance(item, Expression):
406
+ find_select_keys(item._params, keys)
407
+
408
+ elif isinstance(item, Data):
409
+ keys.add( Key(item._row_id) )
410
+
411
+ elif isinstance(item, DataColumn):
412
+ keys.add( Key(item._data) )
413
+
414
+ elif isinstance(item, BranchRef):
415
+ find_select_keys(item._match, keys)
416
+
417
+ elif isinstance(item, Match):
418
+ pass
419
+ elif isinstance(item, Distinct):
420
+ pass
421
+ elif isinstance(item, PY_LITERAL_TYPES):
422
+ pass
423
+ elif hasattr(item, "_to_keys"):
424
+ for sub in item._to_keys():
425
+ find_select_keys(sub, keys)
426
+ else:
427
+ raise ValueError(f"Cannot find keys for {item}")
428
+
429
+ return keys
430
+
431
+
432
+ #--------------------------------------------------
433
+ # Producer
434
+ #--------------------------------------------------
435
+
436
+ class Producer:
437
+ def __init__(self, model:Model|None) -> None:
438
+ self._id = next(_global_id)
439
+ self._model = model
440
+
441
+ #--------------------------------------------------
442
+ # Infix operator overloads
443
+ #--------------------------------------------------
444
+
445
+ def _bin_op(self, op, left, right) -> Expression:
446
+ res = Number.ref("res")
447
+ return Expression(Relationship.builtins[op], left, right, res)
448
+
449
+ def __add__(self, other):
450
+ return self._bin_op("+", self, other)
451
+ def __radd__(self, other):
452
+ return self._bin_op("+", other, self)
453
+
454
+ def __mul__(self, other):
455
+ return self._bin_op("*", self, other)
456
+ def __rmul__(self, other):
457
+ return self._bin_op("*", other, self)
458
+
459
+ def __sub__(self, other):
460
+ return self._bin_op("-", self, other)
461
+ def __rsub__(self, other):
462
+ return self._bin_op("-", other, self)
463
+
464
+ def __truediv__(self, other):
465
+ return self._bin_op("/", self, other)
466
+ def __rtruediv__(self, other):
467
+ return self._bin_op("/", other, self)
468
+
469
+ def __floordiv__(self, other):
470
+ return self._bin_op("//", self, other)
471
+ def __rfloordiv__(self, other):
472
+ return self._bin_op("//", other, self)
473
+
474
+ def __pow__(self, other):
475
+ return self._bin_op("^", self, other)
476
+ def __rpow__(self, other):
477
+ return self._bin_op("^", other, self)
478
+
479
+ def __mod__(self, other):
480
+ return self._bin_op("%", self, other)
481
+ def __rmod__(self, other):
482
+ return self._bin_op("%", other, self)
483
+
484
+ def __neg__(self):
485
+ return self._bin_op("*", self, -1)
486
+
487
+ #--------------------------------------------------
488
+ # Filter overloads
489
+ #--------------------------------------------------
490
+
491
+ def _filter(self, op, left, right) -> Expression:
492
+ return Expression(Relationship.builtins[op], left, right)
493
+
494
+ def __gt__(self, other):
495
+ return self._filter(">", self, other)
496
+ def __ge__(self, other):
497
+ return self._filter(">=", self, other)
498
+ def __lt__(self, other):
499
+ return self._filter("<", self, other)
500
+ def __le__(self, other):
501
+ return self._filter("<=", self, other)
502
+ def __eq__(self, other) -> Any:
503
+ return self._filter("=", self, other)
504
+ def __ne__(self, other) -> Any:
505
+ return self._filter("!=", self, other)
506
+
507
+ #--------------------------------------------------
508
+ # And/Or
509
+ #--------------------------------------------------
510
+
511
+ def __or__(self, other) -> Match:
512
+ return Match(self, other)
513
+
514
+ def __and__(self, other) -> Fragment:
515
+ if isinstance(other, Fragment):
516
+ return other.where(self)
517
+ return where(self, other)
518
+
519
+ #--------------------------------------------------
520
+ # in_
521
+ #--------------------------------------------------
522
+
523
+ def in_(self, values:list[Any]|Fragment) -> Expression:
524
+ columns = None
525
+ if isinstance(values, Fragment):
526
+ return self == values
527
+ if not isinstance(values[0], tuple):
528
+ values = [tuple([v]) for v in values]
529
+ columns = [f"v{i}" for i in range(len(values[0]))]
530
+ d = data(values, columns)
531
+ return self == d[0]
532
+
533
+ #--------------------------------------------------
534
+ # Relationship handling
535
+ #--------------------------------------------------
536
+
537
+ def _get_relationship(self, name:str) -> Relationship|RelationshipRef|RelationshipFieldRef:
538
+ root_type:Concept = to_type(self) or Concept.builtins["Any"]
539
+ namer = NameCache()
540
+ cls = Relationship if root_type is Error else Property
541
+
542
+ r = cls(
543
+ f"{{{root_type}}} has {{{name}:Any}}",
544
+ parent=self,
545
+ short_name=name,
546
+ model=self._model,
547
+ field_refs=cast(list[Ref], [
548
+ root_type.ref(namer.get_name(1, Relationship._sanitize_field_name(root_type._name))),
549
+ Concept.builtins["Any"].ref(namer.get_name(2, name)),
550
+ ]),
551
+ )
552
+ # if we don't know the root type, then this relationship is unresolved and we're
553
+ # really just handing an anonymous relationship back that we expect to be resolved
554
+ # later
555
+ if root_type is Concept.builtins["Any"]:
556
+ r._unresolved = True
557
+ return r
558
+
559
+ #--------------------------------------------------
560
+ # dir and helpers
561
+ #--------------------------------------------------
562
+
563
+ def _dir_extras_from_get_relationship(self):
564
+ # Producer._get_relationship() does not distinguish on
565
+ # new/pre-existing, so we return nothing.
566
+ return set()
567
+
568
+ def _dir_extras_from_getattr(self):
569
+ """
570
+ Helper function for computing `__dir__`
571
+
572
+ See Also
573
+ --------
574
+ :meth:`Producer.__dir__`
575
+ """
576
+ attributes = set()
577
+ relationships = getattr(self, "_relationships", None)
578
+ if relationships is not None and isinstance(relationships, dict):
579
+ attributes.update(relationships.keys())
580
+
581
+ attributes.update(self._dir_extras_from_get_relationship())
582
+
583
+ return attributes
584
+
585
+ def __dir__(self):
586
+ """
587
+ This method provides hints runtime autocompletion in Jupyter, IPython or similar,
588
+ see https://docs.python.org/3/library/functions.html#dir.
589
+
590
+ Our implementation works as follows. We get the "hardcoded" attributes using
591
+ `default_dir`. For the dynamic ones, which are provided via `__getattr__`, we implement
592
+ `_dir_extras_from_getattr`, the idea being that the latter returns the set of strings
593
+ that are a "sensible" input to the former. The former often accepts any string, but
594
+ some values would than fail during compilation, so we mimic the behaviour of
595
+ `__getattr__` up to the point where it starts creating new objects. The `__getattr__`
596
+ often calls `_get_relationship`, due to the virtual dispatch of Python that can belong
597
+ to a different static type (parent or child) than the executed `__getattr__`. Hence,
598
+ to closely mimic the behaviour, we also define `_dir_extras_from_get_relationship`
599
+ which again returns the "sensible" inputs to `_get_relationship`.
600
+ """
601
+
602
+ return sorted(default_dir(self).union(self._dir_extras_from_getattr()))
603
+
604
+ #--------------------------------------------------
605
+ # getattr
606
+ #--------------------------------------------------
607
+
608
+ def __getattr__(self, name:str) -> Any:
609
+ if name.startswith("_"):
610
+ raise AttributeError(f"{type(self).__name__} has no attribute {name}")
611
+ if not hasattr(self, "_relationships"):
612
+ return super().__getattribute__(name)
613
+
614
+ if isinstance(self, (Concept, ConceptNew)):
615
+ concept = self._op if isinstance(self, ConceptNew) else self
616
+ topmost_parent = concept._get_topmost_parent()
617
+ if (concept is not Concept.builtins['Any'] and
618
+ not concept._is_enum() and
619
+ name not in concept._relationships and
620
+ not concept._has_inherited_relationship(name)):
621
+
622
+ if self._model and self._model._strict:
623
+ raise AttributeError(f"{self._name} has no relationship `{name}`")
624
+ if topmost_parent is not concept and topmost_parent not in Concept.builtin_concepts:
625
+ topmost_parent._relationships[name] = topmost_parent._get_relationship(name)
626
+ rich.print(f"[red bold][Implicit Subtype Relationship][/red bold] [yellow]{concept}.{name}[/yellow] appended to topmost parent [yellow]{topmost_parent}[/yellow] instead")
627
+
628
+ if name not in self._relationships:
629
+ self._relationships[name] = self._get_relationship(name)
630
+ return self._relationships[name]
631
+
632
+ def _has_inherited_relationship(self, name:str) -> bool:
633
+ if isinstance(self, Concept):
634
+ for parent in self._extends:
635
+ if not parent._is_primitive():
636
+ if parent._has_relationship(name):
637
+ return True
638
+ return False
639
+
640
+ def _has_relationship(self, name:str) -> bool:
641
+ if name in self._relationships:
642
+ return True
643
+ return self._has_inherited_relationship(name)
644
+
645
+ def __setattr__(self, name: str, value: Any) -> None:
646
+ if name.startswith("_"):
647
+ super().__setattr__(name, value)
648
+ elif isinstance(value, (Relationship, RelationshipReading)):
649
+ value._parent = self
650
+ if not value._passed_short_name:
651
+ value._passed_short_name = name
652
+ if name in self._relationships:
653
+ raise ValueError(f"Cannot set attribute {name} on {type(self).__name__} a second time. Make sure to set the relationship before any usages occur")
654
+ # update the first reading created implicitly
655
+ if isinstance(value, Relationship):
656
+ value._readings[0]._parent = value._parent
657
+ value._readings[0]._passed_short_name = value._passed_short_name
658
+ self._relationships[name] = value
659
+ else:
660
+ raise AttributeError(f"Cannot set attribute {name} on {type(self).__name__}")
661
+
662
+ #--------------------------------------------------
663
+ # ref + alias
664
+ #--------------------------------------------------
665
+
666
+ def ref(self, name:str|None=None) -> Ref|RelationshipRef:
667
+ return Ref(self, name=name)
668
+
669
+ def alias(self, name:str) -> Alias:
670
+ return Alias(self, name)
671
+
672
+ #--------------------------------------------------
673
+ # Find model
674
+ #--------------------------------------------------
675
+
676
+ def _find_model(self, items:list[Any]) -> Model|None:
677
+ if self._model:
678
+ return self._model
679
+
680
+ for item in items:
681
+ if isinstance(item, (Producer, Fragment)) and item._model:
682
+ self._model = item._model
683
+ return item._model
684
+ return None
685
+
686
+ #--------------------------------------------------
687
+ # Hash
688
+ #--------------------------------------------------
689
+
690
+ __hash__ = object.__hash__
691
+
692
+ #--------------------------------------------------
693
+ # _pprint
694
+ #--------------------------------------------------
695
+
696
+ def _pprint(self, indent:int=0) -> str:
697
+ return str(self)
698
+
699
+ #--------------------------------------------------
700
+ # Fallbacks
701
+ #--------------------------------------------------
702
+
703
+ def select(self, *args: Any):
704
+ raise NotImplementedError(f"`{type(self).__name__}.select` not implemented")
705
+
706
+ def where(self, *args: Any):
707
+ raise NotImplementedError(f"`{type(self).__name__}.where` not implemented")
708
+
709
+ def require(self, *args: Any):
710
+ raise NotImplementedError(f"`{type(self).__name__}.require` not implemented")
711
+
712
+ def define(self, *args: Any):
713
+ raise NotImplementedError(f"`{type(self).__name__}.then` not implemented")
714
+
715
+ #--------------------------------------------------
716
+ # Ref
717
+ #--------------------------------------------------
718
+
719
+ class Ref(Producer):
720
+ def __init__(self, thing:Producer, name:str|None=None):
721
+ super().__init__(thing._model)
722
+ self._thing = thing
723
+ self._name = name
724
+ self._no_lookup = False
725
+ self._relationships = {}
726
+
727
+ def _dir_extras_from_get_relationship(self):
728
+ return self._thing._dir_extras_from_getattr()
729
+
730
+ def _get_relationship(self, name: str) -> Relationship | RelationshipRef:
731
+ rel = getattr(self._thing, name)
732
+ return RelationshipRef(self, rel)
733
+
734
+ def __str__(self) -> str:
735
+ if self._name:
736
+ return f"{self._name}{self._id}"
737
+ return f"{self._thing}{self._id}"
738
+
739
+ class TypeRef(Producer):
740
+ """ A reference to the type of a Producer. """
741
+
742
+ def __init__(self, thing:Producer):
743
+ super().__init__(thing._model)
744
+ self._thing = thing
745
+
746
+ def __str__(self) -> str:
747
+ return f"typeof({self._thing})"
748
+
749
+ class ArgumentRef(Producer):
750
+ """ Represents a reference to an argument of an Expression.
751
+ Useful when you need to reuse arguments in another Expression
752
+ while maintaining a link to the original Expression during compilation.
753
+ """
754
+
755
+ def __init__(self, expr:Expression, arg:Producer):
756
+ super().__init__(expr._model)
757
+ self._expr = expr
758
+ self._arg = arg
759
+
760
+ def __str__(self) -> str:
761
+ return f"{self._arg}{self._id}"
762
+
763
+ class RelationshipRef(Producer):
764
+ def __init__(self, parent:Any, relationship:Relationship|RelationshipRef, name:str|None=None):
765
+ super().__init__(find_model([parent, relationship]))
766
+ self._parent = parent
767
+ if isinstance(relationship, RelationshipRef):
768
+ relationship = relationship._relationship
769
+ self._relationship:Relationship = relationship
770
+ self._field_refs = [r.ref() for r in relationship._field_refs]
771
+ if name:
772
+ self._field_refs[-1].name = name
773
+ self._relationships = {}
774
+
775
+ def _dir_extras_from_get_relationship(self):
776
+ return self._relationship._dir_extras_from_getattr()
777
+
778
+ def _get_relationship(self, name: str) -> Relationship|RelationshipRef|RelationshipFieldRef:
779
+ rel = self._relationship._get_relationship(name)
780
+ if isinstance(rel, Relationship):
781
+ return RelationshipRef(self, rel)
782
+ elif isinstance(rel, RelationshipFieldRef):
783
+ return RelationshipFieldRef(self, rel._relationship, rel._field_ix)
784
+ else:
785
+ return RelationshipRef(self, rel)
786
+
787
+
788
+ def __call__(self, *args: Any, **kwargs) -> Any:
789
+ if kwargs and args:
790
+ raise ValueError("Cannot use both positional and keyword arguments")
791
+ if kwargs:
792
+ # check that all fields have been provided
793
+ clean_args = []
794
+ for ix, field in enumerate(self._relationship._field_names):
795
+ if field in kwargs:
796
+ clean_args.append(kwargs.get(field))
797
+ if ix == 0 and self._parent:
798
+ continue
799
+ if field not in kwargs:
800
+ raise ValueError(f"Missing argument {field}")
801
+ else:
802
+ clean_args = list(args)
803
+ if len(clean_args) < self._relationship._arity():
804
+ if self._parent:
805
+ clean_args = [self._parent, *clean_args]
806
+ if len(clean_args) != self._relationship._arity():
807
+ raise ValueError(f"Expected {self._relationship._arity()} arguments, got {len(clean_args)}")
808
+ return Expression(self._relationship, *clean_args)
809
+
810
+ def __str__(self) -> str:
811
+ return f"{self._parent}.{self._relationship._short_name}"
812
+
813
+ class RelationshipFieldRef(Producer):
814
+ def __init__(self, parent:Any, relationship:Relationship|RelationshipRef|RelationshipReading, field_ix:int):
815
+ super().__init__(find_model([relationship]))
816
+ self._parent = parent
817
+ if isinstance(relationship, RelationshipRef):
818
+ relationship = relationship._relationship
819
+ self._relationship:Relationship|RelationshipReading = relationship
820
+ self._field_ix = field_ix
821
+ self._relationships = {}
822
+
823
+ @property
824
+ def _field_ref(self) -> Ref|RelationshipRef:
825
+ return self._relationship._field_refs[self._field_ix]
826
+
827
+ @property
828
+ def _concept(self) -> Concept:
829
+ return field_to_type(self._model, self._relationship._fields[self._field_ix])
830
+
831
+ def _dir_extras_from_get_relationship(self):
832
+ return self._field_ref._dir_extras_from_getattr()
833
+
834
+ def _get_relationship(self, name: str) -> Relationship | RelationshipRef:
835
+ rel = getattr(self._field_ref, name)
836
+ return RelationshipRef(self, rel)
837
+
838
+ def __call__(self, arg: Any) -> Any:
839
+ return self == arg
840
+
841
+ def __str__(self) -> str:
842
+ return f"{self._relationship}.{self._field_ref}"
843
+
844
+
845
+ #--------------------------------------------------
846
+ # typed dicts for annotating ref scheme hierarchy
847
+ #--------------------------------------------------
848
+
849
+ # We define a hierarchy of two dicts, one allowing its key(s) to be ommitted,
850
+ # and another which extends the first with mandatory keys.
851
+ # Up until Python 3.10, this is the only way to create a TypedDict
852
+ # which contains both mandatory and non-mandatory keys.
853
+ # Once we stop supporting Python 3.10, this can
854
+ # be simplified by using Required and NotRequired
855
+ # https://peps.python.org/pep-0655/#motivation
856
+
857
+ class RefSchemeHierarchyParentDict(TypedDict, total=False):
858
+ mapping: Relationship
859
+ class RefSchemeHierarchyDict(RefSchemeHierarchyParentDict, total=True):
860
+ concept: Concept
861
+ scheme: tuple[Relationship|RelationshipReading, ...]
862
+
863
+ #--------------------------------------------------
864
+ # Concept
865
+ #--------------------------------------------------
866
+
867
+ class Concept(Producer):
868
+ # Concept instances created for metamodel builtin types
869
+ builtin_concepts = set()
870
+ # The concepts from above, indexed by name
871
+ builtins = {}
872
+
873
+ globals = {}
874
+
875
+ @staticmethod
876
+ def _validate_concept_name(name: str):
877
+ """
878
+ Validate that a concept name matches the expected format:
879
+ - Format A: [a-zA-Z0-9_.]+
880
+ - Format B: [a-zA-Z0-9_.]+\\([0-9]+,[0-9]+\\) (like Decimal(38,14))
881
+ where the leading character(s) are not underscores.
882
+ """
883
+ if name.startswith("_"):
884
+ raise ValueError("Concept names cannot start with '_'")
885
+
886
+ # Check if it matches either allowed format
887
+ pattern_a = r'^[a-zA-Z0-9_."-]+$'
888
+ pattern_b = r'^[a-zA-Z0-9_."-]+\([0-9]+,[0-9]+\)$'
889
+
890
+ if not (re.match(pattern_a, name) or re.match(pattern_b, name)):
891
+ raise ValueError(f"Concept name '{name}' contains invalid characters. "
892
+ f"Names must contain only letters, digits, dots, double quotes, hyphens, and underscores, "
893
+ f"optionally followed by precision/scale in parentheses like 'Decimal(38,14)'")
894
+
895
+ def __init__(self, name:str, extends:list[Any] = [], model:Model|None=None, identify_by:dict[str, Any]={}):
896
+ super().__init__(model)
897
+
898
+ self._validate_concept_name(name)
899
+ self._name = name
900
+ self._relationships = {}
901
+ self._extends : list[Concept] = []
902
+ self._reference_schemes: list[tuple[Relationship|RelationshipReading, ...]] = []
903
+ self._scheme_mapping:dict[Concept, Relationship] = {}
904
+
905
+ for e in extends:
906
+ if isinstance(e, Concept):
907
+ self._extends.append(e)
908
+ elif python_types_to_concepts.get(e):
909
+ self._extends.append(python_types_to_concepts[e])
910
+ else:
911
+ raise ValueError(f"Unknown concept {e} in extends")
912
+
913
+ if identify_by:
914
+ scheme = []
915
+ for k, v in identify_by.items():
916
+ if python_types_to_concepts.get(v):
917
+ v = python_types_to_concepts[v]
918
+ if isinstance(v, Concept):
919
+ setattr(self, k, Property(f"{{{self._name}}} has {{{k}:{v._name}}}", parent=self, short_name=k, model=self._model))
920
+ elif isinstance(v, type) and issubclass(v, self._model.Enum): #type: ignore
921
+ setattr(self, k, Property(f"{{{self._name}}} has {{{k}:{v._concept._name}}}", parent=self, short_name=k, model=self._model))
922
+ elif isinstance(v, Relationship):
923
+ self._validate_identifier_relationship(v)
924
+ setattr(self, k, v)
925
+ else:
926
+ raise ValueError(f"identify_by must be either a Concept or Relationship: {k}={v}")
927
+ scheme.append(getattr(self, k))
928
+ self._add_ref_scheme(*tuple(scheme))
929
+ self._annotations = []
930
+
931
+ def require(self, *args: Any) -> Fragment:
932
+ return where(self).require(*args)
933
+
934
+ def new(self, ident: Any|None=None, **kwargs) -> ConceptNew:
935
+ self._check_ref_scheme(kwargs)
936
+ return ConceptNew(self, ident, kwargs)
937
+
938
+ def filter_by(self, args: Any|None=None, **kwargs: Any) -> ConceptFilter:
939
+ return ConceptFilter(self, args, kwargs)
940
+
941
+ def to_identity(self, args: Any|None=None, **kwargs: Any) -> ConceptConstruct:
942
+ self._check_ref_scheme(kwargs, shallow=True)
943
+ return ConceptConstruct(self, args, kwargs)
944
+
945
+ def annotate(self, *annos:Expression|Relationship|ir.Annotation) -> Concept:
946
+ self._annotations.extend(annos)
947
+ return self
948
+
949
+ #--------------------------------------------------
950
+ # Reference schemes
951
+ #--------------------------------------------------
952
+
953
+ def identify_by(self, *args: Relationship|RelationshipReading):
954
+ if not args:
955
+ raise ValueError("identify_by requires at least one relationship")
956
+ for rel in args:
957
+ if not isinstance(rel, (Relationship, RelationshipReading)):
958
+ raise ValueError(f"identify_by must be called with a Relationship/RelationshipReading, got {type(rel)}")
959
+ else:
960
+ self._validate_identifier_relationship(rel)
961
+ self._add_ref_scheme(*args)
962
+
963
+ def _add_ref_scheme(self, *rels: Relationship|RelationshipReading):
964
+ # thanks to prior validation we we can safely assume that
965
+ # * the input types are correct due to prior validation
966
+ # * all relationships are binary and defined on this concept
967
+
968
+ self._reference_schemes.append(rels)
969
+
970
+ # for every concept x every field f has at most one value y.
971
+ # f(x,y): x -> y holds
972
+ concept_fields = tuple([rel.__getitem__(0) for rel in rels])
973
+ for field in concept_fields:
974
+ concept_uc = Unique(field, model=self._model)
975
+ require(concept_uc.to_expressions())
976
+
977
+ # for any combination of field values there is at most one concept x.
978
+ # f₁(x,y₁) ∧ … ∧ fₙ(x,yₙ): {y₁,…,yₙ} → {x}
979
+ key_fields = tuple([rel.__getitem__(1) for rel in rels])
980
+ key_uc = Unique(*key_fields, model=self._model)
981
+ require(key_uc.to_expressions())
982
+
983
+ def _validate_identifier_relationship(self, rel:Relationship|RelationshipReading):
984
+ if rel._arity() != 2:
985
+ raise ValueError("identify_by can only be applied on binary relations")
986
+ if rel._fields[0].type_str != self._name:
987
+ raise ValueError("For identify_by all relationships/readings must be defined on the same Concept")
988
+
989
+ def _ref_scheme(self, shallow=False) -> tuple[Relationship, ...] | None:
990
+ ref_schema = []
991
+ if not shallow:
992
+ for parent in self._extends:
993
+ parent_schema = parent._ref_scheme()
994
+ if parent_schema:
995
+ ref_schema.extend(parent_schema)
996
+ break
997
+ if self._reference_schemes:
998
+ ref_schema.extend(self._reference_schemes[0])
999
+ return tuple(ref_schema) if ref_schema else None
1000
+
1001
+ def _check_ref_scheme(self, kwargs: dict[str, Any], shallow=False):
1002
+ scheme = self._ref_scheme(shallow)
1003
+ if not scheme:
1004
+ return
1005
+ ks = [rel._short_name for rel in scheme]
1006
+ for k in ks:
1007
+ if k not in kwargs:
1008
+ raise ValueError(f"Missing argument {k} for {self._name}")
1009
+
1010
+ def _ref_scheme_hierarchy(self):
1011
+
1012
+ ref_schemes: list[RefSchemeHierarchyDict] = []
1013
+ for parent in self._extends:
1014
+ parent_schemes = parent._ref_scheme_hierarchy()
1015
+ if parent_schemes:
1016
+ ref_schemes.extend(parent_schemes)
1017
+ break
1018
+ if self._reference_schemes:
1019
+ ref_schemes.append({"concept": self, "scheme": self._reference_schemes[0]})
1020
+
1021
+ # add mappings
1022
+ top_parent_name = ref_schemes[0]["concept"]._name if ref_schemes else None
1023
+ for ix, scheme in enumerate(ref_schemes[1:]):
1024
+ cur = scheme["concept"]
1025
+ parent = ref_schemes[ix]["concept"]
1026
+ if not self._scheme_mapping.get(parent):
1027
+ self._scheme_mapping[parent] = cur._scheme_mapping.get(parent) or Relationship(
1028
+ f"{{{cur._name}}} to {{{top_parent_name}}}",
1029
+ short_name=f"{cur._name}_to_{parent._name}",
1030
+ model=self._model,
1031
+ )
1032
+ scheme["mapping"] = self._scheme_mapping[parent]
1033
+
1034
+ return ref_schemes
1035
+
1036
+ #--------------------------------------------------
1037
+ # Internals
1038
+ #--------------------------------------------------
1039
+
1040
+ def _get_topmost_parent(self) -> Concept:
1041
+ if not self._extends:
1042
+ return self
1043
+ return self._extends[0]._get_topmost_parent()
1044
+
1045
+ def _dir_extras_from_get_relationship(self):
1046
+ attributes = set()
1047
+ for parent in self._extends:
1048
+ attributes.update(parent._relationships.keys())
1049
+ attributes.update(parent._dir_extras_from_get_relationship())
1050
+ return attributes
1051
+
1052
+ def _get_relationship(self, name: str) -> Relationship | RelationshipRef | RelationshipFieldRef:
1053
+ relationship = self._get_parent_relationship(self, name)
1054
+ return relationship if relationship else super()._get_relationship(name)
1055
+
1056
+ def _get_parent_relationship(self, root:Concept, name: str) -> Relationship | RelationshipRef | RelationshipFieldRef | None:
1057
+ for parent in self._extends:
1058
+ if name in parent._relationships:
1059
+ return RelationshipRef(root, parent._relationships[name])
1060
+ elif not parent._is_primitive():
1061
+ return parent._get_parent_relationship(root, name)
1062
+ return None
1063
+
1064
+ def _isa(self, other:Concept) -> bool:
1065
+ if self is other:
1066
+ return True
1067
+ for parent in self._extends:
1068
+ if parent._isa(other):
1069
+ return True
1070
+ return False
1071
+
1072
+ def _is_primitive(self) -> bool:
1073
+ return self._isa(Primitive)
1074
+
1075
+ def _is_enum(self) -> bool:
1076
+ return self._isa(Concept.builtins["Enum"])
1077
+
1078
+ def _is_filter(self) -> bool:
1079
+ return False
1080
+
1081
+ def __call__(self, identity:Any=None, **kwargs: Any) -> ConceptMember:
1082
+ return ConceptMember(self, identity, kwargs)
1083
+
1084
+ def __str__(self):
1085
+ return self._name
1086
+
1087
+ #--------------------------------------------------
1088
+ # ErrorConcept
1089
+ #--------------------------------------------------
1090
+
1091
+ class ErrorConcept(Concept):
1092
+ _error_props = OrderedSet()
1093
+ _relation = None
1094
+ _overloads:dict[Concept, Relationship] = {}
1095
+
1096
+ def __init__(self, name:str, extends:list[Any] = [], model:Model|None=None):
1097
+ super().__init__(name, extends, model)
1098
+
1099
+ def new(self, ident: Any|None=None, **kwargs) -> ConceptNew:
1100
+ from relationalai.semantics.internal import annotations as annos
1101
+ model = kwargs.get("_model") or find_model([ident, kwargs])
1102
+ if kwargs.get("_model"):
1103
+ del kwargs["_model"]
1104
+
1105
+ if not ErrorConcept._relation:
1106
+ # note: explicitly declaring fields to avoid incorrect madlib lookups
1107
+ ErrorConcept._relation = Relationship(
1108
+ "{Error} has {attribute:String} with {value:Any}",
1109
+ short_name="pyrel_error_attrs",
1110
+ model=model,
1111
+ fields=[
1112
+ Field("error", "Error", Error),
1113
+ Field("attribute", "String", String),
1114
+ Field("value", "Any", Concept.builtins["Any"])
1115
+ ]
1116
+ ).annotate(annos.external)
1117
+ ErrorConcept._relation._unresolved = True
1118
+ source = None
1119
+ if "_source" in kwargs:
1120
+ source = kwargs["_source"]
1121
+ del kwargs["_source"]
1122
+ else:
1123
+ source = runtime_env.get_source_pos()
1124
+ # kwargs["severity"] = "error"
1125
+ if source:
1126
+ source = source.to_source_info()
1127
+ source_id = len(errors.ModelError.error_locations)
1128
+ errors.ModelError.error_locations[source_id] = source
1129
+ kwargs["pyrel_id"] = source_id
1130
+
1131
+ for k, v in kwargs.items():
1132
+ v_type = to_type(v) or python_types_to_concepts.get(type(v)) or Concept.builtins["Any"]
1133
+ if v_type and v_type not in self._overloads:
1134
+ # note: explicitly declaring fields to avoid incorrect madlib lookups
1135
+ self._overloads[v_type] = Relationship(
1136
+ f"{{Error}} has {{attribute:String}} with {{value:{v_type._name}}}",
1137
+ short_name="pyrel_error_attrs",
1138
+ model=model,
1139
+ fields=[
1140
+ Field("error", "Error", Error),
1141
+ Field("attribute", "String", String),
1142
+ Field("value", "v_type._name", v_type)
1143
+ ]
1144
+ ).annotate(annos.external)
1145
+ assert v_type is not None, f"Cannot determine type for {k}={v}"
1146
+ overload = self._overloads[v_type]
1147
+ if (model, k) not in ErrorConcept._error_props and not k.startswith("_"):
1148
+ ErrorConcept._error_props.add((model, k))
1149
+ with root_tracking(True):
1150
+ frag = where(getattr(self, k)).define(
1151
+ overload(self, k, getattr(self, k))
1152
+ )
1153
+ frag._model = model
1154
+
1155
+ return super().new(ident, **kwargs)
1156
+
1157
+ def __call__(self, identity: Any = None, **kwargs: Any) -> Any:
1158
+ raise ValueError("Errors must always be created with a new identity. Use Error.new(..) instead of Error(..)")
1159
+
1160
+ #--------------------------------------------------
1161
+ # Builtin Concepts
1162
+ #--------------------------------------------------
1163
+
1164
+ Primitive = Concept.builtins["Primitive"] = Concept("Primitive")
1165
+ Error = Concept.builtins["Error"] = ErrorConcept("Error")
1166
+
1167
+ def _register_builtin(name):
1168
+ if name == "AnyEntity":
1169
+ c = Concept(name)
1170
+ else:
1171
+ c = Concept(name, extends=[Primitive])
1172
+ Concept.builtin_concepts.add(c)
1173
+ Concept.builtins[name] = c
1174
+
1175
+ # Load builtin types
1176
+ for builtin in types.builtin_types:
1177
+ if isinstance(builtin, ir.ScalarType):
1178
+ _register_builtin(builtin.name)
1179
+
1180
+ AnyEntity = Concept.builtins["AnyEntity"]
1181
+ Float = Concept.builtins["Float"]
1182
+ Number = Concept.builtins["Number"]
1183
+ Int64 = Concept.builtins["Int64"]
1184
+ Int128 = Concept.builtins["Int128"]
1185
+ # Integer aliases to Int128.
1186
+ Integer = Concept.builtins["Int128"]
1187
+ Hash = Concept.builtins["Hash"]
1188
+ String = Concept.builtins["String"]
1189
+ Bool = Concept.builtins["Bool"]
1190
+ Date = Concept.builtins["Date"]
1191
+ DateTime = Concept.builtins["DateTime"]
1192
+
1193
+ # The default Decimal type can be retrieved as "Decimal" or "Decimal(38,14)"
1194
+ Decimal = Concept.builtins["Decimal"] = Concept.builtins[types.Decimal.name]
1195
+
1196
+ def decimal_concept(precision: int = 38, scale: int = 14) -> Concept:
1197
+ """ Get the Concept for a decimal with this precision and scale. """
1198
+ return decimal_concept_by_name(f"Decimal({precision},{scale})")
1199
+
1200
+ def decimal_concept_by_name(name: str) -> Concept:
1201
+ """ Get the Concept for a decimal with this name, e.g. 'Decimal(38,14)'. """
1202
+ if name not in Concept.builtins:
1203
+ # cache for reuse
1204
+ _register_builtin(name)
1205
+ return Concept.builtins[name]
1206
+
1207
+ def is_decimal(concept: Concept) -> bool:
1208
+ """ Check whether this concept represents a Decimal. """
1209
+ return concept._name.startswith("Decimal") and concept in Concept.builtin_concepts
1210
+
1211
+ # The following is a workaround for having the builtin "Int"
1212
+ # but not other the builtin "Integer". The `Relationship`
1213
+ # class relies upon the builtin "Integer" existing in its
1214
+ # _build_inspection_fragment() method.
1215
+ Concept.builtins["Int"] = Concept.builtins["Int128"]
1216
+ Concept.builtins["Integer"] = Concept.builtins["Int128"]
1217
+
1218
+ _np_datetime = np.dtype('datetime64[ns]')
1219
+ python_types_to_concepts : dict[Any, Concept] = {
1220
+ int: Concept.builtins["Int128"],
1221
+ float: Concept.builtins["Float"],
1222
+ str: Concept.builtins["String"],
1223
+ bool: Concept.builtins["Bool"],
1224
+ date: Concept.builtins["Date"],
1225
+ datetime: Concept.builtins["DateTime"],
1226
+ PyDecimal: Decimal,
1227
+
1228
+ Int128Dtype(): Concept.builtins["Int128"],
1229
+
1230
+ # Pandas/NumPy dtype objects
1231
+ np.dtype('int64'): Concept.builtins["Int128"],
1232
+ np.dtype('int32'): Concept.builtins["Int128"],
1233
+ np.dtype('int16'): Concept.builtins["Int128"],
1234
+ np.dtype('int8'): Concept.builtins["Int128"],
1235
+ np.dtype('uint64'): Concept.builtins["Int128"],
1236
+ np.dtype('uint32'): Concept.builtins["Int128"],
1237
+ np.dtype('uint16'): Concept.builtins["Int128"],
1238
+ np.dtype('uint8'): Concept.builtins["Int128"],
1239
+ np.dtype('float64'): Concept.builtins["Float"],
1240
+ np.dtype('float32'): Concept.builtins["Float"],
1241
+ np.dtype('bool'): Concept.builtins["Bool"],
1242
+ np.dtype('object'): Concept.builtins["String"], # Often strings are stored as object dtype
1243
+ _np_datetime: Concept.builtins["DateTime"],
1244
+
1245
+ # Pandas extension dtypes
1246
+ pd.Int64Dtype(): Concept.builtins["Int128"],
1247
+ pd.Int32Dtype(): Concept.builtins["Int128"],
1248
+ pd.Int16Dtype(): Concept.builtins["Int128"],
1249
+ pd.Int8Dtype(): Concept.builtins["Int128"],
1250
+ pd.UInt64Dtype(): Concept.builtins["Int128"],
1251
+ pd.UInt32Dtype(): Concept.builtins["Int128"],
1252
+ pd.UInt16Dtype(): Concept.builtins["Int128"],
1253
+ pd.UInt8Dtype(): Concept.builtins["Int128"],
1254
+ pd.Float64Dtype(): Concept.builtins["Float"],
1255
+ pd.Float32Dtype(): Concept.builtins["Float"],
1256
+ pd.StringDtype(): Concept.builtins["String"],
1257
+ pd.BooleanDtype(): Concept.builtins["Bool"],
1258
+ }
1259
+
1260
+ # this map is required when we need to map standard python type string to a Concept
1261
+ python_types_str_to_concepts = {
1262
+ "int": python_types_to_concepts[int],
1263
+ "float": python_types_to_concepts[float],
1264
+ "str": python_types_to_concepts[str],
1265
+ "bool": python_types_to_concepts[bool],
1266
+ "date": python_types_to_concepts[date],
1267
+ "datetime": python_types_to_concepts[datetime],
1268
+ "decimal": python_types_to_concepts[PyDecimal]
1269
+ }
1270
+
1271
+ #--------------------------------------------------
1272
+ # Relationship
1273
+ #--------------------------------------------------
1274
+
1275
+ @dataclass()
1276
+ class Field():
1277
+ name:str
1278
+ type_str:str
1279
+ type:Concept|None = None
1280
+
1281
+ def __str__(self):
1282
+ return f"{self.name}:{self.type_str}"
1283
+
1284
+ def __hash__(self) -> int:
1285
+ type_str = self.type._name if self.type else self.type_str
1286
+ return hash((self.name, type_str))
1287
+
1288
+ class Relationship(Producer):
1289
+ builtins = {}
1290
+
1291
+ def __init__(self, madlib:str, parent:Producer|None=None, short_name:str="", model:Model|None=None, fields:list[Field]|None=None, field_refs:list[Ref]|None=None, ir_relation:ir.Relation|None=None):
1292
+ found_model = model or find_model(parent) or find_model(args)
1293
+ super().__init__(found_model)
1294
+ self._parent = parent
1295
+ self._madlib = madlib
1296
+ self._passed_short_name = short_name
1297
+ self._relationships = {}
1298
+ if fields is not None:
1299
+ self._fields:list[Field] = fields
1300
+ else:
1301
+ self._fields = self._parse_schema_format(madlib)
1302
+ if not self._fields and not ir_relation:
1303
+ raise ValueError(f"No fields found in relationship {self}")
1304
+ if model and model._strict:
1305
+ self._validate_fields(self._fields)
1306
+ self._ir_relation = ir_relation
1307
+ self._unresolved = False
1308
+ if field_refs is not None:
1309
+ self._field_refs = field_refs
1310
+ else:
1311
+ self._field_refs = [cast(Ref, field_to_type(found_model, field).ref(field.name)) for field in self._fields]
1312
+ for field in self._field_refs:
1313
+ field._no_lookup = True
1314
+ self._internal_constraints:set[FieldsConstraint] = set()
1315
+ self._field_names = [field.name for field in self._fields]
1316
+ self._readings = [RelationshipReading(madlib, alt_of=self, short_name=short_name, fields=self._fields, model=found_model, parent=parent)]
1317
+ self._annotations = []
1318
+ # now that the Relationship is validated, register into the model
1319
+ if found_model is not None:
1320
+ found_model.relationships.append(self)
1321
+
1322
+ @property
1323
+ def _name(self):
1324
+ return self._short_name or self._madlib
1325
+
1326
+ @property
1327
+ def _short_name(self):
1328
+ return self._passed_short_name or _short_name_from_madlib(self._madlib)
1329
+
1330
+ def is_many(self):
1331
+ if self._arity() == 1:
1332
+ return False
1333
+ uc = Unique.to_identity(*(self[i] for i in range(self._arity() - 1)))
1334
+ return uc not in self._internal_constraints
1335
+
1336
+ def _is_filter(self) -> bool:
1337
+ return self._short_name in [">", "<", "=", "!=", ">=", "<="]
1338
+
1339
+ @staticmethod
1340
+ def _sanitize_field_name(name: str) -> str:
1341
+ """
1342
+ Sanitize a field name by converting to lowercase and replacing
1343
+ problematic characters with underscores. Special handling for
1344
+ precision/scale format (like Decimal(38,14)) to avoid trailing underscores.
1345
+
1346
+ This ensures consistent field naming between
1347
+ relationship parsing and field reference creation.
1348
+ """
1349
+ lowered = name.lower()
1350
+
1351
+ # Check if this matches the precision/scale format: word(digits,digits)
1352
+ import re
1353
+ precision_scale_pattern = r'^([a-zA-Z0-9_.]+)\(([0-9]+),([0-9]+)\)$'
1354
+ match = re.match(precision_scale_pattern, lowered)
1355
+
1356
+ if match:
1357
+ # Format B: Convert Decimal(38,14) -> decimal_38_14 (no trailing underscore)
1358
+ base_name, precision, scale = match.groups()
1359
+ # First sanitize the base name part
1360
+ sanitized_base = re.sub(r"[ ,\.\|]", "_", base_name)
1361
+ return f"{sanitized_base}_{precision}_{scale}"
1362
+ else:
1363
+ # Format A: Regular sanitization, preserve original trailing underscores
1364
+ return re.sub(r"[ ,\.\(\)\|]", "_", lowered)
1365
+
1366
+ def _parse_schema_format(self, format_string:str):
1367
+ # Pattern to extract fields like {Type} or {name:Type}, where Type can have precision and scale, like Decimal(38,14)
1368
+ pattern = r'\{([a-zA-Z0-9_."-]+(?:\([0-9]+,[0-9]+\))?)(?::([a-zA-Z0-9_."-]+(?:\([0-9]+,[0-9]+\))?))?\}'
1369
+ matches = re.findall(pattern, format_string)
1370
+
1371
+ namer = NameCache()
1372
+ fields = []
1373
+ match_index = 0
1374
+ ix = 0
1375
+ for field_name, field_type in matches:
1376
+ # If no type is specified, use the field name as the type
1377
+ if not field_type:
1378
+ field_type = field_name
1379
+ # in this case, the field_name is based on the type name,
1380
+ # so sanitize to avoid, for example, ()s in decimal names,
1381
+ # and other problematic special characters.
1382
+ field_name = self._sanitize_field_name(field_name)
1383
+
1384
+ ix += 1
1385
+ field_name = namer.get_name(ix, field_name)
1386
+
1387
+ fields.append(Field(field_name, field_type))
1388
+ match_index +=1
1389
+
1390
+ return fields
1391
+
1392
+ def _dir_extras_from_get_relationship(self) -> Any:
1393
+ return self._field_refs[-1]._dir_extras_from_getattr()
1394
+
1395
+ def _get_relationship(self, name: str) -> Relationship | RelationshipRef | RelationshipFieldRef:
1396
+ rel:RelationshipRef = getattr(self._field_refs[-1], name)
1397
+ return RelationshipRef(self, rel._relationship)
1398
+
1399
+ def _arity(self):
1400
+ return len(self._fields)
1401
+
1402
+ def _dir_extras_from_getattr(self) -> Any:
1403
+ attributes = set()
1404
+ if self._arity() > 2:
1405
+ attributes.update(self._field_names)
1406
+ attributes.update(super()._dir_extras_from_getattr())
1407
+ return attributes
1408
+
1409
+ def __getattr__(self, name: str) -> Any:
1410
+ if self._arity() > 2 and name in self._field_names:
1411
+ return RelationshipFieldRef(self._parent, self, self._field_names.index(name))
1412
+ return super().__getattr__(name)
1413
+
1414
+ def annotate(self, *annos:Expression|Relationship|ir.Annotation) -> Relationship:
1415
+ self._annotations.extend(annos)
1416
+ return self
1417
+
1418
+ def __getitem__(self, arg:str|int|Concept) -> Any:
1419
+ return _get_relationship_item(self, arg)
1420
+
1421
+ def ref(self, name:str|None=None) -> Ref|RelationshipRef:
1422
+ return RelationshipRef(self._parent, self, name=name)
1423
+
1424
+ def alt(self, madlib:Any, short_name:str="", reading:RelationshipReading|None = None) -> RelationshipReading:
1425
+ if not reading:
1426
+ reading = RelationshipReading(madlib, alt_of=self, short_name=short_name, model=self._model)
1427
+ self._readings.append(reading)
1428
+ where(self(*self._field_refs)).define(
1429
+ reading._ignore_root(*reading._field_refs),
1430
+ )
1431
+ return reading
1432
+
1433
+ def _is_same_relationship(self, rel:Relationship|RelationshipReading) -> bool:
1434
+ return self._id == rel._alt_of._id if isinstance(rel, RelationshipReading) else self._id == rel._id
1435
+
1436
+ def _build_inspection_fragment(self):
1437
+ """
1438
+ Helper function for the inspect() and to_df() methods below,
1439
+ that generates a Fragment from the Relationship, inspect()ing
1440
+ or to_df()'ing which yields all tuples in the Relationship.
1441
+ """
1442
+ field_types = [field_to_type(self._model, field) for field in self._fields]
1443
+ field_vars = [field_type.ref() for field_type in field_types]
1444
+ return where(self(*field_vars)).select(*field_vars)
1445
+
1446
+ def inspect(self):
1447
+ return self._build_inspection_fragment().inspect()
1448
+
1449
+ def to_df(self):
1450
+ return self._build_inspection_fragment().to_df()
1451
+
1452
+ def _validate_fields(self, fields:list[Field]):
1453
+ # Check for multiple occurrences of the same type without explicit role names
1454
+ type_occurrences = defaultdict(list)
1455
+ for field in fields:
1456
+ type_occurrences[field.type_str].append(field.name)
1457
+
1458
+ if (len(type_occurrences[field.type_str]) > 1 and
1459
+ any(name == field.type_str.lower() for name in type_occurrences[field.type_str])):
1460
+ raise ValueError(f"The type {field.type_str} occurs multiple times in '{self._madlib}'. Please "
1461
+ f"disambiguate by providing explicit role names for each occurrence.")
1462
+
1463
+ def __call__(self, *args: Any, **kwargs) -> Any:
1464
+ return _relationship_call(self, *args, **kwargs)
1465
+
1466
+ def __str__(self):
1467
+ if self._parent and self._short_name:
1468
+ return f"{self._parent}.{self._short_name}"
1469
+ return self._name
1470
+
1471
+ class Property(Relationship):
1472
+
1473
+ def __init__(self, madlib:str, parent:Producer|None=None, short_name:str="", model:Model|None=None, fields:list[Field]|None=None, field_refs:list[Ref]|None=None, ir_relation:ir.Relation|None=None):
1474
+ super().__init__(madlib, parent, short_name, model, fields, field_refs, ir_relation)
1475
+ # for property should be an implicit unique constraint on the first n-1 fields
1476
+ uc = Unique(*(self[i] for i in range(self._arity() - 1)), model=self._model)
1477
+ require(uc.to_expressions())
1478
+
1479
+
1480
+ class RelationshipReading(Producer):
1481
+
1482
+ def __init__(self, madlib:str, alt_of:Relationship, short_name:str, fields:list[Field]|None=None, model:Model|None=None, parent:Producer|None=None,):
1483
+ found_model = model or find_model(parent)
1484
+ super().__init__(found_model)
1485
+ self._parent = parent
1486
+ self._alt_of = alt_of
1487
+ self._madlib = madlib
1488
+ self._passed_short_name = short_name
1489
+ if fields is not None:
1490
+ self._fields:list[Field] = fields
1491
+ else:
1492
+ self._fields = alt_of._parse_schema_format(madlib)
1493
+ if Counter(self._fields) != Counter(alt_of._fields):
1494
+ raise ValueError(
1495
+ f"Invalid alternative relationship. The alternative group of used fields ({', '.join(str(f) for f in self._fields)}) does not match with the original ({', '.join(str(f) for f in alt_of._fields)})")
1496
+ self._field_refs = [alt_of[f.name]._field_ref for f in self._fields]
1497
+ self._field_names = [field.name for field in self._fields]
1498
+ self._relationships = {}
1499
+ self._annotations = []
1500
+
1501
+ def is_many(self):
1502
+ if self._arity() == 1:
1503
+ return False
1504
+ uc = Unique.to_identity(*(self[i] for i in range(self._arity() - 1)))
1505
+ return uc not in self._alt_of._internal_constraints
1506
+
1507
+ def _arity(self):
1508
+ return len(self._fields)
1509
+
1510
+ def annotate(self, *annos:Expression|Relationship|ir.Annotation) -> RelationshipReading:
1511
+ self._annotations.extend(annos)
1512
+ return self
1513
+
1514
+ @property
1515
+ def _name(self):
1516
+ return self._short_name or self._madlib
1517
+
1518
+ @property
1519
+ def _short_name(self):
1520
+ return self._passed_short_name or _short_name_from_madlib(self._madlib)
1521
+
1522
+ def _ignore_root(self, *args, **kwargs):
1523
+ expr = self(*args, **kwargs)
1524
+ expr._ignore_root = True
1525
+ return expr
1526
+
1527
+ def _dir_extras_from_get_relationship(self) -> Any:
1528
+ return self._field_refs[-1]._dir_extras_from_getattr()
1529
+
1530
+ def _get_relationship(self, name: str) -> Relationship | RelationshipRef | RelationshipFieldRef:
1531
+ rel:RelationshipRef = getattr(self._field_refs[-1], name)
1532
+ return RelationshipRef(self, rel._relationship)
1533
+
1534
+ def _dir_extras_from_getattr(self) -> Any:
1535
+ attributes = set()
1536
+ if self._arity() > 2:
1537
+ attributes.update(self._field_names)
1538
+ attributes.update(self._alt_of._relationships.keys())
1539
+ attributes.update(self._dir_extras_from_get_relationship())
1540
+ return attributes
1541
+
1542
+ def _is_same_relationship(self, rel:Relationship|RelationshipReading) -> bool:
1543
+ return self._alt_of._id == rel._alt_of._id if isinstance(rel, RelationshipReading) else self._alt_of._id == rel._id
1544
+
1545
+ def __getattr__(self, name) -> Any:
1546
+ if not name.startswith("_"):
1547
+ if self._arity() > 2 and name in self._field_names:
1548
+ return RelationshipFieldRef(self._parent, self, self._field_names.index(name))
1549
+ if name not in self._relationships:
1550
+ self._relationships[name] = self._get_relationship(name)
1551
+ return self._relationships[name]
1552
+ return super().__getattr__(name)
1553
+
1554
+ def __getitem__(self, arg: str | int | Concept) -> Any:
1555
+ return _get_relationship_item(self, arg)
1556
+
1557
+ def __call__(self, *args: Any, **kwargs) -> Any:
1558
+ return _relationship_call(self, *args, **kwargs)
1559
+
1560
+ def __str__(self):
1561
+ if self._parent and self._short_name:
1562
+ return f"{self._parent}.{self._short_name}"
1563
+ return self._name
1564
+
1565
+ def _short_name_from_madlib(madlib:Any) -> str:
1566
+ # Replace curly braces, colons, and spaces with underscores.
1567
+ # Then strip leading/trailing underscores.
1568
+ return re.sub(r"[{}: ]", "_", str(madlib)).strip("_")
1569
+
1570
+ def _get_relationship_item(rel:Relationship|RelationshipReading, arg:Any) -> Any:
1571
+ if isinstance(arg, int):
1572
+ if arg < 0:
1573
+ raise ValueError(f"Position should be positive, got {arg}")
1574
+ if rel._arity() <= arg:
1575
+ raise ValueError(f"Relationship '{rel._name}' has only {rel._arity()} fields")
1576
+ return RelationshipFieldRef(rel._parent, rel, arg)
1577
+ elif isinstance(arg, str):
1578
+ if arg not in rel._field_names:
1579
+ raise ValueError(f"Relationship '{rel._name}' has only {rel._field_names} fields")
1580
+ return RelationshipFieldRef(rel._parent, rel, rel._field_names.index(arg))
1581
+ elif isinstance(arg, Concept):
1582
+ return _get_relationship_field_ref(rel, arg)
1583
+ elif isinstance(arg, type) and rel._model is not None and issubclass(arg, rel._model.Enum):
1584
+ return _get_relationship_field_ref(rel, arg._concept)
1585
+ else:
1586
+ raise ValueError(f"Unknown argument {arg}")
1587
+
1588
+ def _get_relationship_field_ref(rel:Relationship|RelationshipReading, concept:Concept) -> Any:
1589
+ result: RelationshipFieldRef | None = None
1590
+ for idx, ref in enumerate(rel._field_refs):
1591
+ if result is None and ref._thing._id == concept._id:
1592
+ result = RelationshipFieldRef(rel._parent, rel, idx)
1593
+ else:
1594
+ if ref._thing._id == concept._id:
1595
+ raise ValueError(
1596
+ f"Ambiguous reference to the field: '{concept._name}' presented in more than one field. Use reference by name or position instead")
1597
+ if result is None:
1598
+ raise ValueError(f"Relationship '{rel._name}' does not have '{concept._name}' as a field")
1599
+ return result
1600
+
1601
+ def _relationship_call(rel:Relationship|RelationshipReading, *args: Any, **kwargs) -> Any:
1602
+ if kwargs and args:
1603
+ raise ValueError("Cannot use both positional and keyword arguments")
1604
+ if kwargs:
1605
+ # check that all fields have been provided
1606
+ clean_args = []
1607
+ for ix, field in enumerate(rel._field_names):
1608
+ if field in kwargs:
1609
+ clean_args.append(kwargs.get(field))
1610
+ if ix == 0 and rel._parent:
1611
+ continue
1612
+ if field not in kwargs:
1613
+ raise ValueError(f"Missing argument {field}")
1614
+ else:
1615
+ clean_args = list(args)
1616
+ if len(clean_args) < rel._arity():
1617
+ if rel._parent:
1618
+ clean_args = [rel._parent, *clean_args]
1619
+ if len(clean_args) != rel._arity():
1620
+ raise ValueError(f"Expected {rel._arity()} arguments, got {len(clean_args)}: {rel}")
1621
+ return Expression(rel, *clean_args)
1622
+
1623
+ #--------------------------------------------------
1624
+ # Builtin Relationships
1625
+ #--------------------------------------------------
1626
+
1627
+ for builtin in builtins.builtin_relations + builtins.builtin_annotations:
1628
+ fields = []
1629
+ for field in builtin.fields:
1630
+ field_type = re.sub(r'[\[\{\(]', '', str(field.type)).strip()
1631
+ field_type = str(field.type).strip()
1632
+ fields.append(f"{field.name}:{field_type}")
1633
+ args = ' '.join([f"{{{f}}}" for f in fields])
1634
+ Relationship.builtins[builtin.name] = Relationship(
1635
+ f"{builtin.name} {args}",
1636
+ parent=None,
1637
+ short_name=builtin.name,
1638
+ ir_relation=builtin,
1639
+ )
1640
+
1641
+ RawSource = Relationship.builtins["raw_source"]
1642
+
1643
+ #--------------------------------------------------
1644
+ # Expression
1645
+ #--------------------------------------------------
1646
+
1647
+ infix_ops = ["+", "-", "*", "/", "//", "^", "%", ">", ">=", "<", "<=", "=", "!="]
1648
+ constraints_ops = [builtins.unique.name, builtins.exclusive.name, builtins.anyof.name]
1649
+
1650
+ class Expression(Producer):
1651
+ def __init__(self, op:Relationship|RelationshipReading|Concept, *params:Any):
1652
+ super().__init__(op._model or find_model(params))
1653
+ self._op = op
1654
+ self._params = params
1655
+ self._ignore_root = False
1656
+ self._source = runtime_env.get_source_pos()
1657
+
1658
+ def __str__(self):
1659
+ return f"({self._op} {' '.join(map(str, self._params))})"
1660
+
1661
+ def _pprint(self, indent:int=0) -> str:
1662
+ if self._op._name in infix_ops:
1663
+ a, b = self._params[0], self._params[1]
1664
+ return f"{' ' * indent}{a} {self._op} {b}"
1665
+ elif self._op._name in constraints_ops:
1666
+ args = []
1667
+ for param in flatten(self._params, True):
1668
+ args.append(f"{param._relationship}[\"{param._field_ref._name}\"]" if isinstance(param, RelationshipFieldRef) else str(param))
1669
+ return f"{' ' * indent}{self._op}({', '.join([str(param) for param in args])})"
1670
+ return f"{' ' * indent}{self._op}({' '.join(map(str, self._params))})"
1671
+
1672
+ def _dir_extras(self):
1673
+ attributes = set()
1674
+ last = self._params[-1]
1675
+ if isinstance(self._op, (Relationship, RelationshipRef)) and isinstance(last, Concept):
1676
+ # lookup the last concept in the relationship, and then lookup the attribute in it
1677
+ concept = self._op.__getitem__(last)
1678
+ attributes.update(dir(concept))
1679
+ return attributes
1680
+
1681
+ def _arg_ref(self, idx:int) -> ArgumentRef:
1682
+ if idx < 0:
1683
+ raise ValueError(f"Argument index should be positive, got {idx}")
1684
+ if len(self._params) <= idx:
1685
+ raise ValueError(f"Expression '{self.__str__()}' has only {len(self._params)} arguments")
1686
+ param = self._params[idx]
1687
+ # if param is an Expression then refer the last param of this expression
1688
+ return ArgumentRef(self, param._params[-1] if isinstance(param, Expression) else param)
1689
+
1690
+ def __getattr__(self, name: str):
1691
+ last = self._params[-1]
1692
+ if isinstance(self._op, (Relationship, RelationshipRef)) and isinstance(last, Concept):
1693
+ # lookup the last concept in the relationship, and then lookup the attribute in it
1694
+ concept = self._op.__getitem__(last)
1695
+ return getattr(concept, name)
1696
+ raise AttributeError(f"Expression has no attribute {name}")
1697
+
1698
+ class ConceptExpression(Expression):
1699
+ def __init__(self, con:Concept, identity:Any, kwargs:dict[str, Any]):
1700
+ super().__init__(con, identity, kwargs)
1701
+ for k, _ in kwargs.items():
1702
+ # make sure to create the properties being referenced
1703
+ getattr(self._op, k)
1704
+ self._relationships = {}
1705
+
1706
+ _remove_roots([v for v in kwargs.values() if isinstance(v, Producer)])
1707
+
1708
+ def _construct_args(self, scheme=None) -> dict[Relationship|Concept, Any]:
1709
+ args = {}
1710
+ scheme = scheme or self._op._ref_scheme()
1711
+ [ident, kwargs] = self._params
1712
+ if scheme:
1713
+ for rel in scheme:
1714
+ args[rel] = kwargs[rel._short_name]
1715
+ else:
1716
+ for k, v in kwargs.items():
1717
+ atr = getattr(self._op, k)
1718
+ if atr:
1719
+ args[atr] = v
1720
+ if ident:
1721
+ args[self._op] = ident
1722
+ return args
1723
+
1724
+ def _dir_extras_from_get_relationship(self):
1725
+ return self._op._dir_extras_from_getattr()
1726
+
1727
+ def _get_relationship(self, name: str) -> Relationship|RelationshipRef:
1728
+ parent_rel = getattr(self._op, name)
1729
+ return RelationshipRef(self, parent_rel)
1730
+
1731
+ def _dir_extras_from_getattr(self):
1732
+ return Producer._dir_extras_from_getattr(self)
1733
+
1734
+ def __getattr__(self, name: str):
1735
+ return Producer.__getattr__(self, name)
1736
+
1737
+ class ConceptMember(ConceptExpression):
1738
+ def __init__(self, con:Concept, identity:Any, kwargs:dict[str, Any]):
1739
+ super().__init__(con, identity, kwargs)
1740
+ if identity is None:
1741
+ class_name = con._name
1742
+ raise ValueError(f"Adding or looking up an instance of Concept requires an identity. If you want to create a new identity, use {class_name}.new(..)")
1743
+ # TODO: when we do reference schemes, the identity might be
1744
+ # in a combination of kwargs rather than in the positionals
1745
+
1746
+
1747
+ class ConceptNew(ConceptExpression):
1748
+ def __str__(self):
1749
+ return f"({self._op}.new {' '.join(map(str, self._params))})"
1750
+
1751
+
1752
+ class ConceptConstruct(ConceptExpression):
1753
+ pass
1754
+
1755
+ class ConceptFilter(ConceptExpression):
1756
+ pass
1757
+
1758
+ #--------------------------------------------------
1759
+ # TupleArg
1760
+ #--------------------------------------------------
1761
+
1762
+ # There are some special relations that require an actual tuple as
1763
+ # an argument. We want to differentiate that from a case where a user
1764
+ # _accidentally_ passes a tuple as an argument.
1765
+
1766
+ class TupleArg(tuple):
1767
+ def _compile_lookup(self, compiler:Compiler, ctx:CompilerContext):
1768
+ return TupleArg(flatten([compiler.lookup(item, ctx) for item in self]))
1769
+
1770
+ #--------------------------------------------------
1771
+ # Aggregate
1772
+ #--------------------------------------------------
1773
+
1774
+ class Aggregate(Producer):
1775
+ def __init__(self, op:Relationship, *args: Any):
1776
+ super().__init__(op._model or find_model(args))
1777
+ self._op = op
1778
+ self._args = args
1779
+ _remove_roots(args)
1780
+ self._group = []
1781
+ self._where = where()
1782
+
1783
+ def where(self, *args: Any) -> Aggregate:
1784
+ new = self.clone()
1785
+ if not new._model:
1786
+ new._model = find_model(args)
1787
+ new._where = new._where.where(*args)
1788
+ return new
1789
+
1790
+ def per(self, *args: Any) -> Aggregate:
1791
+ new = self.clone()
1792
+ if not new._model:
1793
+ new._model = find_model(args)
1794
+ new._group.extend(args)
1795
+ return new
1796
+
1797
+ def clone(self) -> Aggregate:
1798
+ clone = Aggregate(self._op, *self._args)
1799
+ clone._group = self._group.copy()
1800
+ clone._where = self._where
1801
+ return clone
1802
+
1803
+ def _dir_extras_from_getattr(self):
1804
+ return set()
1805
+
1806
+ def __getattr__(self, name: str):
1807
+ raise AttributeError(f"Expression has no attribute {name}")
1808
+
1809
+ def __str__(self):
1810
+ args = ', '.join(map(str, self._args))
1811
+ group = ', '.join(map(str, self._group))
1812
+ where = ""
1813
+ if group:
1814
+ group = f" (per {group})"
1815
+ if self._where._where:
1816
+ items = ', '.join(map(str, self._where._where))
1817
+ where = f" (where {items})"
1818
+ return f"({self._op} {args}{group}{where})"
1819
+
1820
+ class Group():
1821
+ def __init__(self, *args: Any):
1822
+ self._group = args
1823
+
1824
+ def __str__(self):
1825
+ args = ', '.join(map(str, self._group))
1826
+ return f"(per {args})"
1827
+
1828
+ #--------------------------------------------------
1829
+ # Agg funcs
1830
+ #--------------------------------------------------
1831
+
1832
+ def count(self, *args: Any) -> Aggregate:
1833
+ return count(*args).per(*self._group)
1834
+
1835
+ def sum(self, *args: Any) -> Aggregate:
1836
+ return sum(*args).per(*self._group)
1837
+
1838
+ def avg(self, *args: Any) -> Aggregate:
1839
+ return avg(*args).per(*self._group)
1840
+
1841
+ def min(self, *args: Any) -> Aggregate:
1842
+ return min(*args).per(*self._group)
1843
+
1844
+ def max(self, *args: Any) -> Aggregate:
1845
+ return max(*args).per(*self._group)
1846
+
1847
+ #--------------------------------------------------
1848
+ # Aggregate builtins
1849
+ #--------------------------------------------------
1850
+
1851
+ def per(*args: Any) -> Group:
1852
+ return Group(*args)
1853
+
1854
+ def count(*args: Any) -> Aggregate:
1855
+ return Aggregate(Relationship.builtins["count"], *args)
1856
+
1857
+ def sum(*args: Any) -> Aggregate:
1858
+ return Aggregate(Relationship.builtins["sum"], *args)
1859
+
1860
+ def avg(*args: Any) -> Aggregate:
1861
+ return Aggregate(Relationship.builtins["avg"], *args)
1862
+
1863
+ def min(*args: Any) -> Aggregate:
1864
+ return Aggregate(Relationship.builtins["min"], *args)
1865
+
1866
+ def max(*args: Any) -> Aggregate:
1867
+ return Aggregate(Relationship.builtins["max"], *args)
1868
+
1869
+ def experimental_warning(feature: str):
1870
+ rich.print(f"[yellow]Warning:[/yellow] Early access feature '[red]{feature}[/red]' is not yet stable.", file=sys.stderr)
1871
+
1872
+ class RankOrder():
1873
+ ASC = True
1874
+ DESC = False
1875
+
1876
+ def __init__(self, is_asc:bool, *args: Any):
1877
+ self._is_asc = is_asc
1878
+ self._args = args
1879
+
1880
+ def __str__(self):
1881
+ return f"({'asc' if self._is_asc else 'desc'} {', '.join(map(str, self._args))})"
1882
+
1883
+ def asc(*args: Any):
1884
+ experimental_warning("asc")
1885
+ return RankOrder(True, *args)
1886
+
1887
+ def desc(*args: Any):
1888
+ experimental_warning("desc")
1889
+ return RankOrder(False, *args)
1890
+
1891
+ def rank(*args: Any) -> Aggregate:
1892
+ experimental_warning("rank")
1893
+ # A relation is needed further down the pipeline, so we create a dummy one here.
1894
+ dummy_ir_relation = f.relation("rank", [f.field("result", types.Int128)])
1895
+ dummy_relation = Relationship(dummy_ir_relation.name, ir_relation=dummy_ir_relation)
1896
+ return Aggregate(dummy_relation, *args)
1897
+
1898
+ #--------------------------------------------------
1899
+ # Alias
1900
+ #--------------------------------------------------
1901
+
1902
+ class Alias(Producer):
1903
+ def __init__(self, thing:Producer, name:str):
1904
+ super().__init__(thing._model)
1905
+ self._thing = thing
1906
+ self._name = name
1907
+ _remove_roots([thing])
1908
+
1909
+ def __str__(self) -> str:
1910
+ return f"{self._thing} as {self._name}"
1911
+
1912
+ def __setattr__(self, name: str, value: Any) -> None:
1913
+ if name.startswith("_"):
1914
+ super().__setattr__(name, value)
1915
+ else:
1916
+ raise AttributeError(f"Cannot set attribute {name} on {type(self).__name__}")
1917
+
1918
+ #--------------------------------------------------
1919
+ # Match
1920
+ #--------------------------------------------------
1921
+
1922
+ class BranchRef(Producer):
1923
+ def __init__(self, match:Match, ix:int):
1924
+ super().__init__(match._model)
1925
+ self._match = match
1926
+ self._ix = ix
1927
+
1928
+ def __str__(self):
1929
+ return f"{self._match}#{self._ix}"
1930
+
1931
+ class Match(Producer):
1932
+ def __init__(self, *args: Any):
1933
+ super().__init__(find_model(args))
1934
+ self._args = list(self._flatten_args(args))
1935
+ if any(isinstance(arg, Fragment) and arg._is_effect() for arg in self._args):
1936
+ _add_root(self)
1937
+ _remove_roots(args)
1938
+
1939
+ # check for validity
1940
+ is_select = None
1941
+ ret_count = 0
1942
+ for arg in self._args:
1943
+ if isinstance(arg, Fragment) and arg._is_effect():
1944
+ if is_select:
1945
+ raise ValueError("Cannot mix expression and effect clauses in a match")
1946
+ is_select = False
1947
+ elif isinstance(arg, Fragment) and not arg._is_effect():
1948
+ if is_select is False:
1949
+ raise ValueError("Cannot mix effect and expression clauses in a match")
1950
+ is_select = True
1951
+ if ret_count == 0:
1952
+ ret_count = len(arg._select)
1953
+ elif ret_count != len(arg._select):
1954
+ raise ValueError("All clauses must have the same number of return values")
1955
+ elif isinstance(arg, PY_LITERAL_TYPES):
1956
+ if is_select is False:
1957
+ raise ValueError("Cannot mix then and select clauses in a match")
1958
+ is_select = True
1959
+ if ret_count == 0:
1960
+ ret_count = 1
1961
+ elif ret_count != 1:
1962
+ raise ValueError("All clauses must have the same number of return values")
1963
+ elif isinstance(arg, Expression) or isinstance(arg, Aggregate):
1964
+ if is_select is None:
1965
+ is_select = True
1966
+ if not arg._op._is_filter():
1967
+ ret_count = 1
1968
+ elif isinstance(arg, Relationship) or isinstance(arg, Ref):
1969
+ if is_select is None:
1970
+ is_select = True
1971
+ ret_count = 1
1972
+
1973
+ self._is_select = is_select
1974
+ self._ret_count = ret_count
1975
+ self._source = runtime_env.get_source_pos()
1976
+
1977
+ def _flatten_args(self, args):
1978
+ for arg in args:
1979
+ if isinstance(arg, Match):
1980
+ for sub_arg in arg._args:
1981
+ yield sub_arg
1982
+ else:
1983
+ yield arg
1984
+
1985
+ def __iter__(self):
1986
+ for ix in range(self._ret_count):
1987
+ yield BranchRef(self, ix)
1988
+
1989
+ def __str__(self):
1990
+ return " | ".join(map(str, self._args))
1991
+
1992
+ #--------------------------------------------------
1993
+ # Union
1994
+ #--------------------------------------------------
1995
+
1996
+ class Union(Match):
1997
+ def __str__(self):
1998
+ return f"union({', '.join(map(str, self._args))})"
1999
+
2000
+ def union(*args: Any) -> Union:
2001
+ if len(args) == 1:
2002
+ return args[0]
2003
+ return Union(*args)
2004
+
2005
+ #--------------------------------------------------
2006
+ # Negation
2007
+ #--------------------------------------------------
2008
+
2009
+ class Not():
2010
+ def __init__(self, *args: Any):
2011
+ self._args = args
2012
+ self._model = find_model(args)
2013
+ _remove_roots(args)
2014
+
2015
+ def clone(self) -> Not:
2016
+ clone = type(self)(*self._args)
2017
+ return clone
2018
+
2019
+ def __or__(self, other) -> Match:
2020
+ return Match(self, other)
2021
+
2022
+ def __and__(self, other) -> Fragment:
2023
+ if isinstance(other, Fragment):
2024
+ return other.where(self)
2025
+ return where(self, other)
2026
+
2027
+ def __str__(self):
2028
+ args_str = '\n '.join(map(str, self._args))
2029
+ return f"(not {args_str})"
2030
+
2031
+ def not_(*args: Any) -> Not:
2032
+ return Not(*args)
2033
+
2034
+ #--------------------------------------------------
2035
+ # Distinct
2036
+ #--------------------------------------------------
2037
+
2038
+ class Distinct():
2039
+ def __init__(self, *args: Any):
2040
+ self._args = args
2041
+ self._model = find_model(args)
2042
+ _remove_roots(args)
2043
+
2044
+ def distinct(*args: Any) -> Distinct:
2045
+ return Distinct(*args)
2046
+
2047
+ #--------------------------------------------------
2048
+ # Enum
2049
+ #--------------------------------------------------
2050
+
2051
+ def create_enum_class(model: Model):
2052
+
2053
+ class ModelEnumMeta(EnumMeta):
2054
+ _concept: Concept
2055
+ def __setattr__(self, name: str, value: Any) -> None:
2056
+ if name.startswith("_") or isinstance(value, self):
2057
+ super().__setattr__(name, value)
2058
+ elif isinstance(value, (Relationship, RelationshipReading)):
2059
+ value._parent = self._concept
2060
+ if not value._passed_short_name:
2061
+ value._passed_short_name = name
2062
+ if name in self._concept._relationships:
2063
+ raise ValueError(
2064
+ f"Cannot set attribute {name} on {type(self).__name__} a second time. Make sure to set the relationship before any usages occur")
2065
+ self._concept._relationships[name] = value
2066
+ else:
2067
+ raise AttributeError(f"Cannot set attribute {name} on {type(self).__name__}")
2068
+
2069
+ class ModelEnum(Enum, metaclass=ModelEnumMeta):
2070
+ def __init_subclass__(cls, **kwargs):
2071
+ super().__init_subclass__(**kwargs)
2072
+ # this is voodoo black magic that is doing meta meta programming where
2073
+ # we are plugging into anytime a new subtype of this class is created
2074
+ # and then creating a concept to represent the enum. This happens both
2075
+ # when you do `class Foo(Enum)` and when you do `Enum("Foo", [a, b, c])`
2076
+ c = model.Concept(
2077
+ cls.__name__,
2078
+ extends=[Concept.builtins["Enum"]],
2079
+ identify_by={"name": Concept.builtins["String"]}
2080
+ )
2081
+ cls._concept = model.enum_concept[cls] = c
2082
+ model.enums[cls.__name__] = cls
2083
+ cls._has_inited_members = False
2084
+
2085
+ # Python 3.10 doesn't correctly populate __members__ by the time it calls
2086
+ # __init_subclass__, so we need to initialize the members lazily when we
2087
+ # encounter the enum for the first time.
2088
+ def _init_members(self):
2089
+ if self._has_inited_members:
2090
+ return
2091
+ cls = self.__class__
2092
+ c = cls._concept
2093
+ # Add the name and value attributes to the hashes we create for the enum
2094
+ members = [
2095
+ c.new(name=name, value=value.value)
2096
+ for name, value in cls.__members__.items()
2097
+ ]
2098
+ with root_tracking(True):
2099
+ model.define(*members)
2100
+ cls._has_inited_members = True
2101
+
2102
+ def _compile_lookup(self, compiler:Compiler, ctx:CompilerContext):
2103
+ self._init_members()
2104
+ concept = getattr(self.__class__, "_concept")
2105
+ return compiler.lookup(concept.new(name=self.name), ctx)
2106
+
2107
+ @classmethod
2108
+ def lookup(cls, value:Producer|str):
2109
+ concept = cls._concept
2110
+ return concept.new(name=value)
2111
+
2112
+ return ModelEnum
2113
+
2114
+ #--------------------------------------------------
2115
+ # Data
2116
+ #--------------------------------------------------
2117
+
2118
+ class DataColumn(Producer):
2119
+ def __init__(self, data:Data, _type, name:str):
2120
+ self._data = data
2121
+ self._type = _type
2122
+ self._name = name if isinstance(name, str) else f"v{name}"
2123
+ if pd.api.types.is_datetime64_any_dtype(_type):
2124
+ _type = _np_datetime
2125
+ # dates are objects in pandas
2126
+ elif pd.api.types.is_object_dtype(_type) and self._is_date_column():
2127
+ _type = date
2128
+ self._ref = python_types_to_concepts[_type].ref(self._name)
2129
+
2130
+ def _is_date_column(self) -> bool:
2131
+ sample = self._data._data[self._name].dropna()
2132
+ if sample.empty:
2133
+ return False
2134
+ sample_value = sample.iloc[0]
2135
+ return isinstance(sample_value, date) and not isinstance(sample_value, datetime)
2136
+
2137
+ def __str__(self):
2138
+ return f"DataColumn({self._name}, {self._type})"
2139
+
2140
+ class Data(Producer):
2141
+ def __init__(self, data:DataFrame):
2142
+ super().__init__(None)
2143
+ self._data = data
2144
+ self._relationships = {}
2145
+ self._cols : list[DataColumn] = []
2146
+ self._row_id = Integer.ref("row_id")
2147
+ for col in data.columns:
2148
+ t = data[col].dtype
2149
+ self._cols.append(DataColumn(self, t, col))
2150
+ self._relationships[col] = self._cols[-1]
2151
+
2152
+ def into(self, concept:Concept, keys:list[str]=[]):
2153
+ if keys:
2154
+ new = concept.to_identity(**{k.lower(): getattr(self, k) for k in keys})
2155
+ else:
2156
+ new = concept.to_identity(self._row_id)
2157
+ where(self, new).define(
2158
+ concept(new),
2159
+ *[getattr(concept, col._name)(new, col) for col in self._cols]
2160
+ )
2161
+
2162
+ def __getitem__(self, item: str|int) -> DataColumn:
2163
+ if isinstance(item, int):
2164
+ return self._cols[item]
2165
+ if item in self._relationships:
2166
+ return self._relationships[item]
2167
+ raise KeyError(f"Data has no column {item}")
2168
+
2169
+ def _dir_extras_from_get_relationship(self):
2170
+ return set()
2171
+
2172
+ def _get_relationship(self, name: str) -> Relationship | RelationshipRef | RelationshipFieldRef:
2173
+ raise AttributeError(f"Data has no attribute {name}")
2174
+
2175
+ def __str__(self):
2176
+ return f"Data({len(self._data)} rows, [{', '.join([str(c) for c in self._cols])}])"
2177
+
2178
+ def __hash__(self):
2179
+ return hash(self._id)
2180
+
2181
+ def _to_df(data: DataFrame | list[tuple] | list[dict], columns:list[str]|None) -> DataFrame:
2182
+ if isinstance(data, DataFrame):
2183
+ return data
2184
+ if not data:
2185
+ return DataFrame()
2186
+ if isinstance(data, list):
2187
+ if isinstance(data[0], tuple):
2188
+ # Named tuple check
2189
+ if hasattr(data[0], '_fields'):
2190
+ return DataFrame([t._asdict() for t in data]) #type: ignore
2191
+ return DataFrame(data, columns=columns)
2192
+ elif isinstance(data[0], dict):
2193
+ return DataFrame(data)
2194
+ raise TypeError(f"Cannot convert {type(data)} to DataFrame. Use DataFrame, list of tuples, or list of dicts.")
2195
+
2196
+ def data(data:DataFrame|list[tuple]|list[dict], columns:list[str]|None=None) -> Data:
2197
+ return Data(_to_df(data, columns))
2198
+
2199
+ #--------------------------------------------------
2200
+ # Constraints
2201
+ #--------------------------------------------------
2202
+ class Constraint:
2203
+ """Base class for constraints"""
2204
+
2205
+ def __init__(self, model:Model|None=None):
2206
+ self._model = model or find_model(fields)
2207
+ if self._model:
2208
+ self._model.constraints.add(self)
2209
+
2210
+ @property
2211
+ def _id(self):
2212
+ """Returns unique id for this constraint"""
2213
+ raise NotImplementedError(f"`{type(self).__name__}._id` not implemented")
2214
+
2215
+ @property
2216
+ def _relationship(self) -> Relationship:
2217
+ """Returns builtin relationship for this constraint"""
2218
+ raise NotImplementedError(f"`{type(self).__name__}._relationship` not implemented")
2219
+
2220
+ def to_expressions(self) -> tuple[Expression]:
2221
+ """Returns Expressions for this constraint"""
2222
+ raise NotImplementedError(f"`{type(self).__name__}.to_expressions` not implemented")
2223
+
2224
+ def __eq__(self, other):
2225
+ if isinstance(other, Constraint):
2226
+ return self._id == other._id
2227
+ return False
2228
+
2229
+ def __hash__(self):
2230
+ return hash(self._id)
2231
+
2232
+ class FieldsConstraint(Constraint):
2233
+
2234
+ def __init__(self, *fields: RelationshipFieldRef, model:Model|None=None):
2235
+ self._init_constraint(*fields)
2236
+ super().__init__(model)
2237
+
2238
+ def _init_constraint(self, *fields: RelationshipFieldRef) -> None:
2239
+ self._fields = fields
2240
+ ids = [field._field_ref._id for field in fields]
2241
+ ids.sort() # ensures order doesn't matter
2242
+ self._stable_id = f'{type(self).__name__}:{ids}'
2243
+
2244
+ @property
2245
+ def _id(self):
2246
+ return self._stable_id
2247
+
2248
+ def _to_field_expressions(self, fields: tuple[RelationshipFieldRef, ...]) -> Expression:
2249
+ return Expression(self._relationship, TupleArg(fields))
2250
+
2251
+ @classmethod
2252
+ def to_identity(cls, *fields: RelationshipFieldRef):
2253
+ obj = cls.__new__(cls)
2254
+ obj._init_constraint(*fields)
2255
+ return obj
2256
+
2257
+ class Unique(FieldsConstraint):
2258
+
2259
+ def __init__(self, *fields: RelationshipFieldRef, model:Model|None=None):
2260
+ first_field_rel = fields[0]._relationship
2261
+ self._internal = all(first_field_rel._is_same_relationship(f._relationship) for f in fields[1:])
2262
+ super().__init__(*fields, model=model)
2263
+ if self._internal:
2264
+ relationship = first_field_rel._alt_of if isinstance(first_field_rel, RelationshipReading) else first_field_rel
2265
+ relationship._internal_constraints.add(self)
2266
+
2267
+ def internal(self) -> bool:
2268
+ return self._internal
2269
+
2270
+ def to_expressions(self) -> tuple[Expression]:
2271
+ if self.internal():
2272
+ expressions = []
2273
+ rel = self._fields[0]._relationship
2274
+ relationship = rel._alt_of if isinstance(rel, RelationshipReading) else rel
2275
+ # create Expression from the relationship
2276
+ expressions.append(self._to_field_expressions(self._find_fields(relationship)))
2277
+ # skip the first implicitly created reading and create Expressions for all other readings
2278
+ for reading in relationship._readings[1:]:
2279
+ expressions.append(self._to_field_expressions(self._find_fields(reading)))
2280
+ return tuple(expressions)
2281
+ else:
2282
+ return (self._to_field_expressions(self._fields),)
2283
+
2284
+ @property
2285
+ def _relationship(self) -> Relationship:
2286
+ return Relationship.builtins[builtins.unique.name]
2287
+
2288
+ def _find_fields(self, rel:Relationship|RelationshipReading) -> tuple[RelationshipFieldRef]:
2289
+ rel_fields = []
2290
+ for field in self._fields:
2291
+ assert isinstance(field._field_ref, Ref) and isinstance(field._field_ref._name, str)
2292
+ rel_fields.append(rel[field._field_ref._name])
2293
+ return tuple(rel_fields)
2294
+
2295
+ class SubtypeConstraint(Constraint):
2296
+
2297
+ def __init__(self, *concepts: Concept, model:Model|None=None):
2298
+ if len(concepts) < 2:
2299
+ raise ValueError("Invalid subtype constraint. A constraint should hold at least 2 concepts")
2300
+ first = concepts[0]
2301
+ first_super_types = [super_type._id for super_type in first._extends]
2302
+ for c in concepts:
2303
+ if len(c._extends) == 0:
2304
+ raise ValueError(f"Invalid subtype constraint. '{c}' is not a subtype")
2305
+ c_super_types = [super_type._id for super_type in c._extends]
2306
+ if first_super_types != c_super_types:
2307
+ raise ValueError(f"Invalid subtype constraint. '{first}' and '{c}' must have the same parents")
2308
+ self._concepts = concepts
2309
+ ids = [c._id for c in concepts]
2310
+ ids.sort() # ensures order doesn't matter
2311
+ self._stable_id = f'{type(self).__name__}:{ids}'
2312
+ super().__init__(model)
2313
+
2314
+ @property
2315
+ def _id(self):
2316
+ return self._stable_id
2317
+
2318
+ def to_expressions(self) -> tuple[Expression]:
2319
+ return (Expression(self._relationship, TupleArg(self._concepts)),)
2320
+
2321
+ class Exclusive(SubtypeConstraint):
2322
+
2323
+ def __init__(self, *concepts: Concept, model:Model|None=None):
2324
+ super().__init__(*concepts, model=model)
2325
+
2326
+ @property
2327
+ def _relationship(self) -> Relationship:
2328
+ return Relationship.builtins[builtins.exclusive.name]
2329
+
2330
+ class Anyof(SubtypeConstraint):
2331
+
2332
+ def __init__(self, *concepts: Concept, model:Model|None=None):
2333
+ super().__init__(*concepts, model=model)
2334
+
2335
+ @property
2336
+ def _relationship(self) -> Relationship:
2337
+ return Relationship.builtins[builtins.anyof.name]
2338
+
2339
+ #--------------------------------------------------
2340
+ # Fragment
2341
+ #--------------------------------------------------
2342
+
2343
+ class Fragment():
2344
+ def __init__(self, parent:Fragment|None=None, model:Model|None=None):
2345
+ self._id = next(_global_id)
2346
+ self._select = []
2347
+ self._where = []
2348
+ self._require = []
2349
+ self._define = []
2350
+ self._order_by = []
2351
+ self._limit = 0
2352
+ self._model = parent._model if parent else model
2353
+ self._parent = parent
2354
+ self._source = runtime_env.get_source_pos()
2355
+ self._is_export = False
2356
+ self._meta = {}
2357
+ self._annotations = []
2358
+ if parent:
2359
+ self._select.extend(parent._select)
2360
+ self._where.extend(parent._where)
2361
+ self._require.extend(parent._require)
2362
+ self._define.extend(parent._define)
2363
+ self._order_by.extend(parent._order_by)
2364
+ self._limit = parent._limit
2365
+ self._meta.update(parent._meta)
2366
+
2367
+ def _add_items(self, items:PySequence[Any], to_attr:list[Any]):
2368
+ # TODO: ensure that you are _either_ a select, require, or then
2369
+ # not a mix of them
2370
+ _remove_roots(items)
2371
+ to_attr.extend(items)
2372
+
2373
+ if self._define or self._require:
2374
+ if self._parent:
2375
+ _remove_roots([self._parent])
2376
+ _add_root(self)
2377
+
2378
+ if not self._model:
2379
+ self._model = find_model(items)
2380
+ return self
2381
+
2382
+ def where(self, *args: Any) -> Fragment:
2383
+ f = Fragment(parent=self)
2384
+ return f._add_items(args, f._where)
2385
+
2386
+ def select(self, *args: Any) -> Fragment:
2387
+ # Check for Not instances in select arguments (including nested)
2388
+ def _contains_not(item: Any) -> bool:
2389
+ if isinstance(item, Not):
2390
+ return True
2391
+ elif isinstance(item, (Match, Union)):
2392
+ return any(_contains_not(arg) for arg in item._args)
2393
+ elif isinstance(item, Fragment):
2394
+ # Check all fragment components for not_
2395
+ return (
2396
+ any(_contains_not(arg) for arg in item._select)
2397
+ or any(_contains_not(arg) for arg in item._require)
2398
+ or any(_contains_not(arg) for arg in item._define)
2399
+ or any(_contains_not(arg) for arg in item._order_by)
2400
+ )
2401
+ elif isinstance(item, Expression):
2402
+ return any(_contains_not(param) for param in item._params)
2403
+ elif isinstance(item, Aggregate):
2404
+ return (
2405
+ any(_contains_not(arg) for arg in item._args)
2406
+ or any(_contains_not(arg) for arg in item._group)
2407
+ or _contains_not(item._where)
2408
+ )
2409
+ elif isinstance(item, Group):
2410
+ return any(_contains_not(arg) for arg in item._group)
2411
+ elif isinstance(item, (list, tuple)):
2412
+ return any(_contains_not(subitem) for subitem in item)
2413
+ else:
2414
+ return False
2415
+
2416
+ for arg in args:
2417
+ if _contains_not(arg):
2418
+ raise ValueError("`not_` is not allowed in `select` fragments")
2419
+
2420
+ f = Fragment(parent=self)
2421
+ return f._add_items(args, f._select)
2422
+
2423
+ def require(self, *args: Any) -> Fragment:
2424
+ f = Fragment(parent=self)
2425
+ # todo: find a better way to pass multi Expressions to require
2426
+ return f._add_items(tuple(flatten(args)), f._require)
2427
+
2428
+ def define(self, *args: Any) -> Fragment:
2429
+ f = Fragment(parent=self)
2430
+ return f._add_items(args, f._define)
2431
+
2432
+ def order_by(self, *args: Any) -> Fragment:
2433
+ experimental_warning("order_by")
2434
+ f = Fragment(parent=self)
2435
+ return f._add_items(args, f._order_by)
2436
+
2437
+ def limit(self, n:int) -> Fragment:
2438
+ experimental_warning("limit")
2439
+ f = Fragment(parent=self)
2440
+ f._limit = n
2441
+ return f
2442
+
2443
+ def meta(self, **kwargs: Any) -> Fragment:
2444
+ """Add metadata to the query.
2445
+
2446
+ Metadata can be used for debugging and observability purposes.
2447
+
2448
+ Args:
2449
+ **kwargs: Metadata key-value pairs
2450
+
2451
+ Returns:
2452
+ Fragment: Returns self for method chaining
2453
+
2454
+ Example:
2455
+ select(Person.name).meta(workload_name="test", priority=1, enabled=True)
2456
+ """
2457
+ if not kwargs:
2458
+ raise ValueError("meta() requires at least one argument")
2459
+
2460
+ self._meta.update(kwargs)
2461
+ return self
2462
+
2463
+
2464
+ def annotate(self, *annos:Expression|Relationship|ir.Annotation) -> Fragment:
2465
+ self._annotations.extend(annos)
2466
+ return self
2467
+
2468
+ #--------------------------------------------------
2469
+ # helpers
2470
+ #--------------------------------------------------
2471
+
2472
+ def _is_effect(self) -> bool:
2473
+ return bool(self._define or self._require or (self._parent and self._parent._is_effect()))
2474
+
2475
+ def _is_where_only(self) -> bool:
2476
+ return not self._select and not self._define and not self._require and not self._order_by
2477
+
2478
+ #--------------------------------------------------
2479
+ # And/Or
2480
+ #--------------------------------------------------
2481
+
2482
+ def __or__(self, other) -> Match:
2483
+ return Match(self, other)
2484
+
2485
+ def __and__(self, other) -> Fragment:
2486
+ if isinstance(other, Fragment):
2487
+ return other.where(self)
2488
+ return where(self, other)
2489
+
2490
+ #--------------------------------------------------
2491
+ # Stringify
2492
+ #--------------------------------------------------
2493
+
2494
+ def __str__(self):
2495
+ sections = []
2496
+ if self._select:
2497
+ select = '\n '.join(map(str, self._select))
2498
+ sections.append(f"(select\n {select})")
2499
+ if self._where:
2500
+ where = '\n '.join(map(str, self._where))
2501
+ sections.append(f"(where\n {where})")
2502
+ if self._require:
2503
+ require = '\n '.join(map(str, self._require))
2504
+ sections.append(f"(require\n {require})")
2505
+ if self._define:
2506
+ effects = '\n '.join(map(str, self._define))
2507
+ sections.append(f"(then\n {effects})")
2508
+ if self._order_by:
2509
+ order_by = '\n '.join(map(str, self._order_by))
2510
+ sections.append(f"(order_by\n {order_by})")
2511
+ if self._limit:
2512
+ sections.append(f"(limit {self._limit})")
2513
+
2514
+ return "\n".join(sections)
2515
+
2516
+ #--------------------------------------------------
2517
+ # Execute
2518
+ #--------------------------------------------------
2519
+
2520
+ def __iter__(self):
2521
+ # Iterate over the rows of the fragment's results
2522
+ return self.to_df().itertuples(index=False).__iter__()
2523
+
2524
+ def inspect(self):
2525
+ # @TODO what format? maybe ignore row indices?
2526
+ print(self.to_df(in_inspect=True))
2527
+
2528
+ def to_df(self, in_inspect:bool=False):
2529
+ """Convert the fragment's results to a pandas DataFrame."""
2530
+ # @TODO currently this code assumes a Rel executor; should dispatch based on config
2531
+
2532
+ # If there are no selects, then there are no results to return
2533
+ if not self._select:
2534
+ return DataFrame()
2535
+
2536
+ qb_model = self._model or Model("anon")
2537
+ ir_model = qb_model._to_ir()
2538
+ self._source = runtime_env.get_source_pos()
2539
+ # @TODO for now we set tag to None but we need to work out how to properly propagate user-provided tag here
2540
+ with debugging.span("query", tag=None, dsl=str(self), **with_source(self), meta=self._meta) as query_span:
2541
+ query_task = qb_model._compiler.fragment(self)
2542
+ results = qb_model._to_executor().execute(ir_model, query_task, meta=self._meta)
2543
+ query_span["results"] = results
2544
+ # For local debugging mostly
2545
+ dry_run = qb_model._dry_run or bool(qb_model._config.get("compiler.dry_run", False))
2546
+ inspect_df = bool(qb_model._config.get("compiler.inspect_df", False))
2547
+ if not in_inspect and not dry_run and inspect_df:
2548
+ print(results)
2549
+ return results
2550
+
2551
+ def to_snowpark(self):
2552
+ """
2553
+ Convert the fragment's results to a snowflake DataFrame.
2554
+ `snowflake.snowpark.DataFrame` represents a lazily-evaluated relational dataset.
2555
+ The computation is not performed until you call a method that performs an action (e.g. collect(), to_pandas()).
2556
+ """
2557
+ # If there are no selects, then there are no results to return
2558
+ if not self._select:
2559
+ return SnowparkDataFrame()
2560
+
2561
+ qb_model = self._model or Model("anon")
2562
+ clone = Fragment(parent=self)
2563
+ clone._is_export = True
2564
+ ir_model = qb_model._to_ir()
2565
+ clone._source = runtime_env.get_source_pos()
2566
+ # @TODO for now we set tag to None but we need to work out how to properly propagate user-provided tag here
2567
+ with debugging.span("query", tag=None, dsl=str(clone), **with_source(clone), meta=clone._meta) as query_span:
2568
+ query_task = qb_model._compiler.fragment(clone)
2569
+ results = qb_model._to_executor().execute(ir_model, query_task, format="snowpark", meta=clone._meta)
2570
+ query_span["alt_format_results"] = results
2571
+ return results
2572
+
2573
+ def into(self, table:Any, update:bool=False) -> None:
2574
+ from .snowflake import Table
2575
+ assert isinstance(table, Table), "Only Snowflake tables are supported for now"
2576
+
2577
+ clone = Fragment(parent=self)
2578
+ clone._is_export = True
2579
+ qb_model = clone._model or Model("anon")
2580
+ ir_model = qb_model._to_ir()
2581
+ clone._source = runtime_env.get_source_pos()
2582
+ with debugging.span("query", dsl=str(clone), **with_source(clone), meta=clone._meta):
2583
+ query_task = qb_model._compiler.fragment(clone)
2584
+ qb_model._to_executor().execute(ir_model, query_task, export_to=table, update=update, meta=clone._meta)
2585
+
2586
+ #--------------------------------------------------
2587
+ # Select / Where
2588
+ #--------------------------------------------------
2589
+
2590
+ def select(*args: Any, parent:Fragment|None=None, model:Model|None=None) -> Fragment:
2591
+ return Fragment(model=model).select(*args)
2592
+
2593
+ def where(*args: Any, parent:Fragment|None=None, model:Model|None=None) -> Fragment:
2594
+ return Fragment(model=model).where(*args)
2595
+
2596
+ def require(*args: Any, parent:Fragment|None=None, model:Model|None=None) -> Fragment:
2597
+ return Fragment(model=model).require(*args)
2598
+
2599
+ def define(*args: Any, parent:Fragment|None=None, model:Model|None=None) -> Fragment:
2600
+ return Fragment(model=model).define(*args)
2601
+
2602
+ #--------------------------------------------------
2603
+ # Model
2604
+ #--------------------------------------------------
2605
+
2606
+ class Model():
2607
+ def __init__(
2608
+ self,
2609
+ name: str,
2610
+ dry_run: bool = False,
2611
+ keep_model: bool = True,
2612
+ use_lqp: bool | None = None,
2613
+ use_sql: bool = False,
2614
+ strict: bool = False,
2615
+ wide_outputs: bool = False,
2616
+ enable_otel_handler: bool | None = None,
2617
+ connection: Session | None = None,
2618
+ config: Config | None = None,
2619
+ ):
2620
+ self._id = next(_global_id)
2621
+ self.name = f"{name}{overrides('model_suffix', '')}"
2622
+ self._dry_run = cast(bool, overrides('dry_run', dry_run))
2623
+ self._keep_model = cast(bool, overrides('keep_model', keep_model))
2624
+ self._use_sql = cast(bool, overrides('use_sql', use_sql))
2625
+ self._wide_outputs = cast(bool, overrides('wide_outputs', wide_outputs))
2626
+ self._config = config or Config()
2627
+ config_overrides = overrides('config', {})
2628
+ for k, v in config_overrides.items():
2629
+ self._config.set(k, v)
2630
+ self._intrinsic_overrides = get_intrinsic_overrides()
2631
+ self._strict = cast(bool, overrides('strict', strict))
2632
+ self._use_lqp = overridable_flag('reasoner.rule.use_lqp', self._config, use_lqp, default=not self._use_sql)
2633
+ self._enable_otel_handler = overridable_flag('enable_otel_handler', self._config, enable_otel_handler, default=False)
2634
+ if isinstance(runtime_env, SessionEnvironment):
2635
+ self._connection = runtime_env.configure_session(self._config, connection)
2636
+ else:
2637
+ self._connection = connection
2638
+ self.concepts:dict[str, list[Concept]] = {}
2639
+ self.relationships:list[Relationship] = []
2640
+ self.enums:dict[str, Type[Enum]] = {}
2641
+ self.enum_concept:dict[Type[Enum], Concept] = {}
2642
+ self.constraints:set[Constraint] = set()
2643
+
2644
+ # Compiler
2645
+ self._compiler = Compiler()
2646
+ self._root_version = _global_roots.version()
2647
+ self._last_compilation = None
2648
+
2649
+ # Executor
2650
+ self._executor = None
2651
+
2652
+ # Enum
2653
+ self.Enum = create_enum_class(self)
2654
+
2655
+ def _to_ir(self):
2656
+ if not _global_roots.has_changed(self._root_version) and self._last_compilation:
2657
+ return self._last_compilation
2658
+ self._last_compilation = self._compiler.model(self)
2659
+ self._root_version = _global_roots.version()
2660
+ return self._last_compilation
2661
+
2662
+ def _to_executor(self):
2663
+ if not self._executor:
2664
+ if self._use_lqp:
2665
+ self._executor = LQPExecutor(
2666
+ self.name,
2667
+ dry_run=self._dry_run,
2668
+ keep_model=self._keep_model,
2669
+ wide_outputs=self._wide_outputs,
2670
+ connection=self._connection,
2671
+ config=self._config,
2672
+ intrinsic_overrides=self._intrinsic_overrides,
2673
+ )
2674
+ elif self._use_sql:
2675
+ self._executor = SnowflakeExecutor(
2676
+ self.name,
2677
+ self.name,
2678
+ dry_run=self._dry_run,
2679
+ config=self._config,
2680
+ skip_denormalization=True,
2681
+ connection=self._connection,
2682
+ )
2683
+ else:
2684
+ self._executor = RelExecutor(
2685
+ self.name,
2686
+ dry_run=self._dry_run,
2687
+ keep_model=self._keep_model,
2688
+ wide_outputs=self._wide_outputs,
2689
+ connection=self._connection,
2690
+ config=self._config,
2691
+ )
2692
+ configure_otel(self._enable_otel_handler, self._config, self._executor.resources)
2693
+ return self._executor
2694
+
2695
+ def Concept(self, name:str, extends:list[Concept|Any]=[], identify_by:dict[str, Any]={}) -> Concept:
2696
+ concept = Concept(name, model=self, extends=extends, identify_by=identify_by)
2697
+ if name not in self.concepts:
2698
+ self.concepts[name] = list()
2699
+ self.concepts[name].append(concept)
2700
+ return concept
2701
+
2702
+ def Relationship(self, *args, short_name:str="") -> Relationship:
2703
+ return Relationship(*args, parent=None, short_name=short_name, model=self)
2704
+
2705
+ def Property(self, *args, short_name:str="") -> Property:
2706
+ return Property(*args, parent=None, short_name=short_name, model=self)
2707
+
2708
+ def define(self, *args: Any) -> Fragment:
2709
+ return define(*args, model=self)
2710
+
2711
+ #--------------------------------------------------
2712
+ # Compile
2713
+ #--------------------------------------------------
2714
+
2715
+ class CompilerContext():
2716
+ def __init__(self, compiler:Compiler, parent:CompilerContext|None=None):
2717
+ self.compiler = compiler
2718
+ self.parent = parent
2719
+ self.value_map:dict[Any, ir.Value|list[ir.Var]] = parent.value_map.copy() if parent else {}
2720
+ self.items:OrderedSet[ir.Task] = OrderedSet()
2721
+ self.into_vars:list[ir.Var] = parent.into_vars.copy() if parent else []
2722
+ self.global_value_map:dict[Any, ir.Value|list[ir.Var]] = parent.global_value_map if parent else {}
2723
+
2724
+ def to_value(self, item:Any, or_value=None, is_global_or_value=True) -> ir.Value|list[ir.Var]:
2725
+ if item not in self.value_map:
2726
+ if item in self.global_value_map:
2727
+ self.value_map[item] = self.global_value_map[item]
2728
+ elif or_value is not None:
2729
+ if is_global_or_value:
2730
+ # when or_value is global save it in global_value_map as well
2731
+ self.map_var(item, or_value)
2732
+ else:
2733
+ self.value_map[item] = or_value
2734
+ else:
2735
+ name = to_name(item)
2736
+ qb_type = to_type(item)
2737
+ type = self.compiler.to_type(qb_type) if qb_type else types.Any
2738
+ self.map_var(item, f.var(name, type))
2739
+ return self.value_map[item]
2740
+
2741
+ def map_var(self, item:Any, value:ir.Value|list[ir.Var]):
2742
+ self.global_value_map[item] = value
2743
+ self.value_map[item] = value
2744
+ return value
2745
+
2746
+ def fetch_var(self, item:Any):
2747
+ if item in self.value_map:
2748
+ return self.value_map[item]
2749
+ elif item in self.global_value_map:
2750
+ return self.global_value_map[item]
2751
+ return None
2752
+
2753
+ def _has_item(self, item:ir.Task) -> bool:
2754
+ return bool(item in self.items or (self.parent and self.parent._has_item(item)))
2755
+
2756
+ def add(self, item:ir.Task):
2757
+ if not self._has_item(item):
2758
+ self.items.add(item)
2759
+
2760
+ def try_merge_hoists(self, required: PySequence[ir.VarOrDefault], available: PySequence[ir.VarOrDefault]) -> list[ir.VarOrDefault] | None:
2761
+ avail_map = {(item.var if isinstance(item, ir.Default) else item): item for item in available}
2762
+ result = []
2763
+ for req in required:
2764
+ var = req.var if isinstance(req, ir.Default) else req
2765
+ if var not in avail_map:
2766
+ return None
2767
+ # prefer the available default as it would've bubbled up and overridden
2768
+ # the required one, otherwise take the required
2769
+ result.append(avail_map[var] if isinstance(avail_map[var], ir.Default) else req)
2770
+ return result
2771
+
2772
+ def safe_wrap(self, required_hoists:PySequence[ir.VarOrDefault]) -> ir.Task:
2773
+ first = self.items[0]
2774
+ if len(self.items) == 1 and isinstance(first, ir.Logical):
2775
+ merged = self.try_merge_hoists(required_hoists, first.hoisted)
2776
+ if merged is not None:
2777
+ return f.logical(list(first.body), merged)
2778
+ return f.logical(list(self.items), required_hoists)
2779
+
2780
+ def is_hoisted(self, var: ir.Var):
2781
+ return any(isinstance(i, helpers.COMPOSITES) and var in helpers.hoisted_vars(i.hoisted) for i in self.items)
2782
+
2783
+ def clone(self):
2784
+ return CompilerContext(self.compiler, self)
2785
+
2786
+
2787
+ # map literal, python native types to IR types
2788
+ PY_LITERAL_TYPE_MAPPING = {
2789
+ str: types.String,
2790
+ bool: types.Bool,
2791
+ int: types.Int128,
2792
+ float: types.Float,
2793
+ PyDecimal: types.Decimal,
2794
+ date: types.Date,
2795
+ datetime: types.DateTime,
2796
+ }
2797
+
2798
+ PY_LITERAL_TYPES = (str, bool, int, float, date, datetime, PyDecimal)
2799
+
2800
+ def literal_value_to_type(value) -> ir.Type:
2801
+ literal_type = type(value)
2802
+ if literal_type in PY_LITERAL_TYPE_MAPPING:
2803
+ return PY_LITERAL_TYPE_MAPPING[literal_type]
2804
+ raise TypeError(f"Cannot determine type for value: {value} of type {type(value).__name__}")
2805
+
2806
+ class Compiler():
2807
+ def __init__(self):
2808
+ self.types:dict[Concept, ir.ScalarType] = {}
2809
+ self.name_to_type:dict[str, ir.Type] = {}
2810
+ self.relations:dict[Relationship|Concept|ConceptMember|RelationshipRef|RelationshipReading|ir.Relation, ir.Relation] = {}
2811
+ # cache box_type relations
2812
+ self.box_type_relations:dict[tuple[ir.Type, ir.Type], ir.Relation] = {}
2813
+
2814
+ #--------------------------------------------------
2815
+ # Type/Relation conversion
2816
+ #--------------------------------------------------
2817
+
2818
+ def to_annos(self, item:Concept|Relationship|RelationshipReading|Fragment) -> list[ir.Annotation]:
2819
+ annos = []
2820
+ items = item._annotations
2821
+ for item in items:
2822
+ if isinstance(item, Expression):
2823
+ ctx = CompilerContext(self)
2824
+ annos.append(f.annotation(self.to_relation(item._op), flatten([self.lookup(p, ctx) for p in item._params])))
2825
+ elif isinstance(item, Relationship):
2826
+ annos.append(f.annotation(self.to_relation(item), []))
2827
+ elif isinstance(item, ir.Annotation):
2828
+ annos.append(item)
2829
+ else:
2830
+ raise ValueError(f"Cannot convert {type(item).__name__} to annotation")
2831
+ return annos
2832
+
2833
+ def to_type(self, concept:Concept) -> ir.ScalarType:
2834
+ if concept not in self.types:
2835
+ if is_decimal(concept):
2836
+ self.types[concept] = types.decimal_by_type_str(concept._name)
2837
+ elif concept in Concept.builtin_concepts:
2838
+ self.types[concept] = types.builtin_scalar_types_by_name[concept._name]
2839
+ else:
2840
+ parent_types = [self.to_type(parent) for parent in concept._extends]
2841
+ self.types[concept] = f.scalar_type(concept._name, parent_types, annos=self.to_annos(concept))
2842
+ self.name_to_type[concept._name] = self.types[concept]
2843
+ return self.types[concept]
2844
+
2845
+ def to_relation(self, item:Concept|Relationship|RelationshipReading|RelationshipRef|ir.Relation) -> ir.Relation:
2846
+ if item not in self.relations:
2847
+ if isinstance(item, Concept):
2848
+ fields = [f.field(item._name.lower(), self.to_type(item))]
2849
+ annos = self.to_annos(item)
2850
+ builtins.builtin_annotations_by_name
2851
+ annos.append(builtins.concept_relation_annotation)
2852
+ relation = f.relation(item._name, fields, annos=annos)
2853
+ elif isinstance(item, Relationship):
2854
+ if item._ir_relation:
2855
+ relation = item._ir_relation
2856
+ for overload in relation.overloads:
2857
+ self.to_relation(overload)
2858
+ else:
2859
+ fields = []
2860
+ for cur in item._field_refs:
2861
+ assert isinstance(cur._thing, Concept)
2862
+ fields.append(f.field(to_name(cur), self.to_type(cur._thing)))
2863
+ overloads = []
2864
+ if item._unresolved:
2865
+ overloads = [v for k, v in self.relations.items()
2866
+ if isinstance(k, Relationship)
2867
+ and not k._unresolved
2868
+ and k._name == item._name]
2869
+ relation = f.relation(item._name, fields, annos=self.to_annos(item), overloads=overloads)
2870
+ # skip the first reading since it's the same as the Relationship
2871
+ for red in item._readings[1:]:
2872
+ self.to_relation(red)
2873
+ elif isinstance(item, RelationshipReading):
2874
+ fields = []
2875
+ for cur in item._field_refs:
2876
+ assert isinstance(cur._thing, Concept)
2877
+ fields.append(f.field(to_name(cur), self.to_type(cur._thing)))
2878
+ # todo: should we look for overloads in case alt_of Relationship is unresolved?
2879
+ relation = f.relation(item._name, fields, annos=self.to_annos(item))
2880
+ elif isinstance(item, RelationshipRef):
2881
+ relation = self.to_relation(item._relationship)
2882
+ elif isinstance(item, ir.Relation):
2883
+ for overload in item.overloads:
2884
+ self.to_relation(overload)
2885
+ relation = item
2886
+ self.relations[item] = relation
2887
+ return relation
2888
+ else:
2889
+ return self.relations[item]
2890
+
2891
+ #--------------------------------------------------
2892
+ # Model
2893
+ #--------------------------------------------------
2894
+
2895
+ @roots(enabled=False)
2896
+ def model(self, model:Model) -> ir.Model:
2897
+ rules = []
2898
+ for concepts in model.concepts.values():
2899
+ for concept in concepts:
2900
+ if concept not in self.types:
2901
+ self.to_type(concept)
2902
+ self.to_relation(concept)
2903
+ rule = self.concept_inheritance_rule(concept)
2904
+ if rule:
2905
+ rules.append(rule)
2906
+ unresolved = []
2907
+ for relationship in model.relationships:
2908
+ if relationship not in self.relations:
2909
+ if relationship._unresolved:
2910
+ unresolved.append(relationship)
2911
+ else:
2912
+ self.to_relation(relationship)
2913
+ for relationship in unresolved:
2914
+ self.to_relation(relationship)
2915
+ with debugging.span("rule_batch"):
2916
+ for idx, rule in enumerate(_global_roots):
2917
+ if not rule._model or rule._model == model:
2918
+ meta = rule._meta if isinstance(rule, Fragment) else {}
2919
+ with debugging.span("rule", name=f"rule{idx}", dsl=str(rule), **with_source(rule), meta=meta) as rule_span:
2920
+ rule_ir = self.compile_task(rule)
2921
+ rules.append(rule_ir)
2922
+ rule_span["metamodel"] = str(rule_ir)
2923
+ root = f.logical(rules)
2924
+ engines = ordered_set()
2925
+ relations = OrderedSet.from_iterable(self.relations.values())
2926
+ types = OrderedSet.from_iterable(self.types.values())
2927
+ return f.model(engines, relations, types, root)
2928
+
2929
+ #--------------------------------------------------
2930
+ # Compile
2931
+ #--------------------------------------------------
2932
+
2933
+ @roots(enabled=False)
2934
+ def compile_task(self, thing:Expression|Fragment) -> ir.Task:
2935
+ if isinstance(thing, (Expression, Match, Union)):
2936
+ return self.root_expression(thing)
2937
+ elif isinstance(thing, Fragment):
2938
+ return self.fragment(thing)
2939
+
2940
+ #--------------------------------------------------
2941
+ # Root expression
2942
+ #--------------------------------------------------
2943
+
2944
+ @roots(enabled=False)
2945
+ def root_expression(self, item:Expression) -> ir.Task:
2946
+ ctx = CompilerContext(self)
2947
+ self.update(item, ctx)
2948
+ return f.logical(list(ctx.items))
2949
+
2950
+ #--------------------------------------------------
2951
+ # Fragment
2952
+ #--------------------------------------------------
2953
+
2954
+ def _is_rank(self, item) -> bool:
2955
+ return isinstance(item, Aggregate) and item._op._name == "rank"
2956
+
2957
+ def _process_rank(self, items:PySequence[Expression], rank_ctx:CompilerContext):
2958
+ args_to_process = ordered_set()
2959
+ arg_is_ascending = []
2960
+ for item in items:
2961
+ if isinstance(item, RankOrder):
2962
+ args_to_process.update(item._args)
2963
+ arg_is_ascending.extend([item._is_asc] * len(item._args))
2964
+ else:
2965
+ args_to_process.add(item)
2966
+ arg_is_ascending.append(RankOrder.ASC)
2967
+
2968
+ keys = ordered_set()
2969
+ for arg in args_to_process:
2970
+ if isinstance(arg, Distinct):
2971
+ continue
2972
+ keys.update(find_keys(arg))
2973
+ # Expressions go into the rank args if asked directly.
2974
+ # Otherwise they go into the projection if they are keys.
2975
+ projection = OrderedSet.from_iterable(
2976
+ flatten([self.lookup(key, rank_ctx) for key in keys], flatten_tuples=True)
2977
+ )
2978
+ args = OrderedSet.from_iterable(
2979
+ flatten([self.lookup(arg, rank_ctx) for arg in args_to_process], flatten_tuples=True)
2980
+ )
2981
+ return projection, args, arg_is_ascending
2982
+
2983
+ @roots(enabled=False)
2984
+ def fragment(self, fragment:Fragment, parent_ctx:CompilerContext|None=None, into_vars:list[ir.Var] = []) -> ir.Task:
2985
+ ctx = CompilerContext(self, parent_ctx)
2986
+ if fragment._require:
2987
+ self.require(fragment, fragment._require, ctx)
2988
+ else:
2989
+ rank_var = self.order_by_or_limit(fragment, ctx)
2990
+ self.where(fragment, fragment._where, ctx)
2991
+ self.define(fragment, fragment._define, ctx)
2992
+ self.select(fragment, fragment._select, ctx, rank_var)
2993
+ return f.logical(list(ctx.items), ctx.into_vars, annos=self.to_annos(fragment))
2994
+
2995
+ def order_by_or_limit(self, fragment:Fragment, ctx:CompilerContext):
2996
+ if fragment._limit == 0 and not fragment._order_by:
2997
+ return None
2998
+ if fragment._define:
2999
+ raise NotImplementedError("Order_by and/or limit are not supported on define")
3000
+
3001
+ limit_ctx = ctx.clone()
3002
+ inner_ctx = limit_ctx.clone()
3003
+
3004
+ # If there is an order-by, then the limit is applied on the fields there. Otherwise,
3005
+ # the limit is applied on the fields in the select (with a default ranking order).
3006
+ items = fragment._order_by if fragment._order_by else fragment._select
3007
+
3008
+ projection, args, arg_is_ascending = self._process_rank(items, inner_ctx)
3009
+
3010
+ limit_ctx.add(inner_ctx.safe_wrap([]))
3011
+
3012
+ rank_var = f.var("v", types.Int128)
3013
+ limit_ctx.add(f.rank(list(projection), [], list(args), arg_is_ascending, rank_var, fragment._limit))
3014
+ ctx.add(f.logical(list(limit_ctx.items), [rank_var]))
3015
+ return rank_var
3016
+
3017
+ def where(self, fragment:Fragment, items:PySequence[Expression], ctx:CompilerContext):
3018
+ for item in items:
3019
+ self.lookup(item, ctx)
3020
+
3021
+ def select(self, fragment:Fragment, items:PySequence[Expression], ctx:CompilerContext, rank_var:ir.Var|None=None):
3022
+ if not items:
3023
+ return
3024
+
3025
+ namer = NameCache(use_underscore=False)
3026
+ aggregate_keys:OrderedSet[ir.Var] = OrderedSet()
3027
+ out_var_to_keys = {}
3028
+ fields = []
3029
+ if rank_var:
3030
+ fields.append((namer.get_name(len(fields), "rank"), rank_var))
3031
+ keys_present = has_keys(items)
3032
+ for ix, item in enumerate(items):
3033
+ # allow primitive to be a key when at least one key is present and primitive is not the last item
3034
+ # this is needed to avoid cross products in output
3035
+ enable_primitive_key = ix != len(items) - 1 if keys_present else False
3036
+ keys = find_select_keys(item, enable_primitive_key=enable_primitive_key)
3037
+
3038
+ key_vars:list[ir.Var] = []
3039
+ for idx, key in enumerate(keys):
3040
+ # don't add lookups for the keys played by Concepts if they are not on the first position
3041
+ if idx > 0 and isinstance(key.val, Concept):
3042
+ key_var = ctx.to_value(key.val)
3043
+ else:
3044
+ key_var = self.lookup(key.val, ctx)
3045
+ assert isinstance(key_var, ir.Var)
3046
+ key_vars.append(key_var)
3047
+ if key.is_group:
3048
+ aggregate_keys.add(key_var)
3049
+
3050
+ sub_ctx = ctx.clone()
3051
+ result_vars = []
3052
+ # If we lookup a property or a relationship, without a parent (a bare one)
3053
+ # don't assume some value can be None. Add the lookup directly to the parent ctx
3054
+ if isinstance(item, RelationshipRef) and isinstance(item._parent, Property) and not item._parent._parent:
3055
+ self.lookup(item._parent, ctx)
3056
+ result = self.lookup(item, sub_ctx)
3057
+ elif isinstance(item, (Property, Relationship, RelationshipFieldRef)) and not item._parent:
3058
+ result = self.lookup(item, ctx)
3059
+ else:
3060
+ result = self.lookup(item, sub_ctx)
3061
+ if isinstance(result, list):
3062
+ assert all(isinstance(v, ir.Var) for v in result)
3063
+ result_vars.extend(result)
3064
+ else:
3065
+ result_vars.append(result)
3066
+
3067
+ # normalize result vars through fetch_var if available
3068
+ result_vars = [
3069
+ fetched if isinstance(fetched := ctx.fetch_var(v), ir.Var) else v
3070
+ for v in result_vars
3071
+ ]
3072
+
3073
+ extra_nullable_keys: OrderedSet[ir.Var] = OrderedSet()
3074
+ # check if whether we actually added a lookup resulting in the key, in the sub-context
3075
+ # the lookup might have already existed in the parent context, in which case the key is not nullable.
3076
+ # E.g.,
3077
+ # attends(course)
3078
+ # course = ..
3079
+ # Logical ^[..]
3080
+ #
3081
+ # vs
3082
+ #
3083
+ # Logical ^[.., course=None]
3084
+ # attends(course)
3085
+ for it in sub_ctx.items:
3086
+ if isinstance(it, ir.Lookup):
3087
+ vars = helpers.vars(it.args)
3088
+ if vars[-1] in key_vars:
3089
+ extra_nullable_keys.add(vars[-1])
3090
+
3091
+ if len(sub_ctx.items) > 0:
3092
+ args = list(result_vars)
3093
+ for k in extra_nullable_keys:
3094
+ if k not in args:
3095
+ args.append(k)
3096
+ hoisted:list[ir.VarOrDefault] = [ir.Default(v, None) for v in args if isinstance(v, ir.Var)]
3097
+ ctx.add(sub_ctx.safe_wrap(hoisted))
3098
+
3099
+ for v in result_vars:
3100
+ name = "v"
3101
+ if isinstance(item, Alias):
3102
+ name = item._name
3103
+ elif isinstance(v, ir.Var):
3104
+ name = v.name
3105
+ out_var_to_keys[v] = key_vars
3106
+
3107
+ # if this is a nested select that is populating variables rather
3108
+ # than outputting
3109
+ if ctx.into_vars:
3110
+ relation = self.to_relation(builtins.eq)
3111
+ ctx.add(f.lookup(relation, [ctx.into_vars[ix], v]))
3112
+ else:
3113
+ fields.append((namer.get_name(len(fields), name), v))
3114
+
3115
+ if fields:
3116
+ annos = fragment._annotations
3117
+ if fragment._is_export:
3118
+ annos += [builtins.export_annotation]
3119
+
3120
+ # If one of the vars in our output is itself a key, and it's the key of an
3121
+ # aggregation, then we should ignore its keys. This fixes the case where we
3122
+ # return the group of an aggregate and should ignore the keys of the group variables.
3123
+ final_keys = ordered_set()
3124
+ for v, keys in out_var_to_keys.items():
3125
+ if v in aggregate_keys:
3126
+ final_keys.add(v)
3127
+ else:
3128
+ final_keys.update(keys)
3129
+
3130
+ # If we are exporting into a table, we need to add a key to the output
3131
+ # We hash all the values to create a key
3132
+ if not final_keys and fragment._is_export:
3133
+ tmp_var = ctx.to_value(self)
3134
+ assert isinstance(tmp_var, ir.Var)
3135
+ key_var = f.var(tmp_var.name, types.Hash)
3136
+ assert isinstance(key_var, ir.Var)
3137
+ final_keys.add(key_var)
3138
+ values = [ir.Literal(types.String, "NO_KEYS")]
3139
+ for fld in fields:
3140
+ values.append(fld[1])
3141
+ con = ir.Construct(None, tuple(values), key_var, FrozenOrderedSet([]))
3142
+ ctx.add(con)
3143
+
3144
+ ctx.add(f.output(fields, keys=list(final_keys), annos=annos))
3145
+
3146
+ def require(self, fragment:Fragment, items:PySequence[Expression], ctx:CompilerContext):
3147
+ domain_ctx = ctx.clone()
3148
+ self.where(fragment, fragment._where, domain_ctx)
3149
+ domain_vars = OrderedSet.from_iterable(flatten(list(domain_ctx.value_map.values()), flatten_tuples=True))
3150
+ to_hoist = OrderedSet()
3151
+ checks = []
3152
+ for item in items:
3153
+ if isinstance(item, Expression) and item._op is Relationship.builtins[builtins.anyof.name] and not domain_vars:
3154
+ raise ValueError("'anyof' and 'oneof' are not allowed without a domain")
3155
+
3156
+ req_ctx = domain_ctx.clone()
3157
+ self.lookup(item, req_ctx)
3158
+ req_body = f.logical(list(req_ctx.items))
3159
+
3160
+ err_ctx = domain_ctx.clone()
3161
+ item_str = item._pprint() if isinstance(item, Producer) else str(item)
3162
+ keys = {to_name(k): k for k in find_keys(item)}
3163
+ source = item._source if hasattr(item, "_source") else fragment._source
3164
+ e = Error.new(message=f"Requirement not met: {item_str}", **keys, _source=source, _model=fragment._model)
3165
+ self.update(e, err_ctx)
3166
+ err_body = f.logical(list(err_ctx.items))
3167
+ checks.append(f.check(req_body, err_body))
3168
+
3169
+ # find vars that overlap between domain and check/error and hoist them
3170
+ all_values = flatten(list(req_ctx.value_map.values()) + list(err_ctx.value_map.values()))
3171
+ to_hoist.update(domain_vars & OrderedSet.from_iterable(all_values))
3172
+
3173
+ domain = f.logical(list(domain_ctx.items), list(to_hoist))
3174
+ req = f.require(domain, checks)
3175
+ ctx.add(req)
3176
+
3177
+ def define(self, fragment:Fragment, items:PySequence[Expression], ctx:CompilerContext):
3178
+ def _check_item(item: Expression):
3179
+ if isinstance(item, ConceptFilter):
3180
+ raise ValueError("'filter_by' is not allowed in definitions")
3181
+
3182
+ if len(items) == 1:
3183
+ item = items[0]
3184
+ _check_item(item)
3185
+ self.update(item, ctx)
3186
+ return
3187
+
3188
+ for item in items:
3189
+ _check_item(item)
3190
+ sub_ctx = ctx.clone()
3191
+ self.update(item, sub_ctx)
3192
+ if len(sub_ctx.items) > 1:
3193
+ ctx.add(f.logical(list(sub_ctx.items)))
3194
+ elif len(sub_ctx.items) == 1:
3195
+ ctx.add(sub_ctx.items[0])
3196
+
3197
+ #--------------------------------------------------
3198
+ # Reference schemes and concept inheritance
3199
+ #--------------------------------------------------
3200
+
3201
+ def concept_inheritance_rule(self, concept:Concept) -> Optional[ir.Task]:
3202
+ """
3203
+ If the concept extends non-primitive concepts, generate a rule where the body is a
3204
+ lookup for this concept and the head are derives into all non-primitive direct super
3205
+ types.
3206
+ """
3207
+ # filter extends to get only non-primitive parents
3208
+ parents = []
3209
+ for parent in concept._extends:
3210
+ if not parent._is_primitive() and parent is not AnyEntity:
3211
+ parents.append(parent)
3212
+ # always extend AnyEntity for non-primitive types that are not built-in
3213
+ if not concept._is_primitive() and concept not in Concept.builtin_concepts:
3214
+ parents.append(AnyEntity)
3215
+ # only extends primitive types, no need for inheritance rules
3216
+ if not parents:
3217
+ return None
3218
+ # generate the rule
3219
+ ctx = CompilerContext(self)
3220
+ var = self.lookup(concept, ctx)
3221
+ assert isinstance(var, ir.Var)
3222
+ return f.logical([
3223
+ *list(ctx.items),
3224
+ *[f.derive(self.to_relation(parent), [var]) for parent in parents]
3225
+ ])
3226
+
3227
+ def concept_any_entity_rule(self, entities:list[Concept]):
3228
+ """
3229
+ Generate an inheritance rule for all these entities to AnyEntity.
3230
+ """
3231
+ any_entity_relation = self.to_relation(AnyEntity)
3232
+ var = f.var("v", types.Any)
3233
+ return f.logical([
3234
+ f.union([f.lookup(self.to_relation(e), [var]) for e in entities]),
3235
+ f.derive(any_entity_relation, [var])
3236
+ ])
3237
+
3238
+ def relation_dict(self, items:dict[Relationship|Concept, Producer], ctx:CompilerContext) -> dict[ir.Relation, list[ir.Var]]:
3239
+ return {self.to_relation(k): unwrap_list(self.lookup(v, ctx)) for k, v in items.items()}
3240
+
3241
+ def construct_relation_dict(self, items: dict[Relationship | Concept, Producer], ctx: CompilerContext) -> dict[ir.Relation, list[ir.Var]]:
3242
+ result = {}
3243
+ for k, v in items.items():
3244
+ relation = self.to_relation(k)
3245
+ value = unwrap_list(self.lookup(v, ctx))
3246
+
3247
+ # We are able to check types for the `construct` only when we are using binary relations
3248
+ # in the other case we just pass the original value
3249
+ if len(relation.fields) == 2:
3250
+ field = relation.fields[-1]
3251
+ if field and field.type != types.Any and types.is_value_type(field.type):
3252
+ field_base = typer.to_base_primitive(field.type)
3253
+ value_base = typer.to_base_primitive(value.type)
3254
+
3255
+ if field_base != value_base:
3256
+ # cast to expected field type
3257
+ new_out = f.var(helpers.sanitize(to_name(k)), field.type)
3258
+ ctx.add(f.lookup(builtins.cast, [field.type, value, new_out]))
3259
+ result[relation] = new_out
3260
+ self.relations[relation] = builtins.cast
3261
+ continue
3262
+
3263
+ result[relation] = value
3264
+
3265
+ return result
3266
+
3267
+ def explode_ref_schemes(self, item:ConceptExpression, ctx:CompilerContext, update=False):
3268
+ hierarchy = item._op._ref_scheme_hierarchy()
3269
+ if not hierarchy:
3270
+ out = ctx.to_value(item)
3271
+ assert isinstance(out, ir.Var)
3272
+ ctx.add(f.construct(out, self.construct_relation_dict(item._construct_args(), ctx)))
3273
+ return out
3274
+
3275
+ # if we're just doing a lookup, then we only need the last reference scheme
3276
+ if not update:
3277
+ hierarchy = hierarchy[-1:]
3278
+
3279
+ out = None
3280
+ for ix, info in enumerate(hierarchy):
3281
+ concept = info["concept"]
3282
+ scheme = info["scheme"]
3283
+ # the "out" variable, which is constructed for the top-most concept in the
3284
+ # hierarchy and is used to key the generated derives, should be typed with the
3285
+ # most specific type, i.e. the type of the concept expression.
3286
+ # so if this is the top most (out is None) we set or_value_type to the type of
3287
+ # the item instead of the concept.
3288
+ if out is None:
3289
+ x = to_type(item)
3290
+ or_value_type = self.to_type(x) if x else self.to_type(concept)
3291
+ else:
3292
+ or_value_type = self.to_type(concept)
3293
+
3294
+ or_value = f.var(to_name(concept), or_value_type)
3295
+ # or_value is global only when it's the last Concept in hierarchy
3296
+ is_global_or_value = ix == len(hierarchy) - 1
3297
+ cur = ctx.to_value(concept, or_value, is_global_or_value) if isinstance(item, ConceptFilter) \
3298
+ else ctx.to_value((item, ix), or_value, is_global_or_value)
3299
+ assert isinstance(cur, ir.Var)
3300
+ ctx.add(f.construct(cur, self.construct_relation_dict(item._construct_args(scheme), ctx), prefix=[self.to_type(concept)]))
3301
+ if not out:
3302
+ out = cur
3303
+ if r := info.get("mapping"):
3304
+ rel = self.to_relation(r)
3305
+ if out is cur:
3306
+ out = ctx.to_value(item, f.var(to_name(concept), self.to_type(concept)))
3307
+ assert isinstance(out, ir.Var)
3308
+ if update:
3309
+ ctx.add(f.derive(rel, [cur, out]))
3310
+ else:
3311
+ ctx.add(f.lookup(rel, [cur, out]))
3312
+
3313
+ assert out is not None
3314
+ return out
3315
+
3316
+ #--------------------------------------------------
3317
+ # Lookup
3318
+ #--------------------------------------------------
3319
+
3320
+ def lookup(self, item:Any, ctx:CompilerContext) -> ir.Value|list[ir.Var]:
3321
+ if isinstance(item, ConceptExpression):
3322
+ assert isinstance(item._op, Concept)
3323
+ concept = item._op
3324
+ relation = self.to_relation(concept)
3325
+ (ident, kwargs) = item._params
3326
+
3327
+ # If this is a member lookup, check that the identity is a member
3328
+ # and add all the kwargs as lookups
3329
+ if isinstance(item, ConceptMember):
3330
+ out = self.lookup(ident, ctx)
3331
+ if isinstance(out, PY_LITERAL_TYPES):
3332
+ out = f.literal(out, self.to_type(concept))
3333
+ assert isinstance(out, (ir.Var, ir.Literal))
3334
+ if not concept._is_primitive():
3335
+ ctx.add(f.lookup(relation, [out]))
3336
+ rels = {self.to_relation(getattr(concept, k)): unwrap_list(self.lookup(v, ctx))
3337
+ for k, v in kwargs.items()}
3338
+ for k, v in rels.items():
3339
+ assert not isinstance(v, list)
3340
+ ctx.add(f.lookup(k, [out, v]))
3341
+
3342
+ # Boxing operation on value types
3343
+ # E.g., SSN(str_var), box a String to an SSN in the IR
3344
+ op_type = self.to_type(concept)
3345
+ if types.is_value_type(op_type):
3346
+ inner_type = out.type
3347
+ if inner_type == op_type:
3348
+ return out
3349
+
3350
+ # immediately transform string literals in symbol literals if necessary
3351
+ if isinstance(out, ir.Literal) and inner_type == types.String and op_type == types.Symbol:
3352
+ return f.literal(out.value, types.Symbol)
3353
+
3354
+ if ctx.fetch_var(item):
3355
+ new_out = ctx.fetch_var(item)
3356
+ assert not isinstance(new_out, list)
3357
+ else:
3358
+ new_out = f.var(helpers.sanitize(to_name(concept)), op_type)
3359
+ ctx.map_var(item, new_out)
3360
+
3361
+ ctx.add(f.lookup(builtins.cast, [op_type, out, new_out]))
3362
+ self.relations[item] = builtins.cast
3363
+ out = new_out
3364
+
3365
+ return out
3366
+
3367
+ # There are 3 types of kwargs usage:
3368
+ # 1. Only ref schema attributes - generate construct + population relation
3369
+ # 2. Ref schema attributes + some other relations - generate construct + population relation + lookups
3370
+ # 3. Not full ref schema or any other relation - generate set of lookups
3371
+ if isinstance(item, ConceptFilter):
3372
+ scheme = concept._ref_scheme()
3373
+
3374
+ out = None
3375
+ args = kwargs
3376
+
3377
+ if scheme:
3378
+ # Collect expected keys from the reference scheme
3379
+ ks = [rel._short_name for rel in scheme]
3380
+
3381
+ # Check if all scheme keys are present in kwargs
3382
+ full_ref_scheme_present = all(k in kwargs for k in ks)
3383
+
3384
+ if full_ref_scheme_present:
3385
+ # Explode full reference scheme
3386
+ out = self.explode_ref_schemes(item, ctx, update=False)
3387
+ if not concept._is_primitive():
3388
+ ctx.add(f.lookup(relation, [out]))
3389
+
3390
+ # Remove scheme keys from arguments
3391
+ args = {k: v for k, v in args.items() if k not in ks}
3392
+
3393
+ # If nothing left, return early
3394
+ if not args:
3395
+ return out
3396
+
3397
+ # Fallback: simple lookup if no scheme matched
3398
+ if out is None:
3399
+ out = self.lookup(concept, ctx)
3400
+
3401
+ assert isinstance(out, ir.Var)
3402
+
3403
+ # Generate relation lookups for remaining args
3404
+ rels = {self.to_relation(getattr(concept, k)): unwrap_list(self.lookup(v, ctx))
3405
+ for k, v in args.items()}
3406
+
3407
+ for k, v in rels.items():
3408
+ assert not isinstance(v, list)
3409
+ ctx.add(f.lookup(k, [out, v]))
3410
+
3411
+ return out
3412
+
3413
+ # otherwise we have to construct one
3414
+ out = self.explode_ref_schemes(item, ctx, update=False)
3415
+ return out
3416
+
3417
+ elif isinstance(item, Expression):
3418
+ params = [self.lookup(p, ctx) for p in item._params]
3419
+ relation = self.to_relation(item._op)
3420
+ ctx.add(f.lookup(relation, flatten(params)))
3421
+ return params[-1]
3422
+
3423
+ elif isinstance(item, Concept):
3424
+ v = ctx.to_value(item)
3425
+ if not item._isa(Primitive):
3426
+ assert isinstance(v, ir.Var)
3427
+ relation = self.to_relation(item)
3428
+ ctx.add(f.lookup(relation, [v]))
3429
+ return v
3430
+
3431
+ elif isinstance(item, (Relationship, RelationshipRef, RelationshipReading)):
3432
+ params = item._field_refs
3433
+ if item._parent:
3434
+ params = [item._parent] + params[1:]
3435
+ return self.lookup(item(*params), ctx)
3436
+
3437
+ elif isinstance(item, RelationshipFieldRef):
3438
+ rel = item._relationship
3439
+ params = list(rel._field_refs)
3440
+ if item._parent:
3441
+ params = [item._parent] + params[1:]
3442
+ self.lookup(rel(*params), ctx)
3443
+ return self.lookup(params[item._field_ix], ctx)
3444
+
3445
+ elif isinstance(item, Ref):
3446
+ if item._no_lookup:
3447
+ return ctx.to_value(item)
3448
+
3449
+ root = item._thing
3450
+ prev_mapping = ctx.to_value(root)
3451
+ out = ctx.to_value(item)
3452
+ ctx.map_var(root, out)
3453
+ self.lookup(root, ctx)
3454
+ ctx.map_var(root, prev_mapping)
3455
+ return out
3456
+
3457
+ elif isinstance(item, TypeRef):
3458
+ if isinstance(item._thing, Relationship):
3459
+ return self.to_relation(item._thing)
3460
+ concept = to_type(item)
3461
+ if not concept:
3462
+ raise ValueError(f"Cannot find concept for {item}, {type(item)}")
3463
+ return self.to_type(concept)
3464
+
3465
+ elif isinstance(item, ArgumentRef):
3466
+ self.lookup(item._expr, ctx)
3467
+ return ctx.to_value(item._arg)
3468
+
3469
+ elif isinstance(item, Alias):
3470
+ return self.lookup(item._thing, ctx)
3471
+
3472
+ elif isinstance(item, Aggregate):
3473
+ relation = self.to_relation(item._op)
3474
+
3475
+ group = [self.lookup(g, ctx) for g in item._group]
3476
+ group = [item for item in flatten(group, flatten_tuples=True) if isinstance(item, ir.Var)]
3477
+
3478
+ agg_ctx = ctx.clone()
3479
+
3480
+ # additional wheres
3481
+ self.where(item._where, item._where._where, agg_ctx)
3482
+
3483
+ if self._is_rank(item):
3484
+ # The rank output is always an int
3485
+ out = agg_ctx.to_value(item, f.var(to_name(item), types.Int128))
3486
+ assert isinstance(out, ir.Var)
3487
+
3488
+ projection, args, arg_is_ascending = self._process_rank(item._args, agg_ctx)
3489
+ internal_vars = ordered_set()
3490
+
3491
+ ir_node = f.rank(projection.get_list(), group, args.get_list(), arg_is_ascending, out)
3492
+
3493
+ else:
3494
+ out = agg_ctx.to_value(item)
3495
+ assert isinstance(out, ir.Var)
3496
+ arg_count = len(relation.fields) - 1 # skip the result
3497
+ raw_args = flatten([self.lookup(a, agg_ctx) for a in item._args])
3498
+
3499
+ # the projection includes all keys for the args
3500
+ projection = [self.lookup(key, agg_ctx) for key in find_keys(item._args)]
3501
+ # the projection is also all raw_args that aren't consumed by the agg
3502
+ projection += raw_args[:-arg_count] if arg_count else raw_args
3503
+ projection = flatten(projection, flatten_tuples=True)
3504
+ projection = list(dict.fromkeys([item for item in projection if isinstance(item, ir.Var)]))
3505
+
3506
+ # agg args + result var
3507
+ args = raw_args[-arg_count:] if arg_count else []
3508
+ args.append(out)
3509
+
3510
+ internal_vars = set(flatten(raw_args + projection, flatten_tuples=True))
3511
+ ir_node = f.aggregate(relation, projection, group, args)
3512
+
3513
+ final_ctx = ctx.clone()
3514
+ if agg_ctx.items:
3515
+ internal = internal_vars - set(flatten(list(ctx.value_map.values()), flatten_tuples=True))
3516
+ hoisted = [ir.Default(v, None) for v in internal if isinstance(v, ir.Var)]
3517
+ hoisted.sort(key=lambda x: x.var.name)
3518
+ final_ctx.add(f.logical(list(agg_ctx.items), list(hoisted)))
3519
+ final_ctx.add(ir_node)
3520
+ ctx.add(f.logical(list(final_ctx.items), [out]))
3521
+ return out
3522
+
3523
+ elif isinstance(item, Not):
3524
+ not_ctx = ctx.clone()
3525
+ for a in item._args:
3526
+ self.lookup(a, not_ctx)
3527
+ ctx.add(f.not_(f.logical(list(not_ctx.items))))
3528
+
3529
+ elif isinstance(item, Fragment):
3530
+ if item._is_where_only():
3531
+ for where in item._where:
3532
+ self.lookup(where, ctx)
3533
+ return None
3534
+
3535
+ sub_ctx = ctx.clone()
3536
+
3537
+ # if we encounter a select and we aren't already trying to write
3538
+ # it into vars, add some
3539
+ into_vars = ctx.into_vars
3540
+ if not len(into_vars) and item._select:
3541
+ into_vars = sub_ctx.into_vars = flatten([ctx.to_value(s) for s in item._select])
3542
+
3543
+ ctx.add(self.fragment(item, sub_ctx))
3544
+ out = None
3545
+ if len(into_vars) == 1:
3546
+ out = into_vars[0]
3547
+ elif len(into_vars) > 1:
3548
+ out = into_vars
3549
+ elif len(item._select) == 1:
3550
+ out = sub_ctx.to_value(item._select[0])
3551
+ elif len(item._select) > 1:
3552
+ out = flatten([sub_ctx.to_value(s) for s in item._select])
3553
+
3554
+ return out
3555
+
3556
+ elif isinstance(item, (Match, Union)):
3557
+ branches = []
3558
+ vars = []
3559
+ if item._is_select:
3560
+ vars = ctx.fetch_var(item)
3561
+ if isinstance(vars, ir.Var):
3562
+ vars = [vars]
3563
+ elif not vars:
3564
+ vars = [f.var(f"v{i}") for i in range(item._ret_count)]
3565
+ ctx.map_var(item, vars)
3566
+ assert isinstance(vars, list)
3567
+ for branch in item._args:
3568
+ branch_ctx = ctx.clone()
3569
+ branch_ctx.into_vars = vars
3570
+ v = self.lookup(branch, branch_ctx)
3571
+ if not isinstance(v, list):
3572
+ v = [v]
3573
+ for var, ret in zip(vars, v):
3574
+ if var is not ret:
3575
+ relation = self.to_relation(builtins.eq)
3576
+ branch_ctx.add(f.lookup(relation, [var, ret]))
3577
+ # map in parent context the union/match branch var to an original var
3578
+ ctx.map_var(ret, var)
3579
+ branches.append(branch_ctx.safe_wrap(vars))
3580
+ elif all(isinstance(branch, Concept) or isinstance(branch, PY_LITERAL_TYPES) for branch in item._args):
3581
+ vars = [f.var(to_name(item), types.Any)]
3582
+ for branch in item._args:
3583
+ branch_ctx = ctx.clone()
3584
+ v = self.lookup(branch, branch_ctx)
3585
+ assert isinstance(v, (ir.Var, ir.Literal))
3586
+ branch_ctx.add(f.lookup(builtins.eq, [vars[0], v]))
3587
+ branches.append(branch_ctx.safe_wrap(vars))
3588
+ else:
3589
+ for branch in item._args:
3590
+ branch_ctx = ctx.clone()
3591
+ self.update(branch, branch_ctx)
3592
+ branches.append(branch_ctx.safe_wrap([]))
3593
+ if isinstance(item, Union):
3594
+ ctx.add(f.union(branches, vars))
3595
+ else:
3596
+ ctx.add(f.match(branches, vars))
3597
+
3598
+ return vars or None
3599
+
3600
+ elif isinstance(item, BranchRef):
3601
+ vars = ctx.value_map.get(item._match)
3602
+ if not vars:
3603
+ self.lookup(item._match, ctx)
3604
+ vars = ctx.fetch_var(item._match)
3605
+ assert isinstance(vars, list)
3606
+ return vars[item._ix]
3607
+
3608
+ elif isinstance(item, Group):
3609
+ for g in item._group:
3610
+ self.lookup(g, ctx)
3611
+
3612
+ elif isinstance(item, Distinct):
3613
+ vs = [self.lookup(v, ctx) for v in item._args]
3614
+ return flatten(vs)
3615
+
3616
+ elif isinstance(item, Data):
3617
+ refs = [item._row_id] + [i._ref for i in item._cols]
3618
+ vars = flatten([self.lookup(v, ctx) for v in refs])
3619
+ ctx.add(f.data(item._data, vars))
3620
+ return vars[0]
3621
+
3622
+ elif isinstance(item, DataColumn):
3623
+ self.lookup(item._data, ctx)
3624
+ return ctx.to_value(item._ref)
3625
+
3626
+ elif isinstance(item, PY_LITERAL_TYPES):
3627
+ return f.literal(item, literal_value_to_type(item))
3628
+
3629
+ elif item is None:
3630
+ return None
3631
+
3632
+ elif isinstance(item, (ir.Var, ir.Literal)):
3633
+ return item
3634
+
3635
+ elif hasattr(item, "_compile_lookup"):
3636
+ return item._compile_lookup(self, ctx)
3637
+
3638
+ else:
3639
+ raise ValueError(f"Cannot lookup {item}, {type(item)}")
3640
+
3641
+ #--------------------------------------------------
3642
+ # Update
3643
+ #--------------------------------------------------
3644
+
3645
+ def update(self, item:Expression|Match|Union, ctx:CompilerContext) -> ir.Value|list[ir.Var]:
3646
+ if isinstance(item, ConceptExpression):
3647
+ assert isinstance(item._op, Concept)
3648
+ relation = self.to_relation(item._op)
3649
+ (ident, kwargs) = item._params
3650
+ out = ctx.to_value(item)
3651
+ assert isinstance(out, ir.Var)
3652
+
3653
+ # if this is a member lookup, then our out var is just the identity passed in
3654
+ if isinstance(item, ConceptMember):
3655
+ out = self.lookup(ident, ctx)
3656
+ assert not isinstance(out, list)
3657
+ # otherwise we have to construct one
3658
+ else:
3659
+ out = self.explode_ref_schemes(item, ctx, update=True)
3660
+
3661
+ ctx.add(f.derive(relation, [out]))
3662
+ # derive the membership and all the relationships
3663
+ rels = self.relation_dict({attr: v for k, v in kwargs.items() if (attr := getattr(item._op, k, None)) is not None}, ctx)
3664
+ for k, v in rels.items():
3665
+ assert not isinstance(v, list)
3666
+ ctx.add(f.derive(k, [out, v]))
3667
+ return out
3668
+
3669
+ elif isinstance(item, Expression) and item._op is Relationship.builtins["="]:
3670
+ if isinstance(item._params[0], (Relationship, RelationshipRef)):
3671
+ return self.update(item._params[0](item._params[1]), ctx)
3672
+ elif isinstance(item._params[1], (Relationship, RelationshipRef)):
3673
+ return self.update(item._params[1](item._params[0]), ctx)
3674
+ elif isinstance(item._params[0], RelationshipFieldRef) or isinstance(item._params[1], RelationshipFieldRef):
3675
+ raise ValueError("Cannot set fields of a multi-field relationship individually")
3676
+ else:
3677
+ raise ValueError("Cannot set a non-relationship via ==")
3678
+
3679
+ elif isinstance(item, Expression):
3680
+ op = item._op
3681
+ params = flatten([self.lookup(p, ctx) for p in item._params])
3682
+ # the case when root a relationship populated thought a reading
3683
+ if isinstance(op, RelationshipReading) and not item._ignore_root:
3684
+ op = item._op._alt_of
3685
+ # reuse params for the root relationship
3686
+ ref_2_param = {ref: param for ref, param in zip(item._op._field_refs, params)}
3687
+ params = flatten([ref_2_param[ref] for ref in op._field_refs])
3688
+ relation = self.to_relation(op)
3689
+ ctx.add(f.derive(relation, params))
3690
+ return params[-1]
3691
+
3692
+ elif isinstance(item, Relationship) and item._arity() == 1:
3693
+ # implicit update of unary relationships
3694
+ params = flatten([self.lookup(item._parent, ctx)])
3695
+ # normalize parameters through fetch_var if available
3696
+ params = [
3697
+ fetched if isinstance(fetched := ctx.fetch_var(p), ir.Var) else p
3698
+ for p in params
3699
+ ]
3700
+ relation = self.to_relation(item)
3701
+ ctx.add(f.derive(relation, params))
3702
+ return params[-1]
3703
+
3704
+ elif isinstance(item, Fragment):
3705
+ self.lookup(item, ctx)
3706
+
3707
+ elif isinstance(item, (Match, Union)):
3708
+ self.lookup(item, ctx)
3709
+
3710
+ elif hasattr(item, "_compile_update"):
3711
+ return item._compile_update(self, ctx)
3712
+
3713
+ else:
3714
+ raise ValueError(f"Cannot update {item}")
3715
+
3716
+ __all__ = ["select", "where", "require", "define", "distinct", "per", "count", "sum", "min", "max", "avg"]
3717
+
3718
+ #--------------------------------------------------
3719
+ # Todo
3720
+ #--------------------------------------------------
3721
+ """
3722
+ - Syntax
3723
+ ✔ construct
3724
+ ✔ static data handling
3725
+ ✔ Fix fragments to not be chained
3726
+ ✔ Extends
3727
+ ✔ Quantifiers
3728
+ ✔ not
3729
+ ✔ exists
3730
+ ✔ forall
3731
+ ✔ Aggregates
3732
+ ✔ Require
3733
+ ✘ Multi-step chaining
3734
+ ✔ ref
3735
+ ✔ alias
3736
+ ✔ match
3737
+ ✔ union
3738
+ ✔ capture all rules
3739
+ ✔ implement aliasing
3740
+ ✔ support defining relationships via madlibs Relationship("{Person} was born on {birthday:Date}")
3741
+ ✔ distinct
3742
+ ☐ nested fragments
3743
+ ✔ handle relationships with multiple name fields being accessed via prop:
3744
+ Package.shipment = Relationship("{Package} in {Shipment} on {Date}")
3745
+ Package.shipment.date, Package.shipment.shipment, Package.shipment.package
3746
+ ☐ sources
3747
+ ☐ table
3748
+ ☐ csv
3749
+
3750
+ - Compilation
3751
+ ✔ simple expressions
3752
+ ✔ select
3753
+ ✔ then
3754
+ ✔ Quantifiers
3755
+ ✔ exists
3756
+ ✔ forall
3757
+ ✔ not
3758
+ ✔ Aggregates
3759
+ ✔ Determine agg keys from inputs
3760
+ ✔ Group
3761
+ ✔ Require
3762
+ ✔ Alias
3763
+ ✔ Ref
3764
+ ✔ Match
3765
+ ✔ Union
3766
+ ✔ whole model
3767
+ ✔ distinct
3768
+ ✔ add Else to hoists
3769
+ ✔ where(..).define(Person.coolness == 10)
3770
+ ☐ extends
3771
+ ☐ nominals
3772
+ ✔ have require find keys and return the keys in the error
3773
+ ☐ Match/union with multiple branch refs in a select, duplicates the whole match
3774
+ ☐ nested fragments
3775
+
3776
+ ☐ Execution
3777
+ ✔ basic queries
3778
+ ✔ query when iterating over a select
3779
+ ☐ debugger hookup
3780
+ ☐ table sources
3781
+ ☐ graph index
3782
+ ☐ exports
3783
+ ☐ config overhaul
3784
+
3785
+ """