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.
Files changed (377) hide show
  1. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/CHANGELOG.md +10 -0
  2. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/CLAUDE.md +12 -0
  3. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/PKG-INFO +1 -1
  4. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/pyproject.toml +1 -1
  5. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/__init__.py +1 -1
  6. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/cli/commands/index.py +29 -0
  7. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/indexer/indexer.py +5 -13
  8. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/indexer/pool.py +55 -1
  9. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/parsers/base.py +67 -48
  10. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/integration/test_indexer_to_graph.py +22 -26
  11. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_hard_kill_pool.py +47 -0
  12. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_perf_scaling_guard.py +101 -0
  13. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/uv.lock +1 -1
  14. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/.claude/agents/api-documenter.md +0 -0
  15. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/.claude/agents/architect-planner.md +0 -0
  16. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/.claude/agents/architect-reviewer.md +0 -0
  17. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/.claude/agents/code-reviewer.md +0 -0
  18. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/.claude/agents/developer.md +0 -0
  19. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/.claude/agents/plan-reviewer.md +0 -0
  20. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/.claude/agents/sprint-planner.md +0 -0
  21. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  22. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  23. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  24. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/.github/workflows/benchmark.yml +0 -0
  25. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/.github/workflows/e2e-tests.yml +0 -0
  26. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/.github/workflows/release.yml +0 -0
  27. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/.github/workflows/test.yml +0 -0
  28. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/.gitignore +0 -0
  29. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/.pre-commit-config.yaml +0 -0
  30. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/.sqlcgignore +0 -0
  31. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/ARCHITECTURE_REVIEW.md +0 -0
  32. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/README.md +0 -0
  33. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/docs/AIRBNB_PARSE_REPORT.md +0 -0
  34. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/docs/cli.md +0 -0
  35. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/e2e_firstuser_report.md +0 -0
  36. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/e2e_run_20260528_101413.output +0 -0
  37. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/main.py +0 -0
  38. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/WORKFLOW.md +0 -0
  39. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/blueprint.md +0 -0
  40. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/bundle_claude_skill.md +0 -0
  41. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/fix_dynamic_table_parsing.md +0 -0
  42. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/fix_expand_qualify_perf.md +0 -0
  43. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/fix_firstuser_findings.md +0 -0
  44. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/fix_schema_case_mismatch.md +0 -0
  45. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/investigation_e5_e4.md +0 -0
  46. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/living_codebase_resync.md +0 -0
  47. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/measurements/schema_comparison_with_schema.json +0 -0
  48. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/measurements/schema_comparison_without_schema.json +0 -0
  49. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/measurements/sprint_08_changelogs_fullindex.json +0 -0
  50. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/measurements/sprint_08_fullcorpus_index.json +0 -0
  51. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/measurements/sprint_pool_300s_plan.json +0 -0
  52. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/parse_diagnostics.md +0 -0
  53. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/parsing_errors_experiment.md +0 -0
  54. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/snowflake_en_test_suite.md +0 -0
  55. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/sprint_01.md +0 -0
  56. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/sprint_01_deployment_pypi.md +0 -0
  57. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/sprint_02.md +0 -0
  58. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/sprint_02_v0.3.0_core.md +0 -0
  59. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/sprint_03.md +0 -0
  60. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/sprint_031.md +0 -0
  61. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/sprint_03_v0.3.1_postmortem.md +0 -0
  62. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/sprint_04_column_lineage.md +0 -0
  63. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/sprint_04_column_lineage_fix.md +0 -0
  64. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/sprint_05_star_resolution.md +0 -0
  65. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/sprint_06_lineage_coverage.md +0 -0
  66. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/sprint_07_open_ecodes.md +0 -0
  67. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/sprint_07_perf_and_live_test.md +0 -0
  68. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/sprint_08_perf_upsert.md +0 -0
  69. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/sprint_09_lineage_coverage.md +0 -0
  70. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/sprint_10_anchor_tools.md +0 -0
  71. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/sprint_3.1_postmortem.md +0 -0
  72. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/sprint_31_postmortem.md +0 -0
  73. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/sqlcg.md +0 -0
  74. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/plan/trust_layer.md +0 -0
  75. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/profile.html +0 -0
  76. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/pyrightconfig.json +0 -0
  77. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/scripts/collect_parse_errors.py +0 -0
  78. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/scripts/generate_cli_docs.sh +0 -0
  79. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/__main__.py +0 -0
  80. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/cli/__init__.py +0 -0
  81. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/cli/commands/__init__.py +0 -0
  82. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/cli/commands/analyze.py +0 -0
  83. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/cli/commands/db.py +0 -0
  84. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/cli/commands/find.py +0 -0
  85. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/cli/commands/gain.py +0 -0
  86. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/cli/commands/git.py +0 -0
  87. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/cli/commands/install.py +0 -0
  88. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/cli/commands/mcp.py +0 -0
  89. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/cli/commands/reindex.py +0 -0
  90. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/cli/commands/report.py +0 -0
  91. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/cli/commands/uninstall.py +0 -0
  92. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/cli/commands/watch.py +0 -0
  93. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/cli/main.py +0 -0
  94. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/core/__init__.py +0 -0
  95. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/core/config.py +0 -0
  96. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/core/graph_db.py +0 -0
  97. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/core/jobs.py +0 -0
  98. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/core/kuzu_backend.py +0 -0
  99. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/core/neo4j_backend.py +0 -0
  100. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/core/queries.cypher +0 -0
  101. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/core/queries.py +0 -0
  102. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/core/schema.cypher +0 -0
  103. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/core/schema.py +0 -0
  104. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/indexer/__init__.py +0 -0
  105. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/indexer/dbt_adapter.py +0 -0
  106. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/indexer/error_classify.py +0 -0
  107. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/indexer/git_delta.py +0 -0
  108. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/indexer/walker.py +0 -0
  109. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/indexer/watcher.py +0 -0
  110. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/lineage/__init__.py +0 -0
  111. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/lineage/aggregator.py +0 -0
  112. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/lineage/schema_resolver.py +0 -0
  113. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/metrics/__init__.py +0 -0
  114. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/metrics/store.py +0 -0
  115. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/parsers/__init__.py +0 -0
  116. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/parsers/ansi_parser.py +0 -0
  117. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/parsers/bigquery_parser.py +0 -0
  118. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/parsers/postgres_parser.py +0 -0
  119. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/parsers/registry.py +0 -0
  120. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/parsers/snowflake_parser.py +0 -0
  121. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/parsers/tsql_parser.py +0 -0
  122. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/server/__init__.py +0 -0
  123. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/server/exceptions.py +0 -0
  124. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/server/models.py +0 -0
  125. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/server/noise_filter.py +0 -0
  126. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/server/server.py +0 -0
  127. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/server/skill.py +0 -0
  128. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/server/tools.py +0 -0
  129. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/utils/__init__.py +0 -0
  130. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/utils/hashing.py +0 -0
  131. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/utils/ignore.py +0 -0
  132. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/src/sqlcg/utils/logging.py +0 -0
  133. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/__init__.py +0 -0
  134. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/benchmarks/__init__.py +0 -0
  135. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/benchmarks/adversarial/200_join.sql +0 -0
  136. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/benchmarks/adversarial/500_union.sql +0 -0
  137. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/benchmarks/bench_indexer.py +0 -0
  138. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/benchmarks/conftest.py +0 -0
  139. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/benchmarks/golden_corpus/snowflake/case_normalization.sql +0 -0
  140. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/benchmarks/golden_corpus/snowflake/colon_cast.sql +0 -0
  141. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/benchmarks/golden_corpus/snowflake/colon_reserved_word.sql +0 -0
  142. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/benchmarks/golden_corpus/snowflake/copy_into.sql +0 -0
  143. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/benchmarks/golden_corpus/snowflake/create_procedure.sql +0 -0
  144. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/benchmarks/golden_corpus/snowflake/identifier_dynamic.sql +0 -0
  145. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/benchmarks/golden_corpus/snowflake/lateral_flatten.sql +0 -0
  146. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/benchmarks/golden_corpus/snowflake/qualify.sql +0 -0
  147. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/benchmarks/golden_corpus/snowflake/scripting_block.sql +0 -0
  148. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/benchmarks/golden_corpus/snowflake/three_part.sql +0 -0
  149. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/benchmarks/tpch/q01.sql +0 -0
  150. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/benchmarks/tpch/q02.sql +0 -0
  151. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/benchmarks/tpch/q03.sql +0 -0
  152. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/benchmarks/tpch/q04.sql +0 -0
  153. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/benchmarks/tpch/q05.sql +0 -0
  154. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/e2e/__init__.py +0 -0
  155. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/e2e/conftest.py +0 -0
  156. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/e2e/test_F2_skill_install_e2e.py +0 -0
  157. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/e2e/test_airbnb_e2e.py +0 -0
  158. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/e2e/test_cli_index.py +0 -0
  159. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/e2e/test_git_hook_install.py +0 -0
  160. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/e2e/test_golden_lineage.py +0 -0
  161. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/e2e/test_mcp_tools.py +0 -0
  162. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/e2e/test_parse_diagnostics_cli.py +0 -0
  163. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/e2e/test_star_resolution_e2e.py +0 -0
  164. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/e2e/test_watch.py +0 -0
  165. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/airbnb/dim_hosts_cleansed.sql +0 -0
  166. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/airbnb/dim_listings_cleansed.sql +0 -0
  167. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/airbnb/fct_reviews.sql +0 -0
  168. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/airbnb/mart_fullmoon_reviews.sql +0 -0
  169. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/airbnb/raw_hosts.sql +0 -0
  170. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/airbnb/raw_listings.sql +0 -0
  171. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/airbnb/raw_reviews.sql +0 -0
  172. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/airbnb/src_hosts.sql +0 -0
  173. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/airbnb/src_listings.sql +0 -0
  174. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/airbnb/src_reviews.sql +0 -0
  175. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/bigquery/.gitkeep +0 -0
  176. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/jaffle_shop/customers.sql +0 -0
  177. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/jaffle_shop/orders.sql +0 -0
  178. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/jaffle_shop/raw_orders.sql +0 -0
  179. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/snowflake/base_tables.sql +0 -0
  180. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/snowflake/reports.sql +0 -0
  181. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/snowflake/views.sql +0 -0
  182. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/star_corpus/ddl_src.sql +0 -0
  183. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/star_corpus/ddl_tgt.sql +0 -0
  184. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/star_corpus/etl_alias_star.sql +0 -0
  185. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/star_corpus/etl_star.sql +0 -0
  186. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/synthetic/base_tables.sql +0 -0
  187. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/synthetic/reports.sql +0 -0
  188. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/fixtures/synthetic/views.sql +0 -0
  189. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/integration/__init__.py +0 -0
  190. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/integration/snowflake/__init__.py +0 -0
  191. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/integration/snowflake/test_insert_select.py +0 -0
  192. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/integration/test_anchor_tools.py +0 -0
  193. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/integration/test_bulk_upsert.py +0 -0
  194. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/integration/test_column_lineage_e2e.py +0 -0
  195. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/integration/test_cross_file_lineage.py +0 -0
  196. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/integration/test_dialect_matrix.py +0 -0
  197. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/integration/test_e36_xfile_regression_guard.py +0 -0
  198. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/integration/test_indexer_batching.py +0 -0
  199. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/integration/test_indexer_commits.py +0 -0
  200. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/integration/test_live_anchors.py +0 -0
  201. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/integration/test_parse_diagnostics.py +0 -0
  202. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/integration/test_resync.py +0 -0
  203. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/integration/test_star_resolution.py +0 -0
  204. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/integration/test_temp_table_lineage.py +0 -0
  205. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/perf/__init__.py +0 -0
  206. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/perf/test_perf.py +0 -0
  207. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E10/__init__.py +0 -0
  208. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E11/__init__.py +0 -0
  209. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E12/__init__.py +0 -0
  210. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E12/e12_json_path.sql +0 -0
  211. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E12/e12_lateral_flatten.sql +0 -0
  212. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E12/test_e12.py +0 -0
  213. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E13/__init__.py +0 -0
  214. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E14/__init__.py +0 -0
  215. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E15/__init__.py +0 -0
  216. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E16/__init__.py +0 -0
  217. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E16/e16_merge.sql +0 -0
  218. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E16/e16_merge_delete.sql +0 -0
  219. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E16/test_e16.py +0 -0
  220. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E17/__init__.py +0 -0
  221. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E18/__init__.py +0 -0
  222. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E18/e18_decode.sql +0 -0
  223. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E18/e18_iff_decode.sql +0 -0
  224. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E18/e18_nvl2.sql +0 -0
  225. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E18/test_e18.py +0 -0
  226. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E19/__init__.py +0 -0
  227. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E2/__init__.py +0 -0
  228. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E2/e2_expr_alias.sql +0 -0
  229. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E2/e2_function_alias.sql +0 -0
  230. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E2/e2_multiply_alias.sql +0 -0
  231. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E2/test_e2.py +0 -0
  232. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E20/__init__.py +0 -0
  233. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E21/__init__.py +0 -0
  234. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E21/e21_alias_forward_ref.sql +0 -0
  235. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E21/e21_three_level_chain.sql +0 -0
  236. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E21/test_e21.py +0 -0
  237. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E22/__init__.py +0 -0
  238. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E23/__init__.py +0 -0
  239. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E23/e23_stored_proc.sql +0 -0
  240. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E23/e23_stored_proc_multi.sql +0 -0
  241. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E23/test_e23.py +0 -0
  242. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E24/__init__.py +0 -0
  243. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E25/__init__.py +0 -0
  244. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E25/e25_cross_db.sql +0 -0
  245. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E25/e25_two_part.sql +0 -0
  246. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E25/test_e25.py +0 -0
  247. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E25/test_e25_full_id.py +0 -0
  248. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E26/__init__.py +0 -0
  249. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E27/__init__.py +0 -0
  250. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E27/e27_nested_udf.sql +0 -0
  251. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E27/e27_udf.sql +0 -0
  252. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E27/test_e27.py +0 -0
  253. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E28/__init__.py +0 -0
  254. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E29/__init__.py +0 -0
  255. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E3/__init__.py +0 -0
  256. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E3/e3_alter_table.sql +0 -0
  257. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E3/e3_create_sequence.sql +0 -0
  258. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E3/e3_ddl_only.sql +0 -0
  259. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E3/test_e3.py +0 -0
  260. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E30/__init__.py +0 -0
  261. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E31/__init__.py +0 -0
  262. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E32/__init__.py +0 -0
  263. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E33/__init__.py +0 -0
  264. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E34/__init__.py +0 -0
  265. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E35/__init__.py +0 -0
  266. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E36/__init__.py +0 -0
  267. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E36/e36_temp_multi_use.sql +0 -0
  268. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E36/e36_temp_table.sql +0 -0
  269. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E36/test_e36.py +0 -0
  270. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E36/test_e36_xfile.py +0 -0
  271. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E37/__init__.py +0 -0
  272. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E38/__init__.py +0 -0
  273. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E4/__init__.py +0 -0
  274. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E4/e4_execute_immediate.sql +0 -0
  275. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E4/e4_if_not_exists.sql +0 -0
  276. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E4/e4_unexpected_token.sql +0 -0
  277. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E4/e4_unpivot.sql +0 -0
  278. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E4/test_e4.py +0 -0
  279. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E5/__init__.py +0 -0
  280. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E5/e5_cte_missing_source.sql +0 -0
  281. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E5/e5_multi_cte.sql +0 -0
  282. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E5/e5_nested_cte.sql +0 -0
  283. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E5/test_e5.py +0 -0
  284. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E5/test_e5_cte_projection.py +0 -0
  285. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E8/__init__.py +0 -0
  286. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E8/e8_dynamic_sources.sql +0 -0
  287. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E8/e8_seq_nextval.sql +0 -0
  288. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E8/e8_uuid.sql +0 -0
  289. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E8/test_e8.py +0 -0
  290. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E9/__init__.py +0 -0
  291. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E_aggregates/__init__.py +0 -0
  292. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E_aggregates/fixture_sum_absent_cross_schema.sql +0 -0
  293. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E_aggregates/fixture_sum_case_when.sql +0 -0
  294. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E_aggregates/fixture_sum_present_source.sql +0 -0
  295. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E_aggregates/test_e_aggregates.py +0 -0
  296. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E_date_functions/__init__.py +0 -0
  297. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E_date_functions/fixture_date_aliased.sql +0 -0
  298. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E_date_functions/fixture_date_unaliased.sql +0 -0
  299. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E_date_functions/fixture_datediff_unaliased.sql +0 -0
  300. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E_date_functions/fixture_year_unaliased.sql +0 -0
  301. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/E_date_functions/test_e_date_functions.py +0 -0
  302. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/README.md +0 -0
  303. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/__init__.py +0 -0
  304. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/anchors/__init__.py +0 -0
  305. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/anchors/fixture_etl.sql +0 -0
  306. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/anchors/fixture_omloopsnelheid.sql +0 -0
  307. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/anchors/fixture_semantic.sql +0 -0
  308. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/anchors/fixture_source.sql +0 -0
  309. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/anchors/test_anchor_ma_aantal_op_order.py +0 -0
  310. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/anchors/test_anchor_omloopsnelheid.py +0 -0
  311. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/conftest.py +0 -0
  312. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/snowflake/test_plan_review_gates.py +0 -0
  313. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/__init__.py +0 -0
  314. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/snowflake/__init__.py +0 -0
  315. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/snowflake/test_scripting_noise.py +0 -0
  316. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_F2_install_skill.py +0 -0
  317. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_F2_skill_render.py +0 -0
  318. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_F2_uninstall_skill.py +0 -0
  319. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_T09_01_qualify_once.py +0 -0
  320. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_T09_02_ddl_skip.py +0 -0
  321. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_T09_04_subprocess_isolate.py +0 -0
  322. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_T09_06_log_verbosity.py +0 -0
  323. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_aggregator.py +0 -0
  324. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_aggregator_skip.py +0 -0
  325. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_base_parser.py +0 -0
  326. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_branch_monitor.py +0 -0
  327. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_bulk_upsert_invariant.py +0 -0
  328. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_cli.py +0 -0
  329. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_cli_help.py +0 -0
  330. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_closure_depth.py +0 -0
  331. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_column_lineage_wiring.py +0 -0
  332. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_config.py +0 -0
  333. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_data_models.py +0 -0
  334. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_db_info.py +0 -0
  335. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_dominant_cause.py +0 -0
  336. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_firstuser_findings.py +0 -0
  337. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_gain_ratio.py +0 -0
  338. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_git_delta.py +0 -0
  339. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_git_hooks.py +0 -0
  340. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_graph_backend.py +0 -0
  341. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_index_cmd.py +0 -0
  342. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_index_flags.py +0 -0
  343. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_index_progress.py +0 -0
  344. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_indexer_progress.py +0 -0
  345. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_indexer_quality.py +0 -0
  346. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_install.py +0 -0
  347. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_install_message.py +0 -0
  348. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_jobs.py +0 -0
  349. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_judgement.py +0 -0
  350. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_kuzu_backend.py +0 -0
  351. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_kuzu_lock.py +0 -0
  352. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_lineage_conversion.py +0 -0
  353. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_literal_column_skip.py +0 -0
  354. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_mcp_best_practices.py +0 -0
  355. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_metrics.py +0 -0
  356. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_noise_filter.py +0 -0
  357. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_parse_file_dependency_filter.py +0 -0
  358. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_parse_quality.py +0 -0
  359. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_parser.py +0 -0
  360. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_queries_loader.py +0 -0
  361. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_resolve_pass2_passes_dependency_filter.py +0 -0
  362. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_schema_resolver.py +0 -0
  363. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_server.py +0 -0
  364. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_snowflake_strip_alter_set_tag.py +0 -0
  365. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_sprint_06_t04_t05.py +0 -0
  366. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_star_resolution_unit.py +0 -0
  367. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_star_schema_unit.py +0 -0
  368. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_submit_feedback.py +0 -0
  369. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_subprocess_isolate.py +0 -0
  370. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_t02_expression_name_extraction.py +0 -0
  371. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_t03_ddl_skip.py +0 -0
  372. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_timeout_cancel.py +0 -0
  373. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_tools_hints.py +0 -0
  374. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_tools_warnings.py +0 -0
  375. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_uninstall.py +0 -0
  376. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_walker.py +0 -0
  377. {sql_code_graph-1.0.0 → sql_code_graph-1.0.1}/tests/unit/test_watcher.py +0 -0
