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.
- d3_format/__init__.py +14 -0
- d3_format/errors.py +19 -0
- d3_format/format.py +551 -0
- d3_format/spec.py +159 -0
- dataface/DATAFACE_SYNTAX.md +1135 -0
- dataface/__init__.py +93 -0
- dataface/_docs_site.py +20 -0
- dataface/_install_hint.py +26 -0
- dataface/agent_api/__init__.py +79 -0
- dataface/agent_api/_init_templates/__init__.py +0 -0
- dataface/agent_api/_init_templates/agents_dft_snippet.md +26 -0
- dataface/agent_api/_init_templates/dataface.yml +15 -0
- dataface/agent_api/_init_templates/faces-dataface.yml +144 -0
- dataface/agent_api/_init_templates/index.md +24 -0
- dataface/agent_api/_paths.py +118 -0
- dataface/agent_api/_project_agents_md.py +43 -0
- dataface/agent_api/_session_store.py +486 -0
- dataface/agent_api/_state.py +28 -0
- dataface/agent_api/chat.py +221 -0
- dataface/agent_api/dashboards.py +257 -0
- dataface/agent_api/describe.py +366 -0
- dataface/agent_api/describe_query.py +120 -0
- dataface/agent_api/docs/__init__.py +25 -0
- dataface/agent_api/docs/_loader.py +292 -0
- dataface/agent_api/docs/yaml-reference.md +2757 -0
- dataface/agent_api/file_refs.py +118 -0
- dataface/agent_api/init.py +126 -0
- dataface/agent_api/inspect.py +128 -0
- dataface/agent_api/mcp_install.py +170 -0
- dataface/agent_api/query.py +274 -0
- dataface/agent_api/schema.py +658 -0
- dataface/agent_api/schema_search.py +284 -0
- dataface/agent_api/search.py +270 -0
- dataface/agent_api/skill_install.py +141 -0
- dataface/agent_api/skill_render.py +90 -0
- dataface/agent_api/skills.py +293 -0
- dataface/agent_api/surface_aliases.yaml +128 -0
- dataface/agent_api/validate.py +175 -0
- dataface/agent_api/validate_query.py +84 -0
- dataface/ai/__init__.py +39 -0
- dataface/ai/agent.py +139 -0
- dataface/ai/context.py +45 -0
- dataface/ai/events.py +62 -0
- dataface/ai/external_mcp.py +610 -0
- dataface/ai/generate_sql.py +96 -0
- dataface/ai/llm.py +403 -0
- dataface/ai/mcp/__init__.py +51 -0
- dataface/ai/mcp/server.py +289 -0
- dataface/ai/memories.py +85 -0
- dataface/ai/prompts.py +177 -0
- dataface/ai/schema_context.py +138 -0
- dataface/ai/skills/before-after-comparison/SKILL.md +102 -0
- dataface/ai/skills/before-after-comparison/examples/before-after-comparison.yml +24 -0
- dataface/ai/skills/dashboard-build/SKILL.md +212 -0
- dataface/ai/skills/dashboard-build/examples/_smoke.yml +15 -0
- dataface/ai/skills/dashboard-design/SKILL.md +182 -0
- dataface/ai/skills/dashboard-review/SKILL.md +113 -0
- dataface/ai/skills/dashboard-structural-review/SKILL.md +173 -0
- dataface/ai/skills/dashboard-visual-review/SKILL.md +139 -0
- dataface/ai/skills/dataface-mcp-setup/SKILL.md +177 -0
- dataface/ai/skills/dataface-troubleshooting/SKILL.md +225 -0
- dataface/ai/skills/drill-down-link/SKILL.md +112 -0
- dataface/ai/skills/drill-down-link/examples/drill-down-link.yml +27 -0
- dataface/ai/skills/faceted-small-multiples/SKILL.md +116 -0
- dataface/ai/skills/faceted-small-multiples/examples/faceted-small-multiples.yml +33 -0
- dataface/ai/skills/filter-bar-with-variables/SKILL.md +105 -0
- dataface/ai/skills/filter-bar-with-variables/examples/filter-bar-with-variables.yml +49 -0
- dataface/ai/skills/kpi-row/SKILL.md +101 -0
- dataface/ai/skills/kpi-row/examples/kpi-row.yml +55 -0
- dataface/ai/skills/report-design/SKILL.md +184 -0
- dataface/ai/skills/single-metric-bignum/SKILL.md +90 -0
- dataface/ai/skills/single-metric-bignum/examples/single-metric-bignum.yml +27 -0
- dataface/ai/skills/table-heavy-ops-dashboard/SKILL.md +114 -0
- dataface/ai/skills/table-heavy-ops-dashboard/examples/table-heavy-ops-dashboard.yml +48 -0
- dataface/ai/skills/time-series-trend/SKILL.md +93 -0
- dataface/ai/skills/time-series-trend/examples/time-series-trend.yml +26 -0
- dataface/ai/skills/top-n-with-detail/SKILL.md +98 -0
- dataface/ai/skills/top-n-with-detail/examples/top-n-with-detail.yml +45 -0
- dataface/ai/skills/two-by-two-grid-overview/SKILL.md +78 -0
- dataface/ai/skills/two-by-two-grid-overview/examples/two-by-two-grid-overview.yml +64 -0
- dataface/ai/tool_schemas.py +132 -0
- dataface/ai/tools/__init__.py +312 -0
- dataface/ai/yaml_utils.py +57 -0
- dataface/cli/__init__.py +3 -0
- dataface/cli/_console.py +48 -0
- dataface/cli/_error_format.py +83 -0
- dataface/cli/_extras.py +190 -0
- dataface/cli/_json_output.py +8 -0
- dataface/cli/_parsing.py +17 -0
- dataface/cli/_version_info.py +56 -0
- dataface/cli/commands/__init__.py +3 -0
- dataface/cli/commands/_agent_input.py +205 -0
- dataface/cli/commands/_agent_server.py +115 -0
- dataface/cli/commands/chat.py +645 -0
- dataface/cli/commands/describe.py +107 -0
- dataface/cli/commands/docs.py +131 -0
- dataface/cli/commands/extension.py +179 -0
- dataface/cli/commands/init.py +240 -0
- dataface/cli/commands/inspect.py +94 -0
- dataface/cli/commands/mcp_init.py +167 -0
- dataface/cli/commands/query.py +386 -0
- dataface/cli/commands/render.py +291 -0
- dataface/cli/commands/schema.py +411 -0
- dataface/cli/commands/search.py +49 -0
- dataface/cli/commands/serve.py +114 -0
- dataface/cli/commands/skills.py +133 -0
- dataface/cli/commands/skills_init.py +161 -0
- dataface/cli/commands/validate.py +63 -0
- dataface/cli/main.py +1501 -0
- dataface/core/__init__.py +75 -0
- dataface/core/compile/__init__.py +244 -0
- dataface/core/compile/_jinja_helpers.py +78 -0
- dataface/core/compile/channel.py +222 -0
- dataface/core/compile/chart_focus.py +101 -0
- dataface/core/compile/chart_resolved.py +169 -0
- dataface/core/compile/chart_type_detection.py +489 -0
- dataface/core/compile/chart_update.py +261 -0
- dataface/core/compile/colors.py +64 -0
- dataface/core/compile/compiler.py +904 -0
- dataface/core/compile/config.py +823 -0
- dataface/core/compile/custom_chart_types.py +208 -0
- dataface/core/compile/data_table_attachment.py +1287 -0
- dataface/core/compile/detect.py +110 -0
- dataface/core/compile/errors.py +302 -0
- dataface/core/compile/filter_injection.py +319 -0
- dataface/core/compile/introspection.py +527 -0
- dataface/core/compile/jinja.py +511 -0
- dataface/core/compile/labels_env.py +52 -0
- dataface/core/compile/markdown.py +154 -0
- dataface/core/compile/meta.py +388 -0
- dataface/core/compile/models/__init__.py +0 -0
- dataface/core/compile/models/chart/__init__.py +0 -0
- dataface/core/compile/models/chart/authored.py +2137 -0
- dataface/core/compile/models/chart/compiled.py +398 -0
- dataface/core/compile/models/config.py +347 -0
- dataface/core/compile/models/face/__init__.py +0 -0
- dataface/core/compile/models/face/authored.py +659 -0
- dataface/core/compile/models/face/compiled.py +522 -0
- dataface/core/compile/models/factories.py +201 -0
- dataface/core/compile/models/markers.py +40 -0
- dataface/core/compile/models/palette.py +36 -0
- dataface/core/compile/models/primitives.py +415 -0
- dataface/core/compile/models/query/__init__.py +0 -0
- dataface/core/compile/models/query/authored.py +246 -0
- dataface/core/compile/models/query/compiled.py +710 -0
- dataface/core/compile/models/refs.py +137 -0
- dataface/core/compile/models/source.py +611 -0
- dataface/core/compile/models/style/__init__.py +0 -0
- dataface/core/compile/models/style/authored.py +481 -0
- dataface/core/compile/models/style/compiled.py +3399 -0
- dataface/core/compile/models/style/merged.py +1682 -0
- dataface/core/compile/models/theme.py +362 -0
- dataface/core/compile/models/variable/__init__.py +0 -0
- dataface/core/compile/models/variable/authored.py +254 -0
- dataface/core/compile/models/vega_lite/__init__.py +0 -0
- dataface/core/compile/models/vega_lite/config.py +510 -0
- dataface/core/compile/models/vega_lite/contracts.py +171 -0
- dataface/core/compile/normalize_charts.py +494 -0
- dataface/core/compile/normalize_layout.py +1000 -0
- dataface/core/compile/normalize_queries.py +297 -0
- dataface/core/compile/normalize_variables.py +489 -0
- dataface/core/compile/normalizer.py +543 -0
- dataface/core/compile/palette.py +1100 -0
- dataface/core/compile/parameterized.py +658 -0
- dataface/core/compile/parser.py +228 -0
- dataface/core/compile/schema.py +20 -0
- dataface/core/compile/schema_renderers/__init__.py +0 -0
- dataface/core/compile/schema_renderers/json_schema.py +163 -0
- dataface/core/compile/schema_renderers/prompt.py +152 -0
- dataface/core/compile/schema_renderers/vscode_schema.py +301 -0
- dataface/core/compile/sizing.py +2126 -0
- dataface/core/compile/sources.py +518 -0
- dataface/core/compile/sql_authoring_lint.py +56 -0
- dataface/core/compile/style_cascade.py +471 -0
- dataface/core/compile/typography.py +299 -0
- dataface/core/compile/validator.py +301 -0
- dataface/core/compile/variables.py +53 -0
- dataface/core/compile/vega_config.py +98 -0
- dataface/core/compile/vega_lite/__init__.py +6 -0
- dataface/core/compile/vega_lite/validation.py +95 -0
- dataface/core/compile/yaml_error_formatter.py +838 -0
- dataface/core/connections.py +38 -0
- dataface/core/dashboard.py +358 -0
- dataface/core/defaults/default_config.yml +101 -0
- dataface/core/defaults/palettes/categorical/category-10-dark.yml +32 -0
- dataface/core/defaults/palettes/categorical/category-10-light.yml +43 -0
- dataface/core/defaults/palettes/categorical/category-10.yml +31 -0
- dataface/core/defaults/palettes/categorical/category-6-tonal-blue.yml +22 -0
- dataface/core/defaults/palettes/categorical/category-6-tonal-brown.yml +29 -0
- dataface/core/defaults/palettes/categorical/category-6-tonal-green.yml +20 -0
- dataface/core/defaults/palettes/categorical/category-6-tonal-orange.yml +21 -0
- dataface/core/defaults/palettes/categorical/category-6-tonal-purple.yml +20 -0
- dataface/core/defaults/palettes/categorical/editorial-10-dark.yml +32 -0
- dataface/core/defaults/palettes/categorical/editorial-10.yml +40 -0
- dataface/core/defaults/palettes/categorical/hero-6.yml +17 -0
- dataface/core/defaults/palettes/categorical/single-blue.yml +11 -0
- dataface/core/defaults/palettes/categorical/tableau.yml +20 -0
- dataface/core/defaults/palettes/data/xkcd_colors.json +3803 -0
- dataface/core/defaults/palettes/diverging/blue-red.yml +25 -0
- dataface/core/defaults/palettes/diverging/coolwarm.yml +24 -0
- dataface/core/defaults/palettes/diverging/crimson-green.yml +23 -0
- dataface/core/defaults/palettes/diverging/orange-teal.yml +23 -0
- dataface/core/defaults/palettes/diverging/sunset.yml +24 -0
- dataface/core/defaults/palettes/scaffold/dft-creams.yml +38 -0
- dataface/core/defaults/palettes/scaffold/dft-grays.yml +53 -0
- dataface/core/defaults/palettes/sequential/amber.yml +22 -0
- dataface/core/defaults/palettes/sequential/blue.yml +22 -0
- dataface/core/defaults/palettes/sequential/brown.yml +22 -0
- dataface/core/defaults/palettes/sequential/gray.yml +22 -0
- dataface/core/defaults/palettes/sequential/green.yml +22 -0
- dataface/core/defaults/palettes/sequential/purple.yml +22 -0
- dataface/core/defaults/palettes/sequential/rust.yml +22 -0
- dataface/core/defaults/palettes/sequential/teal.yml +22 -0
- dataface/core/defaults/palettes/tone/negative.yml +32 -0
- dataface/core/defaults/palettes/tone/positive.yml +22 -0
- dataface/core/defaults/palettes/tone/warning.yml +22 -0
- dataface/core/defaults/themes/_base.yaml +786 -0
- dataface/core/defaults/themes/bi.yaml +16 -0
- dataface/core/defaults/themes/carbong100.yaml +41 -0
- dataface/core/defaults/themes/cream.yaml +122 -0
- dataface/core/defaults/themes/dark.yaml +40 -0
- dataface/core/defaults/themes/diagnostics-title-angle-extreme.yaml +9 -0
- dataface/core/defaults/themes/diagnostics-title-baseline-extreme.yaml +9 -0
- dataface/core/defaults/themes/diagnostics-title-baseline.yaml +24 -0
- dataface/core/defaults/themes/diagnostics-title-center.yaml +8 -0
- dataface/core/defaults/themes/diagnostics-title-color-extreme.yaml +24 -0
- dataface/core/defaults/themes/diagnostics-title-font-extreme.yaml +25 -0
- dataface/core/defaults/themes/diagnostics-title-left.yaml +8 -0
- dataface/core/defaults/themes/diagnostics-title-offset-extreme.yaml +9 -0
- dataface/core/defaults/themes/diagnostics-title-size-extreme.yaml +24 -0
- dataface/core/defaults/themes/diagnostics-title-weight-extreme.yaml +24 -0
- dataface/core/defaults/themes/editorial.yaml +147 -0
- dataface/core/defaults/themes/light.yaml +30 -0
- dataface/core/defaults/themes/looker.yaml +17 -0
- dataface/core/defaults/themes/stark.yaml +134 -0
- dataface/core/errors/__init__.py +67 -0
- dataface/core/errors/codes_compile.py +56 -0
- dataface/core/errors/codes_execute.py +177 -0
- dataface/core/errors/codes_render.py +106 -0
- dataface/core/errors/codes_unknown.py +15 -0
- dataface/core/errors/hints.py +74 -0
- dataface/core/errors/registry.py +42 -0
- dataface/core/errors/structured.py +92 -0
- dataface/core/execute/__init__.py +91 -0
- dataface/core/execute/adapters/__init__.py +49 -0
- dataface/core/execute/adapters/adapter_registry.py +400 -0
- dataface/core/execute/adapters/base.py +245 -0
- dataface/core/execute/adapters/csv_adapter.py +239 -0
- dataface/core/execute/adapters/dbt_adapter.py +283 -0
- dataface/core/execute/adapters/dbt_adapter_factory.py +212 -0
- dataface/core/execute/adapters/dbt_macro_loader.py +95 -0
- dataface/core/execute/adapters/dbt_utils.py +150 -0
- dataface/core/execute/adapters/http_adapter.py +224 -0
- dataface/core/execute/adapters/metricflow_adapter.py +94 -0
- dataface/core/execute/adapters/schema_resolver_adapter.py +144 -0
- dataface/core/execute/adapters/sql_adapter.py +710 -0
- dataface/core/execute/adapters/values_adapter.py +58 -0
- dataface/core/execute/batch.py +744 -0
- dataface/core/execute/cache_backend.py +135 -0
- dataface/core/execute/cache_keys.py +66 -0
- dataface/core/execute/dbt_jinja.py +21 -0
- dataface/core/execute/dialects/__init__.py +121 -0
- dataface/core/execute/dialects/athena.py +75 -0
- dataface/core/execute/dialects/base.py +302 -0
- dataface/core/execute/dialects/bigquery.py +38 -0
- dataface/core/execute/dialects/databricks.py +68 -0
- dataface/core/execute/dialects/duckdb.py +35 -0
- dataface/core/execute/dialects/mysql.py +68 -0
- dataface/core/execute/dialects/postgres.py +39 -0
- dataface/core/execute/dialects/redshift.py +12 -0
- dataface/core/execute/dialects/snowflake.py +51 -0
- dataface/core/execute/dialects/sqlserver.py +92 -0
- dataface/core/execute/duckdb_cache.py +712 -0
- dataface/core/execute/duckdb_config.py +26 -0
- dataface/core/execute/errors.py +213 -0
- dataface/core/execute/executor.py +1249 -0
- dataface/core/execute/parallel.py +162 -0
- dataface/core/execute/setup_sql.py +58 -0
- dataface/core/execute/source_registry.py +72 -0
- dataface/core/execute/source_resolver.py +255 -0
- dataface/core/execute/sql_guard.py +387 -0
- dataface/core/execute/sql_literals.py +199 -0
- dataface/core/fonts.py +52 -0
- dataface/core/inspect/__init__.py +32 -0
- dataface/core/inspect/cache_factory.py +98 -0
- dataface/core/inspect/db_types.py +162 -0
- dataface/core/inspect/dbt_schema.py +96 -0
- dataface/core/inspect/defaults.yml +37 -0
- dataface/core/inspect/fanout_risk.py +109 -0
- dataface/core/inspect/manifest_utils.py +77 -0
- dataface/core/inspect/partials/categorical.yml +40 -0
- dataface/core/inspect/partials/date.yml +40 -0
- dataface/core/inspect/partials/numeric.yml +55 -0
- dataface/core/inspect/partition_types.py +38 -0
- dataface/core/inspect/query_validator.py +975 -0
- dataface/core/inspect/renderer.py +354 -0
- dataface/core/inspect/resolver.py +808 -0
- dataface/core/inspect/search.py +461 -0
- dataface/core/inspect/sources/__init__.py +32 -0
- dataface/core/inspect/sources/dbt.py +738 -0
- dataface/core/inspect/sources/duckdb_utils.py +66 -0
- dataface/core/inspect/templates/__init__.py +1 -0
- dataface/core/inspect/templates/categorical_column.yml +196 -0
- dataface/core/inspect/templates/charts.yml +109 -0
- dataface/core/inspect/templates/date_column.yml +248 -0
- dataface/core/inspect/templates/model.yml +138 -0
- dataface/core/inspect/templates/numeric_column.yml +261 -0
- dataface/core/inspect/templates/quality.yml +80 -0
- dataface/core/inspect/templates/string_column.yml +263 -0
- dataface/core/project_roots.py +165 -0
- dataface/core/render/__init__.py +87 -0
- dataface/core/render/board_links.py +176 -0
- dataface/core/render/chart/__init__.py +27 -0
- dataface/core/render/chart/arc_attached_table.py +251 -0
- dataface/core/render/chart/artifacts.py +16 -0
- dataface/core/render/chart/callout.py +225 -0
- dataface/core/render/chart/decisions.py +358 -0
- dataface/core/render/chart/geo.py +700 -0
- dataface/core/render/chart/kpi.py +916 -0
- dataface/core/render/chart/labels.py +76 -0
- dataface/core/render/chart/pipeline.py +818 -0
- dataface/core/render/chart/presentation.py +36 -0
- dataface/core/render/chart/profile.py +3438 -0
- dataface/core/render/chart/render_single.py +347 -0
- dataface/core/render/chart/renderers.py +193 -0
- dataface/core/render/chart/rendering.py +565 -0
- dataface/core/render/chart/serialization.py +90 -0
- dataface/core/render/chart/spark.py +496 -0
- dataface/core/render/chart/spark_bar.py +370 -0
- dataface/core/render/chart/spec_builders.py +154 -0
- dataface/core/render/chart/standard_renderer.py +2645 -0
- dataface/core/render/chart/table.py +2957 -0
- dataface/core/render/chart/table_support.py +1452 -0
- dataface/core/render/chart/tick_values.py +66 -0
- dataface/core/render/chart/time_unit_detect.py +809 -0
- dataface/core/render/chart/title_overflow.py +157 -0
- dataface/core/render/chart/type_inference.py +122 -0
- dataface/core/render/chart/validation.py +99 -0
- dataface/core/render/chart/vega_lite.py +125 -0
- dataface/core/render/chart/vega_lite_types.py +268 -0
- dataface/core/render/chart/vl_field_maps.py +346 -0
- dataface/core/render/chart_interactivity.py +24 -0
- dataface/core/render/control_registry.py +287 -0
- dataface/core/render/converters/__init__.py +24 -0
- dataface/core/render/converters/chart.py +276 -0
- dataface/core/render/converters/html.py +98 -0
- dataface/core/render/converters/pdf.py +40 -0
- dataface/core/render/converters/png.py +41 -0
- dataface/core/render/errors.py +144 -0
- dataface/core/render/face_api.py +160 -0
- dataface/core/render/faces.py +1194 -0
- dataface/core/render/font_measurement.py +48 -0
- dataface/core/render/font_support.py +197 -0
- dataface/core/render/fonts/DFTSansTabular-Regular.ttf +0 -0
- dataface/core/render/fonts/DFTSansTabular-Regular.woff2 +0 -0
- dataface/core/render/fonts/DFTSerifOldstyleProportional-Regular.ttf +0 -0
- dataface/core/render/fonts/DFTSerifOldstyleTabular-Regular.ttf +0 -0
- dataface/core/render/fonts/InterVariable.ttf +0 -0
- dataface/core/render/fonts/InterVariable.woff2 +0 -0
- dataface/core/render/fonts/NOTO_COLOR_EMOJI_LICENSE.txt +93 -0
- dataface/core/render/fonts/NOTO_EMOJI_LICENSE.txt +93 -0
- dataface/core/render/fonts/NotoColorEmoji-Regular.ttf +0 -0
- dataface/core/render/fonts/NotoColorEmoji-Regular.woff2 +0 -0
- dataface/core/render/fonts/NotoEmoji-Regular.ttf +0 -0
- dataface/core/render/fonts/NotoEmoji-Regular.woff2 +0 -0
- dataface/core/render/fonts/SOURCE_CODE_PRO_LICENSE.txt +93 -0
- dataface/core/render/fonts/SOURCE_SERIF_4_LICENSE.txt +98 -0
- dataface/core/render/fonts/SourceCodePro-Regular.ttf +0 -0
- dataface/core/render/fonts/SourceSerif4-Regular.ttf +0 -0
- dataface/core/render/fonts/_emoji_font_face.css +43 -0
- dataface/core/render/fonts/source-serif-4-variable-latin.woff2 +0 -0
- dataface/core/render/format_utils.py +329 -0
- dataface/core/render/geo_defaults.yml +28 -0
- dataface/core/render/json_format.py +146 -0
- dataface/core/render/layout_sizing.py +865 -0
- dataface/core/render/layouts.py +541 -0
- dataface/core/render/markdown_defaults.yml +16 -0
- dataface/core/render/missing_vars_prompt.py +79 -0
- dataface/core/render/placeholder.py +389 -0
- dataface/core/render/render_result.py +14 -0
- dataface/core/render/renderer.py +467 -0
- dataface/core/render/script_embedding.py +16 -0
- dataface/core/render/svg_utils.py +212 -0
- dataface/core/render/template_loader.py +69 -0
- dataface/core/render/templates/controls/_styles.css +606 -0
- dataface/core/render/templates/controls/checkbox.html +16 -0
- dataface/core/render/templates/controls/date.html +16 -0
- dataface/core/render/templates/controls/number.html +19 -0
- dataface/core/render/templates/controls/readonly.html +9 -0
- dataface/core/render/templates/controls/select.html +21 -0
- dataface/core/render/templates/controls/slider.html +22 -0
- dataface/core/render/templates/controls/text.html +16 -0
- dataface/core/render/templates/scripts/chart_interactivity.js +191 -0
- dataface/core/render/templates/scripts/variables.js +976 -0
- dataface/core/render/templates/svg/grid_pattern.svg +3 -0
- dataface/core/render/templates/svg/styles.css +51 -0
- dataface/core/render/terminal.py +311 -0
- dataface/core/render/terminal_charts.py +563 -0
- dataface/core/render/terminal_defaults.yml +2 -0
- dataface/core/render/terminal_layouts.py +299 -0
- dataface/core/render/terminal_text.py +31 -0
- dataface/core/render/text/__init__.py +1 -0
- dataface/core/render/text/case.py +113 -0
- dataface/core/render/text_format.py +129 -0
- dataface/core/render/utils.py +106 -0
- dataface/core/render/variable_controls.py +946 -0
- dataface/core/render/variable_input_refinement.py +140 -0
- dataface/core/render/warnings/__init__.py +15 -0
- dataface/core/render/warnings/bar_color_1_to_1_with_x.py +80 -0
- dataface/core/render/warnings/base.py +44 -0
- dataface/core/render/warnings/fanout_risk.py +15 -0
- dataface/core/render/warnings/from_query_diagnostic.py +56 -0
- dataface/core/render/warnings/missing_join_predicate.py +13 -0
- dataface/core/render/warnings/query_parse_error.py +14 -0
- dataface/core/render/warnings/query_returned_zero_rows.py +42 -0
- dataface/core/render/warnings/reaggregation.py +14 -0
- dataface/core/render/warnings/registry.py +45 -0
- dataface/core/render/warnings/suppression.py +46 -0
- dataface/core/render/warnings/temporal_single_point.py +63 -0
- dataface/core/render/warnings/unreferenced_chart.py +15 -0
- dataface/core/render/warnings/y_encoding_mostly_null.py +76 -0
- dataface/core/render/yaml_format.py +167 -0
- dataface/core/resolve_face.py +195 -0
- dataface/core/schema/__init__.py +0 -0
- dataface/core/schema/guidance.py +151 -0
- dataface/core/scoped_paths.py +59 -0
- dataface/core/serve/__init__.py +14 -0
- dataface/core/serve/bootstrap.py +39 -0
- dataface/core/serve/embedded.py +57 -0
- dataface/core/serve/port.py +129 -0
- dataface/core/serve/server.py +938 -0
- dataface/core/serve/templates/__init__.py +0 -0
- dataface/core/serve/templates/directory.yml +6 -0
- dataface/core/serve/templates/error.html.j2 +217 -0
- dataface/core/utils.py +121 -0
- dataface/core/validate.py +64 -0
- dataface/integrations/__init__.py +0 -0
- dataface/integrations/highlighting.py +351 -0
- dataface/integrations/markdown.py +537 -0
- dataface/py.typed +0 -0
- dataface-0.1.2.dist-info/METADATA +375 -0
- dataface-0.1.2.dist-info/RECORD +455 -0
- dataface-0.1.2.dist-info/WHEEL +4 -0
- dataface-0.1.2.dist-info/entry_points.txt +2 -0
- dataface-0.1.2.dist-info/licenses/LICENSE +202 -0
- mdsvg/__init__.py +168 -0
- mdsvg/fonts.py +656 -0
- mdsvg/images.py +299 -0
- mdsvg/parser.py +629 -0
- mdsvg/playground.py +284 -0
- mdsvg/py.typed +2 -0
- mdsvg/renderer.py +1623 -0
- mdsvg/style.py +355 -0
- mdsvg/types.py +200 -0
- 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"
|