sql-code-graph 1.0.0__tar.gz → 1.0.1__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.
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/CHANGELOG.md +10 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/CLAUDE.md +12 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/PKG-INFO +1 -1
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/pyproject.toml +1 -1
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/__init__.py +1 -1
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/cli/commands/index.py +29 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/indexer/indexer.py +5 -13
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/indexer/pool.py +55 -1
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/parsers/base.py +67 -48
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/integration/test_indexer_to_graph.py +22 -26
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_hard_kill_pool.py +47 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_perf_scaling_guard.py +101 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/uv.lock +1 -1
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/.claude/agents/api-documenter.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/.claude/agents/architect-planner.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/.claude/agents/architect-reviewer.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/.claude/agents/code-reviewer.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/.claude/agents/developer.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/.claude/agents/plan-reviewer.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/.claude/agents/sprint-planner.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/.github/workflows/benchmark.yml +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/.github/workflows/e2e-tests.yml +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/.github/workflows/release.yml +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/.github/workflows/test.yml +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/.gitignore +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/.pre-commit-config.yaml +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/.sqlcgignore +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/ARCHITECTURE_REVIEW.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/README.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/docs/AIRBNB_PARSE_REPORT.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/docs/cli.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/e2e_firstuser_report.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/e2e_run_20260528_101413.output +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/main.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/WORKFLOW.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/blueprint.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/bundle_claude_skill.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/fix_dynamic_table_parsing.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/fix_expand_qualify_perf.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/fix_firstuser_findings.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/fix_schema_case_mismatch.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/investigation_e5_e4.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/living_codebase_resync.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/measurements/schema_comparison_with_schema.json +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/measurements/schema_comparison_without_schema.json +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/measurements/sprint_08_changelogs_fullindex.json +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/measurements/sprint_08_fullcorpus_index.json +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/measurements/sprint_pool_300s_plan.json +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/parse_diagnostics.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/parsing_errors_experiment.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/snowflake_en_test_suite.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/sprint_01.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/sprint_01_deployment_pypi.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/sprint_02.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/sprint_02_v0.3.0_core.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/sprint_03.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/sprint_031.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/sprint_03_v0.3.1_postmortem.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/sprint_04_column_lineage.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/sprint_04_column_lineage_fix.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/sprint_05_star_resolution.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/sprint_06_lineage_coverage.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/sprint_07_open_ecodes.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/sprint_07_perf_and_live_test.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/sprint_08_perf_upsert.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/sprint_09_lineage_coverage.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/sprint_10_anchor_tools.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/sprint_3.1_postmortem.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/sprint_31_postmortem.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/sqlcg.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/trust_layer.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/profile.html +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/pyrightconfig.json +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/scripts/collect_parse_errors.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/scripts/generate_cli_docs.sh +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/__main__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/cli/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/cli/commands/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/cli/commands/analyze.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/cli/commands/db.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/cli/commands/find.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/cli/commands/gain.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/cli/commands/git.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/cli/commands/install.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/cli/commands/mcp.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/cli/commands/reindex.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/cli/commands/report.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/cli/commands/uninstall.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/cli/commands/watch.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/cli/main.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/core/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/core/config.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/core/graph_db.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/core/jobs.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/core/kuzu_backend.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/core/neo4j_backend.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/core/queries.cypher +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/core/queries.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/core/schema.cypher +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/core/schema.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/indexer/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/indexer/dbt_adapter.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/indexer/error_classify.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/indexer/git_delta.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/indexer/walker.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/indexer/watcher.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/lineage/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/lineage/aggregator.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/lineage/schema_resolver.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/metrics/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/metrics/store.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/parsers/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/parsers/ansi_parser.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/parsers/bigquery_parser.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/parsers/postgres_parser.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/parsers/registry.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/parsers/snowflake_parser.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/parsers/tsql_parser.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/server/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/server/exceptions.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/server/models.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/server/noise_filter.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/server/server.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/server/skill.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/server/tools.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/utils/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/utils/hashing.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/utils/ignore.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/utils/logging.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/benchmarks/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/benchmarks/adversarial/200_join.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/benchmarks/adversarial/500_union.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/benchmarks/bench_indexer.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/benchmarks/conftest.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/benchmarks/golden_corpus/snowflake/case_normalization.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/benchmarks/golden_corpus/snowflake/colon_cast.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/benchmarks/golden_corpus/snowflake/colon_reserved_word.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/benchmarks/golden_corpus/snowflake/copy_into.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/benchmarks/golden_corpus/snowflake/create_procedure.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/benchmarks/golden_corpus/snowflake/identifier_dynamic.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/benchmarks/golden_corpus/snowflake/lateral_flatten.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/benchmarks/golden_corpus/snowflake/qualify.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/benchmarks/golden_corpus/snowflake/scripting_block.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/benchmarks/golden_corpus/snowflake/three_part.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/benchmarks/tpch/q01.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/benchmarks/tpch/q02.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/benchmarks/tpch/q03.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/benchmarks/tpch/q04.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/benchmarks/tpch/q05.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/e2e/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/e2e/conftest.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/e2e/test_F2_skill_install_e2e.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/e2e/test_airbnb_e2e.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/e2e/test_cli_index.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/e2e/test_git_hook_install.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/e2e/test_golden_lineage.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/e2e/test_mcp_tools.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/e2e/test_parse_diagnostics_cli.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/e2e/test_star_resolution_e2e.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/e2e/test_watch.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/airbnb/dim_hosts_cleansed.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/airbnb/dim_listings_cleansed.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/airbnb/fct_reviews.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/airbnb/mart_fullmoon_reviews.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/airbnb/raw_hosts.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/airbnb/raw_listings.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/airbnb/raw_reviews.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/airbnb/src_hosts.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/airbnb/src_listings.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/airbnb/src_reviews.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/bigquery/.gitkeep +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/jaffle_shop/customers.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/jaffle_shop/orders.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/jaffle_shop/raw_orders.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/snowflake/base_tables.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/snowflake/reports.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/snowflake/views.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/star_corpus/ddl_src.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/star_corpus/ddl_tgt.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/star_corpus/etl_alias_star.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/star_corpus/etl_star.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/synthetic/base_tables.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/synthetic/reports.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/synthetic/views.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/integration/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/integration/snowflake/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/integration/snowflake/test_insert_select.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/integration/test_anchor_tools.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/integration/test_bulk_upsert.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/integration/test_column_lineage_e2e.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/integration/test_cross_file_lineage.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/integration/test_dialect_matrix.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/integration/test_e36_xfile_regression_guard.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/integration/test_indexer_batching.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/integration/test_indexer_commits.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/integration/test_live_anchors.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/integration/test_parse_diagnostics.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/integration/test_resync.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/integration/test_star_resolution.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/integration/test_temp_table_lineage.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/perf/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/perf/test_perf.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E10/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E11/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E12/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E12/e12_json_path.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E12/e12_lateral_flatten.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E12/test_e12.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E13/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E14/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E15/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E16/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E16/e16_merge.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E16/e16_merge_delete.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E16/test_e16.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E17/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E18/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E18/e18_decode.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E18/e18_iff_decode.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E18/e18_nvl2.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E18/test_e18.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E19/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E2/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E2/e2_expr_alias.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E2/e2_function_alias.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E2/e2_multiply_alias.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E2/test_e2.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E20/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E21/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E21/e21_alias_forward_ref.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E21/e21_three_level_chain.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E21/test_e21.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E22/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E23/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E23/e23_stored_proc.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E23/e23_stored_proc_multi.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E23/test_e23.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E24/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E25/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E25/e25_cross_db.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E25/e25_two_part.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E25/test_e25.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E25/test_e25_full_id.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E26/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E27/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E27/e27_nested_udf.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E27/e27_udf.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E27/test_e27.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E28/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E29/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E3/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E3/e3_alter_table.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E3/e3_create_sequence.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E3/e3_ddl_only.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E3/test_e3.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E30/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E31/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E32/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E33/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E34/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E35/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E36/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E36/e36_temp_multi_use.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E36/e36_temp_table.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E36/test_e36.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E36/test_e36_xfile.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E37/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E38/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E4/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E4/e4_execute_immediate.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E4/e4_if_not_exists.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E4/e4_unexpected_token.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E4/e4_unpivot.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E4/test_e4.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E5/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E5/e5_cte_missing_source.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E5/e5_multi_cte.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E5/e5_nested_cte.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E5/test_e5.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E5/test_e5_cte_projection.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E8/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E8/e8_dynamic_sources.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E8/e8_seq_nextval.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E8/e8_uuid.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E8/test_e8.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E9/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E_aggregates/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E_aggregates/fixture_sum_absent_cross_schema.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E_aggregates/fixture_sum_case_when.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E_aggregates/fixture_sum_present_source.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E_aggregates/test_e_aggregates.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E_date_functions/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E_date_functions/fixture_date_aliased.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E_date_functions/fixture_date_unaliased.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E_date_functions/fixture_datediff_unaliased.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E_date_functions/fixture_year_unaliased.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E_date_functions/test_e_date_functions.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/README.md +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/anchors/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/anchors/fixture_etl.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/anchors/fixture_omloopsnelheid.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/anchors/fixture_semantic.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/anchors/fixture_source.sql +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/anchors/test_anchor_ma_aantal_op_order.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/anchors/test_anchor_omloopsnelheid.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/conftest.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/test_plan_review_gates.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/snowflake/__init__.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/snowflake/test_scripting_noise.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_F2_install_skill.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_F2_skill_render.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_F2_uninstall_skill.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_T09_01_qualify_once.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_T09_02_ddl_skip.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_T09_04_subprocess_isolate.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_T09_06_log_verbosity.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_aggregator.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_aggregator_skip.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_base_parser.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_branch_monitor.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_bulk_upsert_invariant.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_cli.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_cli_help.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_closure_depth.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_column_lineage_wiring.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_config.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_data_models.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_db_info.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_dominant_cause.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_firstuser_findings.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_gain_ratio.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_git_delta.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_git_hooks.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_graph_backend.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_index_cmd.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_index_flags.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_index_progress.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_indexer_progress.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_indexer_quality.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_install.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_install_message.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_jobs.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_judgement.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_kuzu_backend.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_kuzu_lock.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_lineage_conversion.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_literal_column_skip.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_mcp_best_practices.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_metrics.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_noise_filter.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_parse_file_dependency_filter.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_parse_quality.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_parser.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_queries_loader.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_resolve_pass2_passes_dependency_filter.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_schema_resolver.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_server.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_snowflake_strip_alter_set_tag.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_sprint_06_t04_t05.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_star_resolution_unit.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_star_schema_unit.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_submit_feedback.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_subprocess_isolate.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_t02_expression_name_extraction.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_t03_ddl_skip.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_timeout_cancel.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_tools_hints.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_tools_warnings.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_uninstall.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_walker.py +0 -0
- {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_watcher.py +0 -0
|
@@ -104,6 +104,9 @@ Any refactor that touches [`base.py`](src/sqlcg/parsers/base.py) or [`indexer.py
|
|
|
104
104
|
| **`body_scope` built once per statement**, reused across all columns | `_extract_column_lineage` | Wide SELECTs (176 cols × 11 joins) took 7 min with per-column qualify; once-per-statement is the fix |
|
|
105
105
|
| **Pure-literal skip** (`if not list(col_expr.find_all(exp.Column)): continue`) | column loop in `_extract_column_lineage` | Literals have no source column; without the skip sg_lineage raises noise E1 errors and wastes time |
|
|
106
106
|
| When `body_scope` is available, **`sources=` is NOT passed to `sg_lineage`** | `sg_kwargs` construction | The scope already embeds full column→table resolution; passing `sources=` would make sg_lineage redundantly re-expand them |
|
|
107
|
+
| When a scope is passed to `sg_lineage`, **`copy=False` + `trim_selects=False` are passed too** | `sg_kwargs` construction | Without `copy=False`, sqlglot deep-copies the whole scope on **every per-column call** → O(cols × scope_size). Regressed in `4234e5d`; measured 28.8s on one 3,344-line file |
|
|
108
|
+
| **`body_scope` is built once per statement BEFORE the column loop** (with a schema-free qualify retry on failure), never lazily inside it | `_extract_column_lineage` | Building it inside the loop re-ran expand+qualify+build_scope for *every* column whenever qualify failed (the failure path). Regressed in `4234e5d` |
|
|
109
|
+
| In the INSERT column-list aliasing path, **`body.copy()` + strip-WITH happen once** before the loop; only the single projection is swapped per column | `_extract_column_lineage` INSERT block | A full-body deepcopy per column is O(cols × body_size). Regressed in `4234e5d` |
|
|
107
110
|
| **`_upsert_parsed_file` uses `upsert_nodes_bulk`/`upsert_edges_bulk`** (Phase A→B→C), never `upsert_node`/`upsert_edge` per row | `indexer.py` `_upsert_parsed_file` | Measured: bulk=181s vs per-node=1020s+ on 1,340-file DWH (~10 execute() calls per file vs ~14,500 total). Commit `4234e5d` accidentally regressed this by rewriting the method during C2 normalization. |
|
|
108
111
|
|
|
109
112
|
Tests covering these invariants live in [`test_T09_01_qualify_once.py`](tests/unit/test_T09_01_qualify_once.py)
|
|
@@ -119,6 +122,15 @@ class, not just the named ones, and uses deterministic op-counts (never wall-clo
|
|
|
119
122
|
flake by machine speed. A red here means something started scaling — find what, do not raise the
|
|
120
123
|
slack. When adding a new hot-path op that must be once-per-statement, add a counter for it there.
|
|
121
124
|
|
|
125
|
+
The guard's **call-count** axes alone are not enough: the three `4234e5d` regressions kept call
|
|
126
|
+
counts flat/linear but regressed *per-call cost* (`copy=False` dropped → per-column scope deepcopy),
|
|
127
|
+
the *failure path* (per-column re-qualify only when qualify throws), and a *per-column body copy*
|
|
128
|
+
(INSERT aliasing). A clean fixture stays green because its qualify succeeds and its scope is small.
|
|
129
|
+
The guard therefore also pins these **behaviourally**: scope-path `sg_lineage` must pass `copy=False`;
|
|
130
|
+
a forced qualify failure must not re-qualify per column; the INSERT body must be copied once. When
|
|
131
|
+
adding a hot-path optimization, prefer a behavioural assertion (kwarg present, op once-per-statement)
|
|
132
|
+
over a volume count — volume counts hide inside a too-simple fixture, which is how this shipped in v1.0.0.
|
|
133
|
+
|
|
122
134
|
- When referring to files in docs or plans, use markdown hyperlinks (e.g. [`schema.py`](src/sqlcg/core/schema.py)) so stale references are immediately visible when file names change.
|
|
123
135
|
- No backward compatibility. Re-index is the migration path.
|
|
124
136
|
- No TODO in the happy path of any feature.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sql-code-graph
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: SQL code graph analyzer and lineage tracer
|
|
5
5
|
Project-URL: Homepage, https://github.com/Warhorze/sql-code-graph
|
|
6
6
|
Project-URL: Repository, https://github.com/Warhorze/sql-code-graph
|
|
@@ -84,6 +84,35 @@ def index_cmd( # noqa: B008
|
|
|
84
84
|
db_path = get_db_path()
|
|
85
85
|
db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
86
86
|
|
|
87
|
+
try:
|
|
88
|
+
_run_index(
|
|
89
|
+
path=path,
|
|
90
|
+
dialect=dialect,
|
|
91
|
+
dbt_manifest=dbt_manifest,
|
|
92
|
+
timeout_per_file=timeout_per_file,
|
|
93
|
+
no_ddl=no_ddl,
|
|
94
|
+
quiet=quiet,
|
|
95
|
+
batch_size=batch_size,
|
|
96
|
+
profile=profile,
|
|
97
|
+
)
|
|
98
|
+
except KeyboardInterrupt:
|
|
99
|
+
# The backend context manager (inside _run_index) has already closed the
|
|
100
|
+
# KuzuDB connection and released the lock by the time we get here.
|
|
101
|
+
console.print("\n[yellow]Interrupted — no partial graph written. Re-run to index.[/yellow]")
|
|
102
|
+
raise typer.Exit(130) from None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _run_index(
|
|
106
|
+
*,
|
|
107
|
+
path: Path,
|
|
108
|
+
dialect: str | None,
|
|
109
|
+
dbt_manifest: Path | None,
|
|
110
|
+
timeout_per_file: int,
|
|
111
|
+
no_ddl: bool,
|
|
112
|
+
quiet: bool,
|
|
113
|
+
batch_size: int,
|
|
114
|
+
profile: bool,
|
|
115
|
+
) -> None:
|
|
87
116
|
with get_backend() as backend:
|
|
88
117
|
backend.init_schema()
|
|
89
118
|
|
|
@@ -251,9 +251,11 @@ class Indexer:
|
|
|
251
251
|
_t_pass2_end = time.perf_counter()
|
|
252
252
|
|
|
253
253
|
except KeyboardInterrupt:
|
|
254
|
-
|
|
255
|
-
#
|
|
256
|
-
|
|
254
|
+
# Kill workers and abort immediately. A partial pass-1-only result is
|
|
255
|
+
# an incomplete graph (no cross-file resolution, no star expansion);
|
|
256
|
+
# writing it would leave a misleading half-index. Re-run `sqlcg index`
|
|
257
|
+
# to index — re-indexing is the migration path.
|
|
258
|
+
logger.warning("Interrupted — workers killed; no partial graph written.")
|
|
257
259
|
raise
|
|
258
260
|
|
|
259
261
|
# Assemble final pass-2 results: start from pass-1, overlay pass-2 where available
|
|
@@ -1104,16 +1106,6 @@ class Indexer:
|
|
|
1104
1106
|
|
|
1105
1107
|
return counts
|
|
1106
1108
|
|
|
1107
|
-
def _upsert_all(self, results: list[ParsedFile], db: GraphBackend) -> None:
|
|
1108
|
-
"""Upsert all parsed files.
|
|
1109
|
-
|
|
1110
|
-
Args:
|
|
1111
|
-
results: List of ParsedFile objects
|
|
1112
|
-
db: GraphBackend instance
|
|
1113
|
-
"""
|
|
1114
|
-
for parsed in results:
|
|
1115
|
-
self._upsert_parsed_file(parsed, db)
|
|
1116
|
-
|
|
1117
1109
|
def _expand_star_sources(self, db: GraphBackend) -> int:
|
|
1118
1110
|
"""Run the post-ingestion star expansion query.
|
|
1119
1111
|
|
|
@@ -194,7 +194,14 @@ class HardKillPool:
|
|
|
194
194
|
) -> None:
|
|
195
195
|
self._dialect = dialect
|
|
196
196
|
self._schema_aliases: dict[str, str] = schema_aliases or {}
|
|
197
|
-
|
|
197
|
+
# Leave 2 logical cores of headroom rather than spawning one worker per
|
|
198
|
+
# logical core. Parsing is CPU-bound, and the main process also does work
|
|
199
|
+
# between passes (closure resolution, batched upserts); saturating every
|
|
200
|
+
# core makes the largest files miss the per-file wall-clock timeout.
|
|
201
|
+
# Measured on the 1,453-file DWH corpus (after the once-per-statement parser
|
|
202
|
+
# fixes): cpu_count → 2 timeouts / 186s; cpu_count-2 → 0 timeouts / 131s
|
|
203
|
+
# (fewer timeouts AND faster, since timed-out files waste work + respawn churn).
|
|
204
|
+
self._n = n_workers or max(1, (os.cpu_count() or 4) - 2)
|
|
198
205
|
self._ctx = mp.get_context("spawn")
|
|
199
206
|
self._workers: list[_WorkerState] = []
|
|
200
207
|
|
|
@@ -322,6 +329,28 @@ class HardKillPool:
|
|
|
322
329
|
w.task_start = time.monotonic()
|
|
323
330
|
busy.add(slot)
|
|
324
331
|
|
|
332
|
+
try:
|
|
333
|
+
return self._run_map_loop(
|
|
334
|
+
tasks, results, busy, kill_counts, _assign, per_task_timeout, on_result, n_tasks
|
|
335
|
+
)
|
|
336
|
+
except KeyboardInterrupt:
|
|
337
|
+
# Workers ignore SIGINT and are CPU-bound, so they will not notice a
|
|
338
|
+
# graceful SHUTDOWN sentinel until their current parse finishes. On
|
|
339
|
+
# interrupt the user wants the process gone now — hard-kill outright.
|
|
340
|
+
self.terminate()
|
|
341
|
+
raise
|
|
342
|
+
|
|
343
|
+
def _run_map_loop(
|
|
344
|
+
self,
|
|
345
|
+
tasks: list[dict],
|
|
346
|
+
results: list[ParsedFile | None],
|
|
347
|
+
busy: set[int],
|
|
348
|
+
kill_counts: dict[str, int],
|
|
349
|
+
_assign: Callable[[int], None],
|
|
350
|
+
per_task_timeout: float,
|
|
351
|
+
on_result: Callable[[], None] | None,
|
|
352
|
+
n_tasks: int,
|
|
353
|
+
) -> list[ParsedFile | None]:
|
|
325
354
|
# Initial dispatch: fill all worker slots
|
|
326
355
|
for i in range(min(self._n, n_tasks)):
|
|
327
356
|
_assign(i)
|
|
@@ -405,6 +434,31 @@ class HardKillPool:
|
|
|
405
434
|
# Shutdown
|
|
406
435
|
# ------------------------------------------------------------------
|
|
407
436
|
|
|
437
|
+
def terminate(self) -> None:
|
|
438
|
+
"""Immediately SIGKILL every worker without a graceful handshake.
|
|
439
|
+
|
|
440
|
+
Unlike :meth:`shutdown`, this sends no ``_SHUTDOWN`` sentinel and does
|
|
441
|
+
not wait for in-flight parses. Workers ignore SIGINT and are CPU-bound,
|
|
442
|
+
so a graceful stop would block on the longest running parse; on
|
|
443
|
+
interrupt we kill outright so the process dies promptly.
|
|
444
|
+
"""
|
|
445
|
+
for w in self._workers:
|
|
446
|
+
try:
|
|
447
|
+
w.conn.close()
|
|
448
|
+
except Exception:
|
|
449
|
+
pass
|
|
450
|
+
try:
|
|
451
|
+
if w.process.is_alive():
|
|
452
|
+
w.process.kill()
|
|
453
|
+
except Exception:
|
|
454
|
+
pass
|
|
455
|
+
for w in self._workers:
|
|
456
|
+
try:
|
|
457
|
+
w.process.join(timeout=1)
|
|
458
|
+
except Exception:
|
|
459
|
+
pass
|
|
460
|
+
self._workers.clear()
|
|
461
|
+
|
|
408
462
|
def shutdown(self) -> None:
|
|
409
463
|
"""Gracefully stop all workers, then force-kill any that linger."""
|
|
410
464
|
for w in self._workers:
|
|
@@ -619,10 +619,6 @@ class SqlParser(ABC):
|
|
|
619
619
|
else:
|
|
620
620
|
return LineageExtraction(edges=edges, star_sources=star_sources)
|
|
621
621
|
|
|
622
|
-
# Build scope once from the body for all-column reuse (T-05 optimization)
|
|
623
|
-
# Defer scope building to just before the column loop to ensure sources
|
|
624
|
-
# are expanded first (avoid rebuilding for each column, but only build
|
|
625
|
-
# after sources are known)
|
|
626
622
|
body_scope = None
|
|
627
623
|
combined_sources = {**(sources or {})}
|
|
628
624
|
|
|
@@ -644,6 +640,54 @@ class SqlParser(ABC):
|
|
|
644
640
|
key = cte_alias.lower()
|
|
645
641
|
combined_sources[key] = cte.this
|
|
646
642
|
|
|
643
|
+
# Build body_scope ONCE per statement, before the column loop, and reuse
|
|
644
|
+
# it for every column (CLAUDE.md invariant: "body_scope built once per
|
|
645
|
+
# statement"). If schema-qualify fails, retry schema-free so we STILL get
|
|
646
|
+
# a scope for the copy=False fast path; only if both fail do we fall back
|
|
647
|
+
# to the per-column sources= path. Building this lazily inside the loop
|
|
648
|
+
# (regressed in 4234e5d) meant a single qualify failure re-ran
|
|
649
|
+
# expand+qualify+build_scope for EVERY column → O(N_cols) full-body
|
|
650
|
+
# deepcopies per statement (measured: 229 qualify calls on one 460-line file).
|
|
651
|
+
if scope is None:
|
|
652
|
+
expanded_body = body
|
|
653
|
+
expand_sources = {
|
|
654
|
+
k: v for k, v in (sources or {}).items() if isinstance(v, exp.Query)
|
|
655
|
+
}
|
|
656
|
+
if expand_sources:
|
|
657
|
+
try:
|
|
658
|
+
expanded_body = exp.expand(
|
|
659
|
+
body,
|
|
660
|
+
expand_sources, # type: ignore
|
|
661
|
+
dialect=self.DIALECT,
|
|
662
|
+
copy=True,
|
|
663
|
+
)
|
|
664
|
+
except Exception:
|
|
665
|
+
expanded_body = body
|
|
666
|
+
try:
|
|
667
|
+
qualified_body = qualify(
|
|
668
|
+
expanded_body,
|
|
669
|
+
dialect=self.DIALECT,
|
|
670
|
+
schema=schema,
|
|
671
|
+
validate_qualify_columns=False,
|
|
672
|
+
identify=False,
|
|
673
|
+
)
|
|
674
|
+
body_scope = build_scope(qualified_body)
|
|
675
|
+
except Exception as _qualify_exc:
|
|
676
|
+
out.errors.append(
|
|
677
|
+
f"col_lineage_skip:qualify_failed:{type(_qualify_exc).__name__}"
|
|
678
|
+
)
|
|
679
|
+
# Schema-free retry: still yields a scope for the copy=False path.
|
|
680
|
+
try:
|
|
681
|
+
qualified_body = qualify(
|
|
682
|
+
expanded_body,
|
|
683
|
+
dialect=self.DIALECT,
|
|
684
|
+
validate_qualify_columns=False,
|
|
685
|
+
identify=False,
|
|
686
|
+
)
|
|
687
|
+
body_scope = build_scope(qualified_body)
|
|
688
|
+
except Exception:
|
|
689
|
+
body_scope = None
|
|
690
|
+
|
|
647
691
|
# Extract output columns
|
|
648
692
|
for col_expr in col_expressions:
|
|
649
693
|
# Skip star projections — sg_lineage requires a concrete column name.
|
|
@@ -723,53 +767,23 @@ class SqlParser(ABC):
|
|
|
723
767
|
continue
|
|
724
768
|
|
|
725
769
|
try:
|
|
726
|
-
# Build scope on first column for reuse across all columns (T-05 optimization).
|
|
727
|
-
# NOTE: We build body_scope locally from the extracted body rather than
|
|
728
|
-
# using a pre-built scope from the statement, because CREATE/INSERT statements
|
|
729
|
-
# have their scope rooted at the outer statement, but the body passed here
|
|
730
|
-
# is the inner SELECT. Reusing the outer scope would produce incorrect
|
|
731
|
-
# qualification. The pre-built scope from parse_file would only be useful
|
|
732
|
-
# if we had a mechanism to extract the matching inner scope, which is
|
|
733
|
-
# complex and not yet implemented (see sprint_06 T-05 deviation for details).
|
|
734
|
-
if body_scope is None and scope is None:
|
|
735
|
-
try:
|
|
736
|
-
# Expand only file-level sources (CTEs, temp tables, CTAS bodies).
|
|
737
|
-
expanded_body = body
|
|
738
|
-
expand_sources = {
|
|
739
|
-
k: v for k, v in (sources or {}).items() if isinstance(v, exp.Query)
|
|
740
|
-
}
|
|
741
|
-
if expand_sources:
|
|
742
|
-
expanded_body = exp.expand(
|
|
743
|
-
body,
|
|
744
|
-
expand_sources, # type: ignore
|
|
745
|
-
dialect=self.DIALECT,
|
|
746
|
-
copy=True,
|
|
747
|
-
)
|
|
748
|
-
|
|
749
|
-
# Qualify the expanded body to prepare for scope building
|
|
750
|
-
qualified_body = qualify(
|
|
751
|
-
expanded_body,
|
|
752
|
-
dialect=self.DIALECT,
|
|
753
|
-
schema=schema,
|
|
754
|
-
validate_qualify_columns=False,
|
|
755
|
-
identify=False,
|
|
756
|
-
)
|
|
757
|
-
body_scope = build_scope(qualified_body)
|
|
758
|
-
except Exception as _qualify_exc:
|
|
759
|
-
# qualify() failure is non-fatal: sg_lineage falls back to
|
|
760
|
-
# its own qualification. Record for observability.
|
|
761
|
-
out.errors.append(
|
|
762
|
-
f"col_lineage_skip:qualify_failed:{type(_qualify_exc).__name__}:{_qualify_exc}"
|
|
763
|
-
)
|
|
764
|
-
body_scope = None
|
|
765
|
-
|
|
766
770
|
# When a scope is available it embeds full column→table resolution.
|
|
767
771
|
# On the qualify-failed fallback path (no scope), pass only the small
|
|
768
772
|
# set of file-level sources so sg_lineage can resolve CTEs/CTAS bodies.
|
|
769
773
|
active_scope = scope if scope is not None else body_scope
|
|
770
774
|
sg_kwargs: dict = {"dialect": self.DIALECT}
|
|
771
775
|
if active_scope is not None:
|
|
776
|
+
# scope= path: the pre-built scope already embeds full
|
|
777
|
+
# column→table resolution. copy=False + trim_selects=False
|
|
778
|
+
# suppress sqlglot's per-call AST deepcopy and per-column
|
|
779
|
+
# trim — neither is needed when the scope is built once and
|
|
780
|
+
# reused across all columns. Dropping these (regressed in
|
|
781
|
+
# 4234e5d) makes lineage() deepcopy the whole scope per
|
|
782
|
+
# column → O(columns × scope_size) (measured: 3.2M deepcopy
|
|
783
|
+
# calls / ~3.8s on a 359-line file).
|
|
772
784
|
sg_kwargs["scope"] = active_scope
|
|
785
|
+
sg_kwargs["copy"] = False
|
|
786
|
+
sg_kwargs["trim_selects"] = False
|
|
773
787
|
else:
|
|
774
788
|
sg_kwargs["sources"] = sources or {}
|
|
775
789
|
root = sg_lineage(col_name, body, **sg_kwargs)
|
|
@@ -912,6 +926,13 @@ class SqlParser(ABC):
|
|
|
912
926
|
# stops sg_lineage at the CTE name boundary (doesn't expand into bodies).
|
|
913
927
|
if isinstance(stmt, exp.Insert) and isinstance(stmt.this, exp.Schema):
|
|
914
928
|
insert_cols = [c.name for c in stmt.this.expressions]
|
|
929
|
+
# Build the WITH-stripped body ONCE before the loop and only swap its
|
|
930
|
+
# single projection per column (regressed in 4234e5d, which moved the
|
|
931
|
+
# full-body body.copy() inside the loop → O(N_cols) full-body deepcopies
|
|
932
|
+
# for wide INSERT ... SELECT). Stripping WITH stops sg_lineage at the CTE
|
|
933
|
+
# name boundary.
|
|
934
|
+
body_no_with = body.copy()
|
|
935
|
+
body_no_with.set("with_", None)
|
|
915
936
|
for idx, col_expr in enumerate(col_expressions):
|
|
916
937
|
if idx >= len(insert_cols):
|
|
917
938
|
break
|
|
@@ -920,10 +941,8 @@ class SqlParser(ABC):
|
|
|
920
941
|
insert_col = insert_cols[idx]
|
|
921
942
|
if not insert_col:
|
|
922
943
|
continue
|
|
923
|
-
#
|
|
924
|
-
#
|
|
925
|
-
body_no_with = body.copy()
|
|
926
|
-
body_no_with.set("with_", None)
|
|
944
|
+
# Patch the shared body with this column's aliased expression so
|
|
945
|
+
# sg_lineage can trace it to the INSERT column name.
|
|
927
946
|
aliased = exp.Alias(this=col_expr.copy(), alias=insert_col)
|
|
928
947
|
body_no_with.set("expressions", [aliased])
|
|
929
948
|
patched_sql = body_no_with.sql(dialect=self.DIALECT)
|
|
@@ -106,8 +106,18 @@ def test_walker_yields_only_sql_files(temp_db):
|
|
|
106
106
|
assert len(files) >= 3
|
|
107
107
|
|
|
108
108
|
|
|
109
|
-
def
|
|
110
|
-
"""
|
|
109
|
+
def test_sigint_during_index_aborts_without_partial_write(temp_db):
|
|
110
|
+
"""SIGINT during indexing must re-raise and write NO partial graph.
|
|
111
|
+
|
|
112
|
+
A partial pass-1-only result is an incomplete graph (no cross-file
|
|
113
|
+
resolution, no star expansion); flushing it would leave a misleading
|
|
114
|
+
half-index that "keeps going" after the user expects the process to die.
|
|
115
|
+
The contract is: kill workers, abort, write nothing — re-run to index.
|
|
116
|
+
|
|
117
|
+
SIGINT lands in the main process while it blocks in HardKillPool.map
|
|
118
|
+
(the worker subprocesses ignore SIGINT), so we patch map to raise
|
|
119
|
+
KeyboardInterrupt — the same point the real interrupt surfaces.
|
|
120
|
+
"""
|
|
111
121
|
from unittest.mock import patch
|
|
112
122
|
|
|
113
123
|
fixtures_path = Path(__file__).parent.parent / "fixtures" / "synthetic"
|
|
@@ -116,29 +126,15 @@ def test_sigint_during_index_flushes_progress(temp_db):
|
|
|
116
126
|
|
|
117
127
|
indexer = Indexer()
|
|
118
128
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if call_count[0] > 1:
|
|
125
|
-
raise KeyboardInterrupt("User interrupted")
|
|
126
|
-
# Return a valid parsed file for the first call
|
|
127
|
-
from sqlcg.parsers.registry import get_parser
|
|
128
|
-
|
|
129
|
-
parser = get_parser(None, None)
|
|
130
|
-
return parser.parse_file(file_path, sql)
|
|
131
|
-
|
|
132
|
-
with patch("sqlcg.parsers.ansi_parser.SqlParser.parse_file", side_effect=mock_parse_file):
|
|
133
|
-
try:
|
|
129
|
+
with patch(
|
|
130
|
+
"sqlcg.indexer.indexer.HardKillPool.map",
|
|
131
|
+
side_effect=KeyboardInterrupt("User interrupted"),
|
|
132
|
+
):
|
|
133
|
+
with pytest.raises(KeyboardInterrupt):
|
|
134
134
|
indexer.index_repo(fixtures_path, dialect=None, db=temp_db, timeout_per_file=30)
|
|
135
|
-
except KeyboardInterrupt:
|
|
136
|
-
pass # Expected
|
|
137
|
-
|
|
138
|
-
# Even with SIGINT, at least the first file should have been upserted
|
|
139
|
-
# Verify by checking node count > 0
|
|
140
|
-
result = temp_db.run_read("MATCH (n) RETURN COUNT(*) as count", {})
|
|
141
|
-
count = result[0]["count"]
|
|
142
135
|
|
|
143
|
-
#
|
|
144
|
-
assert
|
|
136
|
+
# No indexed content should have been written: no partial flush on interrupt.
|
|
137
|
+
# (init_schema seeds a schema-version metadata node, so assert on File nodes,
|
|
138
|
+
# which only the upsert pass — skipped on interrupt — creates.)
|
|
139
|
+
result = temp_db.run_read("MATCH (f:File) RETURN COUNT(*) as count", {})
|
|
140
|
+
assert result[0]["count"] == 0
|
|
@@ -7,6 +7,9 @@ multi-worker logic. Tests are ordered by increasing complexity.
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import time
|
|
10
|
+
from unittest.mock import patch
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
10
13
|
|
|
11
14
|
from sqlcg.indexer.pool import HardKillPool
|
|
12
15
|
from sqlcg.parsers.base import ParsedFile
|
|
@@ -203,3 +206,47 @@ def test_pool_shutdown_terminates_workers():
|
|
|
203
206
|
# After exit, all processes should have been joined / killed
|
|
204
207
|
for p in procs:
|
|
205
208
|
assert not p.is_alive(), f"Worker pid={p.pid} still alive after shutdown"
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# ---------------------------------------------------------------------------
|
|
212
|
+
# Scenario E — interrupt (Ctrl-C) hard-kills workers immediately, no hang
|
|
213
|
+
# ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def test_pool_terminate_kills_workers_immediately():
|
|
217
|
+
"""terminate() SIGKILLs every worker without a graceful handshake and
|
|
218
|
+
clears the worker list."""
|
|
219
|
+
pool = HardKillPool(None, n_workers=2)
|
|
220
|
+
pool._spawn_all()
|
|
221
|
+
procs = [w.process for w in pool._workers]
|
|
222
|
+
assert all(p.is_alive() for p in procs), "Workers should be alive after spawn"
|
|
223
|
+
|
|
224
|
+
pool.terminate()
|
|
225
|
+
|
|
226
|
+
assert pool._workers == [], "terminate() must clear the worker list"
|
|
227
|
+
for p in procs:
|
|
228
|
+
assert not p.is_alive(), f"Worker pid={p.pid} alive after terminate()"
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def test_pool_keyboardinterrupt_in_map_terminates_and_reraises(tmp_path):
|
|
232
|
+
"""A KeyboardInterrupt raised in the dispatch loop (where SIGINT lands in the
|
|
233
|
+
main process) hard-kills all workers and re-raises — it never hangs waiting
|
|
234
|
+
on in-flight CPU-bound parses."""
|
|
235
|
+
p = tmp_path / "a.sql"
|
|
236
|
+
p.write_text("SELECT 1;", encoding="utf-8")
|
|
237
|
+
tasks = [_fast_task(str(p), "SELECT 1;")]
|
|
238
|
+
|
|
239
|
+
pool = HardKillPool(None, n_workers=2)
|
|
240
|
+
pool._spawn_all()
|
|
241
|
+
procs = [w.process for w in pool._workers]
|
|
242
|
+
try:
|
|
243
|
+
with patch.object(pool, "_run_map_loop", side_effect=KeyboardInterrupt):
|
|
244
|
+
with pytest.raises(KeyboardInterrupt):
|
|
245
|
+
pool.map(tasks, per_task_timeout=10.0)
|
|
246
|
+
|
|
247
|
+
# map's handler called terminate(): workers dead, list cleared.
|
|
248
|
+
assert pool._workers == []
|
|
249
|
+
for proc in procs:
|
|
250
|
+
assert not proc.is_alive(), f"Worker pid={proc.pid} alive after interrupt"
|
|
251
|
+
finally:
|
|
252
|
+
pool.shutdown() # no-op if terminate already cleared the workers
|
|
@@ -43,6 +43,13 @@ class HotOpCounters:
|
|
|
43
43
|
self.build_scope = 0
|
|
44
44
|
self.qualify = 0
|
|
45
45
|
self.sg_lineage = 0
|
|
46
|
+
# Behavioural counters added after the v1.0.0 regression (commit 4234e5d
|
|
47
|
+
# dropped copy=False / moved body.copy() into the loop, and the original
|
|
48
|
+
# call-count axes above stayed green because they count CALLS, not per-call
|
|
49
|
+
# cost or the qualify-failure path). These pin the EXACT invariants:
|
|
50
|
+
self.scope_lineage_calls = 0 # sg_lineage calls that pass a pre-built scope
|
|
51
|
+
self.scope_without_copy_false = 0 # ...of those, how many omit copy=False
|
|
52
|
+
self.multi_proj_body_copy = 0 # .copy() of a multi-projection SELECT body
|
|
46
53
|
|
|
47
54
|
|
|
48
55
|
@contextmanager
|
|
@@ -60,6 +67,7 @@ def count_hot_ops():
|
|
|
60
67
|
_extract_column_lineage, so it is patched at the source. No single call
|
|
61
68
|
passes through two wrappers — no double counting.
|
|
62
69
|
"""
|
|
70
|
+
import sqlglot.expressions as sqlglot_exp
|
|
63
71
|
import sqlglot.lineage as sqlglot_lin
|
|
64
72
|
|
|
65
73
|
import sqlcg.parsers.base as base_mod
|
|
@@ -70,10 +78,19 @@ def count_hot_ops():
|
|
|
70
78
|
"lin_lineage": sqlglot_lin.lineage,
|
|
71
79
|
"base_bs": base_mod.build_scope,
|
|
72
80
|
"base_q": base_mod.qualify,
|
|
81
|
+
"exp_copy": sqlglot_exp.Expression.copy,
|
|
73
82
|
}
|
|
74
83
|
|
|
75
84
|
def lineage_wrap(*a, **k):
|
|
76
85
|
c.sg_lineage += 1
|
|
86
|
+
# Invariant (regression #1, commit 4234e5d): when a pre-built scope is
|
|
87
|
+
# passed, copy=False MUST accompany it, else sqlglot deep-copies the whole
|
|
88
|
+
# scope per column (O(cols × scope_size)). The call count alone is linear
|
|
89
|
+
# and would not reveal the dropped kwarg — inspect it directly.
|
|
90
|
+
if k.get("scope") is not None:
|
|
91
|
+
c.scope_lineage_calls += 1
|
|
92
|
+
if k.get("copy") is not False:
|
|
93
|
+
c.scope_without_copy_false += 1
|
|
77
94
|
return orig["lin_lineage"](*a, **k)
|
|
78
95
|
|
|
79
96
|
def build_scope_wrap_factory(key):
|
|
@@ -90,15 +107,25 @@ def count_hot_ops():
|
|
|
90
107
|
|
|
91
108
|
return wrap
|
|
92
109
|
|
|
110
|
+
def copy_wrap(self, *a, **k):
|
|
111
|
+
# Invariant (regression #3): the multi-projection body in the INSERT
|
|
112
|
+
# column-list aliasing path must be copied ONCE per statement, not per
|
|
113
|
+
# column. Count copies of a SELECT that still has >1 projection.
|
|
114
|
+
if isinstance(self, sqlglot_exp.Select) and len(self.expressions) > 1:
|
|
115
|
+
c.multi_proj_body_copy += 1
|
|
116
|
+
return orig["exp_copy"](self, *a, **k)
|
|
117
|
+
|
|
93
118
|
sqlglot_lin.lineage = lineage_wrap
|
|
94
119
|
base_mod.build_scope = build_scope_wrap_factory("base_bs")
|
|
95
120
|
base_mod.qualify = qualify_wrap_factory("base_q")
|
|
121
|
+
sqlglot_exp.Expression.copy = copy_wrap
|
|
96
122
|
try:
|
|
97
123
|
yield c
|
|
98
124
|
finally:
|
|
99
125
|
sqlglot_lin.lineage = orig["lin_lineage"]
|
|
100
126
|
base_mod.build_scope = orig["base_bs"]
|
|
101
127
|
base_mod.qualify = orig["base_q"]
|
|
128
|
+
sqlglot_exp.Expression.copy = orig["exp_copy"]
|
|
102
129
|
|
|
103
130
|
|
|
104
131
|
def assert_flat(base: int, doubled: int, label: str, slack: int = 2) -> None:
|
|
@@ -164,3 +191,77 @@ def test_per_statement_ops_flat_when_columns_double():
|
|
|
164
191
|
assert_flat(base.qualify, doubled.qualify, "qualify")
|
|
165
192
|
# sg_lineage is legitimately per-column; only bound its slope.
|
|
166
193
|
assert_at_most_linear(base.sg_lineage, doubled.sg_lineage, "sg_lineage")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# ---------------------------------------------------------------------------
|
|
197
|
+
# Axis 2 — the v1.0.0 sub-class the call-count axis above could NOT catch.
|
|
198
|
+
# Commit 4234e5d kept call counts flat/linear but (1) dropped copy=False so each
|
|
199
|
+
# scope-path sg_lineage deep-copied the scope, (2) re-qualified per column on the
|
|
200
|
+
# qualify-FAILURE path, and (3) copied the full body per column in the INSERT
|
|
201
|
+
# aliasing path. These three guards pin those exact invariants behaviourally.
|
|
202
|
+
# ---------------------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def test_scope_path_sg_lineage_passes_copy_false():
|
|
206
|
+
"""Regression #1: every sg_lineage call that carries a pre-built scope must also
|
|
207
|
+
pass copy=False. Otherwise sqlglot deep-copies the whole scope per column
|
|
208
|
+
(O(cols × scope_size); measured 28.8s on one 3,344-line file)."""
|
|
209
|
+
parser = AnsiParser(SchemaResolver(dialect=None))
|
|
210
|
+
with count_hot_ops() as c:
|
|
211
|
+
parser.parse_file(Path("scope.sql"), _wide_insert(COLS_BASE))
|
|
212
|
+
|
|
213
|
+
assert c.scope_lineage_calls > 0, (
|
|
214
|
+
"fixture did not exercise the scope= path; the guard would be vacuously green"
|
|
215
|
+
)
|
|
216
|
+
assert c.scope_without_copy_false == 0, (
|
|
217
|
+
f"{c.scope_without_copy_false} of {c.scope_lineage_calls} scope-path sg_lineage "
|
|
218
|
+
"calls omitted copy=False — sqlglot will deep-copy the scope per column "
|
|
219
|
+
"(O(cols × scope_size)). Restore copy=False (CLAUDE.md 'Performance invariants')."
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def test_insert_aliasing_body_copied_once_per_statement():
|
|
224
|
+
"""Regression #3: the multi-projection body in the INSERT column-list aliasing
|
|
225
|
+
path must be copied ONCE per statement, not per column."""
|
|
226
|
+
parser_base = AnsiParser(SchemaResolver(dialect=None))
|
|
227
|
+
parser_2x = AnsiParser(SchemaResolver(dialect=None))
|
|
228
|
+
with count_hot_ops() as base:
|
|
229
|
+
parser_base.parse_file(Path("base.sql"), _wide_insert(COLS_BASE))
|
|
230
|
+
with count_hot_ops() as doubled:
|
|
231
|
+
parser_2x.parse_file(Path("doubled.sql"), _wide_insert(COLS_2X))
|
|
232
|
+
|
|
233
|
+
assert base.multi_proj_body_copy >= 1, (
|
|
234
|
+
"fixture did not exercise the INSERT-aliasing body copy; guard vacuously green"
|
|
235
|
+
)
|
|
236
|
+
assert_flat(base.multi_proj_body_copy, doubled.multi_proj_body_copy, "multi_proj_body_copy")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def test_qualify_not_retried_per_column_when_it_fails():
|
|
240
|
+
"""Regression #2: when qualify() raises for a statement, the parser must NOT
|
|
241
|
+
re-run qualify once per column. Build it once before the loop (with a single
|
|
242
|
+
schema-free retry) and fall through to the per-column sources= path."""
|
|
243
|
+
import sqlcg.parsers.base as base_mod
|
|
244
|
+
|
|
245
|
+
def _count_qualify_calls_when_failing(n_cols: int) -> int:
|
|
246
|
+
calls = 0
|
|
247
|
+
orig_q = base_mod.qualify
|
|
248
|
+
|
|
249
|
+
def always_raise(*a, **k):
|
|
250
|
+
nonlocal calls
|
|
251
|
+
calls += 1
|
|
252
|
+
raise ValueError("forced qualify failure (test)")
|
|
253
|
+
|
|
254
|
+
base_mod.qualify = always_raise
|
|
255
|
+
try:
|
|
256
|
+
AnsiParser(SchemaResolver(dialect=None)).parse_file(
|
|
257
|
+
Path("fail.sql"), _wide_insert(n_cols)
|
|
258
|
+
)
|
|
259
|
+
finally:
|
|
260
|
+
base_mod.qualify = orig_q
|
|
261
|
+
return calls
|
|
262
|
+
|
|
263
|
+
base_calls = _count_qualify_calls_when_failing(COLS_BASE)
|
|
264
|
+
doubled_calls = _count_qualify_calls_when_failing(COLS_2X)
|
|
265
|
+
|
|
266
|
+
assert base_calls >= 1, "fixture did not reach the qualify path"
|
|
267
|
+
assert_flat(base_calls, doubled_calls, "qualify (on the failure path)")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|