snowflake-code-unit-registry 0.8.74__tar.gz → 0.8.76__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 (102) hide show
  1. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/Cargo.lock +39 -13
  2. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/Cargo.toml +4 -1
  3. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/PKG-INFO +1 -1
  4. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/Cargo.toml +1 -0
  5. snowflake_code_unit_registry-0.8.76/crates/scai-state-core/src/database_bindings.rs +473 -0
  6. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/src/lib.rs +5 -0
  7. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/src/registry/tests.rs +248 -0
  8. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/src/registry.rs +37 -1
  9. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-python/src/lib.rs +64 -0
  10. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/pyproject.toml +1 -1
  11. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/python/snowflake_code_unit_registry/__init__.py +100 -1
  12. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/README.md +0 -0
  13. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-error-derive/Cargo.toml +0 -0
  14. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-error-derive/src/lib.rs +0 -0
  15. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-error-derive/tests/derive_integration.rs +0 -0
  16. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-log/Cargo.toml +0 -0
  17. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-log/src/lib.rs +0 -0
  18. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-log/src/tests.rs +0 -0
  19. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/build/file_path_fields.rs +0 -0
  20. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/build/main.rs +0 -0
  21. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/build/output.rs +0 -0
  22. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/build/query_reference.rs +0 -0
  23. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/build/schema.rs +0 -0
  24. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/build/schema_constraints.rs +0 -0
  25. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/build/typify.rs +0 -0
  26. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/build/updated_at_paths.rs +0 -0
  27. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/examples/code-unit.example.json +0 -0
  28. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/examples/custom-code-unit.example.json +0 -0
  29. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/examples/etl-code-unit.example.json +0 -0
  30. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/examples/script-code-unit.example.json +0 -0
  31. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/schemas/code-unit.schema.json +0 -0
  32. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/schemas/history/README.md +0 -0
  33. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/schemas/query-reference.rs.tmpl +0 -0
  34. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/schemas/query-reference.tmpl +0 -0
  35. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/src/bindings.rs +0 -0
  36. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/src/canonical_name.rs +0 -0
  37. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/src/checksum.rs +0 -0
  38. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/src/dependency_view.rs +0 -0
  39. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/src/error.rs +0 -0
  40. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/src/error_trace.rs +0 -0
  41. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/src/filter.rs +0 -0
  42. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/src/generated/file_path_fields.rs +0 -0
  43. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/src/generated/mod.rs +0 -0
  44. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/src/generated/query_reference.rs +0 -0
  45. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/src/generated/schema_constraints.rs +0 -0
  46. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/src/generated/updated_at_paths.rs +0 -0
  47. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/src/identity.rs +0 -0
  48. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/src/migration/mod.rs +0 -0
  49. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/src/migration/versions/mod.rs +0 -0
  50. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/src/registry/README.md +0 -0
  51. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/src/registry/graph.rs +0 -0
  52. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/src/registry/in_memory.rs +0 -0
  53. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/src/registry/loader.rs +0 -0
  54. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/src/registry/paths.rs +0 -0
  55. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/src/registry/query.rs +0 -0
  56. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/src/registry/test_helpers.rs +0 -0
  57. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/src/registry/test_hooks.rs +0 -0
  58. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/src/validation/mod.rs +0 -0
  59. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/src/validation/script_io.rs +0 -0
  60. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/tests/fixtures/kind-rules/custom_with_signature.json +0 -0
  61. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/tests/fixtures/kind-rules/db_object_with_custom_kind.json +0 -0
  62. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/tests/fixtures/kind-rules/etl_part_cloud_status_data_migration.json +0 -0
  63. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/tests/fixtures/kind-rules/etl_part_cloud_status_data_validation.json +0 -0
  64. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/tests/fixtures/kind-rules/etl_part_model_name_on_non_dbt.json +0 -0
  65. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/tests/fixtures/kind-rules/etl_root_cloud_status.json +0 -0
  66. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/tests/fixtures/kind-rules/etl_root_dependencies.json +0 -0
  67. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/tests/fixtures/kind-rules/etl_root_issues.json +0 -0
  68. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/tests/fixtures/kind-rules/etl_root_required_by.json +0 -0
  69. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/tests/fixtures/kind-rules/etl_root_source_format.json +0 -0
  70. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/tests/fixtures/kind-rules/etl_signature.json +0 -0
  71. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/tests/fixtures/kind-rules/parameterized_reference_clean.json +0 -0
  72. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/tests/fixtures/kind-rules/parameterized_reference_with_cloud_status.json +0 -0
  73. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/tests/fixtures/kind-rules/parameterized_reference_with_code_status.json +0 -0
  74. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/tests/fixtures/kind-rules/parameterized_reference_with_files.json +0 -0
  75. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/tests/fixtures/kind-rules/parameterized_reference_with_issues.json +0 -0
  76. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/tests/fixtures/kind-rules/parameterized_reference_with_parts.json +0 -0
  77. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/tests/fixtures/kind-rules/parameterized_reference_with_planning.json +0 -0
  78. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/tests/fixtures/kind-rules/parameterized_reference_with_script_bindings.json +0 -0
  79. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/tests/fixtures/kind-rules/parameterized_reference_with_script_metadata.json +0 -0
  80. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/tests/fixtures/kind-rules/parameterized_reference_with_signature.json +0 -0
  81. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/tests/fixtures/kind-rules/parameterized_reference_with_source_format.json +0 -0
  82. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/tests/fixtures/kind-rules/parameterized_reference_with_target_format.json +0 -0
  83. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/tests/fixtures/kind-rules/sql_with_parts.json +0 -0
  84. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-core/tests/fixtures/kind-rules/sql_with_stabilization.json +0 -0
  85. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-python/Cargo.toml +0 -0
  86. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-python/README.md +0 -0
  87. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-python/tests/conftest.py +0 -0
  88. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-python/tests/test_batch.py +0 -0
  89. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-python/tests/test_checksum.py +0 -0
  90. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-python/tests/test_cloud_state.py +0 -0
  91. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-python/tests/test_crud.py +0 -0
  92. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-python/tests/test_errors.py +0 -0
  93. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-python/tests/test_logging.py +0 -0
  94. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-python/tests/test_migration.py +0 -0
  95. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-python/tests/test_query.py +0 -0
  96. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-python/tests/test_refresh.py +0 -0
  97. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-python/tests/test_script_bindings.py +0 -0
  98. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-python/tests/test_script_metadata.py +0 -0
  99. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-python/tests/test_serialization.py +0 -0
  100. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/crates/scai-state-python/tests/test_validation.py +0 -0
  101. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/python/snowflake_code_unit_registry/cloud_state.py +0 -0
  102. {snowflake_code_unit_registry-0.8.74 → snowflake_code_unit_registry-0.8.76}/python/snowflake_code_unit_registry/types.py +0 -0