@@ -1,3 +1,13 @@
1
+ ## v1.0.1 (2026-05-30)
2
+
3
+ ### Fix
4
+
5
+ - **indexer**: abort immediately on Ctrl-C instead of flushing a partial graph
6
+
7
+ ### Perf
8
+
9
+ - **parser**: recover 3 once-per-statement lineage regressions from 4234e5d
10
+
1
11
  ## v1.0.0 (2026-05-30)
2
12
 
3
13
  ### Feat
@@ -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.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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sql-code-graph"
7
- version = "1.0.0"
7
+ version = "1.0.1"
8
8
  description = "SQL code graph analyzer and lineage tracer"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -1,5 +1,5 @@
1
1
  """SQL Code Graph - SQL lineage and dependency analysis tool."""
2
2
 
3
- __version__ = "1.0.0"
3
+ __version__ = "1.0.1"
4
4
 
5
5
  __all__ = ["__version__"]
@@ -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
- logger.info("SIGINT received flushing pass-1 progress")
255
- # pass1_results may be partial; upsert what we have
256
- self._upsert_all(pass1_results, db)
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
- self._n = n_workers or os.cpu_count() or 4
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
- # Build a patched SELECT: strip WITH, alias the expression with the
924
- # INSERT column name so sg_lineage can trace it.
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 test_sigint_during_index_flushes_progress(temp_db):
110
- """Test that SIGINT during index flushes progress to DB."""
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
- # Mock parser.parse_file to raise KeyboardInterrupt after first file
120
- call_count = [0]
121
-
122
- def mock_parse_file(file_path, sql):
123
- call_count[0] += 1
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
- # Should have at least created some nodes from the first file
144
- assert count > 0
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)")
@@ -2851,7 +2851,7 @@ wheels = [
2851
2851
 
2852
2852
  [[package]]
2853
2853
  name = "sql-code-graph"
2854
- version = "1.0.0"
2854
+ version = "1.0.1"
2855
2855
  source = { editable = "." }
2856
2856
  dependencies = [
2857
2857
  { name = "kuzu" },