dataface 0.1.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 (455) hide show
  1. d3_format/__init__.py +14 -0
  2. d3_format/errors.py +19 -0
  3. d3_format/format.py +551 -0
  4. d3_format/spec.py +159 -0
  5. dataface/DATAFACE_SYNTAX.md +1135 -0
  6. dataface/__init__.py +93 -0
  7. dataface/_docs_site.py +20 -0
  8. dataface/_install_hint.py +26 -0
  9. dataface/agent_api/__init__.py +79 -0
  10. dataface/agent_api/_init_templates/__init__.py +0 -0
  11. dataface/agent_api/_init_templates/agents_dft_snippet.md +26 -0
  12. dataface/agent_api/_init_templates/dataface.yml +15 -0
  13. dataface/agent_api/_init_templates/faces-dataface.yml +144 -0
  14. dataface/agent_api/_init_templates/index.md +24 -0
  15. dataface/agent_api/_paths.py +118 -0
  16. dataface/agent_api/_project_agents_md.py +43 -0
  17. dataface/agent_api/_session_store.py +486 -0
  18. dataface/agent_api/_state.py +28 -0
  19. dataface/agent_api/chat.py +221 -0
  20. dataface/agent_api/dashboards.py +257 -0
  21. dataface/agent_api/describe.py +366 -0
  22. dataface/agent_api/describe_query.py +120 -0
  23. dataface/agent_api/docs/__init__.py +25 -0
  24. dataface/agent_api/docs/_loader.py +292 -0
  25. dataface/agent_api/docs/yaml-reference.md +2757 -0
  26. dataface/agent_api/file_refs.py +118 -0
  27. dataface/agent_api/init.py +126 -0
  28. dataface/agent_api/inspect.py +128 -0
  29. dataface/agent_api/mcp_install.py +170 -0
  30. dataface/agent_api/query.py +274 -0
  31. dataface/agent_api/schema.py +658 -0
  32. dataface/agent_api/schema_search.py +284 -0
  33. dataface/agent_api/search.py +270 -0
  34. dataface/agent_api/skill_install.py +141 -0
  35. dataface/agent_api/skill_render.py +90 -0
  36. dataface/agent_api/skills.py +293 -0
  37. dataface/agent_api/surface_aliases.yaml +128 -0
  38. dataface/agent_api/validate.py +175 -0
  39. dataface/agent_api/validate_query.py +84 -0
  40. dataface/ai/__init__.py +39 -0
  41. dataface/ai/agent.py +139 -0
  42. dataface/ai/context.py +45 -0
  43. dataface/ai/events.py +62 -0
  44. dataface/ai/external_mcp.py +610 -0
  45. dataface/ai/generate_sql.py +96 -0
  46. dataface/ai/llm.py +403 -0
  47. dataface/ai/mcp/__init__.py +51 -0
  48. dataface/ai/mcp/server.py +289 -0
  49. dataface/ai/memories.py +85 -0
  50. dataface/ai/prompts.py +177 -0
  51. dataface/ai/schema_context.py +138 -0
  52. dataface/ai/skills/before-after-comparison/SKILL.md +102 -0
  53. dataface/ai/skills/before-after-comparison/examples/before-after-comparison.yml +24 -0
  54. dataface/ai/skills/dashboard-build/SKILL.md +212 -0
  55. dataface/ai/skills/dashboard-build/examples/_smoke.yml +15 -0
  56. dataface/ai/skills/dashboard-design/SKILL.md +182 -0
  57. dataface/ai/skills/dashboard-review/SKILL.md +113 -0
  58. dataface/ai/skills/dashboard-structural-review/SKILL.md +173 -0
  59. dataface/ai/skills/dashboard-visual-review/SKILL.md +139 -0
  60. dataface/ai/skills/dataface-mcp-setup/SKILL.md +177 -0
  61. dataface/ai/skills/dataface-troubleshooting/SKILL.md +225 -0
  62. dataface/ai/skills/drill-down-link/SKILL.md +112 -0
  63. dataface/ai/skills/drill-down-link/examples/drill-down-link.yml +27 -0
  64. dataface/ai/skills/faceted-small-multiples/SKILL.md +116 -0
  65. dataface/ai/skills/faceted-small-multiples/examples/faceted-small-multiples.yml +33 -0
  66. dataface/ai/skills/filter-bar-with-variables/SKILL.md +105 -0
  67. dataface/ai/skills/filter-bar-with-variables/examples/filter-bar-with-variables.yml +49 -0
  68. dataface/ai/skills/kpi-row/SKILL.md +101 -0
  69. dataface/ai/skills/kpi-row/examples/kpi-row.yml +55 -0
  70. dataface/ai/skills/report-design/SKILL.md +184 -0
  71. dataface/ai/skills/single-metric-bignum/SKILL.md +90 -0
  72. dataface/ai/skills/single-metric-bignum/examples/single-metric-bignum.yml +27 -0
  73. dataface/ai/skills/table-heavy-ops-dashboard/SKILL.md +114 -0
  74. dataface/ai/skills/table-heavy-ops-dashboard/examples/table-heavy-ops-dashboard.yml +48 -0
  75. dataface/ai/skills/time-series-trend/SKILL.md +93 -0
  76. dataface/ai/skills/time-series-trend/examples/time-series-trend.yml +26 -0
  77. dataface/ai/skills/top-n-with-detail/SKILL.md +98 -0
  78. dataface/ai/skills/top-n-with-detail/examples/top-n-with-detail.yml +45 -0
  79. dataface/ai/skills/two-by-two-grid-overview/SKILL.md +78 -0
  80. dataface/ai/skills/two-by-two-grid-overview/examples/two-by-two-grid-overview.yml +64 -0
  81. dataface/ai/tool_schemas.py +132 -0
  82. dataface/ai/tools/__init__.py +312 -0
  83. dataface/ai/yaml_utils.py +57 -0
  84. dataface/cli/__init__.py +3 -0
  85. dataface/cli/_console.py +48 -0
  86. dataface/cli/_error_format.py +83 -0
  87. dataface/cli/_extras.py +190 -0
  88. dataface/cli/_json_output.py +8 -0
  89. dataface/cli/_parsing.py +17 -0
  90. dataface/cli/_version_info.py +56 -0
  91. dataface/cli/commands/__init__.py +3 -0
  92. dataface/cli/commands/_agent_input.py +205 -0
  93. dataface/cli/commands/_agent_server.py +115 -0
  94. dataface/cli/commands/chat.py +645 -0
  95. dataface/cli/commands/describe.py +107 -0
  96. dataface/cli/commands/docs.py +131 -0
  97. dataface/cli/commands/extension.py +179 -0
  98. dataface/cli/commands/init.py +240 -0
  99. dataface/cli/commands/inspect.py +94 -0
  100. dataface/cli/commands/mcp_init.py +167 -0
  101. dataface/cli/commands/query.py +386 -0
  102. dataface/cli/commands/render.py +291 -0
  103. dataface/cli/commands/schema.py +411 -0
  104. dataface/cli/commands/search.py +49 -0
  105. dataface/cli/commands/serve.py +114 -0
  106. dataface/cli/commands/skills.py +133 -0
  107. dataface/cli/commands/skills_init.py +161 -0
  108. dataface/cli/commands/validate.py +63 -0
  109. dataface/cli/main.py +1501 -0
  110. dataface/core/__init__.py +75 -0
  111. dataface/core/compile/__init__.py +244 -0
  112. dataface/core/compile/_jinja_helpers.py +78 -0
  113. dataface/core/compile/channel.py +222 -0
  114. dataface/core/compile/chart_focus.py +101 -0
  115. dataface/core/compile/chart_resolved.py +169 -0
  116. dataface/core/compile/chart_type_detection.py +489 -0
  117. dataface/core/compile/chart_update.py +261 -0
  118. dataface/core/compile/colors.py +64 -0
  119. dataface/core/compile/compiler.py +904 -0
  120. dataface/core/compile/config.py +823 -0
  121. dataface/core/compile/custom_chart_types.py +208 -0
  122. dataface/core/compile/data_table_attachment.py +1287 -0
  123. dataface/core/compile/detect.py +110 -0
  124. dataface/core/compile/errors.py +302 -0
  125. dataface/core/compile/filter_injection.py +319 -0
  126. dataface/core/compile/introspection.py +527 -0
  127. dataface/core/compile/jinja.py +511 -0
  128. dataface/core/compile/labels_env.py +52 -0
  129. dataface/core/compile/markdown.py +154 -0
  130. dataface/core/compile/meta.py +388 -0
  131. dataface/core/compile/models/__init__.py +0 -0
  132. dataface/core/compile/models/chart/__init__.py +0 -0
  133. dataface/core/compile/models/chart/authored.py +2137 -0
  134. dataface/core/compile/models/chart/compiled.py +398 -0
  135. dataface/core/compile/models/config.py +347 -0
  136. dataface/core/compile/models/face/__init__.py +0 -0
  137. dataface/core/compile/models/face/authored.py +659 -0
  138. dataface/core/compile/models/face/compiled.py +522 -0
  139. dataface/core/compile/models/factories.py +201 -0
  140. dataface/core/compile/models/markers.py +40 -0
  141. dataface/core/compile/models/palette.py +36 -0
  142. dataface/core/compile/models/primitives.py +415 -0
  143. dataface/core/compile/models/query/__init__.py +0 -0
  144. dataface/core/compile/models/query/authored.py +246 -0
  145. dataface/core/compile/models/query/compiled.py +710 -0
  146. dataface/core/compile/models/refs.py +137 -0
  147. dataface/core/compile/models/source.py +611 -0
  148. dataface/core/compile/models/style/__init__.py +0 -0
  149. dataface/core/compile/models/style/authored.py +481 -0
  150. dataface/core/compile/models/style/compiled.py +3399 -0
  151. dataface/core/compile/models/style/merged.py +1682 -0
  152. dataface/core/compile/models/theme.py +362 -0
  153. dataface/core/compile/models/variable/__init__.py +0 -0
  154. dataface/core/compile/models/variable/authored.py +254 -0
  155. dataface/core/compile/models/vega_lite/__init__.py +0 -0
  156. dataface/core/compile/models/vega_lite/config.py +510 -0
  157. dataface/core/compile/models/vega_lite/contracts.py +171 -0
  158. dataface/core/compile/normalize_charts.py +494 -0
  159. dataface/core/compile/normalize_layout.py +1000 -0
  160. dataface/core/compile/normalize_queries.py +297 -0
  161. dataface/core/compile/normalize_variables.py +489 -0
  162. dataface/core/compile/normalizer.py +543 -0
  163. dataface/core/compile/palette.py +1100 -0
  164. dataface/core/compile/parameterized.py +658 -0
  165. dataface/core/compile/parser.py +228 -0
  166. dataface/core/compile/schema.py +20 -0
  167. dataface/core/compile/schema_renderers/__init__.py +0 -0
  168. dataface/core/compile/schema_renderers/json_schema.py +163 -0
  169. dataface/core/compile/schema_renderers/prompt.py +152 -0
  170. dataface/core/compile/schema_renderers/vscode_schema.py +301 -0
  171. dataface/core/compile/sizing.py +2126 -0
  172. dataface/core/compile/sources.py +518 -0
  173. dataface/core/compile/sql_authoring_lint.py +56 -0
  174. dataface/core/compile/style_cascade.py +471 -0
  175. dataface/core/compile/typography.py +299 -0
  176. dataface/core/compile/validator.py +301 -0
  177. dataface/core/compile/variables.py +53 -0
  178. dataface/core/compile/vega_config.py +98 -0
  179. dataface/core/compile/vega_lite/__init__.py +6 -0
  180. dataface/core/compile/vega_lite/validation.py +95 -0
  181. dataface/core/compile/yaml_error_formatter.py +838 -0
  182. dataface/core/connections.py +38 -0
  183. dataface/core/dashboard.py +358 -0
  184. dataface/core/defaults/default_config.yml +101 -0
  185. dataface/core/defaults/palettes/categorical/category-10-dark.yml +32 -0
  186. dataface/core/defaults/palettes/categorical/category-10-light.yml +43 -0
  187. dataface/core/defaults/palettes/categorical/category-10.yml +31 -0
  188. dataface/core/defaults/palettes/categorical/category-6-tonal-blue.yml +22 -0
  189. dataface/core/defaults/palettes/categorical/category-6-tonal-brown.yml +29 -0
  190. dataface/core/defaults/palettes/categorical/category-6-tonal-green.yml +20 -0
  191. dataface/core/defaults/palettes/categorical/category-6-tonal-orange.yml +21 -0
  192. dataface/core/defaults/palettes/categorical/category-6-tonal-purple.yml +20 -0
  193. dataface/core/defaults/palettes/categorical/editorial-10-dark.yml +32 -0
  194. dataface/core/defaults/palettes/categorical/editorial-10.yml +40 -0
  195. dataface/core/defaults/palettes/categorical/hero-6.yml +17 -0
  196. dataface/core/defaults/palettes/categorical/single-blue.yml +11 -0
  197. dataface/core/defaults/palettes/categorical/tableau.yml +20 -0
  198. dataface/core/defaults/palettes/data/xkcd_colors.json +3803 -0
  199. dataface/core/defaults/palettes/diverging/blue-red.yml +25 -0
  200. dataface/core/defaults/palettes/diverging/coolwarm.yml +24 -0
  201. dataface/core/defaults/palettes/diverging/crimson-green.yml +23 -0
  202. dataface/core/defaults/palettes/diverging/orange-teal.yml +23 -0
  203. dataface/core/defaults/palettes/diverging/sunset.yml +24 -0
  204. dataface/core/defaults/palettes/scaffold/dft-creams.yml +38 -0
  205. dataface/core/defaults/palettes/scaffold/dft-grays.yml +53 -0
  206. dataface/core/defaults/palettes/sequential/amber.yml +22 -0
  207. dataface/core/defaults/palettes/sequential/blue.yml +22 -0
  208. dataface/core/defaults/palettes/sequential/brown.yml +22 -0
  209. dataface/core/defaults/palettes/sequential/gray.yml +22 -0
  210. dataface/core/defaults/palettes/sequential/green.yml +22 -0
  211. dataface/core/defaults/palettes/sequential/purple.yml +22 -0
  212. dataface/core/defaults/palettes/sequential/rust.yml +22 -0
  213. dataface/core/defaults/palettes/sequential/teal.yml +22 -0
  214. dataface/core/defaults/palettes/tone/negative.yml +32 -0
  215. dataface/core/defaults/palettes/tone/positive.yml +22 -0
  216. dataface/core/defaults/palettes/tone/warning.yml +22 -0
  217. dataface/core/defaults/themes/_base.yaml +786 -0
  218. dataface/core/defaults/themes/bi.yaml +16 -0
  219. dataface/core/defaults/themes/carbong100.yaml +41 -0
  220. dataface/core/defaults/themes/cream.yaml +122 -0
  221. dataface/core/defaults/themes/dark.yaml +40 -0
  222. dataface/core/defaults/themes/diagnostics-title-angle-extreme.yaml +9 -0
  223. dataface/core/defaults/themes/diagnostics-title-baseline-extreme.yaml +9 -0
  224. dataface/core/defaults/themes/diagnostics-title-baseline.yaml +24 -0
  225. dataface/core/defaults/themes/diagnostics-title-center.yaml +8 -0
  226. dataface/core/defaults/themes/diagnostics-title-color-extreme.yaml +24 -0
  227. dataface/core/defaults/themes/diagnostics-title-font-extreme.yaml +25 -0
  228. dataface/core/defaults/themes/diagnostics-title-left.yaml +8 -0
  229. dataface/core/defaults/themes/diagnostics-title-offset-extreme.yaml +9 -0
  230. dataface/core/defaults/themes/diagnostics-title-size-extreme.yaml +24 -0
  231. dataface/core/defaults/themes/diagnostics-title-weight-extreme.yaml +24 -0
  232. dataface/core/defaults/themes/editorial.yaml +147 -0
  233. dataface/core/defaults/themes/light.yaml +30 -0
  234. dataface/core/defaults/themes/looker.yaml +17 -0
  235. dataface/core/defaults/themes/stark.yaml +134 -0
  236. dataface/core/errors/__init__.py +67 -0
  237. dataface/core/errors/codes_compile.py +56 -0
  238. dataface/core/errors/codes_execute.py +177 -0
  239. dataface/core/errors/codes_render.py +106 -0
  240. dataface/core/errors/codes_unknown.py +15 -0
  241. dataface/core/errors/hints.py +74 -0
  242. dataface/core/errors/registry.py +42 -0
  243. dataface/core/errors/structured.py +92 -0
  244. dataface/core/execute/__init__.py +91 -0
  245. dataface/core/execute/adapters/__init__.py +49 -0
  246. dataface/core/execute/adapters/adapter_registry.py +400 -0
  247. dataface/core/execute/adapters/base.py +245 -0
  248. dataface/core/execute/adapters/csv_adapter.py +239 -0
  249. dataface/core/execute/adapters/dbt_adapter.py +283 -0
  250. dataface/core/execute/adapters/dbt_adapter_factory.py +212 -0
  251. dataface/core/execute/adapters/dbt_macro_loader.py +95 -0
  252. dataface/core/execute/adapters/dbt_utils.py +150 -0
  253. dataface/core/execute/adapters/http_adapter.py +224 -0
  254. dataface/core/execute/adapters/metricflow_adapter.py +94 -0
  255. dataface/core/execute/adapters/schema_resolver_adapter.py +144 -0
  256. dataface/core/execute/adapters/sql_adapter.py +710 -0
  257. dataface/core/execute/adapters/values_adapter.py +58 -0
  258. dataface/core/execute/batch.py +744 -0
  259. dataface/core/execute/cache_backend.py +135 -0
  260. dataface/core/execute/cache_keys.py +66 -0
  261. dataface/core/execute/dbt_jinja.py +21 -0
  262. dataface/core/execute/dialects/__init__.py +121 -0
  263. dataface/core/execute/dialects/athena.py +75 -0
  264. dataface/core/execute/dialects/base.py +302 -0
  265. dataface/core/execute/dialects/bigquery.py +38 -0
  266. dataface/core/execute/dialects/databricks.py +68 -0
  267. dataface/core/execute/dialects/duckdb.py +35 -0
  268. dataface/core/execute/dialects/mysql.py +68 -0
  269. dataface/core/execute/dialects/postgres.py +39 -0
  270. dataface/core/execute/dialects/redshift.py +12 -0
  271. dataface/core/execute/dialects/snowflake.py +51 -0
  272. dataface/core/execute/dialects/sqlserver.py +92 -0
  273. dataface/core/execute/duckdb_cache.py +712 -0
  274. dataface/core/execute/duckdb_config.py +26 -0
  275. dataface/core/execute/errors.py +213 -0
  276. dataface/core/execute/executor.py +1249 -0
  277. dataface/core/execute/parallel.py +162 -0
  278. dataface/core/execute/setup_sql.py +58 -0
  279. dataface/core/execute/source_registry.py +72 -0
  280. dataface/core/execute/source_resolver.py +255 -0
  281. dataface/core/execute/sql_guard.py +387 -0
  282. dataface/core/execute/sql_literals.py +199 -0
  283. dataface/core/fonts.py +52 -0
  284. dataface/core/inspect/__init__.py +32 -0
  285. dataface/core/inspect/cache_factory.py +98 -0
  286. dataface/core/inspect/db_types.py +162 -0
  287. dataface/core/inspect/dbt_schema.py +96 -0
  288. dataface/core/inspect/defaults.yml +37 -0
  289. dataface/core/inspect/fanout_risk.py +109 -0
  290. dataface/core/inspect/manifest_utils.py +77 -0
  291. dataface/core/inspect/partials/categorical.yml +40 -0
  292. dataface/core/inspect/partials/date.yml +40 -0
  293. dataface/core/inspect/partials/numeric.yml +55 -0
  294. dataface/core/inspect/partition_types.py +38 -0
  295. dataface/core/inspect/query_validator.py +975 -0
  296. dataface/core/inspect/renderer.py +354 -0
  297. dataface/core/inspect/resolver.py +808 -0
  298. dataface/core/inspect/search.py +461 -0
  299. dataface/core/inspect/sources/__init__.py +32 -0
  300. dataface/core/inspect/sources/dbt.py +738 -0
  301. dataface/core/inspect/sources/duckdb_utils.py +66 -0
  302. dataface/core/inspect/templates/__init__.py +1 -0
  303. dataface/core/inspect/templates/categorical_column.yml +196 -0
  304. dataface/core/inspect/templates/charts.yml +109 -0
  305. dataface/core/inspect/templates/date_column.yml +248 -0
  306. dataface/core/inspect/templates/model.yml +138 -0
  307. dataface/core/inspect/templates/numeric_column.yml +261 -0
  308. dataface/core/inspect/templates/quality.yml +80 -0
  309. dataface/core/inspect/templates/string_column.yml +263 -0
  310. dataface/core/project_roots.py +165 -0
  311. dataface/core/render/__init__.py +87 -0
  312. dataface/core/render/board_links.py +176 -0
  313. dataface/core/render/chart/__init__.py +27 -0
  314. dataface/core/render/chart/arc_attached_table.py +251 -0
  315. dataface/core/render/chart/artifacts.py +16 -0
  316. dataface/core/render/chart/callout.py +225 -0
  317. dataface/core/render/chart/decisions.py +358 -0
  318. dataface/core/render/chart/geo.py +700 -0
  319. dataface/core/render/chart/kpi.py +916 -0
  320. dataface/core/render/chart/labels.py +76 -0
  321. dataface/core/render/chart/pipeline.py +818 -0
  322. dataface/core/render/chart/presentation.py +36 -0
  323. dataface/core/render/chart/profile.py +3438 -0
  324. dataface/core/render/chart/render_single.py +347 -0
  325. dataface/core/render/chart/renderers.py +193 -0
  326. dataface/core/render/chart/rendering.py +565 -0
  327. dataface/core/render/chart/serialization.py +90 -0
  328. dataface/core/render/chart/spark.py +496 -0
  329. dataface/core/render/chart/spark_bar.py +370 -0
  330. dataface/core/render/chart/spec_builders.py +154 -0
  331. dataface/core/render/chart/standard_renderer.py +2645 -0
  332. dataface/core/render/chart/table.py +2957 -0
  333. dataface/core/render/chart/table_support.py +1452 -0
  334. dataface/core/render/chart/tick_values.py +66 -0
  335. dataface/core/render/chart/time_unit_detect.py +809 -0
  336. dataface/core/render/chart/title_overflow.py +157 -0
  337. dataface/core/render/chart/type_inference.py +122 -0
  338. dataface/core/render/chart/validation.py +99 -0
  339. dataface/core/render/chart/vega_lite.py +125 -0
  340. dataface/core/render/chart/vega_lite_types.py +268 -0
  341. dataface/core/render/chart/vl_field_maps.py +346 -0
  342. dataface/core/render/chart_interactivity.py +24 -0
  343. dataface/core/render/control_registry.py +287 -0
  344. dataface/core/render/converters/__init__.py +24 -0
  345. dataface/core/render/converters/chart.py +276 -0
  346. dataface/core/render/converters/html.py +98 -0
  347. dataface/core/render/converters/pdf.py +40 -0
  348. dataface/core/render/converters/png.py +41 -0
  349. dataface/core/render/errors.py +144 -0
  350. dataface/core/render/face_api.py +160 -0
  351. dataface/core/render/faces.py +1194 -0
  352. dataface/core/render/font_measurement.py +48 -0
  353. dataface/core/render/font_support.py +197 -0
  354. dataface/core/render/fonts/DFTSansTabular-Regular.ttf +0 -0
  355. dataface/core/render/fonts/DFTSansTabular-Regular.woff2 +0 -0
  356. dataface/core/render/fonts/DFTSerifOldstyleProportional-Regular.ttf +0 -0
  357. dataface/core/render/fonts/DFTSerifOldstyleTabular-Regular.ttf +0 -0
  358. dataface/core/render/fonts/InterVariable.ttf +0 -0
  359. dataface/core/render/fonts/InterVariable.woff2 +0 -0
  360. dataface/core/render/fonts/NOTO_COLOR_EMOJI_LICENSE.txt +93 -0
  361. dataface/core/render/fonts/NOTO_EMOJI_LICENSE.txt +93 -0
  362. dataface/core/render/fonts/NotoColorEmoji-Regular.ttf +0 -0
  363. dataface/core/render/fonts/NotoColorEmoji-Regular.woff2 +0 -0
  364. dataface/core/render/fonts/NotoEmoji-Regular.ttf +0 -0
  365. dataface/core/render/fonts/NotoEmoji-Regular.woff2 +0 -0
  366. dataface/core/render/fonts/SOURCE_CODE_PRO_LICENSE.txt +93 -0
  367. dataface/core/render/fonts/SOURCE_SERIF_4_LICENSE.txt +98 -0
  368. dataface/core/render/fonts/SourceCodePro-Regular.ttf +0 -0
  369. dataface/core/render/fonts/SourceSerif4-Regular.ttf +0 -0
  370. dataface/core/render/fonts/_emoji_font_face.css +43 -0
  371. dataface/core/render/fonts/source-serif-4-variable-latin.woff2 +0 -0
  372. dataface/core/render/format_utils.py +329 -0
  373. dataface/core/render/geo_defaults.yml +28 -0
  374. dataface/core/render/json_format.py +146 -0
  375. dataface/core/render/layout_sizing.py +865 -0
  376. dataface/core/render/layouts.py +541 -0
  377. dataface/core/render/markdown_defaults.yml +16 -0
  378. dataface/core/render/missing_vars_prompt.py +79 -0
  379. dataface/core/render/placeholder.py +389 -0
  380. dataface/core/render/render_result.py +14 -0
  381. dataface/core/render/renderer.py +467 -0
  382. dataface/core/render/script_embedding.py +16 -0
  383. dataface/core/render/svg_utils.py +212 -0
  384. dataface/core/render/template_loader.py +69 -0
  385. dataface/core/render/templates/controls/_styles.css +606 -0
  386. dataface/core/render/templates/controls/checkbox.html +16 -0
  387. dataface/core/render/templates/controls/date.html +16 -0
  388. dataface/core/render/templates/controls/number.html +19 -0
  389. dataface/core/render/templates/controls/readonly.html +9 -0
  390. dataface/core/render/templates/controls/select.html +21 -0
  391. dataface/core/render/templates/controls/slider.html +22 -0
  392. dataface/core/render/templates/controls/text.html +16 -0
  393. dataface/core/render/templates/scripts/chart_interactivity.js +191 -0
  394. dataface/core/render/templates/scripts/variables.js +976 -0
  395. dataface/core/render/templates/svg/grid_pattern.svg +3 -0
  396. dataface/core/render/templates/svg/styles.css +51 -0
  397. dataface/core/render/terminal.py +311 -0
  398. dataface/core/render/terminal_charts.py +563 -0
  399. dataface/core/render/terminal_defaults.yml +2 -0
  400. dataface/core/render/terminal_layouts.py +299 -0
  401. dataface/core/render/terminal_text.py +31 -0
  402. dataface/core/render/text/__init__.py +1 -0
  403. dataface/core/render/text/case.py +113 -0
  404. dataface/core/render/text_format.py +129 -0
  405. dataface/core/render/utils.py +106 -0
  406. dataface/core/render/variable_controls.py +946 -0
  407. dataface/core/render/variable_input_refinement.py +140 -0
  408. dataface/core/render/warnings/__init__.py +15 -0
  409. dataface/core/render/warnings/bar_color_1_to_1_with_x.py +80 -0
  410. dataface/core/render/warnings/base.py +44 -0
  411. dataface/core/render/warnings/fanout_risk.py +15 -0
  412. dataface/core/render/warnings/from_query_diagnostic.py +56 -0
  413. dataface/core/render/warnings/missing_join_predicate.py +13 -0
  414. dataface/core/render/warnings/query_parse_error.py +14 -0
  415. dataface/core/render/warnings/query_returned_zero_rows.py +42 -0
  416. dataface/core/render/warnings/reaggregation.py +14 -0
  417. dataface/core/render/warnings/registry.py +45 -0
  418. dataface/core/render/warnings/suppression.py +46 -0
  419. dataface/core/render/warnings/temporal_single_point.py +63 -0
  420. dataface/core/render/warnings/unreferenced_chart.py +15 -0
  421. dataface/core/render/warnings/y_encoding_mostly_null.py +76 -0
  422. dataface/core/render/yaml_format.py +167 -0
  423. dataface/core/resolve_face.py +195 -0
  424. dataface/core/schema/__init__.py +0 -0
  425. dataface/core/schema/guidance.py +151 -0
  426. dataface/core/scoped_paths.py +59 -0
  427. dataface/core/serve/__init__.py +14 -0
  428. dataface/core/serve/bootstrap.py +39 -0
  429. dataface/core/serve/embedded.py +57 -0
  430. dataface/core/serve/port.py +129 -0
  431. dataface/core/serve/server.py +938 -0
  432. dataface/core/serve/templates/__init__.py +0 -0
  433. dataface/core/serve/templates/directory.yml +6 -0
  434. dataface/core/serve/templates/error.html.j2 +217 -0
  435. dataface/core/utils.py +121 -0
  436. dataface/core/validate.py +64 -0
  437. dataface/integrations/__init__.py +0 -0
  438. dataface/integrations/highlighting.py +351 -0
  439. dataface/integrations/markdown.py +537 -0
  440. dataface/py.typed +0 -0
  441. dataface-0.1.2.dist-info/METADATA +375 -0
  442. dataface-0.1.2.dist-info/RECORD +455 -0
  443. dataface-0.1.2.dist-info/WHEEL +4 -0
  444. dataface-0.1.2.dist-info/entry_points.txt +2 -0
  445. dataface-0.1.2.dist-info/licenses/LICENSE +202 -0
  446. mdsvg/__init__.py +168 -0
  447. mdsvg/fonts.py +656 -0
  448. mdsvg/images.py +299 -0
  449. mdsvg/parser.py +629 -0
  450. mdsvg/playground.py +284 -0
  451. mdsvg/py.typed +2 -0
  452. mdsvg/renderer.py +1623 -0
  453. mdsvg/style.py +355 -0
  454. mdsvg/types.py +200 -0
  455. mdsvg/utils.py +86 -0