@@ -376,9 +376,9 @@ dependencies = [
376
376
 
377
377
  [[package]]
378
378
  name = "combine"
379
- version = "4.6.7"
379
+ version = "4.6.8"
380
380
  source = "registry+https://github.com/rust-lang/crates.io-index"
381
- checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
381
+ checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e"
382
382
  dependencies = [
383
383
  "bytes",
384
384
  "memchr",
@@ -2315,9 +2315,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
2315
2315
 
2316
2316
  [[package]]
2317
2317
  name = "rand"
2318
- version = "0.8.7"
2318
+ version = "0.8.8"
2319
2319
  source = "registry+https://github.com/rust-lang/crates.io-index"
2320
- checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a"
2320
+ checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c"
2321
2321
  dependencies = [
2322
2322
  "libc",
2323
2323
  "rand_chacha",
@@ -2644,6 +2644,12 @@ version = "1.0.23"
2644
2644
  source = "registry+https://github.com/rust-lang/crates.io-index"
2645
2645
  checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
2646
2646
 
2647
+ [[package]]
2648
+ name = "ryu"
2649
+ version = "1.0.23"
2650
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2651
+ checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
2652
+
2647
2653
  [[package]]
2648
2654
  name = "same-file"
2649
2655
  version = "1.0.6"
@@ -2655,7 +2661,7 @@ dependencies = [
2655
2661
 
2656
2662
  [[package]]
2657
2663
  name = "scai-error-derive"
2658
- version = "0.8.74"
2664
+ version = "0.8.76"
2659
2665
  dependencies = [
2660
2666
  "err_code",
2661
2667
  "heck",
@@ -2669,7 +2675,7 @@ dependencies = [
2669
2675
 
2670
2676
  [[package]]
2671
2677
  name = "scai-log"
2672
- version = "0.8.74"
2678
+ version = "0.8.76"
2673
2679
  dependencies = [
2674
2680
  "chrono",
2675
2681
  "serde",
@@ -2678,7 +2684,7 @@ dependencies = [
2678
2684
 
2679
2685
  [[package]]
2680
2686
  name = "scai-state-core"
2681
- version = "0.8.74"
2687
+ version = "0.8.76"
2682
2688
  dependencies = [
2683
2689
  "chrono",
2684
2690
  "err_code",
@@ -2691,6 +2697,7 @@ dependencies = [
2691
2697
  "schemars",
2692
2698
  "serde",
2693
2699
  "serde_json",
2700
+ "serde_norway",
2694
2701
  "snafu",
2695
2702
  "sqlparser",
2696
2703
  "tempfile",
@@ -2703,7 +2710,7 @@ dependencies = [
2703
2710
 
2704
2711
  [[package]]
2705
2712
  name = "scai-state-csharp"
2706
- version = "0.8.74"
2713
+ version = "0.8.76"
2707
2714
  dependencies = [
2708
2715
  "interoptopus",
2709
2716
  "interoptopus_backend_csharp",
@@ -2715,7 +2722,7 @@ dependencies = [
2715
2722
 
2716
2723
  [[package]]
2717
2724
  name = "scai-state-node"
2718
- version = "0.8.74"
2725
+ version = "0.8.76"
2719
2726
  dependencies = [
2720
2727
  "napi",
2721
2728
  "napi-build",
@@ -2728,7 +2735,7 @@ dependencies = [
2728
2735
 
2729
2736
  [[package]]
2730
2737
  name = "scai-state-perf"
2731
- version = "0.8.74"
2738
+ version = "0.8.76"
2732
2739
  dependencies = [
2733
2740
  "chrono",
2734
2741
  "criterion",
@@ -2744,7 +2751,7 @@ dependencies = [
2744
2751
 
2745
2752
  [[package]]
2746
2753
  name = "scai-state-python"
2747
- version = "0.8.74"
2754
+ version = "0.8.76"
2748
2755
  dependencies = [
2749
2756
  "pyo3",
2750
2757
  "pythonize",
@@ -2880,6 +2887,19 @@ dependencies = [
2880
2887
  "zmij",
2881
2888
  ]
2882
2889
 
2890
+ [[package]]
2891
+ name = "serde_norway"
2892
+ version = "0.9.42"
2893
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2894
+ checksum = "e408f29489b5fd500fab51ff1484fc859bb655f32c671f307dcd733b72e8168c"
2895
+ dependencies = [
2896
+ "indexmap",
2897
+ "itoa",
2898
+ "ryu",
2899
+ "serde",
2900
+ "unsafe-libyaml-norway",
2901
+ ]
2902
+
2883
2903
  [[package]]
2884
2904
  name = "serde_tokenstream"
2885
2905
  version = "0.2.3"
@@ -3425,6 +3445,12 @@ version = "1.13.3"
3425
3445
  source = "registry+https://github.com/rust-lang/crates.io-index"
3426
3446
  checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
3427
3447
 
3448
+ [[package]]
3449
+ name = "unsafe-libyaml-norway"
3450
+ version = "0.2.15"
3451
+ source = "registry+https://github.com/rust-lang/crates.io-index"
3452
+ checksum = "b39abd59bf32521c7f2301b52d05a6a2c975b6003521cbd0c6dc1582f0a22104"
3453
+
3428
3454
  [[package]]
3429
3455
  name = "untrusted"
3430
3456
  version = "0.9.0"
@@ -3451,9 +3477,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
3451
3477
 
3452
3478
  [[package]]
3453
3479
  name = "uuid"
3454
- version = "1.25.0"
3480
+ version = "1.26.0"
3455
3481
  source = "registry+https://github.com/rust-lang/crates.io-index"
3456
- checksum = "f053576934f05a761a402421fbbe3d425d9366f75f978806a037b3ca481abecc"
3482
+ checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812"
3457
3483
  dependencies = [
3458
3484
  "getrandom 0.4.3",
3459
3485
  "js-sys",
@@ -3,7 +3,7 @@ resolver = "2"
3
3
  members = ["crates/scai-error-derive", "crates/scai-log", "crates/scai-state-core", "crates/scai-state-python"]
4
4
 
5
5
  [workspace.package]
6
- version = "0.8.74"
6
+ version = "0.8.76"
7
7
  edition = "2021"
8
8
  license = "SEE LICENSE IN LICENSE"
9
9
  repository = "https://github.com/snowflake-eng/scai-state"
@@ -11,6 +11,9 @@ repository = "https://github.com/snowflake-eng/scai-state"
11
11
  [workspace.dependencies]
12
12
  serde = { version = "1.0", features = ["derive"] }
13
13
  serde_json = "1.0"
14
+ # Maintained fork of the deprecated/unmaintained serde_yaml. Reads the
15
+ # `database-bindings.yml` SnowConvert generates; see `database_bindings`.
16
+ serde_norway = "0.9"
14
17
  snafu = "0.9"
15
18
  err_code = "0.1"
16
19
  uuid = { version = "1.0", features = ["v4"] }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: snowflake-code-unit-registry
3
- Version: 0.8.74
3
+ Version: 0.8.76
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: Apache Software License
@@ -10,6 +10,7 @@ autobenches = false
10
10
  [dependencies]
11
11
  serde.workspace = true
12
12
  serde_json.workspace = true
13
+ serde_norway.workspace = true
13
14
  snafu.workspace = true
14
15
  err_code.workspace = true
15
16
  scai-error-derive = { path = "../scai-error-derive" }
@@ -0,0 +1,473 @@
1
+ //! Reads the `database-bindings.yml` SnowConvert generates and turns it into the
2
+ //! literal token map [`FindOptions::bindings`] consumes, so a read can return
3
+ //! physical database names instead of the tokens a bindable conversion wrote.
4
+ //!
5
+ //! Lives outside `registry` for the same reason [`crate::bindings`] does: it
6
+ //! operates on caller-supplied values, not on registry state.
7
+ //!
8
+ //! # Why this exists
9
+ //!
10
+ //! A bindable conversion records `${NAME}` in `source.database` and `<%NAME%>` in
11
+ //! `target.database` so one artifact can be deployed and tested in whichever
12
+ //! database the operator chose. Every consumer therefore has to resolve those
13
+ //! tokens before treating the value as a real identifier — and each one that
14
+ //! forgets produces a bug far from its cause. Supplying the map at the read
15
+ //! removes the whole class rather than patching instances of it.
16
+ //!
17
+ //! # Grammar ownership
18
+ //!
19
+ //! [`crate::bindings::apply_bindings`] is deliberately grammar-free: it matches
20
+ //! map keys verbatim and knows nothing about wrappers. This module is the *only*
21
+ //! place that knows a wrapper spelling, and it is careful about which:
22
+ //!
23
+ //! * The Snowflake side is Snow CLI templating, `<%NAME%>`, which is invariant —
24
+ //! so [`DatabaseBindings::snow_token_map`] owns it.
25
+ //! * The source side is **not** ownable here. The wrapper is a property of the
26
+ //! source script language and may be customer-specific, and more than one form
27
+ //! can be in play for a single name. [`DatabaseBindings::source_token_map`]
28
+ //! therefore takes the form(s) from the caller and defaults to `${NAME}`.
29
+ //!
30
+ //! # Case
31
+ //!
32
+ //! [`crate::bindings::apply_bindings`] matches byte-exact and leaves an unmatched
33
+ //! token alone, so a bindings file whose key case differs from the case the
34
+ //! conversion wrote would resolve nothing — silently. The token map therefore
35
+ //! emits as-written, all-lower and all-upper spellings of each name, and input
36
+ //! whose intended spelling is ambiguous is rejected outright. See [`wrap`].
37
+ //!
38
+ //! # Hazard: do not bind a read that mints a frozen identity
39
+ //!
40
+ //! `apply_bindings` substitutes `target.name` and `target.canonical_name` as well
41
+ //! as `target.database`. Consumers that derive a stable identity from those — a
42
+ //! baseline lookup key, a stage path, an on-disk layout — must read **without**
43
+ //! bindings, or the identity silently changes and previously stored artifacts stop
44
+ //! matching with no error raised. Read unbound for identity, bound for anything
45
+ //! that needs a physical database.
46
+
47
+ use std::collections::HashMap;
48
+ use std::path::Path;
49
+
50
+ use crate::error::{Error, Result};
51
+
52
+ /// Renders a binding name as the literal token a conversion wrote for it.
53
+ ///
54
+ /// Used by [`DatabaseBindings::source_token_map`] to stay independent of any one
55
+ /// source-side wrapper spelling.
56
+ pub type TokenWrapper = fn(&str) -> String;
57
+
58
+ /// The default source-side wrapper: `${NAME}`, the BTEQ / shell-style form.
59
+ pub fn dollar_brace_token(name: &str) -> String {
60
+ format!("${{{name}}}")
61
+ }
62
+
63
+ /// The invariant Snowflake-side wrapper: `<%NAME%>`, Snow CLI templating.
64
+ pub fn snow_cli_token(name: &str) -> String {
65
+ format!("<%{name}%>")
66
+ }
67
+
68
+ /// The two sides of a `database-bindings.yml`.
69
+ ///
70
+ /// ```yaml
71
+ /// source:
72
+ /// dutchie_test: dutchie_test # identity when the source is named that
73
+ /// snow:
74
+ /// dutchie_test: DUTCHIE_EXTRACT_MIG # the retargeted physical database
75
+ /// ```
76
+ ///
77
+ /// Both sections are optional: a project may be bindable on one side only, and an
78
+ /// absent section yields an empty map rather than an error.
79
+ #[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
80
+ #[serde(deny_unknown_fields)]
81
+ pub struct DatabaseBindings {
82
+ /// Canonical name → physical **source** database.
83
+ #[serde(default)]
84
+ pub source: HashMap<String, String>,
85
+ /// Canonical name → physical **Snowflake** database.
86
+ #[serde(default)]
87
+ pub snow: HashMap<String, String>,
88
+ }
89
+
90
+ /// Parse `database-bindings.yml` contents.
91
+ ///
92
+ /// Unknown top-level sections are rejected: a silently-empty map would make
93
+ /// substitution a no-op and hand tokens back to a caller that believes they were
94
+ /// resolved, which is the exact failure this module exists to prevent.
95
+ ///
96
+ /// Two keys in one section that differ only by case are rejected for the same
97
+ /// reason — see [`reject_case_collisions`].
98
+ ///
99
+ /// Returns `Err(Error::ValidationError)` when the document is not valid YAML or
100
+ /// does not match the two-section shape.
101
+ pub fn parse_database_bindings(yaml: &str) -> Result<DatabaseBindings> {
102
+ let parsed: DatabaseBindings = serde_norway::from_str(yaml)
103
+ .map_err(|e| Error::validation(format!("database-bindings.yml is not valid: {e}")))?;
104
+ reject_case_collisions("source", &parsed.source)?;
105
+ reject_case_collisions("snow", &parsed.snow)?;
106
+ Ok(parsed)
107
+ }
108
+
109
+ /// Reject two keys in one section that differ only by case.
110
+ ///
111
+ /// [`wrap`] emits a case variant per name, so `{db: A, DB: B}` would have both
112
+ /// names claiming the same variant keys and the winner would depend on hash
113
+ /// iteration order — a nondeterministic substitution. Rejecting is the only
114
+ /// honest answer: nothing here can know which spelling the conversion wrote.
115
+ fn reject_case_collisions(section: &str, names: &HashMap<String, String>) -> Result<()> {
116
+ let mut folded: HashMap<String, &str> = HashMap::with_capacity(names.len());
117
+ for name in names.keys() {
118
+ if let Some(other) = folded.insert(name.to_lowercase(), name.as_str()) {
119
+ let (a, b) = if name.as_str() < other {
120
+ (name.as_str(), other)
121
+ } else {
122
+ (other, name.as_str())
123
+ };
124
+ return Err(Error::validation(format!(
125
+ "database-bindings.yml is not valid: `{section}` has two keys differing only by \
126
+ case (`{a}` and `{b}`); the token map emits case variants, so which one applied \
127
+ would depend on hash order"
128
+ )));
129
+ }
130
+ }
131
+ Ok(())
132
+ }
133
+
134
+ /// Read and parse a `database-bindings.yml` from disk.
135
+ ///
136
+ /// Convenience over [`parse_database_bindings`] for callers holding a path. IO
137
+ /// failures surface as `Error::IoError`, parse failures as in
138
+ /// [`parse_database_bindings`].
139
+ pub fn read_database_bindings(path: &Path) -> Result<DatabaseBindings> {
140
+ let text = std::fs::read_to_string(path)?;
141
+ parse_database_bindings(&text)
142
+ }
143
+
144
+ impl DatabaseBindings {
145
+ /// `<%NAME%> → physical database`, for reads that should return Snowflake
146
+ /// names instead of target-side tokens.
147
+ ///
148
+ /// Feed straight into [`FindOptions::bindings`]. Empty when the `snow:`
149
+ /// section is absent, which makes the read a no-op rather than an error.
150
+ ///
151
+ /// See the module-level hazard note before binding a read whose result mints
152
+ /// a frozen identity.
153
+ pub fn snow_token_map(&self) -> HashMap<String, String> {
154
+ wrap(&self.snow, &[snow_cli_token])
155
+ }
156
+
157
+ /// `wrapper(NAME) → physical database` for every wrapper form supplied.
158
+ ///
159
+ /// Pass the form(s) the conversion actually wrote. Every form becomes its own
160
+ /// key, so a name carrying more than one spelling resolves whichever appears;
161
+ /// a form that was never written simply matches nothing.
162
+ ///
163
+ /// Use [`Self::source_token_map_default`] for the common `${NAME}` case.
164
+ pub fn source_token_map(&self, wrappers: &[TokenWrapper]) -> HashMap<String, String> {
165
+ wrap(&self.source, wrappers)
166
+ }
167
+
168
+ /// [`Self::source_token_map`] with the default `${NAME}` wrapper.
169
+ pub fn source_token_map_default(&self) -> HashMap<String, String> {
170
+ wrap(&self.source, &[dollar_brace_token])
171
+ }
172
+
173
+ /// Both sides in one map — `${NAME} → source database` and
174
+ /// `<%NAME%> → Snowflake database` — so one read returns fully bound units.
175
+ ///
176
+ /// Merging is safe because the two wrappers are *disjoint*: a `${…}` key can
177
+ /// never match a `<%…%>` token or vice versa, so a slot only ever matches the
178
+ /// side whose grammar it was written in. This is what lets
179
+ /// [`FindOptions::bindings_path`] take a path and nothing else.
180
+ ///
181
+ /// Uses the **default** `${NAME}` source grammar. A project whose conversion
182
+ /// wrote a customer-specific source wrapper gets no source-side resolution
183
+ /// from this — and, `apply_bindings` leaving unmatched tokens alone, would get
184
+ /// it *silently*. Those callers must build the map explicitly with
185
+ /// [`Self::source_token_map`] / [`Self::source_token_map_from_formats`] and
186
+ /// pass `bindings`. The snow side is unaffected: `<%NAME%>` is invariant.
187
+ pub fn token_map(&self) -> HashMap<String, String> {
188
+ let mut out = self.source_token_map_default();
189
+ out.extend(self.snow_token_map());
190
+ out
191
+ }
192
+
193
+ /// [`Self::source_token_map`] for callers that can only pass *data*, not a
194
+ /// function pointer — the language bindings.
195
+ ///
196
+ /// `forms` are format strings containing `{name}` (e.g. `"${{{name}}}"` or a
197
+ /// customer's `"@{name}@"`). A callable cannot cross the FFI boundary and
198
+ /// [`TokenWrapper`] cannot be built from one, so the bindings pass strings.
199
+ ///
200
+ /// This exists so the case handling in [`wrap`] applies to the bindings too.
201
+ /// Each of them previously expanded these forms inline, which meant the
202
+ /// custom-wrapper path silently skipped the case spellings — the same defect
203
+ /// this module was just fixed for, reintroduced three times over. One
204
+ /// definition, called from all four surfaces.
205
+ ///
206
+ /// Returns `Err` naming the offending form when one lacks `{name}`; each
207
+ /// binding maps that to its own error type.
208
+ pub fn source_token_map_from_formats(
209
+ &self,
210
+ forms: &[String],
211
+ ) -> std::result::Result<HashMap<String, String>, String> {
212
+ // Empty means "not specified", i.e. the default `${NAME}` grammar -- decided
213
+ // here so all four surfaces agree. They cannot agree by convention: the C# FFI
214
+ // receives the forms as JSON and cannot distinguish an absent list from an
215
+ // empty one, so it always treated empty as the default, while Python and Node
216
+ // passed `[]` straight through and produced an *empty* source map. That
217
+ // divergence resolved nothing on the source side and, `apply_bindings` leaving
218
+ // unmatched tokens alone, said nothing about it.
219
+ if forms.is_empty() {
220
+ return Ok(self.source_token_map_default());
221
+ }
222
+ if let Some(bad) = forms.iter().find(|f| !f.contains("{name}")) {
223
+ return Err(format!("source wrapper {bad:?} must contain '{{name}}'"));
224
+ }
225
+ let mut out = HashMap::with_capacity(self.source.len() * forms.len() * 3);
226
+ for (name, value) in &self.source {
227
+ for spelling in case_spellings(name) {
228
+ for form in forms {
229
+ out.insert(form.replace("{name}", &spelling), value.clone());
230
+ }
231
+ }
232
+ }
233
+ Ok(out)
234
+ }
235
+ }
236
+
237
+ /// The spellings a conversion may have written for `name`: as-written, all-lower,
238
+ /// all-upper. Duplicates are harmless — every consumer inserts into a map.
239
+ ///
240
+ /// See [`wrap`] for why more than the as-written spelling is emitted.
241
+ fn case_spellings(name: &str) -> [String; 3] {
242
+ [name.to_string(), name.to_lowercase(), name.to_uppercase()]
243
+ }
244
+
245
+ /// Expand a `name → value` map into `wrapper(spelling) → value` for each wrapper
246
+ /// and each case spelling of the name.
247
+ ///
248
+ /// No bare-name key is ever emitted: a bare name would also match *inside* the
249
+ /// other side's wrapped token and corrupt it.
250
+ ///
251
+ /// # Why case variants
252
+ ///
253
+ /// [`crate::bindings::apply_bindings`] matches keys **byte-exact**
254
+ /// (`substitute_literal` uses `starts_with`), and it leaves an unmatched token
255
+ /// untouched rather than erroring. So a `database-bindings.yml` whose key case
256
+ /// differs from the case the conversion wrote into the registry would resolve
257
+ /// nothing, silently — the token would flow onward and surface far away as a
258
+ /// `001003` syntax error on the `<` of `<%NAME%>`.
259
+ ///
260
+ /// The two cases genuinely differ in practice: the engine emits the canonical
261
+ /// name lower-case while the config normaliser upper-cases it, which is why the
262
+ /// desktop's own `substitute_tokens` casefolds on lookup. Rather than make the
263
+ /// core case-insensitive — it is deliberately literal and grammar-free, and
264
+ /// folding there would affect every other substitution — the compatibility lives
265
+ /// here, in the one place that already knows about spelling.
266
+ ///
267
+ /// Emitting extra *wrapped* forms is safe: each is specific, and a form the
268
+ /// engine never wrote simply matches nothing. This covers as-written, all-lower
269
+ /// and all-upper; an arbitrary mixed-case spelling the conversion invented
270
+ /// (`<%Db_Name%>` from a key `db_name`) is still not matched, which is why
271
+ /// [`reject_case_collisions`] refuses input where the intended spelling is
272
+ /// ambiguous rather than guessing.
273
+ fn wrap(names: &HashMap<String, String>, wrappers: &[TokenWrapper]) -> HashMap<String, String> {
274
+ // 3 spellings per name per wrapper, before dedup.
275
+ let mut out = HashMap::with_capacity(names.len() * wrappers.len() * 3);
276
+ for (name, value) in names {
277
+ // Identical spellings collapse on insert; case collisions across two
278
+ // different names cannot occur, having been rejected at parse time.
279
+ for spelling in case_spellings(name) {
280
+ for wrapper in wrappers {
281
+ out.insert(wrapper(&spelling), value.clone());
282
+ }
283
+ }
284
+ }
285
+ out
286
+ }
287
+
288
+ #[cfg(test)]
289
+ mod tests {
290
+ use super::*;
291
+
292
+ /// The shape SnowConvert actually writes.
293
+ const REAL: &str =
294
+ "source:\n dutchie_test: dutchie_test\nsnow:\n dutchie_test: DUTCHIE_EXTRACT_MIG\n";
295
+
296
+ #[test]
297
+ fn parses_the_real_file_shape() {
298
+ let b = parse_database_bindings(REAL).unwrap();
299
+ assert_eq!(b.source.get("dutchie_test").unwrap(), "dutchie_test");
300
+ assert_eq!(b.snow.get("dutchie_test").unwrap(), "DUTCHIE_EXTRACT_MIG");
301
+ }
302
+
303
+ #[test]
304
+ fn snow_map_is_wrapped_in_snow_cli_form() {
305
+ let b = parse_database_bindings(REAL).unwrap();
306
+ let m = b.snow_token_map();
307
+ // as-written (== lower here) plus the upper spelling: 2 keys, not 1.
308
+ assert_eq!(m.len(), 2);
309
+ assert_eq!(m.get("<%dutchie_test%>").unwrap(), "DUTCHIE_EXTRACT_MIG");
310
+ assert_eq!(m.get("<%DUTCHIE_TEST%>").unwrap(), "DUTCHIE_EXTRACT_MIG");
311
+ // no bare key: it would match inside the source-side token too
312
+ assert!(!m.contains_key("dutchie_test"));
313
+ assert!(!m.contains_key("DUTCHIE_TEST"));
314
+ }
315
+
316
+ /// The regression this module shipped with: byte-exact matching in
317
+ /// `substitute_literal` means a YAML key cased differently from the token the
318
+ /// conversion wrote resolves *nothing*, and `apply_bindings` leaves an
319
+ /// unmatched token alone — so it fails silently and surfaces as a `001003`
320
+ /// syntax error on the `<`, layers from the cause.
321
+ #[test]
322
+ fn key_case_need_not_match_the_token_case() {
323
+ // Upper-case key, as the config normaliser writes it...
324
+ let b = parse_database_bindings("snow:\n DUTCHIE_TEST: DUTCHIE_EXTRACT_MIG\n").unwrap();
325
+ let m = b.snow_token_map();
326
+ // ...still resolves the lower-case token the engine emits.
327
+ assert_eq!(m.get("<%dutchie_test%>").unwrap(), "DUTCHIE_EXTRACT_MIG");
328
+ assert_eq!(m.get("<%DUTCHIE_TEST%>").unwrap(), "DUTCHIE_EXTRACT_MIG");
329
+
330
+ // ...and the mirror case: lower-case key, upper-case token.
331
+ let b = parse_database_bindings("snow:\n dutchie_test: DUTCHIE_EXTRACT_MIG\n").unwrap();
332
+ assert_eq!(
333
+ b.snow_token_map().get("<%DUTCHIE_TEST%>").unwrap(),
334
+ "DUTCHIE_EXTRACT_MIG"
335
+ );
336
+ }
337
+
338
+ /// The language bindings go through this rather than expanding inline; before
339
+ /// they did, the custom-wrapper path skipped the case spellings entirely.
340
+ #[test]
341
+ fn format_string_wrappers_get_the_case_spellings_too() {
342
+ let b = parse_database_bindings("source:\n MyDb: real_db\n").unwrap();
343
+ // Plain format strings carrying a literal `{name}`, per the documented
344
+ // convention (`"@{name}@"`) -- these are data, not Rust `format!` syntax.
345
+ let m = b
346
+ .source_token_map_from_formats(&["@{name}@".to_string(), "[{name}]".to_string()])
347
+ .unwrap();
348
+ // 2 forms x 3 spellings
349
+ assert_eq!(m.len(), 6);
350
+ for key in ["@MyDb@", "@mydb@", "@MYDB@", "[MyDb]", "[mydb]", "[MYDB]"] {
351
+ assert_eq!(
352
+ m.get(key).map(String::as_str),
353
+ Some("real_db"),
354
+ "missing {key}"
355
+ );
356
+ }
357
+ }
358
+
359
+ /// An empty form list means "unspecified", not "resolve nothing" -- the C# FFI
360
+ /// cannot tell absent from empty, so this is the only way the four surfaces agree.
361
+ #[test]
362
+ fn no_wrapper_forms_falls_back_to_the_default_grammar() {
363
+ let b = parse_database_bindings("source:\n db: real\n").unwrap();
364
+ let from_empty = b.source_token_map_from_formats(&[]).unwrap();
365
+ assert_eq!(from_empty, b.source_token_map_default());
366
+ assert_eq!(from_empty.get("${db}").map(String::as_str), Some("real"));
367
+ assert!(
368
+ !from_empty.is_empty(),
369
+ "an empty form list must not silently resolve nothing"
370
+ );
371
+ }
372
+
373
+ #[test]
374
+ fn format_string_wrapper_must_contain_the_name_placeholder() {
375
+ let b = parse_database_bindings("source:\n db: real\n").unwrap();
376
+ let err = b
377
+ .source_token_map_from_formats(&["no-placeholder".to_string()])
378
+ .unwrap_err();
379
+ assert!(err.contains("must contain"), "unexpected error: {err}");
380
+ }
381
+
382
+ #[test]
383
+ fn source_side_gets_the_same_case_treatment() {
384
+ let b = parse_database_bindings("source:\n MyDb: real_db\n").unwrap();
385
+ let m = b.source_token_map_default();
386
+ assert_eq!(m.get("${MyDb}").unwrap(), "real_db");
387
+ assert_eq!(m.get("${mydb}").unwrap(), "real_db");
388
+ assert_eq!(m.get("${MYDB}").unwrap(), "real_db");
389
+ }
390
+
391
+ /// Two names differing only by case make the emitted variants ambiguous, and
392
+ /// nothing here can know which spelling the conversion wrote — so this is
393
+ /// rejected rather than resolved by hash order.
394
+ #[test]
395
+ fn keys_differing_only_by_case_are_rejected() {
396
+ let err =
397
+ parse_database_bindings("snow:\n dutchie_test: A\n DUTCHIE_TEST: B\n").unwrap_err();
398
+ let msg = format!("{err}");
399
+ assert!(msg.contains("differing only by"), "unexpected error: {msg}");
400
+ assert!(msg.contains("dutchie_test") && msg.contains("DUTCHIE_TEST"));
401
+ }
402
+
403
+ /// The collision check is per-section: the same name on both sides is normal
404
+ /// (that is what the real file looks like) and must stay legal.
405
+ #[test]
406
+ fn the_same_name_on_both_sides_is_not_a_collision() {
407
+ assert!(parse_database_bindings(REAL).is_ok());
408
+ }
409
+
410
+ #[test]
411
+ fn source_map_defaults_to_dollar_brace() {
412
+ let b = parse_database_bindings(REAL).unwrap();
413
+ let m = b.source_token_map_default();
414
+ assert_eq!(m.get("${dutchie_test}").unwrap(), "dutchie_test");
415
+ assert!(!m.contains_key("dutchie_test"));
416
+ }
417
+
418
+ /// The identity mapping in the real file is not a no-op to detect: the *key*
419
+ /// is still a token, so substitution still has work to do.
420
+ #[test]
421
+ fn identity_mapping_still_strips_the_token() {
422
+ let b = parse_database_bindings(REAL).unwrap();
423
+ let m = b.source_token_map_default();
424
+ assert_eq!(m.get("${dutchie_test}").unwrap(), "dutchie_test");
425
+ }
426
+
427
+ #[test]
428
+ fn multiple_source_wrappers_each_get_a_key() {
429
+ fn at_form(name: &str) -> String {
430
+ format!("@{name}@")
431
+ }
432
+ let b = parse_database_bindings(REAL).unwrap();
433
+ let m = b.source_token_map(&[dollar_brace_token, at_form]);
434
+ // 2 wrappers x 2 distinct case spellings of a lower-case name.
435
+ assert_eq!(m.len(), 4);
436
+ assert_eq!(m.get("${dutchie_test}").unwrap(), "dutchie_test");
437
+ assert_eq!(m.get("@dutchie_test@").unwrap(), "dutchie_test");
438
+ assert_eq!(m.get("${DUTCHIE_TEST}").unwrap(), "dutchie_test");
439
+ assert_eq!(m.get("@DUTCHIE_TEST@").unwrap(), "dutchie_test");
440
+ }
441
+
442
+ #[test]
443
+ fn one_sided_file_is_accepted() {
444
+ let b = parse_database_bindings("snow:\n db: PHYSICAL\n").unwrap();
445
+ assert!(b.source.is_empty());
446
+ assert_eq!(b.snow_token_map().get("<%db%>").unwrap(), "PHYSICAL");
447
+ assert_eq!(b.snow_token_map().get("<%DB%>").unwrap(), "PHYSICAL");
448
+ // an absent side yields an empty map, so the read is a no-op not an error
449
+ assert!(b.source_token_map_default().is_empty());
450
+ }
451
+
452
+ #[test]
453
+ fn empty_document_is_accepted_as_empty_maps() {
454
+ let b = parse_database_bindings("{}").unwrap();
455
+ assert!(b.source.is_empty() && b.snow.is_empty());
456
+ }
457
+
458
+ #[test]
459
+ fn unknown_section_is_rejected() {
460
+ // Silently ignoring it would hand back tokens the caller believes are resolved.
461
+ let err = parse_database_bindings("source:\n a: b\nsnowflake:\n a: b\n").unwrap_err();
462
+ assert!(
463
+ format!("{err}").contains("database-bindings.yml is not valid"),
464
+ "unexpected error: {err}"
465
+ );
466
+ }
467
+
468
+ #[test]
469
+ fn malformed_yaml_is_rejected() {
470
+ let err = parse_database_bindings("source: [unclosed").unwrap_err();
471
+ assert!(format!("{err}").contains("database-bindings.yml is not valid"));
472
+ }
473
+ }
@@ -6,6 +6,7 @@
6
6
  pub mod bindings;
7
7
  pub mod canonical_name;
8
8
  pub mod checksum;
9
+ pub mod database_bindings;
9
10
  pub(crate) mod dependency_view;
10
11
  pub mod error;
11
12
  pub mod error_trace;
@@ -33,6 +34,10 @@ pub use checksum::ChecksumValidationStatus;
33
34
  pub use checksum::FileChange;
34
35
  pub use checksum::FileType;
35
36
  pub use checksum::SourceCodeChanges;
37
+ pub use database_bindings::{
38
+ dollar_brace_token, parse_database_bindings, read_database_bindings, snow_cli_token,
39
+ DatabaseBindings, TokenWrapper,
40
+ };
36
41
  pub use error::BatchFailure;
37
42
  pub use error::BatchResult;
38
43
  pub use error::Error;