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,744 @@
|
|
|
1
|
+
"""Batch query execution with temp table optimization.
|
|
2
|
+
|
|
3
|
+
Stage: EXECUTE
|
|
4
|
+
Purpose: Generate batch SQL with temp tables for queries that are referenced
|
|
5
|
+
by multiple other queries via {{ queries.X }} syntax.
|
|
6
|
+
|
|
7
|
+
Entry Points:
|
|
8
|
+
- extract_query_refs(sql: str) -> Set[str]
|
|
9
|
+
- build_dependency_graph(queries: Dict[str, Query]) -> DependencyGraph
|
|
10
|
+
- generate_batch_sql(queries, graph, dialect) -> List[BatchStatement]
|
|
11
|
+
- collect_face_queries(face) -> Dict[str, AnyQuery]
|
|
12
|
+
- group_by_source(queries) -> Dict[str, Dict[str, AnyQuery]]
|
|
13
|
+
|
|
14
|
+
When multiple queries reference the same base query:
|
|
15
|
+
- The base query is executed ONCE and stored in a temp table
|
|
16
|
+
- Dependent queries SELECT from the temp table
|
|
17
|
+
|
|
18
|
+
Example:
|
|
19
|
+
queries:
|
|
20
|
+
orders:
|
|
21
|
+
sql: SELECT * FROM orders WHERE status = 'completed'
|
|
22
|
+
high_value:
|
|
23
|
+
sql: SELECT * FROM {{ queries.orders }} WHERE amount > 1000
|
|
24
|
+
recent:
|
|
25
|
+
sql: SELECT * FROM {{ queries.orders }} WHERE date >= CURRENT_DATE - 7
|
|
26
|
+
|
|
27
|
+
Generated batch:
|
|
28
|
+
1. CREATE TEMP TABLE _df_temp_orders AS SELECT * FROM orders WHERE status = 'completed'
|
|
29
|
+
2. SELECT * FROM _df_temp_orders WHERE amount > 1000 (for high_value)
|
|
30
|
+
3. SELECT * FROM _df_temp_orders WHERE date >= CURRENT_DATE - 7 (for recent)
|
|
31
|
+
|
|
32
|
+
Dependencies:
|
|
33
|
+
- dataface.execute.dialects (for temp table syntax)
|
|
34
|
+
- dataface.core.compile.models.query.compiled (AnyQuery, is_sql_query)
|
|
35
|
+
|
|
36
|
+
Errors:
|
|
37
|
+
- CrossSourceReferenceError: When {{ queries.X }} references a query
|
|
38
|
+
on a different database source
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
import re
|
|
42
|
+
from dataclasses import dataclass, field
|
|
43
|
+
from typing import Any
|
|
44
|
+
|
|
45
|
+
from dataface.core.compile.models.face.compiled import Face
|
|
46
|
+
from dataface.core.compile.models.query.compiled import (
|
|
47
|
+
AnyQuery,
|
|
48
|
+
is_sql_query,
|
|
49
|
+
)
|
|
50
|
+
from dataface.core.execute.dialects import SQLDialect, get_dialect
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class CrossSourceReferenceError(Exception):
|
|
54
|
+
"""Error raised when a query references another query on a different source.
|
|
55
|
+
|
|
56
|
+
Cross-database references are not supported because temp tables are
|
|
57
|
+
connection-local and cannot be shared between different database connections.
|
|
58
|
+
|
|
59
|
+
Example:
|
|
60
|
+
queries:
|
|
61
|
+
orders:
|
|
62
|
+
source: postgres
|
|
63
|
+
analysis:
|
|
64
|
+
sql: SELECT * FROM {{ queries.orders }}
|
|
65
|
+
source: mysql # ERROR: Can't reference postgres query from mysql
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
source_query: str,
|
|
71
|
+
query_source: str,
|
|
72
|
+
target_query: str,
|
|
73
|
+
target_source: str,
|
|
74
|
+
):
|
|
75
|
+
"""Initialize CrossSourceReferenceError.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
source_query: Name of the query making the reference
|
|
79
|
+
query_source: Source of the query making the reference
|
|
80
|
+
target_query: Name of the referenced query
|
|
81
|
+
target_source: Source of the referenced query
|
|
82
|
+
"""
|
|
83
|
+
self.source_query = source_query
|
|
84
|
+
self.query_source = query_source
|
|
85
|
+
self.target_query = target_query
|
|
86
|
+
self.target_source = target_source
|
|
87
|
+
super().__init__(
|
|
88
|
+
f"Query '{source_query}' (source: {query_source}) cannot reference "
|
|
89
|
+
f"query '{target_query}' (source: {target_source}). "
|
|
90
|
+
f"Cross-database references are not supported."
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class DependencyGraph:
|
|
96
|
+
"""Directed graph representing query dependencies.
|
|
97
|
+
|
|
98
|
+
Nodes are query names. Edges point from a query to queries that depend on it.
|
|
99
|
+
For example, if query B references {{ queries.A }}, there's an edge A -> B.
|
|
100
|
+
|
|
101
|
+
Attributes:
|
|
102
|
+
edges: Dict mapping query name to set of queries that depend on it
|
|
103
|
+
reverse_edges: Dict mapping query name to set of queries it depends on
|
|
104
|
+
all_nodes: Set of all query names in the graph
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
edges: dict[str, set[str]] = field(default_factory=dict)
|
|
108
|
+
reverse_edges: dict[str, set[str]] = field(default_factory=dict)
|
|
109
|
+
all_nodes: set[str] = field(default_factory=set)
|
|
110
|
+
|
|
111
|
+
def add_node(self, name: str) -> None:
|
|
112
|
+
"""Add a node to the graph.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
name: Query name to add
|
|
116
|
+
"""
|
|
117
|
+
self.all_nodes.add(name)
|
|
118
|
+
if name not in self.edges:
|
|
119
|
+
self.edges[name] = set()
|
|
120
|
+
if name not in self.reverse_edges:
|
|
121
|
+
self.reverse_edges[name] = set()
|
|
122
|
+
|
|
123
|
+
def add_edge(self, from_node: str, to_node: str) -> None:
|
|
124
|
+
"""Add a directed edge from from_node to to_node.
|
|
125
|
+
|
|
126
|
+
Indicates that to_node depends on from_node.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
from_node: The referenced query (dependency)
|
|
130
|
+
to_node: The query that references from_node (dependent)
|
|
131
|
+
"""
|
|
132
|
+
self.add_node(from_node)
|
|
133
|
+
self.add_node(to_node)
|
|
134
|
+
self.edges[from_node].add(to_node)
|
|
135
|
+
self.reverse_edges[to_node].add(from_node)
|
|
136
|
+
|
|
137
|
+
def get_dependents(self, node: str) -> set[str]:
|
|
138
|
+
"""Get all queries that depend on this node.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
node: Query name
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Set of query names that reference this query
|
|
145
|
+
"""
|
|
146
|
+
return self.edges.get(node, set())
|
|
147
|
+
|
|
148
|
+
def get_dependencies(self, node: str) -> set[str]:
|
|
149
|
+
"""Get all queries this node depends on.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
node: Query name
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Set of query names this query references
|
|
156
|
+
"""
|
|
157
|
+
return self.reverse_edges.get(node, set())
|
|
158
|
+
|
|
159
|
+
def out_degree(self, node: str) -> int:
|
|
160
|
+
"""Get the number of queries that depend on this node.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
node: Query name
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Number of dependent queries (out-edges)
|
|
167
|
+
"""
|
|
168
|
+
return len(self.get_dependents(node))
|
|
169
|
+
|
|
170
|
+
def get_temp_table_candidates(self) -> set[str]:
|
|
171
|
+
"""Get queries that should be materialized as temp tables.
|
|
172
|
+
|
|
173
|
+
A query is a temp table candidate if it is referenced by at least
|
|
174
|
+
one other query (out_degree > 0).
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Set of query names that should become temp tables
|
|
178
|
+
"""
|
|
179
|
+
return {node for node in self.all_nodes if self.out_degree(node) > 0}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@dataclass
|
|
183
|
+
class BatchStatement:
|
|
184
|
+
"""A single statement in a batch execution plan.
|
|
185
|
+
|
|
186
|
+
Attributes:
|
|
187
|
+
name: Identifier for this statement (query name or _create_X for temp tables)
|
|
188
|
+
sql: The SQL to execute
|
|
189
|
+
is_temp_table_create: True if this creates a temp table
|
|
190
|
+
query_name: The logical query name this statement is for (may differ from name)
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
name: str
|
|
194
|
+
sql: str
|
|
195
|
+
is_temp_table_create: bool = False
|
|
196
|
+
query_name: str | None = None
|
|
197
|
+
|
|
198
|
+
def __post_init__(self) -> None:
|
|
199
|
+
"""Set query_name to name if not provided."""
|
|
200
|
+
if self.query_name is None:
|
|
201
|
+
self.query_name = self.name
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# Pattern to match {{ queries.X }} references
|
|
205
|
+
# Handles variations like {{queries.X}}, {{ queries.X }}, {{ queries.X }}
|
|
206
|
+
_QUERY_REF_PATTERN = re.compile(r"\{\{\s*queries\.(\w+)\s*\}\}")
|
|
207
|
+
|
|
208
|
+
# Prefix for temp table names created by batch execution
|
|
209
|
+
# This prefix helps identify dataface-created temp tables and avoids conflicts
|
|
210
|
+
TEMP_TABLE_PREFIX = "_df_temp_"
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def extract_query_refs(sql: str) -> set[str]:
|
|
214
|
+
"""Extract query references from SQL.
|
|
215
|
+
|
|
216
|
+
Finds all {{ queries.X }} patterns in the SQL string and returns
|
|
217
|
+
the set of referenced query names.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
sql: SQL string potentially containing {{ queries.X }} references
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
Set of query names referenced in the SQL
|
|
224
|
+
|
|
225
|
+
Example:
|
|
226
|
+
>>> extract_query_refs("SELECT * FROM {{ queries.orders }} WHERE amount > 100")
|
|
227
|
+
{'orders'}
|
|
228
|
+
>>> extract_query_refs("SELECT a.*, b.* FROM {{ queries.a }} a JOIN {{ queries.b }} b")
|
|
229
|
+
{'a', 'b'}
|
|
230
|
+
"""
|
|
231
|
+
if not sql or "queries." not in sql:
|
|
232
|
+
return set()
|
|
233
|
+
|
|
234
|
+
return set(_QUERY_REF_PATTERN.findall(sql))
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def build_dependency_graph(
|
|
238
|
+
queries: dict[str, AnyQuery],
|
|
239
|
+
) -> DependencyGraph:
|
|
240
|
+
"""Build a dependency graph from a set of queries.
|
|
241
|
+
|
|
242
|
+
Scans each query's SQL for {{ queries.X }} references and builds
|
|
243
|
+
a directed graph where edges point from referenced queries to
|
|
244
|
+
the queries that reference them.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
queries: Dict mapping query names to AnyQuery objects
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
DependencyGraph with nodes and edges
|
|
251
|
+
|
|
252
|
+
Example:
|
|
253
|
+
Given queries:
|
|
254
|
+
orders: SELECT * FROM orders
|
|
255
|
+
high_value: SELECT * FROM {{ queries.orders }} WHERE amount > 1000
|
|
256
|
+
|
|
257
|
+
Returns graph with:
|
|
258
|
+
nodes: {orders, high_value}
|
|
259
|
+
edges: {orders: {high_value}} (orders -> high_value)
|
|
260
|
+
"""
|
|
261
|
+
graph = DependencyGraph()
|
|
262
|
+
|
|
263
|
+
for name, query in queries.items():
|
|
264
|
+
graph.add_node(name)
|
|
265
|
+
|
|
266
|
+
# Only SQL queries can have query references
|
|
267
|
+
if not is_sql_query(query):
|
|
268
|
+
continue
|
|
269
|
+
|
|
270
|
+
sql = query.sql
|
|
271
|
+
refs = extract_query_refs(sql)
|
|
272
|
+
|
|
273
|
+
for ref in refs:
|
|
274
|
+
if ref in queries: # Only add edges for known queries
|
|
275
|
+
graph.add_edge(ref, name)
|
|
276
|
+
|
|
277
|
+
return graph
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _find_cycle(graph: DependencyGraph) -> list[str]:
|
|
281
|
+
"""Find a cycle in the dependency graph using DFS.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
graph: The dependency graph to search
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
List of query names forming the cycle, or empty list if no cycle
|
|
288
|
+
"""
|
|
289
|
+
visited: set[str] = set()
|
|
290
|
+
rec_stack: set[str] = set()
|
|
291
|
+
path: list[str] = []
|
|
292
|
+
|
|
293
|
+
def dfs(node: str) -> list[str] | None:
|
|
294
|
+
visited.add(node)
|
|
295
|
+
rec_stack.add(node)
|
|
296
|
+
path.append(node)
|
|
297
|
+
|
|
298
|
+
for dep in graph.get_dependencies(node):
|
|
299
|
+
if dep not in visited:
|
|
300
|
+
cycle = dfs(dep)
|
|
301
|
+
if cycle:
|
|
302
|
+
return cycle
|
|
303
|
+
elif dep in rec_stack:
|
|
304
|
+
# Found cycle - extract the cycle path
|
|
305
|
+
cycle_start = path.index(dep)
|
|
306
|
+
return path[cycle_start:] + [dep]
|
|
307
|
+
|
|
308
|
+
path.pop()
|
|
309
|
+
rec_stack.remove(node)
|
|
310
|
+
return None
|
|
311
|
+
|
|
312
|
+
for node in graph.all_nodes:
|
|
313
|
+
if node not in visited:
|
|
314
|
+
cycle = dfs(node)
|
|
315
|
+
if cycle:
|
|
316
|
+
return cycle
|
|
317
|
+
|
|
318
|
+
return []
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def topological_sort(graph: DependencyGraph) -> list[str]:
|
|
322
|
+
"""Topologically sort nodes in the dependency graph.
|
|
323
|
+
|
|
324
|
+
Returns nodes in an order where dependencies come before dependents.
|
|
325
|
+
Uses Kahn's algorithm for topological sorting.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
graph: The dependency graph to sort
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
List of query names in topological order (dependencies first)
|
|
332
|
+
|
|
333
|
+
Raises:
|
|
334
|
+
ValueError: If the graph contains a cycle (includes cycle path in message)
|
|
335
|
+
|
|
336
|
+
Example:
|
|
337
|
+
If orders -> high_value (high_value depends on orders),
|
|
338
|
+
returns ['orders', 'high_value']
|
|
339
|
+
"""
|
|
340
|
+
# Compute in-degree (number of dependencies) for each node
|
|
341
|
+
in_degree: dict[str, int] = dict.fromkeys(graph.all_nodes, 0)
|
|
342
|
+
for node in graph.all_nodes:
|
|
343
|
+
for dependent in graph.get_dependents(node):
|
|
344
|
+
in_degree[dependent] = in_degree.get(dependent, 0) + 1
|
|
345
|
+
|
|
346
|
+
# Start with nodes that have no dependencies
|
|
347
|
+
queue: list[str] = [node for node in graph.all_nodes if in_degree[node] == 0]
|
|
348
|
+
result: list[str] = []
|
|
349
|
+
|
|
350
|
+
while queue:
|
|
351
|
+
# Sort for deterministic ordering
|
|
352
|
+
queue.sort()
|
|
353
|
+
node = queue.pop(0)
|
|
354
|
+
result.append(node)
|
|
355
|
+
|
|
356
|
+
# Reduce in-degree for dependents
|
|
357
|
+
for dependent in sorted(graph.get_dependents(node)):
|
|
358
|
+
in_degree[dependent] -= 1
|
|
359
|
+
if in_degree[dependent] == 0:
|
|
360
|
+
queue.append(dependent)
|
|
361
|
+
|
|
362
|
+
if len(result) != len(graph.all_nodes):
|
|
363
|
+
# Find and report the actual cycle for better debugging
|
|
364
|
+
cycle = _find_cycle(graph)
|
|
365
|
+
if cycle:
|
|
366
|
+
cycle_str = " -> ".join(cycle)
|
|
367
|
+
raise ValueError(
|
|
368
|
+
f"Circular dependency detected in query references: {cycle_str}"
|
|
369
|
+
)
|
|
370
|
+
raise ValueError("Circular dependency detected in query references")
|
|
371
|
+
|
|
372
|
+
return result
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def collect_layout_chart_query_names(face: Face) -> set[str]:
|
|
376
|
+
"""Walk the layout tree and return query names directly used by charts.
|
|
377
|
+
|
|
378
|
+
Shared layout-walk utility used by both ``collect_face_queries``
|
|
379
|
+
(which expands transitive deps) and the parallel pre-execution path
|
|
380
|
+
in ``renderer.py`` (which does not, since ``{{ queries.X }}`` is
|
|
381
|
+
Jinja-inlined).
|
|
382
|
+
"""
|
|
383
|
+
names: set[str] = set()
|
|
384
|
+
|
|
385
|
+
def _walk(items: list[Any]) -> None:
|
|
386
|
+
for item in items:
|
|
387
|
+
if item.chart and item.chart.query_name:
|
|
388
|
+
names.add(item.chart.query_name)
|
|
389
|
+
if item.face and item.face.layout:
|
|
390
|
+
_walk(item.face.layout.items)
|
|
391
|
+
|
|
392
|
+
_walk(face.layout.items)
|
|
393
|
+
|
|
394
|
+
return names
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def collect_face_queries(
|
|
398
|
+
face: Face,
|
|
399
|
+
chart_queries_only: bool = True,
|
|
400
|
+
) -> dict[str, AnyQuery]:
|
|
401
|
+
"""Collect all queries needed for a dataface.
|
|
402
|
+
|
|
403
|
+
Collects queries directly used by charts, plus any queries they
|
|
404
|
+
reference via {{ queries.X }}.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
face: Face to collect queries from
|
|
408
|
+
chart_queries_only: If True, only include queries used by charts.
|
|
409
|
+
If False, include all queries defined in the face.
|
|
410
|
+
|
|
411
|
+
Returns:
|
|
412
|
+
Dict mapping query names to AnyQuery objects
|
|
413
|
+
|
|
414
|
+
Example:
|
|
415
|
+
Given face with:
|
|
416
|
+
queries: {orders, high_value, recent, unused}
|
|
417
|
+
charts: [chart using high_value, chart using recent]
|
|
418
|
+
|
|
419
|
+
Returns {orders, high_value, recent} (orders is a dependency)
|
|
420
|
+
"""
|
|
421
|
+
queries: dict[str, AnyQuery] = {}
|
|
422
|
+
|
|
423
|
+
if not chart_queries_only:
|
|
424
|
+
# Include all queries from the face
|
|
425
|
+
return dict(face.queries)
|
|
426
|
+
|
|
427
|
+
# Collect queries used by charts
|
|
428
|
+
chart_query_names = collect_layout_chart_query_names(face)
|
|
429
|
+
|
|
430
|
+
# Also check charts dict directly
|
|
431
|
+
for chart in face.charts.values():
|
|
432
|
+
if chart.query_name:
|
|
433
|
+
chart_query_names.add(chart.query_name)
|
|
434
|
+
|
|
435
|
+
# Collect queries and their dependencies
|
|
436
|
+
def add_query_and_deps(name: str) -> None:
|
|
437
|
+
"""Recursively add a query and its dependencies."""
|
|
438
|
+
if name in queries or name not in face.queries:
|
|
439
|
+
return
|
|
440
|
+
|
|
441
|
+
query = face.queries[name]
|
|
442
|
+
queries[name] = query
|
|
443
|
+
|
|
444
|
+
# Find dependencies
|
|
445
|
+
if is_sql_query(query):
|
|
446
|
+
refs = extract_query_refs(query.sql)
|
|
447
|
+
for ref in refs:
|
|
448
|
+
add_query_and_deps(ref)
|
|
449
|
+
|
|
450
|
+
for query_name in chart_query_names:
|
|
451
|
+
add_query_and_deps(query_name)
|
|
452
|
+
|
|
453
|
+
return queries
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def get_query_source(query: AnyQuery) -> str:
|
|
457
|
+
"""Get the database source for a query.
|
|
458
|
+
|
|
459
|
+
Args:
|
|
460
|
+
query: AnyQuery object
|
|
461
|
+
|
|
462
|
+
Returns:
|
|
463
|
+
Source string (defaults to 'default' if not specified)
|
|
464
|
+
"""
|
|
465
|
+
source = getattr(query, "source", None)
|
|
466
|
+
if isinstance(source, str):
|
|
467
|
+
return source
|
|
468
|
+
return "default"
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def group_by_source(
|
|
472
|
+
queries: dict[str, AnyQuery],
|
|
473
|
+
) -> dict[str, dict[str, AnyQuery]]:
|
|
474
|
+
"""Group queries by their database source.
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
queries: Dict mapping query names to queries
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
Dict mapping source to dict of queries with that source
|
|
481
|
+
|
|
482
|
+
Example:
|
|
483
|
+
Given queries with sources:
|
|
484
|
+
orders: postgres
|
|
485
|
+
users: postgres
|
|
486
|
+
events: mysql
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
{'postgres': {'orders': ..., 'users': ...}, 'mysql': {'events': ...}}
|
|
490
|
+
"""
|
|
491
|
+
groups: dict[str, dict[str, AnyQuery]] = {}
|
|
492
|
+
|
|
493
|
+
for name, query in queries.items():
|
|
494
|
+
source = get_query_source(query)
|
|
495
|
+
if source not in groups:
|
|
496
|
+
groups[source] = {}
|
|
497
|
+
groups[source][name] = query
|
|
498
|
+
|
|
499
|
+
return groups
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def validate_cross_source_references(
|
|
503
|
+
queries: dict[str, AnyQuery],
|
|
504
|
+
graph: DependencyGraph,
|
|
505
|
+
) -> None:
|
|
506
|
+
"""Validate that no queries reference queries on different sources.
|
|
507
|
+
|
|
508
|
+
Args:
|
|
509
|
+
queries: Dict of queries to validate
|
|
510
|
+
graph: Dependency graph for the queries
|
|
511
|
+
|
|
512
|
+
Raises:
|
|
513
|
+
CrossSourceReferenceError: If a cross-source reference is detected
|
|
514
|
+
"""
|
|
515
|
+
for name, query in queries.items():
|
|
516
|
+
if not is_sql_query(query):
|
|
517
|
+
continue
|
|
518
|
+
|
|
519
|
+
query_source = get_query_source(query)
|
|
520
|
+
deps = graph.get_dependencies(name)
|
|
521
|
+
|
|
522
|
+
for dep_name in deps:
|
|
523
|
+
if dep_name not in queries:
|
|
524
|
+
continue
|
|
525
|
+
|
|
526
|
+
dep_query = queries[dep_name]
|
|
527
|
+
dep_source = get_query_source(dep_query)
|
|
528
|
+
|
|
529
|
+
if query_source != dep_source:
|
|
530
|
+
raise CrossSourceReferenceError(
|
|
531
|
+
source_query=name,
|
|
532
|
+
query_source=query_source,
|
|
533
|
+
target_query=dep_name,
|
|
534
|
+
target_source=dep_source,
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
class TempTableNotSupportedError(Exception):
|
|
539
|
+
"""Error raised when the dialect doesn't support temporary tables.
|
|
540
|
+
|
|
541
|
+
Some databases (e.g., certain cloud analytics platforms) may not support
|
|
542
|
+
temp tables, in which case batch optimization cannot be used.
|
|
543
|
+
"""
|
|
544
|
+
|
|
545
|
+
def __init__(self, dialect_name: str):
|
|
546
|
+
self.dialect_name = dialect_name
|
|
547
|
+
super().__init__(
|
|
548
|
+
f"Dialect '{dialect_name}' does not support temporary tables. "
|
|
549
|
+
f"Batch execution with temp table optimization is not available."
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def generate_batch_sql(
|
|
554
|
+
queries: dict[str, AnyQuery],
|
|
555
|
+
graph: DependencyGraph,
|
|
556
|
+
dialect: SQLDialect,
|
|
557
|
+
direct_chart_queries: set[str] | None = None,
|
|
558
|
+
) -> list[BatchStatement]:
|
|
559
|
+
"""Generate batch SQL statements with temp table optimization.
|
|
560
|
+
|
|
561
|
+
For queries that are referenced by other queries:
|
|
562
|
+
1. Create a temp table with the base query
|
|
563
|
+
2. Rewrite dependent queries to SELECT from the temp table
|
|
564
|
+
|
|
565
|
+
Args:
|
|
566
|
+
queries: Dict of queries to include in the batch
|
|
567
|
+
graph: Dependency graph for the queries
|
|
568
|
+
dialect: SQL dialect for temp table syntax
|
|
569
|
+
direct_chart_queries: Set of query names directly used by charts.
|
|
570
|
+
If a temp table query is also used directly, we add a SELECT *
|
|
571
|
+
statement for it.
|
|
572
|
+
|
|
573
|
+
Returns:
|
|
574
|
+
List of BatchStatement objects in execution order
|
|
575
|
+
|
|
576
|
+
Raises:
|
|
577
|
+
TempTableNotSupportedError: If dialect doesn't support temp tables
|
|
578
|
+
|
|
579
|
+
Example:
|
|
580
|
+
Given:
|
|
581
|
+
orders: SELECT * FROM orders
|
|
582
|
+
high_value: SELECT * FROM {{ queries.orders }} WHERE amount > 1000
|
|
583
|
+
|
|
584
|
+
Returns:
|
|
585
|
+
[
|
|
586
|
+
BatchStatement("_create_orders", "CREATE TEMP TABLE _df_temp_orders AS SELECT * FROM orders", True),
|
|
587
|
+
BatchStatement("orders", "SELECT * FROM _df_temp_orders"), # If orders is directly used
|
|
588
|
+
BatchStatement("high_value", "SELECT * FROM _df_temp_orders WHERE amount > 1000"),
|
|
589
|
+
]
|
|
590
|
+
"""
|
|
591
|
+
if direct_chart_queries is None:
|
|
592
|
+
direct_chart_queries = set()
|
|
593
|
+
|
|
594
|
+
statements: list[BatchStatement] = []
|
|
595
|
+
temp_candidates = graph.get_temp_table_candidates()
|
|
596
|
+
sorted_queries = topological_sort(graph)
|
|
597
|
+
|
|
598
|
+
# Track which temp tables have been created
|
|
599
|
+
created_temp_tables: set[str] = set()
|
|
600
|
+
|
|
601
|
+
# Check if dialect supports temp tables (only needed if we have candidates)
|
|
602
|
+
if temp_candidates and not dialect.supports_temp_tables:
|
|
603
|
+
raise TempTableNotSupportedError(dialect.name)
|
|
604
|
+
|
|
605
|
+
for query_name in sorted_queries:
|
|
606
|
+
if query_name not in queries:
|
|
607
|
+
continue
|
|
608
|
+
|
|
609
|
+
query = queries[query_name]
|
|
610
|
+
|
|
611
|
+
# Only SQL queries can be batched with temp tables
|
|
612
|
+
if not is_sql_query(query):
|
|
613
|
+
# For non-SQL queries, just add as-is (they'll be executed separately)
|
|
614
|
+
continue
|
|
615
|
+
|
|
616
|
+
sql = query.sql
|
|
617
|
+
setup = query.setup_sql.strip() + "\n" if query.setup_sql else ""
|
|
618
|
+
|
|
619
|
+
# Rewrite {{ queries.X }} refs to temp table names
|
|
620
|
+
# Note: We use a precompiled pattern (_QUERY_REF_PATTERN) for extraction,
|
|
621
|
+
# but need a dynamic pattern here to match specific query names
|
|
622
|
+
for ref in extract_query_refs(sql):
|
|
623
|
+
if ref in created_temp_tables:
|
|
624
|
+
temp_ref = dialect.temp_table_ref(f"{TEMP_TABLE_PREFIX}{ref}")
|
|
625
|
+
# Replace the Jinja template with the temp table reference
|
|
626
|
+
# Use re.escape() to handle any regex metacharacters in query names
|
|
627
|
+
escaped_ref = re.escape(ref)
|
|
628
|
+
pattern = rf"\{{\{{\s*queries\.{escaped_ref}\s*\}}\}}"
|
|
629
|
+
sql = re.sub(pattern, temp_ref, sql)
|
|
630
|
+
|
|
631
|
+
if query_name in temp_candidates:
|
|
632
|
+
# This query is referenced by other queries, so we create a temp table.
|
|
633
|
+
# This ensures the query runs only ONCE, and dependents read from the
|
|
634
|
+
# temp table instead of re-executing the query.
|
|
635
|
+
temp_name = f"{TEMP_TABLE_PREFIX}{query_name}"
|
|
636
|
+
temp_sql = setup + dialect.temp_table(temp_name, sql)
|
|
637
|
+
statements.append(
|
|
638
|
+
BatchStatement(
|
|
639
|
+
name=f"_create_{query_name}",
|
|
640
|
+
sql=temp_sql,
|
|
641
|
+
is_temp_table_create=True,
|
|
642
|
+
query_name=query_name,
|
|
643
|
+
)
|
|
644
|
+
)
|
|
645
|
+
created_temp_tables.add(query_name)
|
|
646
|
+
|
|
647
|
+
# Special case: Query is BOTH a dependency AND directly used by a chart.
|
|
648
|
+
# Example: "orders" is referenced by "high_value" AND has its own chart.
|
|
649
|
+
# We need TWO statements:
|
|
650
|
+
# 1. CREATE TEMP TABLE (above) - for dependents to reference
|
|
651
|
+
# 2. SELECT * FROM temp table (below) - to get results for the chart
|
|
652
|
+
if query_name in direct_chart_queries:
|
|
653
|
+
temp_ref = dialect.temp_table_ref(temp_name)
|
|
654
|
+
statements.append(
|
|
655
|
+
BatchStatement(
|
|
656
|
+
name=query_name,
|
|
657
|
+
sql=f"SELECT * FROM {temp_ref}",
|
|
658
|
+
is_temp_table_create=False,
|
|
659
|
+
query_name=query_name,
|
|
660
|
+
)
|
|
661
|
+
)
|
|
662
|
+
else:
|
|
663
|
+
# Regular query (not a temp table candidate)
|
|
664
|
+
statements.append(
|
|
665
|
+
BatchStatement(
|
|
666
|
+
name=query_name,
|
|
667
|
+
sql=setup + sql,
|
|
668
|
+
is_temp_table_create=False,
|
|
669
|
+
query_name=query_name,
|
|
670
|
+
)
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
return statements
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def create_batch_execution_plan(
|
|
677
|
+
face: Face,
|
|
678
|
+
) -> dict[str, list[BatchStatement]]:
|
|
679
|
+
"""Create a complete batch execution plan for a dataface.
|
|
680
|
+
|
|
681
|
+
This is the main entry point for batch execution planning.
|
|
682
|
+
It collects queries, groups by source, validates cross-source
|
|
683
|
+
references, and generates batch SQL for each source.
|
|
684
|
+
|
|
685
|
+
Args:
|
|
686
|
+
face: Face to create execution plan for
|
|
687
|
+
|
|
688
|
+
Returns:
|
|
689
|
+
Dict mapping source to list of BatchStatements
|
|
690
|
+
|
|
691
|
+
Raises:
|
|
692
|
+
CrossSourceReferenceError: If a query references another
|
|
693
|
+
query on a different database source
|
|
694
|
+
|
|
695
|
+
Example:
|
|
696
|
+
>>> plan = create_batch_execution_plan(face)
|
|
697
|
+
>>> for source, statements in plan.items():
|
|
698
|
+
... print(f"Source: {source}")
|
|
699
|
+
... for stmt in statements:
|
|
700
|
+
... print(f" {stmt.name}: {stmt.sql[:50]}...")
|
|
701
|
+
"""
|
|
702
|
+
# Collect all queries used by the dataface
|
|
703
|
+
all_queries = collect_face_queries(face, chart_queries_only=True)
|
|
704
|
+
|
|
705
|
+
if not all_queries:
|
|
706
|
+
return {}
|
|
707
|
+
|
|
708
|
+
# Build the full dependency graph first (for validation)
|
|
709
|
+
full_graph = build_dependency_graph(all_queries)
|
|
710
|
+
|
|
711
|
+
# Validate no cross-source references
|
|
712
|
+
validate_cross_source_references(all_queries, full_graph)
|
|
713
|
+
|
|
714
|
+
# Collect query names directly used by charts
|
|
715
|
+
direct_chart_queries: set[str] = set()
|
|
716
|
+
for chart in face.charts.values():
|
|
717
|
+
if chart.query_name:
|
|
718
|
+
direct_chart_queries.add(chart.query_name)
|
|
719
|
+
|
|
720
|
+
# Group queries by source
|
|
721
|
+
source_groups = group_by_source(all_queries)
|
|
722
|
+
|
|
723
|
+
# Generate batch SQL for each source
|
|
724
|
+
result: dict[str, list[BatchStatement]] = {}
|
|
725
|
+
|
|
726
|
+
for source, source_queries in source_groups.items():
|
|
727
|
+
# Build graph for this source's queries
|
|
728
|
+
graph = build_dependency_graph(source_queries)
|
|
729
|
+
|
|
730
|
+
# Get dialect for this source
|
|
731
|
+
dialect = get_dialect(source)
|
|
732
|
+
|
|
733
|
+
# Generate batch statements
|
|
734
|
+
statements = generate_batch_sql(
|
|
735
|
+
queries=source_queries,
|
|
736
|
+
graph=graph,
|
|
737
|
+
dialect=dialect,
|
|
738
|
+
direct_chart_queries=direct_chart_queries,
|
|
739
|
+
)
|
|
740
|
+
|
|
741
|
+
if statements:
|
|
742
|
+
result[source] = statements
|
|
743
|
+
|
|
744
|
+
return result
|