|
13 | 13 | /// |
14 | 14 | /// The resulting plan is then used by the execution module to apply the changes. |
15 | 15 | use crate::framework::core::infra_reality_checker::{InfraRealityChecker, RealityCheckError}; |
16 | | -use crate::framework::core::infrastructure::consumption_webserver::ConsumptionApiWebServer; |
17 | | -use crate::framework::core::infrastructure::olap_process::OlapProcess; |
18 | 16 | use crate::framework::core::infrastructure_map::{ |
19 | 17 | InfraChanges, InfrastructureMap, OlapChange, TableChange, |
20 | 18 | }; |
@@ -302,22 +300,8 @@ pub async fn plan_changes( |
302 | 300 | .unwrap_or("Could not serialize current infrastructure map".to_string()) |
303 | 301 | ); |
304 | 302 |
|
305 | | - let current_map_or_empty = current_infra_map.unwrap_or_else(|| InfrastructureMap { |
306 | | - default_database: project.clickhouse_config.db_name.clone(), |
307 | | - topics: Default::default(), |
308 | | - api_endpoints: Default::default(), |
309 | | - tables: Default::default(), |
310 | | - views: Default::default(), |
311 | | - topic_to_table_sync_processes: Default::default(), |
312 | | - topic_to_topic_sync_processes: Default::default(), |
313 | | - function_processes: Default::default(), |
314 | | - block_db_processes: OlapProcess {}, |
315 | | - consumption_api_web_server: ConsumptionApiWebServer {}, |
316 | | - orchestration_workers: Default::default(), |
317 | | - sql_resources: Default::default(), |
318 | | - workflows: Default::default(), |
319 | | - web_apps: Default::default(), |
320 | | - }); |
| 303 | + let current_map_or_empty = |
| 304 | + current_infra_map.unwrap_or_else(|| InfrastructureMap::empty_from_project(project)); |
321 | 305 |
|
322 | 306 | // Reconcile the current map with reality before diffing, but only if OLAP is enabled |
323 | 307 | let reconciled_map = if project.features.olap { |
@@ -403,6 +387,7 @@ mod tests { |
403 | 387 | use crate::infrastructure::olap::OlapChangesError; |
404 | 388 | use crate::infrastructure::olap::OlapOperations; |
405 | 389 | use async_trait::async_trait; |
| 390 | + use protobuf::Message; |
406 | 391 |
|
407 | 392 | // Mock OLAP client for testing |
408 | 393 | struct MockOlapClient { |
@@ -744,4 +729,172 @@ mod tests { |
744 | 729 | // Compare the tables to ensure they are identical |
745 | 730 | assert_eq!(reconciled.tables.values().next().unwrap(), &table); |
746 | 731 | } |
| 732 | + |
| 733 | + #[tokio::test] |
| 734 | + async fn test_custom_database_name_preserved_on_first_migration() { |
| 735 | + // This test reproduces ENG-1160: custom database name should be preserved |
| 736 | + // on first migration when no prior state exists |
| 737 | + |
| 738 | + const CUSTOM_DB_NAME: &str = "my_custom_database"; |
| 739 | + |
| 740 | + // Create a project with a CUSTOM database name (not "local") |
| 741 | + let mut project = create_test_project(); |
| 742 | + project.clickhouse_config.db_name = CUSTOM_DB_NAME.to_string(); |
| 743 | + |
| 744 | + // Create an infrastructure map as if it's the target map |
| 745 | + // (this simulates what InfrastructureMap::new would create) |
| 746 | + let mut target_map = InfrastructureMap { |
| 747 | + default_database: CUSTOM_DB_NAME.to_string(), |
| 748 | + ..Default::default() |
| 749 | + }; |
| 750 | + |
| 751 | + // Add a test table to make it realistic |
| 752 | + let table = create_test_table("test_table"); |
| 753 | + target_map.tables.insert(table.id(CUSTOM_DB_NAME), table); |
| 754 | + |
| 755 | + // Simulate storing to Redis (serialize to protobuf) |
| 756 | + let proto_bytes = target_map.to_proto().write_to_bytes().unwrap(); |
| 757 | + |
| 758 | + // Simulate loading from Redis (deserialize from protobuf) |
| 759 | + let loaded_map = InfrastructureMap::from_proto(proto_bytes).unwrap(); |
| 760 | + |
| 761 | + // ASSERTION: The custom database name should be preserved after round-trip |
| 762 | + assert_eq!( |
| 763 | + loaded_map.default_database, CUSTOM_DB_NAME, |
| 764 | + "Custom database name '{}' was not preserved after serialization round-trip. Got: '{}'", |
| 765 | + CUSTOM_DB_NAME, loaded_map.default_database |
| 766 | + ); |
| 767 | + |
| 768 | + // Also verify that reconciliation preserves the database name |
| 769 | + let mock_client = MockOlapClient { tables: vec![] }; |
| 770 | + |
| 771 | + let target_table_names = HashSet::new(); |
| 772 | + let reconciled = |
| 773 | + reconcile_with_reality(&project, &loaded_map, &target_table_names, mock_client) |
| 774 | + .await |
| 775 | + .unwrap(); |
| 776 | + |
| 777 | + assert_eq!( |
| 778 | + reconciled.default_database, CUSTOM_DB_NAME, |
| 779 | + "Custom database name '{}' was not preserved after reconciliation. Got: '{}'", |
| 780 | + CUSTOM_DB_NAME, reconciled.default_database |
| 781 | + ); |
| 782 | + } |
| 783 | + |
| 784 | + #[tokio::test] |
| 785 | + async fn test_loading_old_proto_without_default_database_field() { |
| 786 | + // This test simulates loading an infrastructure map from an old proto |
| 787 | + // that was serialized before the default_database field was added (field #15) |
| 788 | + |
| 789 | + const CUSTOM_DB_NAME: &str = "my_custom_database"; |
| 790 | + |
| 791 | + // Create a project with a CUSTOM database name |
| 792 | + let mut project = create_test_project(); |
| 793 | + project.clickhouse_config.db_name = CUSTOM_DB_NAME.to_string(); |
| 794 | + |
| 795 | + // Manually create a proto WITHOUT the default_database field |
| 796 | + // by creating an empty proto (which won't have default_database set) |
| 797 | + use crate::proto::infrastructure_map::InfrastructureMap as ProtoInfrastructureMap; |
| 798 | + let old_proto = ProtoInfrastructureMap::new(); |
| 799 | + // Note: NOT setting old_proto.default_database - simulates old proto |
| 800 | + |
| 801 | + let proto_bytes = old_proto.write_to_bytes().unwrap(); |
| 802 | + |
| 803 | + // Load it back |
| 804 | + let loaded_map = InfrastructureMap::from_proto(proto_bytes).unwrap(); |
| 805 | + |
| 806 | + // BUG: When loading an old proto, the default_database will be empty string "" |
| 807 | + // This should fail if the bug exists |
| 808 | + println!( |
| 809 | + "Loaded map default_database: '{}'", |
| 810 | + loaded_map.default_database |
| 811 | + ); |
| 812 | + |
| 813 | + // The bug manifests here: loading an old proto results in empty string for default_database |
| 814 | + // which might get replaced with DEFAULT_DATABASE_NAME ("local") somewhere |
| 815 | + assert_eq!( |
| 816 | + loaded_map.default_database, "", |
| 817 | + "Old proto should have empty default_database, got: '{}'", |
| 818 | + loaded_map.default_database |
| 819 | + ); |
| 820 | + |
| 821 | + // Now test reconciliation - this is where the fix should be applied |
| 822 | + let mock_client = MockOlapClient { tables: vec![] }; |
| 823 | + |
| 824 | + let target_table_names = HashSet::new(); |
| 825 | + let reconciled = |
| 826 | + reconcile_with_reality(&project, &loaded_map, &target_table_names, mock_client) |
| 827 | + .await |
| 828 | + .unwrap(); |
| 829 | + |
| 830 | + // After reconciliation, the database name should be set from the project config |
| 831 | + assert_eq!( |
| 832 | + reconciled.default_database, CUSTOM_DB_NAME, |
| 833 | + "After reconciliation, custom database name should be set from project. Got: '{}'", |
| 834 | + reconciled.default_database |
| 835 | + ); |
| 836 | + } |
| 837 | + |
| 838 | + #[tokio::test] |
| 839 | + #[allow(clippy::unnecessary_literal_unwrap)] // Test intentionally demonstrates buggy pattern |
| 840 | + async fn test_bug_eng_1160_default_overwrites_custom_db_name() { |
| 841 | + // This test demonstrates the actual bug pattern found in local_webserver.rs |
| 842 | + // where `Ok(None) => InfrastructureMap::default()` is used instead of |
| 843 | + // creating an InfrastructureMap with the project's db_name. |
| 844 | + |
| 845 | + const CUSTOM_DB_NAME: &str = "my_custom_database"; |
| 846 | + let mut project = create_test_project(); |
| 847 | + project.clickhouse_config.db_name = CUSTOM_DB_NAME.to_string(); |
| 848 | + |
| 849 | + // Simulate the buggy pattern: when no state exists, use default() |
| 850 | + let loaded_map_buggy: Option<InfrastructureMap> = None; |
| 851 | + let buggy_map = loaded_map_buggy.unwrap_or_default(); |
| 852 | + |
| 853 | + // BUG: This will use "local" instead of "my_custom_database" |
| 854 | + assert_eq!( |
| 855 | + buggy_map.default_database, "local", |
| 856 | + "BUG REPRODUCED: default() returns 'local' instead of project's db_name" |
| 857 | + ); |
| 858 | + assert_ne!( |
| 859 | + buggy_map.default_database, CUSTOM_DB_NAME, |
| 860 | + "Bug confirmed: custom database name is lost" |
| 861 | + ); |
| 862 | + |
| 863 | + // CORRECT PATTERN: Create InfrastructureMap with project's config |
| 864 | + let loaded_map_correct: Option<InfrastructureMap> = None; |
| 865 | + let correct_map = |
| 866 | + loaded_map_correct.unwrap_or_else(|| InfrastructureMap::empty_from_project(&project)); |
| 867 | + |
| 868 | + assert_eq!( |
| 869 | + correct_map.default_database, CUSTOM_DB_NAME, |
| 870 | + "Correct pattern: InfrastructureMap uses project's db_name" |
| 871 | + ); |
| 872 | + } |
| 873 | + |
| 874 | + #[test] |
| 875 | + fn test_only_default_database_field_is_config_driven() { |
| 876 | + // Verify that default_database is the ONLY field in InfrastructureMap |
| 877 | + // that comes directly from project clickhouse_config.db_name. |
| 878 | + // This is the critical field for ENG-1160: when InfrastructureMap::default() |
| 879 | + // is used instead of InfrastructureMap::new(), default_database gets "local" |
| 880 | + // instead of the project's configured database name. |
| 881 | + |
| 882 | + const CUSTOM_DB_NAME: &str = "custom_db"; |
| 883 | + let mut project = create_test_project(); |
| 884 | + project.clickhouse_config.db_name = CUSTOM_DB_NAME.to_string(); |
| 885 | + |
| 886 | + let primitive_map = PrimitiveMap::default(); |
| 887 | + let infra_map = InfrastructureMap::new(&project, primitive_map); |
| 888 | + |
| 889 | + // Critical: default_database must be set from project config |
| 890 | + assert_eq!( |
| 891 | + infra_map.default_database, CUSTOM_DB_NAME, |
| 892 | + "default_database must use project's clickhouse_config.db_name, not hardcoded 'local'" |
| 893 | + ); |
| 894 | + |
| 895 | + // Note: Other fields may be populated based on project properties |
| 896 | + // (e.g., orchestration_workers is created based on project.language) |
| 897 | + // but they don't directly use clickhouse_config.db_name. |
| 898 | + // The bug in ENG-1160 is specifically about default_database being hardcoded to "local". |
| 899 | + } |
747 | 900 | } |
0 commit comments