quickdbd-cli 0.4.3__tar.gz → 0.6.0__tar.gz
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.
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/PKG-INFO +3 -2
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/README.md +1 -1
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/pyproject.toml +2 -1
- quickdbd_cli-0.6.0/src/quickdbd_cli/__init__.py +1 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/src/quickdbd_cli/assets/syntax-reference.md +2 -1
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/src/quickdbd_cli/cli.py +88 -10
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/src/quickdbd_cli/convert.py +14 -3
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/src/quickdbd_cli/dbml.py +23 -3
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/src/quickdbd_cli/dbt.py +5 -3
- quickdbd_cli-0.6.0/src/quickdbd_cli/dbtyml.py +275 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/src/quickdbd_cli/diff.py +1 -1
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/src/quickdbd_cli/html.py +25 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/src/quickdbd_cli/measure.py +10 -8
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/src/quickdbd_cli/model.py +36 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/src/quickdbd_cli/parser.py +51 -7
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/src/quickdbd_cli/serve.py +20 -9
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/src/quickdbd_cli/tags.py +2 -2
- quickdbd_cli-0.6.0/tests/fixtures/dbt-yml/dbt_project.yml +4 -0
- quickdbd_cli-0.6.0/tests/fixtures/dbt-yml/erd/schema.txt +34 -0
- quickdbd_cli-0.6.0/tests/fixtures/dbt-yml/models/marts/customers.sql +1 -0
- quickdbd_cli-0.6.0/tests/fixtures/dbt-yml/models/marts/docs.md +7 -0
- quickdbd_cli-0.6.0/tests/fixtures/dbt-yml/models/marts/orders.sql +1 -0
- quickdbd_cli-0.6.0/tests/fixtures/dbt-yml/models/marts/payments.sql +1 -0
- quickdbd_cli-0.6.0/tests/fixtures/dbt-yml/models/marts/schema.yml +36 -0
- quickdbd_cli-0.6.0/tests/test_dbtyml.py +229 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/test_pipeline.py +49 -0
- quickdbd_cli-0.4.3/src/quickdbd_cli/__init__.py +0 -1
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/.gitignore +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/LICENSE +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/NOTICE +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/examples/quickdbd-sample.txt +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/examples/synthetic/finance.txt +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/examples/synthetic/hr.txt +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/examples/synthetic/inventory.txt +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/examples/synthetic/marketing.txt +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/examples/synthetic/qdbd.layout.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/examples/synthetic/sales.txt +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/scripts/build_gallery.py +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/scripts/gen_font_metrics.py +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/scripts/gen_schema.py +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/scripts/gen_thumbnails.py +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/scripts/import_dbml_corpus.py +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/scripts/release.sh +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/src/quickdbd_cli/__main__.py +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/src/quickdbd_cli/agent.py +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/src/quickdbd_cli/assets/Inter-Bold.ttf +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/src/quickdbd_cli/assets/Inter-Bold.woff2 +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/src/quickdbd_cli/assets/Inter-LICENSE.txt +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/src/quickdbd_cli/assets/Inter-Regular.ttf +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/src/quickdbd_cli/assets/Inter-Regular.woff2 +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/src/quickdbd_cli/assets/editor.js +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/src/quickdbd_cli/assets/elk-LICENSE.md +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/src/quickdbd_cli/assets/elk.bundled.js +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/src/quickdbd_cli/assets/font-metrics.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/src/quickdbd_cli/assets/icon.svg +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/src/quickdbd_cli/assets/render.js +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/src/quickdbd_cli/assets/style.css +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/src/quickdbd_cli/assets/viewer.js +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/src/quickdbd_cli/export.py +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/src/quickdbd_cli/jsrt.py +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/src/quickdbd_cli/layout.py +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/src/quickdbd_cli/mcp.py +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/src/quickdbd_cli/project.py +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/src/quickdbd_cli/share.py +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/src/quickdbd_cli/targets.py +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml/team.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/LICENSE +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/README.md +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/binder/alias_of_duplicated_names.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/binder/alias_of_duplicated_names.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/binder/dep.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/binder/dep.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/binder/duplicate_columns.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/binder/duplicate_columns.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/binder/duplicate_enum_field.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/binder/duplicate_enum_field.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/binder/duplicate_name.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/binder/duplicate_name.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/binder/duplicate_names.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/binder/duplicate_names.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/binder/duplicate_table_partial_injections.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/binder/duplicate_table_partial_injections.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/binder/enum_as_default_column_value.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/binder/enum_as_default_column_value.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/binder/enum_name.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/binder/enum_name.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/binder/erroneous.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/binder/erroneous.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/binder/multiple_notes_in_table_group.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/binder/multiple_notes_in_table_group.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/binder/nonexisting_column_in_index.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/binder/nonexisting_column_in_index.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/binder/nonexisting_column_in_index_table_partial.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/binder/nonexisting_column_in_index_table_partial.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/binder/nonexisting_inline_ref_column_in_table.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/binder/nonexisting_inline_ref_column_in_table.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/binder/nonexisting_inline_ref_column_in_table_partial.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/binder/nonexisting_inline_ref_column_in_table_partial.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/binder/ref_validator.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/binder/ref_validator.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/binder/sticky_notes_validator.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/binder/sticky_notes_validator.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/binder/table_partial.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/binder/table_partial.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/binder/unknown_table_group_field.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/binder/unknown_table_group_field.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/array_type.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/array_type.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/checks.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/checks.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/circular_ref_1_inline_1_element.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/circular_ref_1_inline_1_element.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/circular_ref_2_elements.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/circular_ref_2_elements.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/circular_ref_2_inlines.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/circular_ref_2_inlines.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/column_caller_type.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/column_caller_type.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/comment.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/comment.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/composite_pk.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/composite_pk.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/default_tables.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/default_tables.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/dep.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/dep.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/dep_color.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/dep_color.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/dep_duplicate.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/dep_duplicate.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/dep_pipeline.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/dep_pipeline.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/enum_as_default_column_value.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/enum_as_default_column_value.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/enum_tables.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/enum_tables.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/erroneous.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/erroneous.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/general_schema.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/general_schema.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/header_color_tables.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/header_color_tables.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/index_table_partial.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/index_table_partial.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/index_tables.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/index_tables.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/metadata.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/metadata.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/metadata_errors.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/metadata_errors.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/multi_notes.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/multi_notes.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/multiline_string.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/multiline_string.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/negative_number.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/negative_number.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/note_normalize.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/note_normalize.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/note_normalize_with_top_empty_lines.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/note_normalize_with_top_empty_lines.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/old_undocumented_syntax.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/old_undocumented_syntax.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/primary_key.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/primary_key.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/project.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/project.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/ref_name_and_color_setting.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/ref_name_and_color_setting.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/ref_settings.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/ref_settings.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/referential_actions.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/referential_actions.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/self_referential_ref_in_tablepartial.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/self_referential_ref_in_tablepartial.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/sticky_notes.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/sticky_notes.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/table_element.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/table_element.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/table_group.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/table_group.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/table_group_element.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/table_group_element.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/table_group_settings.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/table_group_settings.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/table_partial.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/table_partial.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/table_reappear_tablegroup.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/table_reappear_tablegroup.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/table_settings.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/table_settings.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/tablepartial_causing_circular_ref.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/tablepartial_causing_circular_ref.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/two_inline_refs.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/interpreter/two_inline_refs.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/parser/call_expression.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/parser/call_expression.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/parser/element_in_simple_body.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/parser/element_in_simple_body.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/parser/erroneous_setting.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/parser/erroneous_setting.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/parser/expression.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/parser/expression.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/parser/last_invalid_number.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/parser/last_invalid_number.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/parser/list_expression.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/parser/list_expression.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/parser/trailing_comments.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/parser/trailing_comments.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/parser/tuple_expression.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/parser/tuple_expression.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/validator/checks.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/validator/checks.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/validator/complex_names.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/validator/complex_names.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/validator/dep.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/validator/dep.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/validator/enum.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/validator/enum.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/validator/invalid_args.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/validator/invalid_args.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/validator/public_schema.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/validator/public_schema.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/validator/redefined_note.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/validator/redefined_note.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/validator/ref_error_setting.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/validator/ref_error_setting.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/validator/ref_in_table.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/validator/ref_in_table.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/validator/schema_nested_tablegroup.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/validator/schema_nested_tablegroup.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/validator/table_group_settings.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/validator/table_group_settings.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/validator/table_partial_check.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/validator/table_partial_check.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/validator/table_partial_settings_general.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/validator/table_partial_settings_general.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/validator/table_settings_check.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/validator/table_settings_check.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/validator/table_settings_general.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/validator/table_settings_general.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/validator/wrong_sub_element_declarations.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/validator/wrong_sub_element_declarations.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/validator/wrong_table_partial_injection_syntax.dbml +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/dbml-ref/validator/wrong_table_partial_injection_syntax.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/jaffle/LICENSE +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/jaffle/README.md +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/jaffle/catalog.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/jaffle/manifest.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/fixtures/targets/cases.json +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/test_agent.py +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/test_convert.py +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/test_dbml.py +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/test_dbml_pipeline.py +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/test_dbt.py +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/test_diff.py +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/test_gallery.py +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/test_home_hook.py +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/test_mcp.py +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/test_packaging.py +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/test_parser.py +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/test_path.py +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/test_render.py +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/test_select.py +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/test_serve.py +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/test_share.py +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/test_tags.py +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/test_targets.py +0 -0
- {quickdbd_cli-0.4.3 → quickdbd_cli-0.6.0}/tests/test_viewer_contract.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: quickdbd-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Compile QuickDBD-syntax schema files into interactive HTML ER diagrams, with no table limit.
|
|
5
5
|
Project-URL: Homepage, https://qdbd.dev/
|
|
6
6
|
Project-URL: Documentation, https://qdbd.dev/guide/
|
|
@@ -25,6 +25,7 @@ Classifier: Topic :: Database
|
|
|
25
25
|
Classifier: Topic :: Software Development :: Documentation
|
|
26
26
|
Requires-Python: >=3.11
|
|
27
27
|
Requires-Dist: mini-racer>=0.14
|
|
28
|
+
Requires-Dist: pyyaml>=6
|
|
28
29
|
Requires-Dist: resvg-py>=0.5
|
|
29
30
|
Provides-Extra: dev
|
|
30
31
|
Requires-Dist: fonttools>=4.50; extra == 'dev'
|
|
@@ -193,7 +194,7 @@ jobs:
|
|
|
193
194
|
permissions: { contents: read, pull-requests: write }
|
|
194
195
|
steps:
|
|
195
196
|
- uses: actions/checkout@v4
|
|
196
|
-
- uses: MarkPhamm/quickdbd-cli@v0.
|
|
197
|
+
- uses: MarkPhamm/quickdbd-cli@v0.6.0
|
|
197
198
|
with:
|
|
198
199
|
path: erd/ # schema file(s) or folder(s)
|
|
199
200
|
output: site/index.html
|
|
@@ -155,7 +155,7 @@ jobs:
|
|
|
155
155
|
permissions: { contents: read, pull-requests: write }
|
|
156
156
|
steps:
|
|
157
157
|
- uses: actions/checkout@v4
|
|
158
|
-
- uses: MarkPhamm/quickdbd-cli@v0.
|
|
158
|
+
- uses: MarkPhamm/quickdbd-cli@v0.6.0
|
|
159
159
|
with:
|
|
160
160
|
path: erd/ # schema file(s) or folder(s)
|
|
161
161
|
output: site/index.html
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "quickdbd-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.6.0"
|
|
8
8
|
description = "Compile QuickDBD-syntax schema files into interactive HTML ER diagrams, with no table limit."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -25,6 +25,7 @@ classifiers = [
|
|
|
25
25
|
]
|
|
26
26
|
dependencies = [
|
|
27
27
|
"mini-racer>=0.14",
|
|
28
|
+
"pyyaml>=6",
|
|
28
29
|
"resvg-py>=0.5",
|
|
29
30
|
]
|
|
30
31
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.6.0"
|
|
@@ -79,6 +79,7 @@ Declare a relationship once, in either direction.
|
|
|
79
79
|
- Tags give tables a role and a header colour: `# @tag: gold, pii` above a table. Names like `dim_`, `fct_`, `stg_`, `int_`, `raw_` are tagged automatically.
|
|
80
80
|
- `# @color gold #b8860b` sets a tag's colour (anywhere in any file).
|
|
81
81
|
- Columns take tags too, on or above the column line: `discount decimal # @tag: added`. The row is tinted in the tag's colour; `added` is green, `removed` is red and struck through. Use them to mark proposed column changes for review.
|
|
82
|
+
- `# @dbt: ../my_dbt_project` (anywhere in one file; the path is relative to that file) links a dbt project. Tables matching a model, seed, snapshot or source (`"source_name.table"`) by name get its yml description (shown before the comment), tags and `data_type` (for columns with no type). Where the schema and the yml disagree, `qdbd check` warns (`--strict` fails). `# @dbt: none` above a table skips it.
|
|
82
83
|
|
|
83
84
|
## Settings and layout sections
|
|
84
85
|
|
|
@@ -128,7 +129,7 @@ Ref: order_items.(order_id, region) > orders.(id, region)
|
|
|
128
129
|
- Drawn: tables (`schema.table`, `public.users` = `users`), columns and types, `pk`, `null`, `unique`, `increment`, `default:`, indexes, `note`/`Note`, every `Ref` form (short, long, inline, composite, with `?` for an optional side), `TableGroup`, `headercolor`, `TablePartial`.
|
|
129
130
|
- Accepted but not drawn: `Project`, `Enum` (the column shows the enum name as its type), `checks`, `records`, sticky notes, ref `delete:`/`update:`, `TableGroup` colours and notes.
|
|
130
131
|
- `use` imports aren't followed: every file you give qdbd is read into one namespace.
|
|
131
|
-
- `// @tag
|
|
132
|
+
- `// @tag:`, `// @color` and `// @dbt:` comments work; plain `//` comments are not documentation (use `note`).
|
|
132
133
|
|
|
133
134
|
# Notes
|
|
134
135
|
|
|
@@ -32,11 +32,12 @@ def _open(args) -> Project:
|
|
|
32
32
|
return Project.open([Path(p) for p in args.paths], Path(args.layout) if getattr(args, "layout", None) else None)
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
def _name(p: Path) -> str:
|
|
36
|
+
return p.stem if p.is_file() else (p.resolve().name or "diagram")
|
|
37
|
+
|
|
38
|
+
|
|
35
39
|
def _title(project: Project) -> str:
|
|
36
|
-
if len(project.inputs) == 1
|
|
37
|
-
p = project.inputs[0]
|
|
38
|
-
return p.stem if p.is_file() else (p.resolve().name or "diagram")
|
|
39
|
-
return "diagram"
|
|
40
|
+
return _name(project.inputs[0]) if len(project.inputs) == 1 else "diagram"
|
|
40
41
|
|
|
41
42
|
|
|
42
43
|
def _github_annotation(d: Diagnostic, level: str) -> str:
|
|
@@ -57,14 +58,14 @@ def _symbols(schema: Schema) -> dict:
|
|
|
57
58
|
tables = []
|
|
58
59
|
for t in schema.tables.values():
|
|
59
60
|
cols = [
|
|
60
|
-
{"name": c.name, "line": c.line, "type": c.
|
|
61
|
+
{"name": c.name, "line": c.line, "type": c.shown_type(), "pk": c.pk, "fk": c.fk}
|
|
61
62
|
for c in t.columns.values()
|
|
62
63
|
if not c.virtual
|
|
63
64
|
]
|
|
64
65
|
end = t.end_line or max([t.line + 1] + [c["line"] for c in cols])
|
|
65
66
|
table = {"name": t.name, "aliases": t.aliases, "file": t.path, "line": t.line, "endLine": end, "columns": cols}
|
|
66
|
-
if t.
|
|
67
|
-
table["doc"] =
|
|
67
|
+
if t.doc():
|
|
68
|
+
table["doc"] = t.doc()
|
|
68
69
|
tables.append(table)
|
|
69
70
|
|
|
70
71
|
def end(e) -> dict:
|
|
@@ -76,7 +77,30 @@ def _symbols(schema: Schema) -> dict:
|
|
|
76
77
|
return {"tables": tables, "rels": rels}
|
|
77
78
|
|
|
78
79
|
|
|
80
|
+
def _check_each(args) -> int:
|
|
81
|
+
"""Check each PATH on its own, as ``build --site`` builds them; folders with no schema files are skipped."""
|
|
82
|
+
from .project import discover
|
|
83
|
+
|
|
84
|
+
if args.format == "json" or args.stdin_file:
|
|
85
|
+
print("error: --each can't be used with --format json or --stdin-file", file=sys.stderr)
|
|
86
|
+
return 2
|
|
87
|
+
code, checked = 0, 0
|
|
88
|
+
for p in args.paths:
|
|
89
|
+
if Path(p).is_dir() and not discover([Path(p)]):
|
|
90
|
+
print(f"Skipped {p}: no .txt or .dbml schema files", file=sys.stderr)
|
|
91
|
+
continue
|
|
92
|
+
print(f"{p}:")
|
|
93
|
+
code = max(code, cmd_check(argparse.Namespace(**{**vars(args), "paths": [p], "each": False})))
|
|
94
|
+
checked += 1
|
|
95
|
+
if not checked:
|
|
96
|
+
print("error: no schema files found in: " + ", ".join(args.paths), file=sys.stderr)
|
|
97
|
+
return 2
|
|
98
|
+
return code
|
|
99
|
+
|
|
100
|
+
|
|
79
101
|
def cmd_check(args) -> int:
|
|
102
|
+
if args.each:
|
|
103
|
+
return _check_each(args)
|
|
80
104
|
project = _open(args)
|
|
81
105
|
overrides = None
|
|
82
106
|
if args.stdin_file:
|
|
@@ -143,7 +167,52 @@ def _built_page(args) -> tuple[str, dict] | int:
|
|
|
143
167
|
return render_page(model, pos, _title(project)), model
|
|
144
168
|
|
|
145
169
|
|
|
170
|
+
def _build_site(args) -> int:
|
|
171
|
+
"""One page per PATH plus an index linking them, in the folder ``args.site``."""
|
|
172
|
+
from urllib.parse import quote
|
|
173
|
+
|
|
174
|
+
from .html import render_index
|
|
175
|
+
from .project import discover
|
|
176
|
+
|
|
177
|
+
if args.layout:
|
|
178
|
+
print("error: --layout can't be used with --site: each PATH uses its own layout file", file=sys.stderr)
|
|
179
|
+
return 2
|
|
180
|
+
pages, seen, failed = [], {}, 0
|
|
181
|
+
for p in map(Path, args.paths):
|
|
182
|
+
if p.is_dir() and not discover([p]):
|
|
183
|
+
print(f"Skipped {p}: no .txt or .dbml schema files", file=sys.stderr)
|
|
184
|
+
continue
|
|
185
|
+
name = _name(p)
|
|
186
|
+
if name.lower() in seen or name.lower() == "index":
|
|
187
|
+
other = seen.get(name.lower(), "the index page")
|
|
188
|
+
print(f"error: {p} and {other} would both be written to {name}.html", file=sys.stderr)
|
|
189
|
+
return 2
|
|
190
|
+
seen[name.lower()] = p
|
|
191
|
+
res = _built_page(argparse.Namespace(**{**vars(args), "paths": [str(p)]}))
|
|
192
|
+
if isinstance(res, int):
|
|
193
|
+
failed = max(failed, res)
|
|
194
|
+
continue
|
|
195
|
+
pages.append((name, *res))
|
|
196
|
+
if failed: # nothing is written, so a failed build never leaves a half-updated site
|
|
197
|
+
return failed
|
|
198
|
+
if not pages:
|
|
199
|
+
print("error: no schema files found in: " + ", ".join(args.paths), file=sys.stderr)
|
|
200
|
+
return 2
|
|
201
|
+
out = Path(args.site)
|
|
202
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
203
|
+
index = []
|
|
204
|
+
for name, page, model in pages:
|
|
205
|
+
(out / f"{name}.html").write_text(page, encoding="utf-8")
|
|
206
|
+
index.append((name, quote(f"{name}.html"), len(model["tables"]), len(model["rels"])))
|
|
207
|
+
print(f"Wrote {out / f'{name}.html'} ({len(model['tables'])} tables, {len(model['rels'])} relationships)")
|
|
208
|
+
(out / "index.html").write_text(render_index(index), encoding="utf-8")
|
|
209
|
+
print(f"Wrote {out / 'index.html'} ({len(pages)} diagrams)")
|
|
210
|
+
return 0
|
|
211
|
+
|
|
212
|
+
|
|
146
213
|
def cmd_build(args) -> int:
|
|
214
|
+
if args.site:
|
|
215
|
+
return _build_site(args)
|
|
147
216
|
res = _built_page(args)
|
|
148
217
|
if isinstance(res, int):
|
|
149
218
|
return res
|
|
@@ -337,13 +406,13 @@ def cmd_convert(args) -> int:
|
|
|
337
406
|
root = next((p for p in inputs if p.is_dir() and f.resolve().is_relative_to(p.resolve())), None)
|
|
338
407
|
rel = f.resolve().relative_to(root.resolve()).as_posix() if root else f.name
|
|
339
408
|
sources.append((str(f), rel, f.read_text(encoding="utf-8")))
|
|
340
|
-
schema = compile_sources([(d, t) for d, _, t in sources])
|
|
409
|
+
schema = compile_sources([(d, t) for d, _, t in sources], dbt=False)
|
|
341
410
|
if not _print_diagnostics(schema, False):
|
|
342
411
|
return 1
|
|
343
412
|
res = convert.convert(sources, args.to, split_groups=to_dir)
|
|
344
413
|
for w in res.warnings:
|
|
345
414
|
print(w.format(), file=sys.stderr)
|
|
346
|
-
again = compile_sources(list(res.files.items()))
|
|
415
|
+
again = compile_sources(list(res.files.items()), dbt=False)
|
|
347
416
|
if again.errors or convert.canonical(again, res.lost) != convert.canonical(schema, res.lost):
|
|
348
417
|
for d in again.errors:
|
|
349
418
|
print(d.format(), file=sys.stderr)
|
|
@@ -575,11 +644,20 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
575
644
|
metavar="PATH",
|
|
576
645
|
help="read this schema file's text from stdin instead of disk (an editor's unsaved buffer)",
|
|
577
646
|
)
|
|
647
|
+
sp.add_argument(
|
|
648
|
+
"--each",
|
|
649
|
+
action="store_true",
|
|
650
|
+
help="check each PATH as its own schema, as build --site does (folders with no schema files are skipped)",
|
|
651
|
+
)
|
|
578
652
|
sp.set_defaults(func=cmd_check)
|
|
579
653
|
|
|
580
654
|
sp = sub.add_parser("build", help="write an interactive HTML diagram")
|
|
581
655
|
paths(sp)
|
|
582
|
-
sp.
|
|
656
|
+
out = sp.add_mutually_exclusive_group()
|
|
657
|
+
out.add_argument("-o", "--output", default="diagram.html")
|
|
658
|
+
out.add_argument(
|
|
659
|
+
"--site", metavar="DIR", help="one page per PATH (named after it) plus an index.html linking them, in DIR"
|
|
660
|
+
)
|
|
583
661
|
selectors(sp)
|
|
584
662
|
sp.add_argument(
|
|
585
663
|
"--diff-base", metavar="GIT_REF", help="colour tables/columns changed since this git ref (e.g. origin/main)"
|
|
@@ -38,7 +38,7 @@ class ConvertResult:
|
|
|
38
38
|
|
|
39
39
|
def convert(sources: list[tuple[str, str, str]], to: str, split_groups: bool) -> ConvertResult:
|
|
40
40
|
"""``sources``: ``(display path, output-relative path, text)``. Files already in ``to`` are copied as is."""
|
|
41
|
-
schema, parses = compile_files([(d, t) for d, _, t in sources])
|
|
41
|
+
schema, parses = compile_files([(d, t) for d, _, t in sources], dbt=False)
|
|
42
42
|
res = ConvertResult()
|
|
43
43
|
for t in schema.tables.values():
|
|
44
44
|
if t.id in schema.layout:
|
|
@@ -88,7 +88,10 @@ def canonical(schema: Schema, lost: set[tuple]) -> dict:
|
|
|
88
88
|
if ("col", t.name, c.name, k) in lost:
|
|
89
89
|
del col[k]
|
|
90
90
|
cols[c.name] = col
|
|
91
|
-
table = {
|
|
91
|
+
table = {
|
|
92
|
+
"cols": cols, "doc": _doc(t.comments), "aliases": t.aliases, "tags": t.tags, "dbt_tags": t.dbt_tags,
|
|
93
|
+
"dbt_skip": t.dbt_skip,
|
|
94
|
+
} # fmt: skip
|
|
92
95
|
for k in list(table):
|
|
93
96
|
if ("table", t.name, k) in lost:
|
|
94
97
|
del table[k]
|
|
@@ -153,7 +156,9 @@ class _DbmlWriter:
|
|
|
153
156
|
self.warn(line, f"settings: [settings] {k.capitalize()} has no DBML equivalent; dropped")
|
|
154
157
|
for tag, (color, _) in sorted(fp.colors.items()):
|
|
155
158
|
lines.append(f"// @color {tag} {color}")
|
|
156
|
-
if fp.
|
|
159
|
+
if fp.dbt: # shortcut: kept as written, so it breaks if -o puts the file in another folder
|
|
160
|
+
lines.append(f"// @dbt: {fp.dbt[0]}")
|
|
161
|
+
if fp.colors or fp.dbt:
|
|
157
162
|
lines.append("")
|
|
158
163
|
for t in (t for t in self.schema.tables.values() if t.path == fp.path):
|
|
159
164
|
lines += self.table(t)
|
|
@@ -170,6 +175,8 @@ class _DbmlWriter:
|
|
|
170
175
|
out.append(f"// @tag: {', '.join(t.tags)}")
|
|
171
176
|
if t.dbt_tags:
|
|
172
177
|
out.append(f"// @dbt-tags: {', '.join(t.dbt_tags)}")
|
|
178
|
+
if t.dbt_skip:
|
|
179
|
+
out.append("// @dbt: none")
|
|
173
180
|
head = f"Table {_dbml_name(t.name)}"
|
|
174
181
|
if t.aliases:
|
|
175
182
|
head += f" as {_dbml_name(t.aliases[0])}"
|
|
@@ -313,6 +320,8 @@ class _QuickdbdWriter:
|
|
|
313
320
|
header.append(f"# enum {name}: {', '.join(values)}")
|
|
314
321
|
for tag, (color, _) in sorted(fp.colors.items()):
|
|
315
322
|
header.append(f"# @color {tag} {color}")
|
|
323
|
+
if fp.dbt: # shortcut: kept as written, so it breaks if -o puts the file in another folder
|
|
324
|
+
header.append(f"# @dbt: {fp.dbt[0]}")
|
|
316
325
|
if not self.split:
|
|
317
326
|
for group, line in fp.groups.items():
|
|
318
327
|
self.warn(line, f"TableGroup {group}: QuickDBD groups tables by file (use -o DIR to split); dropped")
|
|
@@ -347,6 +356,8 @@ class _QuickdbdWriter:
|
|
|
347
356
|
out.append(f"# @tag: {', '.join(t.tags)}")
|
|
348
357
|
if t.dbt_tags:
|
|
349
358
|
out.append(f"# @dbt-tags: {', '.join(t.dbt_tags)}")
|
|
359
|
+
if t.dbt_skip:
|
|
360
|
+
out.append("# @dbt: none")
|
|
350
361
|
out.append(_qdbd_name(t.name) + (f" as {', '.join(t.aliases)}" if t.aliases else ""))
|
|
351
362
|
out.append("-")
|
|
352
363
|
for c in t.columns.values():
|
|
@@ -16,7 +16,18 @@ import unicodedata
|
|
|
16
16
|
from dataclasses import dataclass, field
|
|
17
17
|
|
|
18
18
|
from .model import Column, Diagnostic, Table
|
|
19
|
-
from .parser import
|
|
19
|
+
from .parser import (
|
|
20
|
+
COLOR_RE,
|
|
21
|
+
DBT_RE,
|
|
22
|
+
HEX_RE,
|
|
23
|
+
TAG_NAME_RE,
|
|
24
|
+
TAG_RE,
|
|
25
|
+
FileParse,
|
|
26
|
+
PendingRel,
|
|
27
|
+
encode,
|
|
28
|
+
is_dbt_none,
|
|
29
|
+
split_tags,
|
|
30
|
+
)
|
|
20
31
|
|
|
21
32
|
# Relationship operator -> (cardinality of the left end, of the right end). A `?` makes its own side optional.
|
|
22
33
|
_BASE = {"-": ("one", "one"), "<>": ("many", "many"), ">": ("many", "one"), "<": ("one", "many")}
|
|
@@ -1287,7 +1298,9 @@ class _Interpreter:
|
|
|
1287
1298
|
line = t.line - 1
|
|
1288
1299
|
while line in self.comments and line not in self.code_lines:
|
|
1289
1300
|
m = TAG_RE.match(self.comments[line])
|
|
1290
|
-
if
|
|
1301
|
+
if is_dbt_none(self.comments[line]):
|
|
1302
|
+
t.dbt_skip = True
|
|
1303
|
+
elif m:
|
|
1291
1304
|
target = t.dbt_tags if m.group(1).lower().startswith("dbt") else t.tags
|
|
1292
1305
|
target[:0] = [x for x in split_tags(m.group(2)) if x not in target]
|
|
1293
1306
|
line -= 1
|
|
@@ -1306,10 +1319,17 @@ class _Interpreter:
|
|
|
1306
1319
|
|
|
1307
1320
|
def colors(self) -> None:
|
|
1308
1321
|
first_code = min(self.code_lines, default=0)
|
|
1322
|
+
|
|
1323
|
+
def directive(text: str) -> bool:
|
|
1324
|
+
return bool(COLOR_RE.match(text) or (DBT_RE.match(text) and not is_dbt_none(text)))
|
|
1325
|
+
|
|
1309
1326
|
self.fp.general_comments = [
|
|
1310
|
-
text for line, text in sorted(self.comments.items()) if line < first_code and not
|
|
1327
|
+
text for line, text in sorted(self.comments.items()) if line < first_code and not directive(text)
|
|
1311
1328
|
]
|
|
1312
1329
|
for line, text in sorted(self.comments.items()):
|
|
1330
|
+
d = DBT_RE.match(text)
|
|
1331
|
+
if d and not is_dbt_none(text):
|
|
1332
|
+
self.fp.set_dbt(d.group(1), line)
|
|
1313
1333
|
m = COLOR_RE.match(text)
|
|
1314
1334
|
if not m:
|
|
1315
1335
|
continue
|
|
@@ -174,10 +174,12 @@ def _comment_lines(text: str) -> list[str]:
|
|
|
174
174
|
return out
|
|
175
175
|
|
|
176
176
|
|
|
177
|
-
def
|
|
177
|
+
def node_tags(n: dict) -> list[str]:
|
|
178
178
|
"""dbt tags (node-level and config), lowercased, in order, usable as qdbd tag names."""
|
|
179
179
|
out: list[str] = []
|
|
180
|
-
raw
|
|
180
|
+
raw: list = []
|
|
181
|
+
for v in (n.get("tags"), (n.get("config") or {}).get("tags")):
|
|
182
|
+
raw += [v] if isinstance(v, str) else list(v or []) # yml allows `tags: pii`
|
|
181
183
|
for tag in raw:
|
|
182
184
|
tag = re.sub(r"[^a-z0-9_.-]+", "-", str(tag).lower()).strip("-.")
|
|
183
185
|
if tag and tag not in out:
|
|
@@ -243,7 +245,7 @@ class _Importer:
|
|
|
243
245
|
name = self._base_name(n)
|
|
244
246
|
if names[name] > 1: # same name in two packages
|
|
245
247
|
name = f"{n.get('package_name')}.{name}"
|
|
246
|
-
t = Tbl(n["unique_id"], name, self._group(n), n.get("description") or "", tags=
|
|
248
|
+
t = Tbl(n["unique_id"], name, self._group(n), n.get("description") or "", tags=node_tags(n))
|
|
247
249
|
self._columns(t, n)
|
|
248
250
|
self.tables[t.uid] = t
|
|
249
251
|
self.by_name.setdefault(n.get("name", "").lower(), []).append(t.uid)
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""`# @dbt: PATH`: descriptions, tags and types from a dbt project's yml, for hand-written schema files.
|
|
2
|
+
|
|
3
|
+
No manifest.json is needed (dbt Cloud projects have none locally). qdbd reads ``dbt_project.yml`` and the yml, md
|
|
4
|
+
and sql files under its resource paths (the root project only; ``dbt_packages/`` and ``target/`` are skipped).
|
|
5
|
+
|
|
6
|
+
- Tables match models, seeds, snapshots and sources (``source_name.table_name``) by name, ignoring case;
|
|
7
|
+
columns match by name inside the matched table. ``# @dbt: none`` above a table opts it out.
|
|
8
|
+
- The dbt description is shown before the schema file's own comment; ``{{ doc('x') }}`` is filled from
|
|
9
|
+
``{% docs x %}`` blocks. Model and column tags rank below ``# @tag:`` and name prefixes. ``data_type`` fills
|
|
10
|
+
columns that have no type.
|
|
11
|
+
- Drift is reported as warnings (``--strict`` fails on them): a table dbt doesn't have, a column only one side has,
|
|
12
|
+
a type that differs (by base type: ``varchar(50)`` and ``string`` agree).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
import re
|
|
19
|
+
from collections.abc import Callable
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
import yaml
|
|
24
|
+
|
|
25
|
+
from .dbt import node_tags, norm_type
|
|
26
|
+
from .model import DbtInfo, Schema
|
|
27
|
+
|
|
28
|
+
SKIP_DIRS = {"dbt_packages", "dbt_modules", "target", "logs", "node_modules"}
|
|
29
|
+
WATCHED = (".yml", ".yaml", ".md", ".sql", ".py", ".csv")
|
|
30
|
+
_DOC_REF = re.compile(r"""\{\{\s*doc\(\s*(?:['"][^'"]*['"]\s*,\s*)?['"]([^'"]+)['"]\s*\)\s*\}\}""")
|
|
31
|
+
_DOCS_BLOCK = re.compile(r"\{%-?\s*docs\s+([\w.-]+)\s*-?%\}(.*?)\{%-?\s*enddocs\s*-?%\}", re.S)
|
|
32
|
+
_SNAPSHOT = re.compile(r"\{%-?\s*snapshot\s+(\w+)\s*-?%\}")
|
|
33
|
+
|
|
34
|
+
# Base types that mean the same thing across warehouses; sizes and precision are ignored.
|
|
35
|
+
_FAMILIES = {
|
|
36
|
+
"int": "int integer int2 int4 int8 int16 int32 int64 smallint bigint tinyint byteint mediumint hugeint long short",
|
|
37
|
+
"decimal": "decimal numeric number dec bignumeric",
|
|
38
|
+
"float": "float float4 float8 float32 float64 double double_precision real",
|
|
39
|
+
"string": "string text varchar char character nvarchar nchar bpchar varchar2 nvarchar2 citext clob",
|
|
40
|
+
"bool": "bool boolean",
|
|
41
|
+
"timestamp": "timestamp timestamptz datetime datetime2 smalldatetime datetimeoffset timestamp_ntz timestamp_ltz "
|
|
42
|
+
"timestamp_tz",
|
|
43
|
+
"time": "time timetz",
|
|
44
|
+
"json": "json jsonb variant object super",
|
|
45
|
+
"binary": "binary varbinary bytes bytea blob",
|
|
46
|
+
"uuid": "uuid uniqueidentifier",
|
|
47
|
+
"array": "array list",
|
|
48
|
+
}
|
|
49
|
+
_BASE = {name: family for family, names in _FAMILIES.items() for name in names.split()}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class DbtError(Exception):
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class DbtColumn:
|
|
58
|
+
name: str
|
|
59
|
+
description: str
|
|
60
|
+
data_type: str | None
|
|
61
|
+
tags: list[str]
|
|
62
|
+
line: int
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class DbtModel:
|
|
67
|
+
name: str # model / seed / snapshot name, or `source_name.table_name`
|
|
68
|
+
description: str
|
|
69
|
+
tags: list[str]
|
|
70
|
+
columns: dict[str, DbtColumn] # by lowercased name
|
|
71
|
+
path: str # yml file, relative to the project root
|
|
72
|
+
line: int
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class DbtProject:
|
|
77
|
+
root: Path
|
|
78
|
+
models: dict[str, DbtModel] = field(default_factory=dict) # by lowercased name
|
|
79
|
+
known: set[str] = field(default_factory=set) # lowercased names that exist as files, documented or not
|
|
80
|
+
ambiguous: set[str] = field(default_factory=set)
|
|
81
|
+
docs: dict[str, str] = field(default_factory=dict) # `{% docs name %}` blocks
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class _Loader(getattr(yaml, "CSafeLoader", yaml.SafeLoader)): # type: ignore[misc]
|
|
85
|
+
"""Records each mapping's line as `__line__`, so drift messages can point into the yml."""
|
|
86
|
+
|
|
87
|
+
def construct_mapping(self, node, deep=False):
|
|
88
|
+
m = super().construct_mapping(node, deep=deep)
|
|
89
|
+
m["__line__"] = node.start_mark.line + 1
|
|
90
|
+
return m
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# (path, parser) -> (mtime_ns, parsed): keeps `qdbd serve` recompiles cheap
|
|
94
|
+
_cache: dict[tuple[Path, Callable], tuple[int, object]] = {}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _cached(path: Path, parse: Callable[[str], object]) -> object:
|
|
98
|
+
mtime = path.stat().st_mtime_ns
|
|
99
|
+
hit = _cache.get((path, parse))
|
|
100
|
+
if hit is None or hit[0] != mtime:
|
|
101
|
+
hit = _cache[(path, parse)] = (mtime, parse(path.read_text(encoding="utf-8")))
|
|
102
|
+
return hit[1]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _load_yaml(text: str) -> object:
|
|
106
|
+
return yaml.load(text, Loader=_Loader) # a SafeLoader: no arbitrary Python objects
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _yaml(path: Path, root: Path) -> object:
|
|
110
|
+
try:
|
|
111
|
+
return _cached(path, _load_yaml)
|
|
112
|
+
except yaml.YAMLError as e:
|
|
113
|
+
mark = getattr(e, "problem_mark", None)
|
|
114
|
+
where = f"{path.relative_to(root).as_posix()}:{mark.line + 1}" if mark else path.relative_to(root).as_posix()
|
|
115
|
+
raise DbtError(f"{where}: invalid YAML ({getattr(e, 'problem', None) or e})") from None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def find_root(path: Path) -> Path:
|
|
119
|
+
"""The dbt project a `# @dbt:` path names: its folder, or its dbt_project.yml."""
|
|
120
|
+
root = path.parent if path.name == "dbt_project.yml" else path
|
|
121
|
+
if not (root / "dbt_project.yml").is_file():
|
|
122
|
+
raise DbtError(f"no dbt_project.yml in {root}")
|
|
123
|
+
return root
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _paths(root: Path) -> tuple[dict[str, list[Path]], list[Path]]:
|
|
127
|
+
"""(resource kind -> folders, docs folders), from dbt_project.yml with dbt's defaults."""
|
|
128
|
+
cfg = _yaml(root / "dbt_project.yml", root)
|
|
129
|
+
cfg = cfg if isinstance(cfg, dict) else {}
|
|
130
|
+
|
|
131
|
+
def get(key: str, old: str | None = None, default: str | None = None) -> list[Path]:
|
|
132
|
+
v = cfg.get(key) or (cfg.get(old) if old else None) or ([default] if default else [])
|
|
133
|
+
out = []
|
|
134
|
+
for p in [v] if isinstance(v, str) else v:
|
|
135
|
+
# dbt resolves these inside the project; anything else (`/`, `../..`) would walk the whole disk
|
|
136
|
+
if not (root / str(p)).resolve().is_relative_to(root.resolve()):
|
|
137
|
+
raise DbtError(f"dbt_project.yml: {key} '{p}' is outside the project")
|
|
138
|
+
out.append(root / str(p))
|
|
139
|
+
return out
|
|
140
|
+
|
|
141
|
+
kinds = {
|
|
142
|
+
"model": get("model-paths", "source-paths", "models"),
|
|
143
|
+
"seed": get("seed-paths", "data-paths", "seeds"),
|
|
144
|
+
"snapshot": get("snapshot-paths", None, "snapshots"),
|
|
145
|
+
}
|
|
146
|
+
doc_dirs = get("docs-paths")
|
|
147
|
+
if not doc_dirs: # dbt's default: every resource path
|
|
148
|
+
doc_dirs = [d for ds in kinds.values() for d in ds]
|
|
149
|
+
doc_dirs += get("analysis-paths", None, "analyses") + get("macro-paths", None, "macros")
|
|
150
|
+
return kinds, doc_dirs
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _walk(dirs: list[Path], suffixes: tuple[str, ...]) -> list[Path]:
|
|
154
|
+
out: dict[Path, None] = {}
|
|
155
|
+
for d in dirs:
|
|
156
|
+
for dirpath, dirnames, filenames in os.walk(d):
|
|
157
|
+
dirnames[:] = sorted(n for n in dirnames if not n.startswith(".") and n not in SKIP_DIRS)
|
|
158
|
+
for n in sorted(filenames):
|
|
159
|
+
if n.lower().endswith(suffixes):
|
|
160
|
+
out[Path(dirpath, n)] = None
|
|
161
|
+
return list(out)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def watched_files(root: Path) -> list[Path]:
|
|
165
|
+
"""Every file `load_project` reads (or may read once it exists), for `qdbd serve`'s change polling."""
|
|
166
|
+
kinds, doc_dirs = _paths(root)
|
|
167
|
+
return [root / "dbt_project.yml", *_walk([d for ds in kinds.values() for d in ds] + doc_dirs, WATCHED)]
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _text(v: object) -> str:
|
|
171
|
+
return str(v).strip() if v is not None else ""
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _column_name(v: object) -> str:
|
|
175
|
+
return _text(v).strip('"`')
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def load_project(root: Path) -> DbtProject:
|
|
179
|
+
kinds, doc_dirs = _paths(root)
|
|
180
|
+
project = DbtProject(root)
|
|
181
|
+
for p in _walk(kinds["model"], (".sql", ".py")):
|
|
182
|
+
project.known.add(p.stem.lower())
|
|
183
|
+
for p in _walk(kinds["seed"], (".csv",)):
|
|
184
|
+
project.known.add(p.stem.lower())
|
|
185
|
+
for p in _walk(kinds["snapshot"], (".sql",)):
|
|
186
|
+
project.known.update(m.lower() for m in _cached(p, _SNAPSHOT.findall)) # type: ignore[union-attr]
|
|
187
|
+
for p in _walk(doc_dirs, (".md",)):
|
|
188
|
+
for name, body in _cached(p, _DOCS_BLOCK.findall): # type: ignore[union-attr]
|
|
189
|
+
project.docs[name] = body.strip()
|
|
190
|
+
for p in _walk([d for ds in kinds.values() for d in ds], (".yml", ".yaml")):
|
|
191
|
+
data = _yaml(p, root)
|
|
192
|
+
if not isinstance(data, dict):
|
|
193
|
+
continue
|
|
194
|
+
rel = p.relative_to(root).as_posix()
|
|
195
|
+
for key in ("models", "seeds", "snapshots"):
|
|
196
|
+
for n in data.get(key) or []:
|
|
197
|
+
if isinstance(n, dict) and n.get("name"):
|
|
198
|
+
_add(project, _text(n["name"]), n, rel)
|
|
199
|
+
for src in data.get("sources") or []:
|
|
200
|
+
if not isinstance(src, dict) or not src.get("name"):
|
|
201
|
+
continue
|
|
202
|
+
for n in src.get("tables") or []:
|
|
203
|
+
if isinstance(n, dict) and n.get("name"):
|
|
204
|
+
_add(project, f"{_text(src['name'])}.{_text(n['name'])}", n, rel)
|
|
205
|
+
return project
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _add(project: DbtProject, name: str, n: dict, rel: str) -> None:
|
|
209
|
+
key = name.lower()
|
|
210
|
+
if key in project.models:
|
|
211
|
+
project.ambiguous.add(key)
|
|
212
|
+
return
|
|
213
|
+
cols: dict[str, DbtColumn] = {}
|
|
214
|
+
for c in n.get("columns") or []:
|
|
215
|
+
if isinstance(c, dict) and _column_name(c.get("name")):
|
|
216
|
+
cname = _column_name(c["name"])
|
|
217
|
+
dtype = _text(c.get("data_type")) or None
|
|
218
|
+
cols[cname.lower()] = DbtColumn(cname, _text(c.get("description")), dtype, node_tags(c), c["__line__"])
|
|
219
|
+
project.models[key] = DbtModel(name, _text(n.get("description")), node_tags(n), cols, rel, n["__line__"])
|
|
220
|
+
project.known.add(key)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def base_type(t: str) -> str:
|
|
224
|
+
"""`varchar(255)` -> `string`, `INTEGER` -> `int`: what two types must share to count as the same."""
|
|
225
|
+
s = re.split(r"[(<\[]", (norm_type(t) or "").lower())[0].strip("_ ")
|
|
226
|
+
return _BASE.get(s, s)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
Report = Callable[[str, int, str, str], None] # (path, line, message, severity)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def apply(schema: Schema, project: DbtProject, report: Report) -> None:
|
|
233
|
+
"""Attach dbt descriptions, tags and types to the matching tables and columns, and report drift."""
|
|
234
|
+
|
|
235
|
+
def doc(text: str, path: str, line: int) -> str:
|
|
236
|
+
def sub(m: re.Match) -> str:
|
|
237
|
+
if m.group(1) in project.docs:
|
|
238
|
+
return project.docs[m.group(1)]
|
|
239
|
+
report(path, line, f"dbt: no docs block named '{m.group(1)}'", "warning")
|
|
240
|
+
return m.group(0)
|
|
241
|
+
|
|
242
|
+
return _DOC_REF.sub(sub, text)
|
|
243
|
+
|
|
244
|
+
for t in schema.tables.values():
|
|
245
|
+
key = t.name.lower()
|
|
246
|
+
if t.dbt_skip or key in project.ambiguous:
|
|
247
|
+
continue
|
|
248
|
+
m = project.models.get(key)
|
|
249
|
+
if m is None:
|
|
250
|
+
if key not in project.known:
|
|
251
|
+
report(
|
|
252
|
+
t.path,
|
|
253
|
+
t.line,
|
|
254
|
+
f"dbt: no model, seed, snapshot or source named '{t.name}' "
|
|
255
|
+
"(put '# @dbt: none' above the table if it isn't one)",
|
|
256
|
+
"warning",
|
|
257
|
+
)
|
|
258
|
+
continue
|
|
259
|
+
t.dbt = DbtInfo(doc(m.description, t.path, t.line), list(m.tags))
|
|
260
|
+
cols = {c.name.lower(): c for c in t.columns.values() if not c.virtual}
|
|
261
|
+
for ckey, dc in m.columns.items():
|
|
262
|
+
c = cols.get(ckey)
|
|
263
|
+
if c is None:
|
|
264
|
+
msg = f"dbt: column '{dc.name}' ({m.path}:{dc.line}) is missing from '{t.name}'"
|
|
265
|
+
report(t.path, t.line, msg, "warning")
|
|
266
|
+
continue
|
|
267
|
+
c.dbt = DbtInfo(doc(dc.description, t.path, c.line), list(dc.tags), norm_type(dc.data_type))
|
|
268
|
+
if dc.data_type and c.type and not c.type_default and base_type(c.type) != base_type(dc.data_type):
|
|
269
|
+
msg = f"dbt: type '{c.type}' differs from data_type '{dc.data_type}' ({m.path}:{dc.line})"
|
|
270
|
+
report(t.path, c.line, msg, "warning")
|
|
271
|
+
if m.columns:
|
|
272
|
+
for ckey, c in cols.items():
|
|
273
|
+
if ckey not in m.columns:
|
|
274
|
+
msg = f"dbt: column '{c.name}' is not in the yml for '{m.name}' ({m.path}:{m.line})"
|
|
275
|
+
report(t.path, c.line, msg, "warning")
|
|
@@ -175,7 +175,7 @@ def sources_at_ref(ref: str, inputs: list[Path]) -> list[tuple[str, str]]:
|
|
|
175
175
|
|
|
176
176
|
|
|
177
177
|
def schema_at_ref(ref: str, inputs: list[Path]) -> Schema:
|
|
178
|
-
return compile_sources(sources_at_ref(ref, inputs))
|
|
178
|
+
return compile_sources(sources_at_ref(ref, inputs), dbt=False)
|
|
179
179
|
|
|
180
180
|
|
|
181
181
|
# --------------------------------------------------------------------------- output
|