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,710 @@
|
|
|
1
|
+
"""SQL query adapter for executing raw SQL queries.
|
|
2
|
+
|
|
3
|
+
Stage: EXECUTE
|
|
4
|
+
Purpose: Execute SQL queries via dbt adapters or direct DuckDB connections.
|
|
5
|
+
|
|
6
|
+
DuckDB queries execute via the raw duckdb driver (already a hard dep) with
|
|
7
|
+
full parameterized query support ($1, $2 placeholders). All other warehouses
|
|
8
|
+
execute via build_adapter() from dbt_adapter_factory — the adapter package for
|
|
9
|
+
that warehouse must be installed (e.g. pip install dbt-postgres).
|
|
10
|
+
|
|
11
|
+
Security: DuckDB uses parameterized queries. Non-DuckDB queries inline params
|
|
12
|
+
after Jinja rendering — values are validated dashboard YAML variables, not
|
|
13
|
+
raw user input from HTTP.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
import re
|
|
20
|
+
import threading
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import TYPE_CHECKING, Any
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from dataface.core.compile.models.source import SourceConfig
|
|
26
|
+
from dataface.core.execute.adapters.base import ResolvedRelation
|
|
27
|
+
|
|
28
|
+
from dataface.core.compile.filter_injection import inject_filters
|
|
29
|
+
from dataface.core.compile.models.face.compiled import VariableValues
|
|
30
|
+
from dataface.core.compile.models.query.compiled import (
|
|
31
|
+
AnyQuery,
|
|
32
|
+
is_sql_query,
|
|
33
|
+
)
|
|
34
|
+
from dataface.core.compile.parameterized import render_parameterized
|
|
35
|
+
from dataface.core.execute.adapters.base import (
|
|
36
|
+
BaseAdapter,
|
|
37
|
+
QueryParams,
|
|
38
|
+
QueryResult,
|
|
39
|
+
handle_adapter_error,
|
|
40
|
+
)
|
|
41
|
+
from dataface.core.execute.dbt_jinja import has_dbt_jinja
|
|
42
|
+
from dataface.core.execute.dialects import get_dialect
|
|
43
|
+
from dataface.core.execute.duckdb_config import normalize_duckdb_config
|
|
44
|
+
from dataface.core.execute.sql_guard import validate_select_only, validate_setup_sql
|
|
45
|
+
from dataface.core.execute.sql_literals import (
|
|
46
|
+
inline_dialect_params as _inline_dialect_params,
|
|
47
|
+
inline_params as _inline_params,
|
|
48
|
+
inline_qmark_params as _inline_qmark_params,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
logger = logging.getLogger(__name__)
|
|
52
|
+
|
|
53
|
+
# DuckDB resolves relative read_csv()/read_parquet() paths against the process cwd.
|
|
54
|
+
# The adapter temporarily switches cwd to the face's project root so those queries
|
|
55
|
+
# work, but that global mutation must be serialized across requests.
|
|
56
|
+
_DUCKDB_CWD_LOCK = threading.RLock()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class SqlAdapter(BaseAdapter):
|
|
60
|
+
"""Adapter for executing raw SQL queries.
|
|
61
|
+
|
|
62
|
+
DuckDB sources use the raw duckdb driver (parameterized, fast).
|
|
63
|
+
All other warehouses use build_adapter() from dbt_adapter_factory.
|
|
64
|
+
|
|
65
|
+
Supported query types: sql
|
|
66
|
+
|
|
67
|
+
Example:
|
|
68
|
+
>>> adapter = SqlAdapter()
|
|
69
|
+
>>> query = SqlQuery(sql="SELECT * FROM users WHERE id = {{ user_id }}")
|
|
70
|
+
>>> result = adapter.execute(query, {"user_id": 1})
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
dbt_project_path: str | None = None,
|
|
76
|
+
use_example_db: bool = False,
|
|
77
|
+
connection_string: str | None = None,
|
|
78
|
+
project_root: Path | None = None,
|
|
79
|
+
profile_type: str = "duckdb",
|
|
80
|
+
read_only: bool = True,
|
|
81
|
+
duckdb_config: dict[str, Any] | None = None,
|
|
82
|
+
allow_external_access_in_readonly: bool = False,
|
|
83
|
+
):
|
|
84
|
+
"""Initialize SQL adapter.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
dbt_project_path: Path to dbt project (default: current directory)
|
|
88
|
+
use_example_db: If True, use DuckDB example database instead of dbt config
|
|
89
|
+
connection_string: DuckDB connection string (e.g., ":memory:" or file path)
|
|
90
|
+
project_root: Root directory for resolving relative file paths in read_csv()
|
|
91
|
+
profile_type: Database type for dialect selection (e.g., 'postgres', 'duckdb')
|
|
92
|
+
read_only: If True (default), open DuckDB connections in read-only mode.
|
|
93
|
+
File-based DuckDB: opened with read_only=True — the driver refuses all writes.
|
|
94
|
+
In-memory DuckDB (:memory:): always opened read-write regardless of this flag
|
|
95
|
+
because there is no other way to populate an in-memory database; this is a
|
|
96
|
+
known exception. enable_external_access=False is still forced for :memory:
|
|
97
|
+
when read_only=True unless allow_external_access_in_readonly=True.
|
|
98
|
+
|
|
99
|
+
When read_only=True (and allow_external_access_in_readonly=False),
|
|
100
|
+
enable_external_access=False is forced on the DuckDB config regardless of
|
|
101
|
+
any user-supplied duckdb_config. This blocks httpfs URL fetches and
|
|
102
|
+
read_csv/read_parquet/read_json against local and remote paths.
|
|
103
|
+
To allow external access on the default path, use read_only=False.
|
|
104
|
+
|
|
105
|
+
Non-DuckDB warehouses (Postgres, Snowflake, BigQuery, etc.) are not affected
|
|
106
|
+
by this flag — they have no native read-only driver knob. Use SELECT-only
|
|
107
|
+
credentials/roles at the warehouse level for those.
|
|
108
|
+
|
|
109
|
+
Callers that must write (cache backends, test fixture setup) should pass
|
|
110
|
+
read_only=False explicitly.
|
|
111
|
+
duckdb_config: Optional DuckDB config dict for the default connection.
|
|
112
|
+
When read_only=True and allow_external_access_in_readonly=False,
|
|
113
|
+
enable_external_access in this dict is overridden to False.
|
|
114
|
+
To enable external access with read_only=True, set both
|
|
115
|
+
allow_external_access_in_readonly=True AND
|
|
116
|
+
duckdb_config={"enable_external_access": True}.
|
|
117
|
+
allow_external_access_in_readonly: Security opt-in. When True AND
|
|
118
|
+
read_only=True AND duckdb_config contains enable_external_access=True,
|
|
119
|
+
the adapter passes enable_external_access=True through to DuckDB instead
|
|
120
|
+
of forcing it to False. Default False preserves the existing defense:
|
|
121
|
+
read_only=True always implies enable_external_access=False.
|
|
122
|
+
|
|
123
|
+
This flag is for local-dev surfaces (e.g. the playground server) that
|
|
124
|
+
need BOTH a non-exclusive file lock (for coexistence with concurrent
|
|
125
|
+
dft render / Cursor preview processes) AND external file access
|
|
126
|
+
(read_csv, read_json_auto). It must NOT be set in multi-tenant or
|
|
127
|
+
deployed surfaces. Approved callsites are listed on
|
|
128
|
+
LOCAL_AUTHORING_REGISTRY_KWARGS in adapter_registry.py
|
|
129
|
+
(playground, MCP, chat, serve).
|
|
130
|
+
"""
|
|
131
|
+
self.dbt_project_path = Path(dbt_project_path) if dbt_project_path else None
|
|
132
|
+
self.use_example_db = use_example_db
|
|
133
|
+
self.connection_string = connection_string or ":memory:"
|
|
134
|
+
self.project_root = project_root
|
|
135
|
+
self.profile_type = profile_type
|
|
136
|
+
self.read_only = read_only
|
|
137
|
+
self.allow_external_access_in_readonly = allow_external_access_in_readonly
|
|
138
|
+
self._duckdb_config = duckdb_config
|
|
139
|
+
self._connection: Any = None # Lazy-loaded DuckDB connection (default)
|
|
140
|
+
# Thread-local storage for per-source DuckDB connection caches.
|
|
141
|
+
self._tls = threading.local()
|
|
142
|
+
# Track all connections opened across all threads so close() can clean up.
|
|
143
|
+
self._all_conns: list[Any] = []
|
|
144
|
+
self._all_conns_lock = threading.Lock()
|
|
145
|
+
self._manifest: dict[str, Any] | None = None # Lazy-loaded dbt manifest
|
|
146
|
+
self._manifest_from_snapshot: bool = False
|
|
147
|
+
self._prod_manifest: dict[str, Any] | None = None
|
|
148
|
+
self._prod_manifest_loaded: bool = False
|
|
149
|
+
self._dialect = get_dialect(profile_type)
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def supported_types(self) -> set[str]:
|
|
153
|
+
"""Return supported query types."""
|
|
154
|
+
return {"sql"}
|
|
155
|
+
|
|
156
|
+
def _can_execute(self, query: AnyQuery) -> bool:
|
|
157
|
+
"""Check if this adapter can execute the query.
|
|
158
|
+
|
|
159
|
+
DbtAdapter owns: SQL with dbt jinja and no source (needs manifest resolution).
|
|
160
|
+
SqlAdapter owns: everything else — plain SQL, or any query with a source.
|
|
161
|
+
"""
|
|
162
|
+
if not is_sql_query(query):
|
|
163
|
+
return False
|
|
164
|
+
# Dbt-jinja with no source → DbtAdapter's territory (manifest resolution needed)
|
|
165
|
+
return not (has_dbt_jinja(query.sql) and query.source is None)
|
|
166
|
+
|
|
167
|
+
def _execute(
|
|
168
|
+
self,
|
|
169
|
+
query: AnyQuery,
|
|
170
|
+
variables: VariableValues | None = None,
|
|
171
|
+
params: QueryParams = None,
|
|
172
|
+
source_config: SourceConfig | None = None,
|
|
173
|
+
) -> QueryResult:
|
|
174
|
+
"""Execute a SQL query.
|
|
175
|
+
|
|
176
|
+
Uses parameterized queries (DuckDB) or safe literal inlining (dbt adapters).
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
query: AnyQuery object (SqlQuery expected)
|
|
180
|
+
variables: Variable values for Jinja resolution
|
|
181
|
+
params: Optional pre-computed parameter values.
|
|
182
|
+
source_config: Typed source config from SourceResolver. When None,
|
|
183
|
+
the adapter uses its default connection and configured profile_type.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
QueryResult with data or error
|
|
187
|
+
"""
|
|
188
|
+
if not is_sql_query(query):
|
|
189
|
+
return QueryResult(
|
|
190
|
+
data=[],
|
|
191
|
+
error=f"Expected SQL query, got {query.query_type}",
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
sql = query.sql
|
|
195
|
+
resolved_relations: list[ResolvedRelation] = []
|
|
196
|
+
|
|
197
|
+
if self.dbt_project_path:
|
|
198
|
+
sql, resolved_relations = self._resolve_dbt_sql(sql)
|
|
199
|
+
|
|
200
|
+
# Resolver-provided source_config is the single source of truth. When
|
|
201
|
+
# absent, the adapter falls through to its own default connection using
|
|
202
|
+
# the configured profile_type.
|
|
203
|
+
raw_config: dict[str, Any] | None = (
|
|
204
|
+
source_config.model_dump(by_alias=True)
|
|
205
|
+
if source_config is not None
|
|
206
|
+
else None
|
|
207
|
+
)
|
|
208
|
+
dialect_name: str = (
|
|
209
|
+
source_config.type if source_config is not None else self.profile_type
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# Resolve setup_sql Jinja before branching on dialect.
|
|
213
|
+
resolved_setup_sql: str | None = None
|
|
214
|
+
if query.setup_sql:
|
|
215
|
+
resolved = self._resolve_setup_sql(query.setup_sql, variables)
|
|
216
|
+
if isinstance(resolved, QueryResult):
|
|
217
|
+
return resolved
|
|
218
|
+
resolved_setup_sql = resolved
|
|
219
|
+
|
|
220
|
+
if params is not None:
|
|
221
|
+
resolved_sql = sql
|
|
222
|
+
resolved_params = params
|
|
223
|
+
else:
|
|
224
|
+
try:
|
|
225
|
+
parameterized = render_parameterized(
|
|
226
|
+
sql,
|
|
227
|
+
variables=variables or {},
|
|
228
|
+
dialect=get_dialect(dialect_name),
|
|
229
|
+
)
|
|
230
|
+
resolved_sql = parameterized.sql
|
|
231
|
+
resolved_params = parameterized.params
|
|
232
|
+
except (ValueError, KeyError, TypeError) as e:
|
|
233
|
+
return handle_adapter_error("SQL parameterization", e)
|
|
234
|
+
|
|
235
|
+
if query.filters:
|
|
236
|
+
try:
|
|
237
|
+
dialect_obj = get_dialect(dialect_name)
|
|
238
|
+
injected_sql, filter_params = inject_filters(
|
|
239
|
+
resolved_sql,
|
|
240
|
+
query.filters,
|
|
241
|
+
variables or {},
|
|
242
|
+
dialect_obj,
|
|
243
|
+
param_offset=len(resolved_params),
|
|
244
|
+
)
|
|
245
|
+
resolved_sql = injected_sql
|
|
246
|
+
resolved_params = list(resolved_params) + filter_params
|
|
247
|
+
except (ValueError, KeyError, TypeError) as e:
|
|
248
|
+
return handle_adapter_error("filter injection", e)
|
|
249
|
+
|
|
250
|
+
if self.use_example_db or dialect_name == "duckdb":
|
|
251
|
+
# Execute DuckDB setup_sql before main query on the same connection.
|
|
252
|
+
if resolved_setup_sql:
|
|
253
|
+
err = self._execute_setup_sql_duckdb(resolved_setup_sql, raw_config)
|
|
254
|
+
if err is not None:
|
|
255
|
+
return err
|
|
256
|
+
result = self._execute_duckdb(
|
|
257
|
+
resolved_sql, resolved_params, query, raw_config
|
|
258
|
+
)
|
|
259
|
+
else:
|
|
260
|
+
result = self._execute_via_dbt_adapter(
|
|
261
|
+
resolved_sql,
|
|
262
|
+
resolved_params,
|
|
263
|
+
query,
|
|
264
|
+
raw_config,
|
|
265
|
+
dialect_name,
|
|
266
|
+
setup_sql=resolved_setup_sql,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
if resolved_relations:
|
|
270
|
+
result.resolved_relations = resolved_relations
|
|
271
|
+
|
|
272
|
+
return result
|
|
273
|
+
|
|
274
|
+
def _resolve_setup_sql(
|
|
275
|
+
self,
|
|
276
|
+
setup_sql: str,
|
|
277
|
+
variables: VariableValues | None,
|
|
278
|
+
) -> str | QueryResult:
|
|
279
|
+
"""Resolve Jinja in setup_sql. Returns resolved string or a QueryResult on error."""
|
|
280
|
+
from dataface.core.compile.jinja import resolve_jinja_template
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
return resolve_jinja_template(setup_sql, variables=variables or {})
|
|
284
|
+
except Exception as e: # noqa: BLE001
|
|
285
|
+
return handle_adapter_error("setup_sql template resolution", e)
|
|
286
|
+
|
|
287
|
+
def _execute_setup_sql_duckdb(
|
|
288
|
+
self,
|
|
289
|
+
resolved_setup_sql: str,
|
|
290
|
+
source_config: dict[str, Any] | None = None,
|
|
291
|
+
) -> QueryResult | None:
|
|
292
|
+
"""Execute setup_sql on a DuckDB connection. Returns None on success."""
|
|
293
|
+
try:
|
|
294
|
+
validate_setup_sql(resolved_setup_sql, dialect="duckdb")
|
|
295
|
+
with _DUCKDB_CWD_LOCK:
|
|
296
|
+
duckdb_conn = self._get_duckdb_connection_for_query(source_config)
|
|
297
|
+
duckdb_conn.execute(resolved_setup_sql)
|
|
298
|
+
except Exception as e: # noqa: BLE001
|
|
299
|
+
return handle_adapter_error("setup_sql execution", e)
|
|
300
|
+
return None
|
|
301
|
+
|
|
302
|
+
def _execute_duckdb(
|
|
303
|
+
self,
|
|
304
|
+
sql: str,
|
|
305
|
+
params: list[Any],
|
|
306
|
+
query: AnyQuery,
|
|
307
|
+
source_config: dict[str, Any] | None = None,
|
|
308
|
+
) -> QueryResult:
|
|
309
|
+
"""Execute SQL query using DuckDB with parameterized execution.
|
|
310
|
+
|
|
311
|
+
Uses parameterized queries to prevent SQL injection attacks.
|
|
312
|
+
Parameters are passed separately from the SQL string.
|
|
313
|
+
"""
|
|
314
|
+
import importlib.util
|
|
315
|
+
import os
|
|
316
|
+
|
|
317
|
+
if importlib.util.find_spec("duckdb") is None:
|
|
318
|
+
return QueryResult(
|
|
319
|
+
data=[],
|
|
320
|
+
error="DuckDB not installed - install with: pip install duckdb",
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
try:
|
|
324
|
+
validate_select_only(sql, dialect="duckdb")
|
|
325
|
+
with _DUCKDB_CWD_LOCK:
|
|
326
|
+
conn = self._get_duckdb_connection_for_query(source_config)
|
|
327
|
+
|
|
328
|
+
original_cwd = os.getcwd()
|
|
329
|
+
if self.project_root:
|
|
330
|
+
os.chdir(self.project_root)
|
|
331
|
+
|
|
332
|
+
try:
|
|
333
|
+
result = conn.execute(sql, params or [])
|
|
334
|
+
columns = (
|
|
335
|
+
[desc[0] for desc in result.description]
|
|
336
|
+
if result.description
|
|
337
|
+
else []
|
|
338
|
+
)
|
|
339
|
+
rows = result.fetchall()
|
|
340
|
+
finally:
|
|
341
|
+
os.chdir(original_cwd)
|
|
342
|
+
|
|
343
|
+
if query.limit and query.limit > 0:
|
|
344
|
+
rows = rows[: query.limit]
|
|
345
|
+
|
|
346
|
+
data = [dict(zip(columns, row, strict=False)) for row in rows]
|
|
347
|
+
col_descs = (
|
|
348
|
+
{desc[0]: tuple(desc) for desc in result.description}
|
|
349
|
+
if result.description
|
|
350
|
+
else None
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
return QueryResult(
|
|
354
|
+
data=data, columns=columns, column_descriptions=col_descs
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
except Exception as e: # noqa: BLE001
|
|
358
|
+
return handle_adapter_error("DuckDB SQL execution", e)
|
|
359
|
+
|
|
360
|
+
@staticmethod
|
|
361
|
+
def _push_limit_into_sql(sql: str, limit: int) -> str:
|
|
362
|
+
"""Wrap sql with an outer LIMIT N.
|
|
363
|
+
|
|
364
|
+
Always wraps — the outer LIMIT bounds the inner one when both are present,
|
|
365
|
+
yielding the smaller of the two values (safe and idempotent). Checking for
|
|
366
|
+
'limit' in sql.lower() is brittle: it matches column names (limit_amount),
|
|
367
|
+
CTEs, comments, and string literals, causing silent full-table scans on
|
|
368
|
+
large tables when the wrapping is skipped.
|
|
369
|
+
|
|
370
|
+
Trailing semicolons are stripped before wrapping — they appear in SQL pasted
|
|
371
|
+
from IDEs and would produce a parse error inside the subquery on BigQuery,
|
|
372
|
+
Snowflake, Redshift, and Postgres.
|
|
373
|
+
"""
|
|
374
|
+
stripped = sql.rstrip().rstrip(";").rstrip()
|
|
375
|
+
return f"SELECT * FROM ({stripped}) AS _dft_limit_wrap LIMIT {limit}"
|
|
376
|
+
|
|
377
|
+
def _execute_via_dbt_adapter(
|
|
378
|
+
self,
|
|
379
|
+
sql: str,
|
|
380
|
+
params: list[Any],
|
|
381
|
+
query: AnyQuery,
|
|
382
|
+
source_config: dict[str, Any] | None,
|
|
383
|
+
dialect_name: str,
|
|
384
|
+
setup_sql: str | None = None,
|
|
385
|
+
) -> QueryResult:
|
|
386
|
+
"""Execute SQL via the dbt adapter for non-DuckDB warehouses.
|
|
387
|
+
|
|
388
|
+
Params are inlined as SQL literals — dbt adapter.execute() only accepts
|
|
389
|
+
plain SQL. This is safe because params come from compiled dashboard YAML
|
|
390
|
+
variables, not raw user input.
|
|
391
|
+
|
|
392
|
+
For dialects that return the full result set (e.g. BigQuery), query.limit
|
|
393
|
+
is pushed into the SQL string before execution to avoid full-table scans.
|
|
394
|
+
|
|
395
|
+
setup_sql (if any) runs inside the same connection_named("dataface_query")
|
|
396
|
+
block as the main query so temp functions/tables are visible to the query.
|
|
397
|
+
BigQuery CREATE TEMP FUNCTION is session-scoped — two connection_named calls
|
|
398
|
+
produce two distinct connections and the temp function would not be visible.
|
|
399
|
+
"""
|
|
400
|
+
from dataface.core.execute.adapters.dbt_adapter_factory import build_adapter
|
|
401
|
+
|
|
402
|
+
if source_config is None:
|
|
403
|
+
return QueryResult(
|
|
404
|
+
data=[],
|
|
405
|
+
error=(
|
|
406
|
+
f"No source config found for dialect '{dialect_name}'. "
|
|
407
|
+
f"Provide an inline source: block or a named source in _sources.yaml."
|
|
408
|
+
),
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
try:
|
|
412
|
+
param_list = list(params) if params else []
|
|
413
|
+
# The dbt adapter.execute() accepts only plain SQL — no bound params.
|
|
414
|
+
# Inline params as SQL literals before passing the SQL string.
|
|
415
|
+
# Named-param dialects (BigQuery @paramN, Databricks :paramN, SQL
|
|
416
|
+
# Server @pN) use inline_dialect_params with the dialect's param()
|
|
417
|
+
# method so each placeholder format is matched correctly.
|
|
418
|
+
# All other dialects use $N positional placeholders.
|
|
419
|
+
dialect_obj = get_dialect(dialect_name)
|
|
420
|
+
if dialect_obj.uses_named_params:
|
|
421
|
+
inlined_sql = _inline_dialect_params(sql, param_list, dialect_obj.param)
|
|
422
|
+
elif dialect_obj.param(1) == "?":
|
|
423
|
+
inlined_sql = _inline_qmark_params(sql, param_list)
|
|
424
|
+
else:
|
|
425
|
+
inlined_sql = _inline_params(sql, param_list)
|
|
426
|
+
|
|
427
|
+
# Push LIMIT into the SQL for all non-DuckDB dialects. dbt-adapters'
|
|
428
|
+
# execute(..., fetch=True) materializes the full agate.Table in memory
|
|
429
|
+
# before returning — Python slicing after the fact pulls the entire
|
|
430
|
+
# result set over the wire. DuckDB is excluded because it executes
|
|
431
|
+
# locally and its native cursor handles LIMIT correctly without rewriting.
|
|
432
|
+
if dialect_name != "duckdb" and query.limit and query.limit > 0:
|
|
433
|
+
inlined_sql = self._push_limit_into_sql(inlined_sql, query.limit)
|
|
434
|
+
|
|
435
|
+
# Validate setup_sql + main SQL separately (different allowlists)
|
|
436
|
+
# before opening the dbt connection — fail-closed if either piece
|
|
437
|
+
# contains mutating SQL. validate_setup_sql also runs in the DuckDB
|
|
438
|
+
# setup path; running it here too is the correct re-check for the
|
|
439
|
+
# combined-script branch since the strings hit a different driver.
|
|
440
|
+
if setup_sql:
|
|
441
|
+
validate_setup_sql(setup_sql, dialect=dialect_name)
|
|
442
|
+
validate_select_only(inlined_sql, dialect=dialect_name)
|
|
443
|
+
|
|
444
|
+
adapter = build_adapter(source_config)
|
|
445
|
+
with adapter.connection_named("dataface_query"):
|
|
446
|
+
if setup_sql:
|
|
447
|
+
# BigQuery rejects standalone "CREATE TEMP FUNCTION" — it
|
|
448
|
+
# must appear in the same script as a query that uses
|
|
449
|
+
# (or at least follows) it. Sending setup_sql in a separate
|
|
450
|
+
# execute() call also drops session-scoped TEMP entities
|
|
451
|
+
# before the main query runs. Concatenate so dbt sends them
|
|
452
|
+
# as one multi-statement script.
|
|
453
|
+
combined_sql = setup_sql.rstrip().rstrip(";") + ";\n" + inlined_sql
|
|
454
|
+
_, table = adapter.execute(
|
|
455
|
+
combined_sql, auto_begin=True, fetch=True
|
|
456
|
+
)
|
|
457
|
+
else:
|
|
458
|
+
_, table = adapter.execute(inlined_sql, auto_begin=True, fetch=True)
|
|
459
|
+
|
|
460
|
+
columns = [col.lower() for col in table.column_names]
|
|
461
|
+
rows = list(table.rows)
|
|
462
|
+
|
|
463
|
+
# Python-side limit as safety net for non-BQ dialects
|
|
464
|
+
if query.limit and query.limit > 0:
|
|
465
|
+
rows = rows[: query.limit]
|
|
466
|
+
|
|
467
|
+
data = [dict(zip(columns, row, strict=False)) for row in rows]
|
|
468
|
+
return QueryResult(data=data, columns=columns)
|
|
469
|
+
|
|
470
|
+
except Exception as e: # noqa: BLE001
|
|
471
|
+
return handle_adapter_error(f"{dialect_name} SQL execution", e)
|
|
472
|
+
|
|
473
|
+
def _get_duckdb_connection_for_query(
|
|
474
|
+
self, source_config: dict[str, Any] | None = None
|
|
475
|
+
) -> Any:
|
|
476
|
+
"""Get DuckDB connection for a query.
|
|
477
|
+
|
|
478
|
+
Uses the typed ``source_config`` (resolver-provided) when it points at a
|
|
479
|
+
DuckDB source; otherwise falls through to the adapter's default
|
|
480
|
+
connection. The thread-local connection cache is keyed by
|
|
481
|
+
``source_config['path']`` — two named DuckDB sources that point at the
|
|
482
|
+
same path share a connection. For ``:memory:`` this means two distinct
|
|
483
|
+
named sources both using ``:memory:`` collapse into one in-memory DB;
|
|
484
|
+
callers needing isolated in-memory DBs must use distinct paths (e.g.
|
|
485
|
+
``:memory:db_a``, ``:memory:db_b``) or different connection_strings.
|
|
486
|
+
"""
|
|
487
|
+
if source_config and source_config.get("type") == "duckdb":
|
|
488
|
+
schema = source_config.get("schema") or ""
|
|
489
|
+
path = str(source_config.get("path") or ":memory:")
|
|
490
|
+
cache_key = f"{path}\0{schema}" if schema else path
|
|
491
|
+
return self._create_duckdb_connection_from_config(source_config, cache_key)
|
|
492
|
+
return self._get_duckdb_connection()
|
|
493
|
+
|
|
494
|
+
def _resolve_duckdb_config(
|
|
495
|
+
self, source_config: dict[str, Any]
|
|
496
|
+
) -> dict[str, Any] | None:
|
|
497
|
+
"""Resolve DuckDB config: source-level overrides adapter-level default."""
|
|
498
|
+
return normalize_duckdb_config(source_config) or self._duckdb_config
|
|
499
|
+
|
|
500
|
+
def _create_duckdb_connection_from_config(
|
|
501
|
+
self, source_config: dict[str, Any], cache_key: str
|
|
502
|
+
) -> Any:
|
|
503
|
+
"""Create or retrieve a cached DuckDB connection from source config."""
|
|
504
|
+
import duckdb
|
|
505
|
+
|
|
506
|
+
if "database" in source_config and "path" not in source_config:
|
|
507
|
+
raise ValueError(
|
|
508
|
+
"DuckDB source_config uses the unsupported 'database' key. "
|
|
509
|
+
'Use \'path\' instead (e.g. {"type": "duckdb", "path": "db.duckdb"}).'
|
|
510
|
+
)
|
|
511
|
+
db_path = source_config.get("path", ":memory:")
|
|
512
|
+
raw_config = self._resolve_duckdb_config(source_config)
|
|
513
|
+
duckdb_config = raw_config or None
|
|
514
|
+
|
|
515
|
+
sources = getattr(self._tls, "sources", None)
|
|
516
|
+
if sources is None:
|
|
517
|
+
sources = self._tls.sources = {}
|
|
518
|
+
|
|
519
|
+
if cache_key not in sources:
|
|
520
|
+
resolved_path = db_path
|
|
521
|
+
if (
|
|
522
|
+
self.project_root
|
|
523
|
+
and db_path != ":memory:"
|
|
524
|
+
and not Path(db_path).is_absolute()
|
|
525
|
+
):
|
|
526
|
+
resolved_path = str(self.project_root / db_path)
|
|
527
|
+
|
|
528
|
+
conn = duckdb.connect(
|
|
529
|
+
resolved_path,
|
|
530
|
+
**self._resolve_duckdb_connect_kwargs(
|
|
531
|
+
resolved_path,
|
|
532
|
+
self.read_only,
|
|
533
|
+
duckdb_config,
|
|
534
|
+
self.allow_external_access_in_readonly,
|
|
535
|
+
),
|
|
536
|
+
)
|
|
537
|
+
schema = source_config.get("schema") or ""
|
|
538
|
+
if schema:
|
|
539
|
+
if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", schema):
|
|
540
|
+
raise ValueError(
|
|
541
|
+
f"Invalid schema name {schema!r}: must match "
|
|
542
|
+
r"^[A-Za-z_][A-Za-z0-9_]*$"
|
|
543
|
+
)
|
|
544
|
+
conn.execute("SET search_path = ?", [f"{schema},main"])
|
|
545
|
+
sources[cache_key] = conn
|
|
546
|
+
with self._all_conns_lock:
|
|
547
|
+
self._all_conns.append(conn)
|
|
548
|
+
|
|
549
|
+
return sources[cache_key]
|
|
550
|
+
|
|
551
|
+
def close(self) -> None:
|
|
552
|
+
"""Close all database connections."""
|
|
553
|
+
if self._connection:
|
|
554
|
+
self._connection.close()
|
|
555
|
+
self._connection = None
|
|
556
|
+
|
|
557
|
+
with self._all_conns_lock:
|
|
558
|
+
conns, self._all_conns = self._all_conns, []
|
|
559
|
+
for conn in conns:
|
|
560
|
+
conn.close()
|
|
561
|
+
|
|
562
|
+
if hasattr(self._tls, "sources"):
|
|
563
|
+
self._tls.sources = {}
|
|
564
|
+
|
|
565
|
+
@staticmethod
|
|
566
|
+
def _resolve_duckdb_connect_kwargs(
|
|
567
|
+
path: str,
|
|
568
|
+
read_only: bool,
|
|
569
|
+
duckdb_config: dict[str, Any] | None,
|
|
570
|
+
allow_external_access_in_readonly: bool = False,
|
|
571
|
+
) -> dict[str, Any]:
|
|
572
|
+
"""Build duckdb.connect() kwargs for the given path and security posture.
|
|
573
|
+
|
|
574
|
+
When read_only=True and allow_external_access_in_readonly=False (default):
|
|
575
|
+
- Adds read_only=True for file-based paths (not :memory: — in-memory DuckDB
|
|
576
|
+
must always be opened read-write; this is a known exception).
|
|
577
|
+
- Forces enable_external_access=False in the config, overriding any
|
|
578
|
+
user-supplied duckdb_config value. This is the defense-in-depth contract:
|
|
579
|
+
no quiet bypass via config when the adapter is set to read-only.
|
|
580
|
+
|
|
581
|
+
When read_only=True and allow_external_access_in_readonly=True:
|
|
582
|
+
- Same read_only=True driver flag for file-based paths (non-exclusive lock).
|
|
583
|
+
- enable_external_access is NOT forced to False; caller's duckdb_config value
|
|
584
|
+
passes through. This is the deliberate local-dev opt-in for playground,
|
|
585
|
+
MCP, chat, and serve: non-exclusive lock + external file reads. Approved
|
|
586
|
+
callsites are listed on LOCAL_AUTHORING_REGISTRY_KWARGS in adapter_registry.py.
|
|
587
|
+
|
|
588
|
+
When read_only=False:
|
|
589
|
+
- No read_only driver flag.
|
|
590
|
+
- duckdb_config is passed through as-is.
|
|
591
|
+
"""
|
|
592
|
+
kwargs: dict[str, Any] = {}
|
|
593
|
+
if read_only and path != ":memory:":
|
|
594
|
+
kwargs["read_only"] = True
|
|
595
|
+
if duckdb_config is not None or read_only:
|
|
596
|
+
config = dict(duckdb_config or {})
|
|
597
|
+
if read_only and not (
|
|
598
|
+
allow_external_access_in_readonly
|
|
599
|
+
and config.get("enable_external_access") is True
|
|
600
|
+
):
|
|
601
|
+
config["enable_external_access"] = False
|
|
602
|
+
if config:
|
|
603
|
+
kwargs["config"] = config
|
|
604
|
+
return kwargs
|
|
605
|
+
|
|
606
|
+
def _get_duckdb_connection(self) -> Any:
|
|
607
|
+
"""Get or create the default DuckDB connection."""
|
|
608
|
+
if self._connection is None:
|
|
609
|
+
import duckdb
|
|
610
|
+
|
|
611
|
+
if self.use_example_db:
|
|
612
|
+
self._connection = duckdb.connect(":memory:")
|
|
613
|
+
elif self.dbt_project_path:
|
|
614
|
+
from dataface.core.execute.adapters.dbt_utils import (
|
|
615
|
+
DBT_PROJECT_DB_NAMES,
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
conn = None
|
|
619
|
+
for db_name in DBT_PROJECT_DB_NAMES:
|
|
620
|
+
db_path = self.dbt_project_path / "data" / db_name
|
|
621
|
+
if db_path.exists():
|
|
622
|
+
conn = duckdb.connect(
|
|
623
|
+
str(db_path),
|
|
624
|
+
**self._resolve_duckdb_connect_kwargs(
|
|
625
|
+
str(db_path),
|
|
626
|
+
self.read_only,
|
|
627
|
+
self._duckdb_config,
|
|
628
|
+
self.allow_external_access_in_readonly,
|
|
629
|
+
),
|
|
630
|
+
)
|
|
631
|
+
break
|
|
632
|
+
|
|
633
|
+
if conn is None:
|
|
634
|
+
conn = duckdb.connect(
|
|
635
|
+
self.connection_string,
|
|
636
|
+
**self._resolve_duckdb_connect_kwargs(
|
|
637
|
+
self.connection_string,
|
|
638
|
+
self.read_only,
|
|
639
|
+
self._duckdb_config,
|
|
640
|
+
self.allow_external_access_in_readonly,
|
|
641
|
+
),
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
self._connection = conn
|
|
645
|
+
else:
|
|
646
|
+
self._connection = duckdb.connect(
|
|
647
|
+
self.connection_string,
|
|
648
|
+
**self._resolve_duckdb_connect_kwargs(
|
|
649
|
+
self.connection_string,
|
|
650
|
+
self.read_only,
|
|
651
|
+
self._duckdb_config,
|
|
652
|
+
self.allow_external_access_in_readonly,
|
|
653
|
+
),
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
return self._connection
|
|
657
|
+
|
|
658
|
+
def _get_manifest(self) -> dict[str, Any] | None:
|
|
659
|
+
"""Load dbt manifest.json if available."""
|
|
660
|
+
if self._manifest is not None:
|
|
661
|
+
return self._manifest
|
|
662
|
+
if not self.dbt_project_path:
|
|
663
|
+
return None
|
|
664
|
+
|
|
665
|
+
target_path = self.dbt_project_path / "target" / "manifest.json"
|
|
666
|
+
snapshot_path = self.dbt_project_path / "manifest.snapshot.json"
|
|
667
|
+
|
|
668
|
+
from dataface.core.execute.adapters.dbt_utils import load_dbt_manifest
|
|
669
|
+
|
|
670
|
+
self._manifest = load_dbt_manifest(self.dbt_project_path)
|
|
671
|
+
self._manifest_from_snapshot = (
|
|
672
|
+
self._manifest is not None
|
|
673
|
+
and not target_path.exists()
|
|
674
|
+
and snapshot_path.exists()
|
|
675
|
+
)
|
|
676
|
+
return self._manifest
|
|
677
|
+
|
|
678
|
+
def _get_prod_manifest(self) -> dict[str, Any] | None:
|
|
679
|
+
"""Load the committed prod manifest snapshot, if available."""
|
|
680
|
+
if self._prod_manifest_loaded:
|
|
681
|
+
return self._prod_manifest
|
|
682
|
+
self._prod_manifest_loaded = True
|
|
683
|
+
if not self.dbt_project_path:
|
|
684
|
+
return None
|
|
685
|
+
if self._manifest_from_snapshot:
|
|
686
|
+
return None
|
|
687
|
+
snapshot_path = self.dbt_project_path / "manifest.snapshot.json"
|
|
688
|
+
if not snapshot_path.exists():
|
|
689
|
+
return None
|
|
690
|
+
import json
|
|
691
|
+
|
|
692
|
+
try:
|
|
693
|
+
with open(snapshot_path) as f:
|
|
694
|
+
self._prod_manifest = json.load(f)
|
|
695
|
+
except (OSError, json.JSONDecodeError) as e:
|
|
696
|
+
logger.debug("Failed to load prod manifest snapshot: %s", e)
|
|
697
|
+
return self._prod_manifest
|
|
698
|
+
|
|
699
|
+
def _resolve_dbt_sql(self, sql: str) -> tuple[str, list[ResolvedRelation]]:
|
|
700
|
+
"""Resolve SQL with dbt-specific Jinja functions (ref, source)."""
|
|
701
|
+
from dataface.core.execute.adapters.dbt_utils import (
|
|
702
|
+
resolve_dbt_refs_with_provenance,
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
manifest = self._get_manifest()
|
|
706
|
+
if not manifest:
|
|
707
|
+
return sql, []
|
|
708
|
+
|
|
709
|
+
prod_manifest = self._get_prod_manifest()
|
|
710
|
+
return resolve_dbt_refs_with_provenance(sql, manifest, prod_manifest)
|