package dbwrapper import ( "strings" "testing" "git.ksdemosapps.com/kylesoda/go-migrate/internal/app/config" "git.ksdemosapps.com/kylesoda/go-migrate/internal/app/models" ) func TestBuildExtractQueryMssql_NoJsonColumns(t *testing.T) { q := ExtractionQuery{ Schema: "dbo", Table: "Users", PrimaryKey: "ID", Columns: []models.ColumnType{ models.NewColumnType("ID", false, false, "INT", "int", "INT", false, 0, 0, 0), models.NewColumnType("Name", true, false, "VARCHAR", "varchar", "VARCHAR", true, 255, 0, 0), }, FromJsonColumns: []config.FromJsonItem{}, LowerLimit: ExtractorQueryLimit{IsValid: false}, UpperLimit: ExtractorQueryLimit{IsValid: false}, } query, err := buildExtractQueryMssql(q) if err != nil { t.Fatalf("Expected no error, got: %v", err) } if !strings.Contains(query, "SELECT [ID], [Name]") { t.Errorf("Expected columns in query, got: %s", query) } if !strings.Contains(query, "FROM [dbo].[Users]") { t.Errorf("Expected FROM clause, got: %s", query) } if !strings.Contains(query, "ORDER BY [ID] ASC") { t.Errorf("Expected ORDER BY clause, got: %s", query) } } func TestBuildExtractQueryMssql_WithJsonColumns_ExactColumnMatch(t *testing.T) { // Test that the actual column name is used as alias, not a generated one q := ExtractionQuery{ Schema: "dbo", Table: "Events", PrimaryKey: "EventID", Columns: []models.ColumnType{ models.NewColumnType("EventID", false, false, "INT", "int", "INT", false, 0, 0, 0), models.NewColumnType("EventData", true, false, "VARCHAR", "varchar", "VARCHAR", true, 500, 0, 0), }, FromJsonColumns: []config.FromJsonItem{ {Column: "EventData", Field: ".userId"}, {Column: "EventData", Field: ".timestamp"}, }, LowerLimit: ExtractorQueryLimit{IsValid: false}, UpperLimit: ExtractorQueryLimit{IsValid: false}, } query, err := buildExtractQueryMssql(q) if err != nil { t.Fatalf("Expected no error, got: %v", err) } if !strings.HasPrefix(query, "SELECT [EventID], JSON_VALUE([EventData], '$.userId') AS [EventData], JSON_VALUE([EventData], '$.timestamp') AS [EventData]") { t.Errorf("Expected JSON columns to replace EventData in-order, got: %s", query) } if strings.Contains(query, "SELECT [EventID], [EventData]") { t.Errorf("Expected EventData to be replaced by JSON extraction, got: %s", query) } // Alias should be exactly "EventData", not "EventData_userId" if !strings.Contains(query, "JSON_VALUE([EventData], '$.userId') AS [EventData]") { t.Errorf("Expected JSON alias to be [EventData], got: %s", query) } if !strings.Contains(query, "JSON_VALUE([EventData], '$.timestamp') AS [EventData]") { t.Errorf("Expected JSON alias to be [EventData], got: %s", query) } // Should have comma separating them if !strings.Contains(query, "JSON_VALUE([EventData], '$.userId') AS [EventData], JSON_VALUE([EventData], '$.timestamp') AS [EventData]") { t.Errorf("Expected comma-separated JSON values, got: %s", query) } } func TestBuildExtractQueryMssql_WithWildcardPattern(t *testing.T) { // Test that wildcard pattern matching finds the correct column q := ExtractionQuery{ Schema: "dbo", Table: "Events", PrimaryKey: "ID", Columns: []models.ColumnType{ models.NewColumnType("ID", false, false, "INT", "int", "INT", false, 0, 0, 0), models.NewColumnType("NodeMetadata", true, false, "VARCHAR", "varchar", "VARCHAR", true, 500, 0, 0), }, FromJsonColumns: []config.FromJsonItem{ {Column: "NodeMeta*", Field: ".id"}, }, LowerLimit: ExtractorQueryLimit{IsValid: false}, UpperLimit: ExtractorQueryLimit{IsValid: false}, } query, err := buildExtractQueryMssql(q) if err != nil { t.Fatalf("Expected no error, got: %v", err) } // Should find "NodeMetadata" from pattern "NodeMeta*" and use it as alias if !strings.Contains(query, "JSON_VALUE([NodeMetadata], '$.id') AS [NodeMetadata]") { t.Errorf("Expected to find and use NodeMetadata column by pattern, got: %s", query) } if strings.Contains(query, "SELECT [ID], [NodeMetadata]") { t.Errorf("Expected NodeMetadata to be replaced by JSON extraction, got: %s", query) } } func TestBuildExtractQueryMssql_ColumnNotFound_Error(t *testing.T) { // Test that an error is returned when column is not found q := ExtractionQuery{ Schema: "dbo", Table: "Events", PrimaryKey: "ID", Columns: []models.ColumnType{ models.NewColumnType("ID", false, false, "INT", "int", "INT", false, 0, 0, 0), }, FromJsonColumns: []config.FromJsonItem{ {Column: "NonExistentColumn", Field: ".id"}, }, LowerLimit: ExtractorQueryLimit{IsValid: false}, UpperLimit: ExtractorQueryLimit{IsValid: false}, } query, err := buildExtractQueryMssql(q) if err == nil { t.Fatalf("Expected error for missing column, got no error. Query: %s", query) } if !strings.Contains(err.Error(), "NonExistentColumn") { t.Errorf("Expected error message to contain column name, got: %v", err) } } func TestBuildExtractQueryMssql_WildcardPatternNotMatched_Error(t *testing.T) { // Test that an error is returned when wildcard pattern doesn't match any column q := ExtractionQuery{ Schema: "dbo", Table: "Events", PrimaryKey: "ID", Columns: []models.ColumnType{ models.NewColumnType("ID", false, false, "INT", "int", "INT", false, 0, 0, 0), models.NewColumnType("EventData", true, false, "VARCHAR", "varchar", "VARCHAR", true, 500, 0, 0), }, FromJsonColumns: []config.FromJsonItem{ {Column: "NonMatching*", Field: ".id"}, }, LowerLimit: ExtractorQueryLimit{IsValid: false}, UpperLimit: ExtractorQueryLimit{IsValid: false}, } query, err := buildExtractQueryMssql(q) if err == nil { t.Fatalf("Expected error for non-matching wildcard pattern, got no error. Query: %s", query) } if !strings.Contains(err.Error(), "NonMatching*") { t.Errorf("Expected error message to contain pattern, got: %v", err) } } func TestBuildExtractQueryMssql_NestedJsonFields(t *testing.T) { q := ExtractionQuery{ Schema: "dbo", Table: "Data", PrimaryKey: "ID", Columns: []models.ColumnType{ models.NewColumnType("ID", false, false, "INT", "int", "INT", false, 0, 0, 0), models.NewColumnType("NodeData", true, false, "VARCHAR", "varchar", "VARCHAR", true, 500, 0, 0), }, FromJsonColumns: []config.FromJsonItem{ {Column: "NodeData", Field: ".user.name"}, {Column: "NodeData", Field: ".user.email"}, }, LowerLimit: ExtractorQueryLimit{IsValid: false}, UpperLimit: ExtractorQueryLimit{IsValid: false}, } query, err := buildExtractQueryMssql(q) if err != nil { t.Fatalf("Expected no error, got: %v", err) } if !strings.Contains(query, "JSON_VALUE([NodeData], '$.user.name') AS [NodeData]") { t.Errorf("Expected nested JSON path for user.name, got: %s", query) } if !strings.Contains(query, "JSON_VALUE([NodeData], '$.user.email') AS [NodeData]") { t.Errorf("Expected nested JSON path for user.email, got: %s", query) } if strings.Contains(query, "SELECT [ID], [NodeData]") { t.Errorf("Expected NodeData to be replaced by JSON extraction, got: %s", query) } } func TestBuildExtractQueryMssql_WithRangeLimits(t *testing.T) { q := ExtractionQuery{ Schema: "dbo", Table: "Products", PrimaryKey: "ProductID", Columns: []models.ColumnType{ models.NewColumnType("ProductID", false, false, "INT", "int", "INT", false, 0, 0, 0), models.NewColumnType("Details", true, false, "VARCHAR", "varchar", "VARCHAR", true, 500, 0, 0), }, FromJsonColumns: []config.FromJsonItem{ {Column: "Details", Field: ".price"}, }, LowerLimit: ExtractorQueryLimit{IsValid: true, IsInclusive: true, Value: 100}, UpperLimit: ExtractorQueryLimit{IsValid: true, IsInclusive: false, Value: 500}, } query, err := buildExtractQueryMssql(q) if err != nil { t.Fatalf("Expected no error, got: %v", err) } if !strings.Contains(query, "WHERE [ProductID] >= @min") { t.Errorf("Expected WHERE clause with >=, got: %s", query) } if !strings.Contains(query, "[ProductID] < @max") { t.Errorf("Expected upper limit with <, got: %s", query) } if !strings.Contains(query, "JSON_VALUE([Details], '$.price') AS [Details]") { t.Errorf("Expected JSON_VALUE for Details, got: %s", query) } if strings.Contains(query, "SELECT [ProductID], [Details]") { t.Errorf("Expected Details to be replaced by JSON extraction, got: %s", query) } } func TestBuildJsonPathMssql(t *testing.T) { tests := []struct { input string expected string }{ {".id", "$.id"}, {"id", "$.id"}, {".user.name", "$.user.name"}, {"user.name", "$.user.name"}, {".location.coordinates.lat", "$.location.coordinates.lat"}, {"", "$."}, } for _, tt := range tests { result := buildJsonPathMssql(tt.input) if result != tt.expected { t.Errorf("buildJsonPathMssql(%q) = %q, want %q", tt.input, result, tt.expected) } } } func TestFindColumnByPattern_ExactMatch(t *testing.T) { columns := []models.ColumnType{ models.NewColumnType("ID", false, false, "INT", "int", "INT", false, 0, 0, 0), models.NewColumnType("Metadata", true, false, "VARCHAR", "varchar", "VARCHAR", true, 500, 0, 0), models.NewColumnType("EventData", true, false, "VARCHAR", "varchar", "VARCHAR", true, 500, 0, 0), } result, err := findColumnByPattern(columns, "Metadata") if err != nil { t.Fatalf("Expected no error, got: %v", err) } if result != "Metadata" { t.Errorf("Expected 'Metadata', got '%s'", result) } } func TestFindColumnByPattern_WildcardMatch(t *testing.T) { columns := []models.ColumnType{ models.NewColumnType("ID", false, false, "INT", "int", "INT", false, 0, 0, 0), models.NewColumnType("NodeMetadata", true, false, "VARCHAR", "varchar", "VARCHAR", true, 500, 0, 0), models.NewColumnType("EventData", true, false, "VARCHAR", "varchar", "VARCHAR", true, 500, 0, 0), } result, err := findColumnByPattern(columns, "NodeMeta*") if err != nil { t.Fatalf("Expected no error, got: %v", err) } if result != "NodeMetadata" { t.Errorf("Expected 'NodeMetadata', got '%s'", result) } } func TestFindColumnByPattern_NotFound(t *testing.T) { columns := []models.ColumnType{ models.NewColumnType("ID", false, false, "INT", "int", "INT", false, 0, 0, 0), models.NewColumnType("Metadata", true, false, "VARCHAR", "varchar", "VARCHAR", true, 500, 0, 0), } result, err := findColumnByPattern(columns, "NonExistent") if err == nil { t.Fatalf("Expected error, got no error. Result: %s", result) } if !strings.Contains(err.Error(), "NonExistent") { t.Errorf("Expected error to contain column name, got: %v", err) } } func TestFindColumnByPattern_WildcardNotFound(t *testing.T) { columns := []models.ColumnType{ models.NewColumnType("ID", false, false, "INT", "int", "INT", false, 0, 0, 0), models.NewColumnType("Metadata", true, false, "VARCHAR", "varchar", "VARCHAR", true, 500, 0, 0), } result, err := findColumnByPattern(columns, "Event*") if err == nil { t.Fatalf("Expected error, got no error. Result: %s", result) } if !strings.Contains(err.Error(), "Event*") { t.Errorf("Expected error to contain pattern, got: %v", err) } } func TestBuildExtractQueryMssql_OnlyJsonColumns(t *testing.T) { // Test when all columns are used via JSON extraction q := ExtractionQuery{ Schema: "dbo", Table: "Data", PrimaryKey: "ID", Columns: []models.ColumnType{ models.NewColumnType("ID", false, false, "INT", "int", "INT", false, 0, 0, 0), models.NewColumnType("JsonData", true, false, "VARCHAR", "varchar", "VARCHAR", true, 500, 0, 0), }, FromJsonColumns: []config.FromJsonItem{ {Column: "JsonData", Field: ".field1"}, }, LowerLimit: ExtractorQueryLimit{IsValid: false}, UpperLimit: ExtractorQueryLimit{IsValid: false}, } query, err := buildExtractQueryMssql(q) if err != nil { t.Fatalf("Expected no error, got: %v", err) } if !strings.HasPrefix(query, "SELECT [ID], JSON_VALUE([JsonData], '$.field1') AS [JsonData]") { t.Errorf("Expected JsonData to be replaced by JSON extraction, got: %s", query) } if strings.Contains(query, "SELECT [ID], [JsonData]") { t.Errorf("Expected JsonData to be excluded from raw selection, got: %s", query) } } func TestBuildExtractQueryMssql_JsonColumnsReplaceInOrder(t *testing.T) { q := ExtractionQuery{ Schema: "dbo", Table: "Users", PrimaryKey: "UserID", Columns: []models.ColumnType{ models.NewColumnType("UserID", false, false, "INT", "int", "INT", false, 0, 0, 0), models.NewColumnType("Name", true, false, "VARCHAR", "varchar", "VARCHAR", false, 255, 0, 0), models.NewColumnType("Email", true, false, "VARCHAR", "varchar", "VARCHAR", false, 255, 0, 0), models.NewColumnType("Metadata", true, false, "NVARCHAR", "nvarchar", "NVARCHAR", true, 4000, 0, 0), models.NewColumnType("Profile", true, false, "NVARCHAR", "nvarchar", "NVARCHAR", true, 4000, 0, 0), models.NewColumnType("Settings", true, false, "NVARCHAR", "nvarchar", "NVARCHAR", true, 4000, 0, 0), }, FromJsonColumns: []config.FromJsonItem{ {Column: "Metadata", Field: ".id"}, {Column: "Profile", Field: ".id"}, {Column: "Settings", Field: ".id"}, }, LowerLimit: ExtractorQueryLimit{IsValid: false}, UpperLimit: ExtractorQueryLimit{IsValid: false}, } query, err := buildExtractQueryMssql(q) if err != nil { t.Fatalf("Expected no error, got: %v", err) } expected := "SELECT [UserID], [Name], [Email], JSON_VALUE([Metadata], '$.id') AS [Metadata], JSON_VALUE([Profile], '$.id') AS [Profile], JSON_VALUE([Settings], '$.id') AS [Settings] FROM [dbo].[Users] ORDER BY [UserID] ASC" if query != expected { t.Errorf("Unexpected query.\nExpected: %s\nGot: %s", expected, query) } }