@@ -0,0 +1,1249 @@
1
+ """Query execution engine.
2
+
3
+ Stage: EXECUTE (Service)
4
+ Purpose: Execute queries for compiled datafaces.
5
+
6
+ Entry Points:
7
+ - Executor.execute_query(query_name, variables) -> List[Dict]
8
+ - Executor.execute_chart(chart, variables) -> List[Dict]
9
+
10
+ The executor:
11
+ - Resolves which adapter to use for each query
12
+ - Executes queries and returns data
13
+ - Caches results for efficiency
14
+
15
+ Does NOT:
16
+ - Compile datafaces
17
+ - Render charts
18
+
19
+ Dependencies:
20
+ - dataface.compile (for types)
21
+ - .adapters (for data source adapters)
22
+
23
+ See also:
24
+ - render/renderer.py: Uses executor for data
25
+ """
26
+
27
+ import hashlib
28
+ import json
29
+ import logging
30
+ import re
31
+ from functools import lru_cache
32
+ from pathlib import Path
33
+ from typing import TYPE_CHECKING, Any
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+ from dataface.core.compile.jinja import resolve_jinja_template
38
+ from dataface.core.compile.models.chart.authored import FilterDef
39
+ from dataface.core.compile.models.chart.compiled import (
40
+ Chart,
41
+ )
42
+ from dataface.core.compile.models.face.compiled import Face, VariableValues
43
+ from dataface.core.compile.models.query.compiled import (
44
+ AnyQuery,
45
+ SqlQuery,
46
+ is_sql_query,
47
+ )
48
+ from dataface.core.compile.variables import parse_variable_json_strings
49
+ from dataface.core.execute.errors import ExecutionError, QueryError
50
+
51
+ if TYPE_CHECKING:
52
+ from dataface.core.execute.adapters.adapter_registry import AdapterRegistry
53
+ from dataface.core.execute.adapters.base import ResolvedRelation
54
+ from dataface.core.execute.batch import BatchStatement
55
+ from dataface.core.execute.cache_backend import QueryResultCache
56
+
57
+
58
+ @lru_cache(maxsize=256)
59
+ def _sql_like_to_regex(pattern: str) -> re.Pattern[str]:
60
+ """Compile a SQL LIKE pattern to a regex (cached — same pattern reused across rows).
61
+
62
+ SQL specials: ``%`` → any sequence (``.*``), ``_`` → any one char (``.``).
63
+ All other characters — including fnmatch specials ``*``, ``?``, ``[``, ``]``
64
+ which are *literal* in SQL LIKE — are escaped via ``re.escape``.
65
+ Backslash escapes: ``\\%`` → literal ``%``, ``\\_`` → literal ``_``.
66
+ """
67
+ out: list[str] = []
68
+ i = 0
69
+ while i < len(pattern):
70
+ ch = pattern[i]
71
+ if ch == "\\" and i + 1 < len(pattern):
72
+ out.append(re.escape(pattern[i + 1]))
73
+ i += 2
74
+ elif ch == "%":
75
+ out.append(".*")
76
+ i += 1
77
+ elif ch == "_":
78
+ out.append(".")
79
+ i += 1
80
+ else:
81
+ out.append(re.escape(ch))
82
+ i += 1
83
+ return re.compile(r"\A(?:" + "".join(out) + r")\Z", re.DOTALL)
84
+
85
+
86
+ # Matches a Jinja template that is a pure variable reference: "{{ var_name }}".
87
+ # Used in _apply_filters to short-circuit through a type-preserving dict lookup
88
+ # instead of Jinja rendering (which always produces strings and would break
89
+ # numeric/boolean column comparisons).
90
+ _SIMPLE_JINJA_VAR_RE: re.Pattern[str] = re.compile(r"^\{\{\s*(\w+)\s*\}\}$")
91
+
92
+ # Comparison functions for ordered operators — hoisted to module level to
93
+ # avoid rebuilding the dict on every _row_matches call.
94
+ _CMP_FUNCS: dict[str, Any] = {
95
+ "gt": lambda a, b: a > b,
96
+ "gte": lambda a, b: a >= b,
97
+ "lt": lambda a, b: a < b,
98
+ "lte": lambda a, b: a <= b,
99
+ }
100
+
101
+
102
+ def _row_matches(op: str, row_val: Any, filter_val: Any) -> bool:
103
+ """Return True if ``row_val`` satisfies the filter predicate.
104
+
105
+ Used by ``Executor._apply_filters`` for post-execution row filtering.
106
+ Mirrors the operator semantics of ``filter_injection._build_condition``
107
+ but applied to Python values instead of a SQL AST.
108
+
109
+ Args:
110
+ op: Operator key — one of eq, neq, gt, gte, lt, lte, like, ilike,
111
+ in, not_in, between.
112
+ row_val: Value from the result row.
113
+ filter_val: Resolved variable value.
114
+
115
+ Returns:
116
+ True if the row passes the predicate; False to exclude it.
117
+
118
+ Raises:
119
+ ValueError: If ``filter_val`` is the wrong type for the operator
120
+ (e.g. non-list for ``in``/``not_in``, non-2-element list for
121
+ ``between``) or if the comparison raises TypeError due to
122
+ incompatible types.
123
+ ValueError: If ``op`` is not a recognized operator key.
124
+ """
125
+ if op == "in" or (op == "eq" and isinstance(filter_val, list)):
126
+ if not isinstance(filter_val, (list, tuple)):
127
+ raise ValueError(
128
+ f"Filter operator 'in' requires a list value, got {type(filter_val).__name__}"
129
+ )
130
+ return row_val in filter_val
131
+ if op == "not_in":
132
+ if not isinstance(filter_val, (list, tuple)):
133
+ raise ValueError(
134
+ f"Filter operator 'not_in' requires a list value, got {type(filter_val).__name__}"
135
+ )
136
+ return row_val not in filter_val
137
+ if op == "between":
138
+ if not isinstance(filter_val, (list, tuple)) or len(filter_val) != 2:
139
+ raise ValueError(
140
+ f"Filter operator 'between' requires a 2-element [start, end] list, "
141
+ f"got {filter_val!r}"
142
+ )
143
+ start, end = filter_val
144
+ if start is None or end is None:
145
+ raise ValueError(
146
+ f"Filter operator 'between' bounds must be non-None, got {filter_val!r}"
147
+ )
148
+ try:
149
+ return start <= row_val <= end
150
+ except TypeError as e:
151
+ raise ValueError(
152
+ f"Filter 'between' cannot compare {type(row_val).__name__} with "
153
+ f"bounds of type {type(start).__name__}: {e}"
154
+ ) from e
155
+ if op == "neq":
156
+ return row_val != filter_val
157
+ if op in _CMP_FUNCS:
158
+ try:
159
+ return _CMP_FUNCS[op](row_val, filter_val)
160
+ except TypeError as e:
161
+ raise ValueError(
162
+ f"Filter '{op}' cannot compare {type(row_val).__name__} "
163
+ f"with {type(filter_val).__name__}: {e}"
164
+ ) from e
165
+ if op == "like":
166
+ return bool(_sql_like_to_regex(str(filter_val)).match(str(row_val)))
167
+ if op == "ilike":
168
+ return bool(
169
+ _sql_like_to_regex(str(filter_val).lower()).match(str(row_val).lower())
170
+ )
171
+ if op == "eq":
172
+ return row_val == filter_val
173
+ raise ValueError(f"Unknown filter operator {op!r}")
174
+
175
+
176
+ def _get_relevant_variables(
177
+ query: AnyQuery, variables: VariableValues | None
178
+ ) -> dict[str, Any]:
179
+ """Filter variables to only those this query depends on."""
180
+ if variables and query.variable_dependencies:
181
+ return {k: v for k, v in variables.items() if k in query.variable_dependencies}
182
+ return {}
183
+
184
+
185
+ def _query_name_for(query: AnyQuery, registry: dict[str, AnyQuery]) -> str | None:
186
+ """Find the registry name for a query object, or None if not found."""
187
+ for name, q in registry.items():
188
+ if q is query:
189
+ return name
190
+ return None
191
+
192
+
193
+ class Executor:
194
+ """Executes queries for datafaces.
195
+
196
+ Stage: EXECUTE (Service Module)
197
+
198
+ The executor manages query execution for a compiled dataface.
199
+ It handles:
200
+ - Query lookup from the face
201
+ - Adapter selection based on query type
202
+ - Result caching for efficiency
203
+ - Variable substitution
204
+
205
+ Does NOT:
206
+ - Compile datafaces (use compile module)
207
+ - Render charts (use render module)
208
+
209
+ Attributes:
210
+ face: The compiled dataface
211
+ adapter_registry: Registry of adapters
212
+ query_registry: Complete query registry (for cross-references)
213
+
214
+ Example:
215
+ >>> from pathlib import Path
216
+ >>> from dataface.core.compile import compile
217
+ >>> from dataface.core.execute import Executor
218
+ >>> from dataface.core.execute.adapters import build_adapter_registry
219
+ >>>
220
+ >>> result = compile(yaml_content)
221
+ >>> registry = build_adapter_registry(Path.cwd())
222
+ >>> executor = Executor(result.face, registry, query_registry=result.query_registry)
223
+ >>>
224
+ >>> # Execute a query
225
+ >>> data = executor.execute_query("sales", {"year": 2024})
226
+ >>> print(data) # [{"date": "2024-01", "amount": 1000}, ...]
227
+ """
228
+
229
+ def __init__(
230
+ self,
231
+ face: Face,
232
+ adapter_registry: "AdapterRegistry",
233
+ query_registry: dict[str, AnyQuery] | None = None,
234
+ project_root: Path | None = None,
235
+ use_cache: bool = True,
236
+ duckdb_cache: "QueryResultCache | None" = None,
237
+ ):
238
+ """Initialize executor.
239
+
240
+ Args:
241
+ face: Compiled dataface
242
+ adapter_registry: Adapter registry. Build at the entry point with
243
+ ``build_adapter_registry(project_root, ...)``; the executor
244
+ does not invent one from cwd.
245
+ query_registry: Optional query registry for cross-file references
246
+ project_root: Root directory for resolving relative file paths
247
+ use_cache: Default cache behavior for query execution
248
+ duckdb_cache: Optional DuckDBCache for persistent caching (Suite context)
249
+ """
250
+ self.face = face
251
+ self.query_registry = query_registry or {}
252
+ self._cache: dict[str, list[dict[str, Any]]] = {}
253
+ self._descriptions_cache: dict[str, dict[str, tuple]] = {}
254
+ self._provenance_cache: dict[str, list[ResolvedRelation]] = {}
255
+ self._query_errors: dict[str, Exception] = {}
256
+ self._use_cache = use_cache
257
+ self._duckdb_cache = duckdb_cache
258
+ self.project_root = project_root
259
+ self.adapter_registry = adapter_registry
260
+
261
+ def execute_query(
262
+ self,
263
+ query_name: str,
264
+ variables: VariableValues | None = None,
265
+ use_cache: bool | None = None,
266
+ force_refresh: bool = False,
267
+ ) -> list[dict[str, Any]]:
268
+ """Execute a query and return results.
269
+
270
+ Stage: EXECUTE (Main Entry Point)
271
+
272
+ Looks up the query by name, resolves the appropriate adapter,
273
+ executes the query, and returns the data.
274
+
275
+ Args:
276
+ query_name: Name of query to execute (may include "queries." prefix)
277
+ variables: Variable values for query resolution
278
+ use_cache: Whether to use cached results if available
279
+ force_refresh: If True, clear both success and failure caches
280
+ for this query and re-run it from scratch.
281
+
282
+ Returns:
283
+ List of dictionaries with query results (each dict is a row)
284
+
285
+ Raises:
286
+ ExecutionError: If query not found or execution fails
287
+ CachedQueryFailure: If a cached failure exists within TTL
288
+
289
+ Example:
290
+ >>> data = executor.execute_query("sales", {"year": 2024})
291
+ >>> for row in data:
292
+ ... print(f"{row['date']}: {row['amount']}")
293
+ """
294
+ # ────────────────────────────────────────────────────────────────
295
+ # Step 1: Normalize query name (strip "queries." prefix)
296
+ # ────────────────────────────────────────────────────────────────
297
+ if query_name.startswith("queries."):
298
+ query_name = query_name[8:]
299
+
300
+ # ────────────────────────────────────────────────────────────────
301
+ # Step 1b: Merge variables with face defaults
302
+ # ────────────────────────────────────────────────────────────────
303
+ # Trust the normalizer - use pre-computed variable_defaults
304
+ variable_registry = self.face.variable_registry or {}
305
+
306
+ # Merge variables: start with None for all vars, then defaults, then user values
307
+ all_variables: dict[str, Any] = dict.fromkeys(variable_registry)
308
+ all_variables.update(self.face.variable_defaults) # Pre-computed by normalizer
309
+ # Parse JSON strings in variables (from URL parameters) and merge
310
+ parsed_variables = parse_variable_json_strings(variables or {})
311
+ variables = {**all_variables, **parsed_variables}
312
+
313
+ # ────────────────────────────────────────────────────────────────
314
+ # Step 2: Look up query
315
+ # ────────────────────────────────────────────────────────────────
316
+ query = self._get_query(query_name)
317
+
318
+ # ────────────────────────────────────────────────────────────────
319
+ # Step 3: Check cache (use instance default if not explicitly set)
320
+ # ────────────────────────────────────────────────────────────────
321
+ should_use_cache = use_cache if use_cache is not None else self._use_cache
322
+ cache_key = self._cache_key(query, variables)
323
+
324
+ # force_refresh: clear all caches for this key before running
325
+ if force_refresh:
326
+ self._cache.pop(cache_key, None)
327
+ self._query_errors.pop(query_name, None)
328
+ if self._duckdb_cache:
329
+ from dataface.core.execute.duckdb_cache import (
330
+ compute_query_hash,
331
+ compute_source_hash,
332
+ compute_variables_hash,
333
+ )
334
+
335
+ relevant_vars = _get_relevant_variables(query, variables)
336
+ variables_hash = compute_variables_hash(relevant_vars)
337
+ if is_sql_query(query):
338
+ pre_sql = query.sql
339
+ if query.setup_sql:
340
+ pre_sql += "\n" + query.setup_sql
341
+ q_hash = compute_query_hash(pre_sql)
342
+ source_hash = compute_source_hash(
343
+ query.source, face_sources=self.face.sources
344
+ )
345
+ else:
346
+ q_hash = compute_query_hash(query.source_description)
347
+ source_hash = compute_source_hash(None)
348
+ self._duckdb_cache.clear(source_hash, q_hash, variables_hash)
349
+ else:
350
+ # Check in-memory cache first (CLI context)
351
+ if should_use_cache and cache_key in self._cache:
352
+ return self._cache[cache_key]
353
+
354
+ # ────────────────────────────────────────────────────────────
355
+ # Step 3a: Check for stored pre-execution error
356
+ # ────────────────────────────────────────────────────────────
357
+ if query_name in self._query_errors:
358
+ stored = self._query_errors[query_name]
359
+ raise QueryError(str(stored), query_name) from stored
360
+
361
+ # ────────────────────────────────────────────────────────────
362
+ # Step 3b: Check DuckDB failure then success cache
363
+ # ────────────────────────────────────────────────────────────
364
+ if should_use_cache and self._duckdb_cache:
365
+ from dataface.core.execute.cache_backend import CachedQueryFailure
366
+ from dataface.core.execute.duckdb_cache import (
367
+ compute_query_hash,
368
+ compute_source_hash,
369
+ compute_variables_hash,
370
+ )
371
+
372
+ relevant_vars = _get_relevant_variables(query, variables)
373
+ variables_hash = compute_variables_hash(relevant_vars)
374
+ if is_sql_query(query):
375
+ pre_sql = query.sql
376
+ if query.setup_sql:
377
+ pre_sql += "\n" + query.setup_sql
378
+ q_hash = compute_query_hash(pre_sql)
379
+ source_hash = compute_source_hash(
380
+ query.source, face_sources=self.face.sources
381
+ )
382
+ else:
383
+ q_hash = compute_query_hash(query.source_description)
384
+ source_hash = compute_source_hash(None)
385
+
386
+ # Step 3b: failure cache check
387
+
388
+ cached_failure = self._duckdb_cache.get_failure(
389
+ source_hash, q_hash, variables_hash
390
+ )
391
+ if isinstance(cached_failure, CachedQueryFailure):
392
+ raise cached_failure
393
+
394
+ # Step 3c: Check DuckDB success cache.
395
+ # Key: (source_hash, query_hash, variables_hash) — no face/query noise.
396
+ # A different query_hash (changed SQL) is a miss by definition since the
397
+ # key doesn't match.
398
+ cached_rows = self._duckdb_cache.get(
399
+ source_hash, q_hash, variables_hash
400
+ )
401
+ if cached_rows is not None:
402
+ self._cache[cache_key] = cached_rows # warm in-memory cache
403
+ return cached_rows
404
+
405
+ # ────────────────────────────────────────────────────────────────
406
+ # Step 3d: Check for {{ results.X }} references (Suite context)
407
+ # ────────────────────────────────────────────────────────────────
408
+ if self._has_results_refs(query):
409
+ # {{ results.X }} queries bypass _resolve_query_references,
410
+ # so setup_sql is not supported on this path.
411
+ if is_sql_query(query) and query.setup_sql:
412
+ raise QueryError(
413
+ "setup_sql is not supported on queries using {{ results.X }} references",
414
+ query_name,
415
+ )
416
+ result_data = self._execute_results_query(query, variables)
417
+ if should_use_cache:
418
+ self._cache[cache_key] = result_data
419
+ return result_data
420
+
421
+ # ────────────────────────────────────────────────────────────────
422
+ # Step 4: Resolve query references in SQL
423
+ # ────────────────────────────────────────────────────────────────
424
+ query = self._resolve_query_references(query, variables, query_name)
425
+
426
+ # ────────────────────────────────────────────────────────────────
427
+ # Step 5: Execute via adapter
428
+ # ────────────────────────────────────────────────────────────────
429
+ try:
430
+ result = self.adapter_registry.execute(query, variables, face=self.face)
431
+ except Exception as e:
432
+ self._cache_failure_to_duckdb(query_name, query, variables, e)
433
+ raise QueryError(str(e), query_name) from e
434
+
435
+ if not result.is_success:
436
+ err = QueryError(result.error or "Unknown error", query_name)
437
+ self._cache_failure_to_duckdb(query_name, query, variables, err)
438
+ raise err
439
+
440
+ # ────────────────────────────────────────────────────────────────
441
+ # Step 6: Cache and return
442
+ # ────────────────────────────────────────────────────────────────
443
+ if should_use_cache:
444
+ self._cache[cache_key] = result.data
445
+
446
+ # Cache column descriptions from cursor.description for chart decisions
447
+ if result.column_descriptions:
448
+ self._descriptions_cache[query_name] = result.column_descriptions
449
+
450
+ # Cache dbt relation provenance for chart schema-status badges
451
+ if result.resolved_relations:
452
+ self._provenance_cache[query_name] = result.resolved_relations
453
+
454
+ # Also cache to DuckDB if configured (Suite context)
455
+ if self._duckdb_cache:
456
+ self._cache_to_duckdb(query_name, query, result.data, variables)
457
+
458
+ return result.data
459
+
460
+ def execute_chart(
461
+ self,
462
+ chart: Chart | str,
463
+ variables: VariableValues | None = None,
464
+ use_cache: bool | None = None,
465
+ ) -> list[dict[str, Any]]:
466
+ """Execute the query for a chart.
467
+
468
+ Convenience method that handles chart → query lookup.
469
+
470
+ Args:
471
+ chart: Chart or chart name string
472
+ variables: Variable values
473
+ use_cache: Whether to use cache (defaults to instance setting)
474
+
475
+ Returns:
476
+ Query results for the chart
477
+
478
+ Example:
479
+ >>> chart = face.charts["revenue"]
480
+ >>> data = executor.execute_chart(chart, {"year": 2024})
481
+ """
482
+ if isinstance(chart, str):
483
+ if chart not in self.face.charts:
484
+ raise ExecutionError(f"Chart '{chart}' not found")
485
+ chart = self.face.charts[chart]
486
+
487
+ # Handle blank charts (no query) - return empty data
488
+ if chart.query_name is None:
489
+ return []
490
+
491
+ # Get data (use_cache will fall back to instance default in execute_query)
492
+ data = self.execute_query(chart.query_name, variables, use_cache)
493
+
494
+ # Apply chart-level filters if present
495
+ if chart.filters:
496
+ data = self._apply_filters(data, chart.filters, variables)
497
+
498
+ return data
499
+
500
+ def get_column_descriptions(self, query_name: str) -> dict[str, tuple] | None:
501
+ """Get cached PEP 249 cursor.description tuples for a query.
502
+
503
+ Returns the full description tuples captured from cursor.description
504
+ during the last execution. Each value is a 7-tuple:
505
+ (name, type_code, display_size, internal_size, precision, scale, null_ok).
506
+
507
+ Returns None if the query hasn't been executed or the adapter
508
+ didn't provide descriptions (e.g. CSV, HTTP adapters).
509
+ """
510
+ if query_name and query_name.startswith("queries."):
511
+ query_name = query_name[8:]
512
+ return self._descriptions_cache.get(query_name)
513
+
514
+ def get_query_provenance(self, query_name: str) -> list["ResolvedRelation"] | None:
515
+ """Get cached dbt relation provenance for a query.
516
+
517
+ Returns a list of ResolvedRelation objects captured during execution,
518
+ or None if no provenance is available (non-dbt queries, not yet executed).
519
+ """
520
+ if query_name and query_name.startswith("queries."):
521
+ query_name = query_name[8:]
522
+ return self._provenance_cache.get(query_name)
523
+
524
+ def execute_face_batch(
525
+ self,
526
+ variables: VariableValues | None = None,
527
+ ) -> dict[str, list[dict[str, Any]]]:
528
+ """Execute all dataface queries using batch temp table optimization.
529
+
530
+ This method executes all queries needed for the dataface using temp tables
531
+ for queries that are referenced by other queries. This is more efficient than
532
+ executing queries individually when there are shared dependencies.
533
+
534
+ The batch execution:
535
+ 1. Collects all queries used by charts (and their dependencies)
536
+ 2. Groups queries by database source
537
+ 3. For each source, generates batch SQL with temp tables
538
+ 4. Executes the batch and caches results
539
+ 5. Cleans up temp tables after execution
540
+
541
+ IMPORTANT: Batching + Caching Interaction
542
+ ------------------------------------------
543
+ While queries are EXECUTED together as a batch, they are CACHED individually.
544
+ This means:
545
+ - If you re-run the dataface with different variables, only queries that
546
+ depend on those variables will be re-executed (cache miss)
547
+ - Queries that don't use the changed variables will return cached results
548
+ - Each query's cache key is based on its SQL content + relevant variables,
549
+ NOT on whether it was part of a batch
550
+
551
+ Example: Dataface with 10 queries, 2 use variable 'region':
552
+ Initial load (region=west): Executes all 10 queries, caches all 10
553
+ Second load (region=east): Re-executes only 2 queries, uses cache for 8
554
+
555
+ IMPORTANT: Connection/Session Requirements
556
+ ------------------------------------------
557
+ Temp tables are session-scoped in most databases. For batch execution
558
+ to work correctly, all statements in a batch MUST execute on the same
559
+ database connection/session. If the adapter creates a new connection
560
+ per query, temp tables created in earlier statements won't be visible
561
+ to later statements.
562
+
563
+ Note:
564
+ Variable names should not include "queries" as a key, as this is
565
+ a reserved namespace for query references.
566
+
567
+ Args:
568
+ variables: Variable values for query resolution
569
+
570
+ Returns:
571
+ Dict mapping query names to their results
572
+
573
+ Raises:
574
+ CrossSourceReferenceError: If a query references another
575
+ query on a different database source
576
+ ValueError: If variables contain reserved namespace "queries"
577
+
578
+ Example:
579
+ >>> executor = Executor(compiled_face, adapter_registry)
580
+ >>> results = executor.execute_face_batch({"region": "west"})
581
+ >>> orders_data = results["orders"]
582
+ >>> high_value_data = results["high_value"]
583
+ """
584
+ from dataface.core.execute.batch import (
585
+ TEMP_TABLE_PREFIX,
586
+ create_batch_execution_plan,
587
+ )
588
+
589
+ # Validate variables don't conflict with reserved namespaces
590
+ if variables and "queries" in variables:
591
+ raise ValueError(
592
+ "Variable name 'queries' is reserved for query references. "
593
+ "Please use a different variable name."
594
+ )
595
+
596
+ # Generate batch execution plan
597
+ plan = create_batch_execution_plan(self.face)
598
+
599
+ results: dict[str, list[dict[str, Any]]] = {}
600
+ created_temp_tables: dict[str, list[str]] = {} # source -> temp table names
601
+
602
+ try:
603
+ for source, statements in plan.items():
604
+ # Track temp tables created for cleanup
605
+ created_temp_tables[source] = [
606
+ f"{TEMP_TABLE_PREFIX}{stmt.query_name}"
607
+ for stmt in statements
608
+ if stmt.is_temp_table_create
609
+ ]
610
+
611
+ # Execute each statement in the batch
612
+ source_results = self._execute_batch_statements(
613
+ statements, source, variables
614
+ )
615
+ results.update(source_results)
616
+ finally:
617
+ # Clean up temp tables
618
+ self._cleanup_temp_tables(created_temp_tables)
619
+
620
+ return results
621
+
622
+ def _cleanup_temp_tables(
623
+ self,
624
+ temp_tables: dict[str, list[str]],
625
+ ) -> None:
626
+ """Clean up temporary tables created during batch execution.
627
+
628
+ This method attempts to drop all temp tables created during batch
629
+ execution. Failures are logged but don't raise exceptions, since
630
+ temp tables typically auto-drop at session end anyway.
631
+
632
+ Args:
633
+ temp_tables: Dict mapping source to list of temp table names
634
+ """
635
+ from dataface.core.execute.dialects import get_dialect
636
+
637
+ for source, table_names in temp_tables.items():
638
+ dialect = get_dialect(source)
639
+
640
+ for table_name in table_names:
641
+ try:
642
+ drop_sql = dialect.drop_temp_table(table_name)
643
+ temp_query = SqlQuery(sql=drop_sql, source=source)
644
+ self.adapter_registry.execute(temp_query, None, face=self.face)
645
+ except Exception as e: # noqa: BLE001
646
+ # Log but don't fail - temp tables auto-drop at session end
647
+ logger.warning("Failed to drop temp table '%s': %s", table_name, e)
648
+
649
+ def _execute_batch_statements(
650
+ self,
651
+ statements: list["BatchStatement"],
652
+ source: str,
653
+ variables: VariableValues | None,
654
+ ) -> dict[str, list[dict[str, Any]]]:
655
+ """Execute a list of batch statements for a single source.
656
+
657
+ IMPORTANT: Connection/Session Requirements
658
+ ------------------------------------------
659
+ Temp tables are session-scoped in most databases. For batch execution
660
+ to work correctly, all statements in a batch MUST execute on the same
661
+ database connection/session. The adapter_registry should maintain a
662
+ persistent connection for the duration of the batch.
663
+
664
+ If the adapter creates a new connection per query, temp tables created
665
+ in earlier statements won't be visible to later statements.
666
+
667
+ Security:
668
+ ---------
669
+ Uses parameterized queries via render_parameterized() for SQL injection
670
+ prevention. Variable values are passed as parameters to the database
671
+ driver, not interpolated into the SQL string.
672
+
673
+ Args:
674
+ statements: List of BatchStatement objects to execute
675
+ source: Database source name
676
+ variables: Variable values for query resolution
677
+
678
+ Returns:
679
+ Dict mapping query names to their results
680
+ """
681
+ from dataface.core.compile.parameterized import render_parameterized
682
+ from dataface.core.execute.dialects import get_dialect
683
+
684
+ results: dict[str, list[dict[str, Any]]] = {}
685
+
686
+ # Get dialect for parameterization
687
+ dialect = get_dialect(source)
688
+
689
+ for stmt in statements:
690
+ # TWO-PHASE JINJA RESOLUTION:
691
+ # Phase 1 (already done in generate_batch_sql): {{ queries.X }} refs
692
+ # are replaced with temp table references (_df_temp_X)
693
+ # Phase 2 (here): Variable templates like {{ region }} are resolved
694
+ # using parameterized queries for SQL injection prevention
695
+ parameterized = render_parameterized(
696
+ stmt.sql,
697
+ variables=variables or {},
698
+ dialect=dialect,
699
+ )
700
+
701
+ # Create a temporary SqlQuery for execution with parameterized SQL
702
+ temp_query = SqlQuery(sql=parameterized.sql, source=source)
703
+
704
+ try:
705
+ # Pass params directly to the adapter to skip re-parameterization
706
+ result = self.adapter_registry.execute(
707
+ temp_query, variables, params=parameterized.params, face=self.face
708
+ )
709
+
710
+ if not result.is_success:
711
+ # Add context about whether this was a temp table creation
712
+ error_context = (
713
+ f"Failed to create temp table for '{stmt.query_name}'"
714
+ if stmt.is_temp_table_create
715
+ else f"Query execution failed for '{stmt.query_name}'"
716
+ )
717
+ raise QueryError(
718
+ f"{error_context}: {result.error or 'Unknown error'}",
719
+ stmt.name,
720
+ )
721
+
722
+ # For temp table creation, we don't need to store results
723
+ # (the temp table itself is the "result" - accessible by later queries)
724
+ if not stmt.is_temp_table_create:
725
+ # Use query_name for caching/results (not statement name)
726
+ query_name = stmt.query_name or stmt.name
727
+ results[query_name] = result.data
728
+
729
+ # Cache each query result INDIVIDUALLY (not the whole batch)
730
+ # This is critical: even though we execute as a batch, we cache
731
+ # per-query so that future requests can benefit from granular caching.
732
+ #
733
+ # Example: If 2 out of 10 queries change due to variable changes,
734
+ # only those 2 will cache-miss and re-execute. The other 8 will
735
+ # return from cache without hitting the database.
736
+ if self._use_cache:
737
+ cache_key = self._cache_key(temp_query, variables)
738
+ self._cache[cache_key] = result.data
739
+ if result.column_descriptions:
740
+ self._descriptions_cache[query_name] = (
741
+ result.column_descriptions
742
+ )
743
+ if result.resolved_relations:
744
+ self._provenance_cache[query_name] = result.resolved_relations
745
+
746
+ except Exception as e:
747
+ if isinstance(e, QueryError):
748
+ raise
749
+ # Add context about whether this was a temp table creation
750
+ error_context = (
751
+ f"Failed to create temp table for '{stmt.query_name}'"
752
+ if stmt.is_temp_table_create
753
+ else f"Query execution failed for '{stmt.query_name}'"
754
+ )
755
+ raise QueryError(f"{error_context}: {e}", stmt.name) from e
756
+
757
+ return results
758
+
759
+ def clear_cache(self) -> None:
760
+ """Clear all caches (query results, column descriptions, provenance, errors)."""
761
+ self._cache.clear()
762
+ self._descriptions_cache.clear()
763
+ self._provenance_cache.clear()
764
+ self._query_errors.clear()
765
+
766
+ def _get_query(self, query_name: str) -> AnyQuery:
767
+ """Look up a query by name.
768
+
769
+ Checks face.queries first, then query_registry.
770
+
771
+ Args:
772
+ query_name: Query name
773
+
774
+ Returns:
775
+ AnyQuery
776
+
777
+ Raises:
778
+ ExecutionError: If query not found
779
+ """
780
+ if query_name in self.face.queries:
781
+ return self.face.queries[query_name]
782
+
783
+ if query_name in self.query_registry:
784
+ return self.query_registry[query_name]
785
+
786
+ raise ExecutionError(f"Query '{query_name}' not found")
787
+
788
+ def _cache_key(
789
+ self,
790
+ query: AnyQuery,
791
+ variables: VariableValues | None,
792
+ ) -> str:
793
+ """Generate cache key from query content hash + relevant variables.
794
+
795
+ The cache key is designed to:
796
+ 1. Use query content (SQL template, file path, etc.) - not query name
797
+ so that changing SQL invalidates the cache even if name stays same
798
+ 2. Only include variables the query actually uses (from variable_dependencies)
799
+ so changing unrelated variables doesn't cause cache misses
800
+ 3. Use deterministic hashing (SHA-256) that works across processes
801
+
802
+ This granular cache key design enables efficient partial re-execution:
803
+ When variables change, only queries that depend on those specific variables
804
+ will have different cache keys (cache miss). Other queries return from cache.
805
+
806
+ Example: Dataface with queries A (uses var 'region'), B (uses var 'date'),
807
+ C (uses no variables). When 'region' changes:
808
+ - Query A: cache miss (key changes) → re-executes
809
+ - Query B: cache hit (key unchanged) → returns cached data
810
+ - Query C: cache hit (key unchanged) → returns cached data
811
+
812
+ Args:
813
+ query: The compiled query object
814
+ variables: All available variable values
815
+
816
+ Returns:
817
+ Cache key in format "{query_hash}:{variables_hash}"
818
+ """
819
+ # Hash the query content - use SQL + setup_sql for SQL queries
820
+ if is_sql_query(query):
821
+ query_content = query.sql
822
+ if query.setup_sql:
823
+ query_content += "\n" + query.setup_sql
824
+ else:
825
+ query_content = query.source_description
826
+
827
+ query_hash = hashlib.sha256(query_content.encode()).hexdigest()[:16]
828
+
829
+ relevant_vars = _get_relevant_variables(query, variables)
830
+
831
+ # Deterministic hash using json.dumps with sorted keys
832
+ # sorted(items()) ensures consistent ordering
833
+ if relevant_vars:
834
+ variables_hash = hashlib.sha256(
835
+ json.dumps(sorted(relevant_vars.items()), default=str).encode()
836
+ ).hexdigest()[:16]
837
+ else:
838
+ variables_hash = "0" * 16
839
+
840
+ return f"{query_hash}:{variables_hash}"
841
+
842
+ def _resolve_query_references(
843
+ self,
844
+ query: AnyQuery,
845
+ variables: VariableValues | None,
846
+ query_name: str | None = None,
847
+ ) -> AnyQuery:
848
+ """Resolve {{ queries.* }} references in SQL.
849
+
850
+ Resolves nested references in topological dependency order so that
851
+ multi-level query composition (e.g. style -> calc -> base) works.
852
+ Also propagates setup_sql from referenced queries so that their
853
+ preambles run before the dependent query.
854
+
855
+ Args:
856
+ query: Query to resolve
857
+ variables: Variable values
858
+ query_name: Name of this query (for dependency-ordered lookup)
859
+
860
+ Returns:
861
+ Query with fully resolved SQL and accumulated setup_sql
862
+ """
863
+ # Only SQL queries can have query references
864
+ if not is_sql_query(query):
865
+ return query
866
+
867
+ # Merge face queries and registry for resolution
868
+ all_queries = {**self.face.queries, **self.query_registry}
869
+
870
+ # Collect setup_sql from the full dependency chain
871
+ from dataface.core.execute.setup_sql import collect_setup_sql
872
+
873
+ setup_stmts = collect_setup_sql(
874
+ # We need a name to look up, but we may be called with an unnamed query.
875
+ # Pass the query's own name by searching all_queries; fallback to just
876
+ # using the query's own setup_sql.
877
+ _query_name_for(query, all_queries) or "__anonymous__",
878
+ {**all_queries, "__anonymous__": query},
879
+ )
880
+ merged_setup = "\n".join(setup_stmts) if setup_stmts else None
881
+
882
+ if "{{ queries." not in query.sql:
883
+ # No Jinja refs to resolve, but may still need propagated setup_sql
884
+ if merged_setup != query.setup_sql:
885
+ return query.model_copy(update={"setup_sql": merged_setup})
886
+ return query
887
+
888
+ from dataface.core.compile.jinja import resolve_query_sql_with_dependencies
889
+
890
+ resolved_sql = resolve_query_sql_with_dependencies(
891
+ query_name or "",
892
+ all_queries,
893
+ variables=variables,
894
+ )
895
+
896
+ # Create new SqlQuery with resolved SQL and propagated setup_sql
897
+ updates: dict = {"sql": resolved_sql}
898
+ if merged_setup:
899
+ updates["setup_sql"] = merged_setup
900
+ return query.model_copy(update=updates)
901
+
902
+ def _apply_filters(
903
+ self,
904
+ data: list[dict[str, Any]],
905
+ filters: dict[str, FilterDef],
906
+ variables: VariableValues | None,
907
+ ) -> list[dict[str, Any]]:
908
+ """Apply chart-level FilterDef bindings to data rows (AND logic).
909
+
910
+ Each binding specifies an operator and either a plain variable name
911
+ (``var``) or a Jinja template (``template``). The value is resolved once
912
+ before the row loop; if it is None, "", or the string "none" (case-insensitive)
913
+ the predicate is skipped entirely (null-value means "no filter applied").
914
+
915
+ Args:
916
+ data: Query result rows.
917
+ filters: Mapping of column name → FilterDef instance.
918
+ variables: Runtime variable values.
919
+
920
+ Returns:
921
+ Filtered rows (all bindings must match — AND semantics).
922
+ """
923
+ if not filters or not data:
924
+ return data
925
+
926
+ vars_: dict[str, Any] = variables or {}
927
+
928
+ # Resolve all filter values once before the row loop.
929
+ # active_filters holds only predicates that are not skipped.
930
+ active_filters: list[tuple[str, str, Any]] = [] # (col, op, value)
931
+ for col, binding in filters.items():
932
+ if binding.template is not None:
933
+ # Simple "{{ var_name }}" → type-preserving direct lookup so
934
+ # numeric/boolean column values compare correctly (Jinja always
935
+ # renders to strings, which would break eq on non-string columns).
936
+ m = _SIMPLE_JINJA_VAR_RE.match(binding.template)
937
+ if m:
938
+ value: Any = vars_.get(m.group(1))
939
+ if value is None or value == "":
940
+ continue
941
+ else:
942
+ # Complex expression (e.g. "{{ x if x else '' }}") — render
943
+ # through Jinja; result is always a string.
944
+ value = resolve_jinja_template(
945
+ binding.template, vars_, strict=False
946
+ )
947
+ # "" / "none" (Jinja renders Python None as "None") → skip.
948
+ if not value or str(value).lower() == "none":
949
+ continue
950
+ else:
951
+ assert binding.var is not None
952
+ value = vars_.get(binding.var)
953
+ if value is None or value == "":
954
+ continue
955
+
956
+ # Empty collection → skip (mirrors SQL: no IN()/NOT IN() clause emitted).
957
+ # Also applies to eq when value is a list (eq delegates to in).
958
+ if (
959
+ binding.op in ("in", "not_in", "eq")
960
+ and isinstance(value, (list, tuple))
961
+ and not value
962
+ ):
963
+ continue
964
+ # between with None bound → skip (mirrors SQL: no BETWEEN clause emitted).
965
+ if (
966
+ binding.op == "between"
967
+ and isinstance(value, (list, tuple))
968
+ and len(value) == 2
969
+ and (value[0] is None or value[1] is None)
970
+ ):
971
+ continue
972
+ active_filters.append((col, binding.op, value))
973
+
974
+ if not active_filters:
975
+ return data
976
+
977
+ filtered = []
978
+ for row in data:
979
+ keep = True
980
+ for col, op, value in active_filters:
981
+ if not _row_matches(op, row.get(col), value):
982
+ keep = False
983
+ break
984
+ if keep:
985
+ filtered.append(row)
986
+
987
+ return filtered
988
+
989
+ # ─────────────────────────────────────────────────────────────────────
990
+ # DuckDB Cache Integration (Suite context)
991
+ # ─────────────────────────────────────────────────────────────────────
992
+
993
+ def _has_results_refs(self, query: AnyQuery) -> bool:
994
+ """Check if query has {{ results.X }} references.
995
+
996
+ Args:
997
+ query: Query to check
998
+
999
+ Returns:
1000
+ True if query references cached results
1001
+ """
1002
+ if not is_sql_query(query):
1003
+ return False
1004
+ return "{{ results." in query.sql or "{{results." in query.sql
1005
+
1006
+ def _extract_results_refs(self, sql: str) -> list[str]:
1007
+ """Extract {{ results.X }} reference names from SQL.
1008
+
1009
+ Args:
1010
+ sql: SQL with potential results references
1011
+
1012
+ Returns:
1013
+ List of query names referenced
1014
+ """
1015
+ import re
1016
+
1017
+ pattern = r"\{\{\s*results\.(\w+)\s*\}\}"
1018
+ return re.findall(pattern, sql)
1019
+
1020
+ def _cache_to_duckdb(
1021
+ self,
1022
+ query_name: str,
1023
+ query: AnyQuery,
1024
+ data: list[dict[str, Any]],
1025
+ variables: VariableValues | None,
1026
+ ) -> None:
1027
+ """Cache query result to DuckDB using the new (source, query, vars) key."""
1028
+ if not self._duckdb_cache or not data:
1029
+ return
1030
+
1031
+ from dataface.core.execute.duckdb_cache import (
1032
+ compute_query_hash,
1033
+ compute_source_hash,
1034
+ compute_variables_hash,
1035
+ )
1036
+
1037
+ if is_sql_query(query):
1038
+ query_content = query.sql
1039
+ if query.setup_sql:
1040
+ query_content += "\n" + query.setup_sql
1041
+ query_hash = compute_query_hash(query_content)
1042
+ source_hash = compute_source_hash(
1043
+ query.source, face_sources=self.face.sources
1044
+ )
1045
+ else:
1046
+ query_hash = compute_query_hash(query.source_description)
1047
+ source_hash = compute_source_hash(None)
1048
+
1049
+ relevant_vars = _get_relevant_variables(query, variables)
1050
+ variables_hash = compute_variables_hash(relevant_vars)
1051
+ face_slug = self._get_face_slug()
1052
+
1053
+ self._duckdb_cache.put(
1054
+ source_hash,
1055
+ query_hash,
1056
+ variables_hash,
1057
+ data,
1058
+ face_slug=face_slug,
1059
+ query_name=query_name,
1060
+ variables=relevant_vars,
1061
+ )
1062
+
1063
+ def _cache_failure_to_duckdb(
1064
+ self,
1065
+ query_name: str,
1066
+ query: AnyQuery,
1067
+ variables: VariableValues | None,
1068
+ exception: Exception,
1069
+ ) -> None:
1070
+ """Cache a query failure to DuckDB (if configured). Never raises."""
1071
+ if not self._duckdb_cache:
1072
+ return
1073
+ try:
1074
+ from dataface.core.execute.duckdb_cache import (
1075
+ compute_query_hash,
1076
+ compute_source_hash,
1077
+ compute_variables_hash,
1078
+ )
1079
+
1080
+ if is_sql_query(query):
1081
+ query_content = query.sql
1082
+ if query.setup_sql:
1083
+ query_content += "\n" + query.setup_sql
1084
+ query_hash = compute_query_hash(query_content)
1085
+ source_hash = compute_source_hash(
1086
+ query.source, face_sources=self.face.sources
1087
+ )
1088
+ else:
1089
+ query_hash = compute_query_hash(query.source_description)
1090
+ source_hash = compute_source_hash(None)
1091
+
1092
+ relevant_vars = _get_relevant_variables(query, variables)
1093
+ variables_hash = compute_variables_hash(relevant_vars)
1094
+ face_slug = self._get_face_slug()
1095
+
1096
+ self._duckdb_cache.put_failure(
1097
+ source_hash,
1098
+ query_hash,
1099
+ variables_hash,
1100
+ exception,
1101
+ face_slug=face_slug,
1102
+ query_name=query_name,
1103
+ )
1104
+ except Exception: # noqa: BLE001
1105
+ logger.debug(
1106
+ "Failed to cache query failure for '%s'",
1107
+ query_name,
1108
+ exc_info=True,
1109
+ )
1110
+
1111
+ def _execute_results_query(
1112
+ self,
1113
+ query: AnyQuery,
1114
+ variables: VariableValues | None,
1115
+ ) -> list[dict[str, Any]]:
1116
+ """Execute a query that uses {{ results.X }} references.
1117
+
1118
+ First ensures all referenced queries are cached, then executes
1119
+ the query against DuckDB.
1120
+
1121
+ Args:
1122
+ query: Query with results references
1123
+ variables: Variable values
1124
+
1125
+ Returns:
1126
+ Query result data
1127
+
1128
+ Raises:
1129
+ ExecutionError: If DuckDB cache not configured
1130
+ """
1131
+ if not self._duckdb_cache:
1132
+ raise ExecutionError(
1133
+ "{{ results.X }} queries require a query result cache. "
1134
+ "Use Suite context or configure duckdb_cache."
1135
+ )
1136
+
1137
+ if not self._duckdb_cache.supports_results_refs():
1138
+ raise ExecutionError(
1139
+ "{{ results.X }} requires a SQL-engine cache backend. "
1140
+ "The configured cache does not support result references."
1141
+ )
1142
+
1143
+ if not is_sql_query(query):
1144
+ raise ExecutionError("{{ results.X }} can only be used in SQL queries")
1145
+
1146
+ # Extract referenced queries
1147
+ refs = self._extract_results_refs(query.sql)
1148
+
1149
+ # Ensure each referenced query is cached
1150
+ for ref_name in refs:
1151
+ ref_query = self._get_query(ref_name)
1152
+ ref_data = self._execute_and_cache_for_results(
1153
+ ref_name, ref_query, variables
1154
+ )
1155
+ if ref_data is None:
1156
+ raise ExecutionError(
1157
+ f"Failed to cache query '{ref_name}' for results reference"
1158
+ )
1159
+
1160
+ # Rewrite SQL to use DuckDB view names
1161
+ face_slug = self._get_face_slug()
1162
+ rewritten_sql = self._duckdb_cache.rewrite_results_refs(face_slug, query.sql)
1163
+
1164
+ # Execute against DuckDB
1165
+ return self._duckdb_cache.execute_results_query(face_slug, rewritten_sql)
1166
+
1167
+ def _execute_and_cache_for_results(
1168
+ self,
1169
+ query_name: str,
1170
+ query: AnyQuery,
1171
+ variables: VariableValues | None,
1172
+ ) -> list[dict[str, Any]] | None:
1173
+ """Execute a query and cache it for {{ results.X }} access.
1174
+
1175
+ Args:
1176
+ query_name: Query name
1177
+ query: Query to execute
1178
+ variables: Variable values
1179
+
1180
+ Returns:
1181
+ Query result data
1182
+ """
1183
+ if not self._duckdb_cache:
1184
+ return None
1185
+
1186
+ from dataface.core.execute.duckdb_cache import (
1187
+ compute_query_hash,
1188
+ compute_source_hash,
1189
+ compute_variables_hash,
1190
+ )
1191
+
1192
+ relevant_vars = _get_relevant_variables(query, variables)
1193
+ variables_hash = compute_variables_hash(relevant_vars)
1194
+
1195
+ if is_sql_query(query):
1196
+ pre_sql = query.sql
1197
+ if query.setup_sql:
1198
+ pre_sql += "\n" + query.setup_sql
1199
+ q_hash = compute_query_hash(pre_sql)
1200
+ source_hash = compute_source_hash(
1201
+ query.source, face_sources=self.face.sources
1202
+ )
1203
+ else:
1204
+ q_hash = compute_query_hash(query.source_description)
1205
+ source_hash = compute_source_hash(None)
1206
+
1207
+ cached = self._duckdb_cache.get(source_hash, q_hash, variables_hash)
1208
+ if cached:
1209
+ return cached
1210
+
1211
+ # Execute the query
1212
+ try:
1213
+ if is_sql_query(query):
1214
+ resolved_query = self._resolve_query_references(
1215
+ query, variables, query_name
1216
+ )
1217
+ result = self.adapter_registry.execute(
1218
+ resolved_query, variables, face=self.face
1219
+ )
1220
+ else:
1221
+ result = self.adapter_registry.execute(query, variables, face=self.face)
1222
+
1223
+ if not result.is_success:
1224
+ return None
1225
+
1226
+ # Cache to DuckDB
1227
+ self._cache_to_duckdb(query_name, query, result.data, variables)
1228
+
1229
+ return result.data
1230
+
1231
+ except Exception: # noqa: BLE001
1232
+ # Log but don't fail - returns None to indicate caching failed
1233
+ # This is intentional: the query may still succeed via direct execution
1234
+ logging.getLogger(__name__).debug(
1235
+ "Failed to execute and cache query '%s' for results reference",
1236
+ query_name,
1237
+ exc_info=True,
1238
+ )
1239
+ return None
1240
+
1241
+ def _get_face_slug(self) -> str:
1242
+ """Get face slug for table naming.
1243
+
1244
+ Returns:
1245
+ Sanitized face slug
1246
+ """
1247
+ # Trust normalizer guarantees - use direct attribute access
1248
+ # Face always has title (may be empty string) and id (always present)
1249
+ return self.face.title or self.face.id or "default"