You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							393 lines
						
					
					
						
							12 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							393 lines
						
					
					
						
							12 KiB
						
					
					
				| package engine | |
| 
 | |
| import ( | |
| 	"context" | |
| 	"testing" | |
| 
 | |
| 	"github.com/seaweedfs/seaweedfs/weed/pb/schema_pb" | |
| ) | |
| 
 | |
| func TestStringFunctions(t *testing.T) { | |
| 	engine := NewTestSQLEngine() | |
| 
 | |
| 	t.Run("LENGTH function tests", func(t *testing.T) { | |
| 		tests := []struct { | |
| 			name      string | |
| 			value     *schema_pb.Value | |
| 			expected  int64 | |
| 			expectErr bool | |
| 		}{ | |
| 			{ | |
| 				name:      "Length of string", | |
| 				value:     &schema_pb.Value{Kind: &schema_pb.Value_StringValue{StringValue: "Hello World"}}, | |
| 				expected:  11, | |
| 				expectErr: false, | |
| 			}, | |
| 			{ | |
| 				name:      "Length of empty string", | |
| 				value:     &schema_pb.Value{Kind: &schema_pb.Value_StringValue{StringValue: ""}}, | |
| 				expected:  0, | |
| 				expectErr: false, | |
| 			}, | |
| 			{ | |
| 				name:      "Length of number", | |
| 				value:     &schema_pb.Value{Kind: &schema_pb.Value_Int64Value{Int64Value: 12345}}, | |
| 				expected:  5, | |
| 				expectErr: false, | |
| 			}, | |
| 			{ | |
| 				name:      "Length of null value", | |
| 				value:     nil, | |
| 				expected:  0, | |
| 				expectErr: true, | |
| 			}, | |
| 		} | |
| 
 | |
| 		for _, tt := range tests { | |
| 			t.Run(tt.name, func(t *testing.T) { | |
| 				result, err := engine.Length(tt.value) | |
| 
 | |
| 				if tt.expectErr { | |
| 					if err == nil { | |
| 						t.Errorf("Expected error but got none") | |
| 					} | |
| 					return | |
| 				} | |
| 
 | |
| 				if err != nil { | |
| 					t.Errorf("Unexpected error: %v", err) | |
| 					return | |
| 				} | |
| 
 | |
| 				intVal, ok := result.Kind.(*schema_pb.Value_Int64Value) | |
| 				if !ok { | |
| 					t.Errorf("LENGTH should return int64 value, got %T", result.Kind) | |
| 					return | |
| 				} | |
| 
 | |
| 				if intVal.Int64Value != tt.expected { | |
| 					t.Errorf("Expected %d, got %d", tt.expected, intVal.Int64Value) | |
| 				} | |
| 			}) | |
| 		} | |
| 	}) | |
| 
 | |
| 	t.Run("UPPER/LOWER function tests", func(t *testing.T) { | |
| 		// Test UPPER | |
| 		result, err := engine.Upper(&schema_pb.Value{Kind: &schema_pb.Value_StringValue{StringValue: "Hello World"}}) | |
| 		if err != nil { | |
| 			t.Errorf("UPPER failed: %v", err) | |
| 		} | |
| 		stringVal, _ := result.Kind.(*schema_pb.Value_StringValue) | |
| 		if stringVal.StringValue != "HELLO WORLD" { | |
| 			t.Errorf("Expected 'HELLO WORLD', got '%s'", stringVal.StringValue) | |
| 		} | |
| 
 | |
| 		// Test LOWER | |
| 		result, err = engine.Lower(&schema_pb.Value{Kind: &schema_pb.Value_StringValue{StringValue: "Hello World"}}) | |
| 		if err != nil { | |
| 			t.Errorf("LOWER failed: %v", err) | |
| 		} | |
| 		stringVal, _ = result.Kind.(*schema_pb.Value_StringValue) | |
| 		if stringVal.StringValue != "hello world" { | |
| 			t.Errorf("Expected 'hello world', got '%s'", stringVal.StringValue) | |
| 		} | |
| 	}) | |
| 
 | |
| 	t.Run("TRIM function tests", func(t *testing.T) { | |
| 		tests := []struct { | |
| 			name     string | |
| 			function func(*schema_pb.Value) (*schema_pb.Value, error) | |
| 			input    string | |
| 			expected string | |
| 		}{ | |
| 			{"TRIM whitespace", engine.Trim, "  Hello World  ", "Hello World"}, | |
| 			{"LTRIM whitespace", engine.LTrim, "  Hello World  ", "Hello World  "}, | |
| 			{"RTRIM whitespace", engine.RTrim, "  Hello World  ", "  Hello World"}, | |
| 			{"TRIM with tabs and newlines", engine.Trim, "\t\nHello\t\n", "Hello"}, | |
| 		} | |
| 
 | |
| 		for _, tt := range tests { | |
| 			t.Run(tt.name, func(t *testing.T) { | |
| 				result, err := tt.function(&schema_pb.Value{Kind: &schema_pb.Value_StringValue{StringValue: tt.input}}) | |
| 				if err != nil { | |
| 					t.Errorf("Function failed: %v", err) | |
| 					return | |
| 				} | |
| 
 | |
| 				stringVal, ok := result.Kind.(*schema_pb.Value_StringValue) | |
| 				if !ok { | |
| 					t.Errorf("Function should return string value, got %T", result.Kind) | |
| 					return | |
| 				} | |
| 
 | |
| 				if stringVal.StringValue != tt.expected { | |
| 					t.Errorf("Expected '%s', got '%s'", tt.expected, stringVal.StringValue) | |
| 				} | |
| 			}) | |
| 		} | |
| 	}) | |
| 
 | |
| 	t.Run("SUBSTRING function tests", func(t *testing.T) { | |
| 		testStr := &schema_pb.Value{Kind: &schema_pb.Value_StringValue{StringValue: "Hello World"}} | |
| 
 | |
| 		// Test substring with start and length | |
| 		result, err := engine.Substring(testStr, | |
| 			&schema_pb.Value{Kind: &schema_pb.Value_Int64Value{Int64Value: 7}}, | |
| 			&schema_pb.Value{Kind: &schema_pb.Value_Int64Value{Int64Value: 5}}) | |
| 		if err != nil { | |
| 			t.Errorf("SUBSTRING failed: %v", err) | |
| 		} | |
| 		stringVal, _ := result.Kind.(*schema_pb.Value_StringValue) | |
| 		if stringVal.StringValue != "World" { | |
| 			t.Errorf("Expected 'World', got '%s'", stringVal.StringValue) | |
| 		} | |
| 
 | |
| 		// Test substring with just start position | |
| 		result, err = engine.Substring(testStr, | |
| 			&schema_pb.Value{Kind: &schema_pb.Value_Int64Value{Int64Value: 7}}) | |
| 		if err != nil { | |
| 			t.Errorf("SUBSTRING failed: %v", err) | |
| 		} | |
| 		stringVal, _ = result.Kind.(*schema_pb.Value_StringValue) | |
| 		if stringVal.StringValue != "World" { | |
| 			t.Errorf("Expected 'World', got '%s'", stringVal.StringValue) | |
| 		} | |
| 	}) | |
| 
 | |
| 	t.Run("CONCAT function tests", func(t *testing.T) { | |
| 		result, err := engine.Concat( | |
| 			&schema_pb.Value{Kind: &schema_pb.Value_StringValue{StringValue: "Hello"}}, | |
| 			&schema_pb.Value{Kind: &schema_pb.Value_StringValue{StringValue: " "}}, | |
| 			&schema_pb.Value{Kind: &schema_pb.Value_StringValue{StringValue: "World"}}, | |
| 		) | |
| 		if err != nil { | |
| 			t.Errorf("CONCAT failed: %v", err) | |
| 		} | |
| 		stringVal, _ := result.Kind.(*schema_pb.Value_StringValue) | |
| 		if stringVal.StringValue != "Hello World" { | |
| 			t.Errorf("Expected 'Hello World', got '%s'", stringVal.StringValue) | |
| 		} | |
| 
 | |
| 		// Test with mixed types | |
| 		result, err = engine.Concat( | |
| 			&schema_pb.Value{Kind: &schema_pb.Value_StringValue{StringValue: "Number: "}}, | |
| 			&schema_pb.Value{Kind: &schema_pb.Value_Int64Value{Int64Value: 42}}, | |
| 		) | |
| 		if err != nil { | |
| 			t.Errorf("CONCAT failed: %v", err) | |
| 		} | |
| 		stringVal, _ = result.Kind.(*schema_pb.Value_StringValue) | |
| 		if stringVal.StringValue != "Number: 42" { | |
| 			t.Errorf("Expected 'Number: 42', got '%s'", stringVal.StringValue) | |
| 		} | |
| 	}) | |
| 
 | |
| 	t.Run("REPLACE function tests", func(t *testing.T) { | |
| 		result, err := engine.Replace( | |
| 			&schema_pb.Value{Kind: &schema_pb.Value_StringValue{StringValue: "Hello World World"}}, | |
| 			&schema_pb.Value{Kind: &schema_pb.Value_StringValue{StringValue: "World"}}, | |
| 			&schema_pb.Value{Kind: &schema_pb.Value_StringValue{StringValue: "Universe"}}, | |
| 		) | |
| 		if err != nil { | |
| 			t.Errorf("REPLACE failed: %v", err) | |
| 		} | |
| 		stringVal, _ := result.Kind.(*schema_pb.Value_StringValue) | |
| 		if stringVal.StringValue != "Hello Universe Universe" { | |
| 			t.Errorf("Expected 'Hello Universe Universe', got '%s'", stringVal.StringValue) | |
| 		} | |
| 	}) | |
| 
 | |
| 	t.Run("POSITION function tests", func(t *testing.T) { | |
| 		result, err := engine.Position( | |
| 			&schema_pb.Value{Kind: &schema_pb.Value_StringValue{StringValue: "World"}}, | |
| 			&schema_pb.Value{Kind: &schema_pb.Value_StringValue{StringValue: "Hello World"}}, | |
| 		) | |
| 		if err != nil { | |
| 			t.Errorf("POSITION failed: %v", err) | |
| 		} | |
| 		intVal, _ := result.Kind.(*schema_pb.Value_Int64Value) | |
| 		if intVal.Int64Value != 7 { | |
| 			t.Errorf("Expected 7, got %d", intVal.Int64Value) | |
| 		} | |
| 
 | |
| 		// Test not found | |
| 		result, err = engine.Position( | |
| 			&schema_pb.Value{Kind: &schema_pb.Value_StringValue{StringValue: "NotFound"}}, | |
| 			&schema_pb.Value{Kind: &schema_pb.Value_StringValue{StringValue: "Hello World"}}, | |
| 		) | |
| 		if err != nil { | |
| 			t.Errorf("POSITION failed: %v", err) | |
| 		} | |
| 		intVal, _ = result.Kind.(*schema_pb.Value_Int64Value) | |
| 		if intVal.Int64Value != 0 { | |
| 			t.Errorf("Expected 0 for not found, got %d", intVal.Int64Value) | |
| 		} | |
| 	}) | |
| 
 | |
| 	t.Run("LEFT/RIGHT function tests", func(t *testing.T) { | |
| 		testStr := &schema_pb.Value{Kind: &schema_pb.Value_StringValue{StringValue: "Hello World"}} | |
| 
 | |
| 		// Test LEFT | |
| 		result, err := engine.Left(testStr, &schema_pb.Value{Kind: &schema_pb.Value_Int64Value{Int64Value: 5}}) | |
| 		if err != nil { | |
| 			t.Errorf("LEFT failed: %v", err) | |
| 		} | |
| 		stringVal, _ := result.Kind.(*schema_pb.Value_StringValue) | |
| 		if stringVal.StringValue != "Hello" { | |
| 			t.Errorf("Expected 'Hello', got '%s'", stringVal.StringValue) | |
| 		} | |
| 
 | |
| 		// Test RIGHT | |
| 		result, err = engine.Right(testStr, &schema_pb.Value{Kind: &schema_pb.Value_Int64Value{Int64Value: 5}}) | |
| 		if err != nil { | |
| 			t.Errorf("RIGHT failed: %v", err) | |
| 		} | |
| 		stringVal, _ = result.Kind.(*schema_pb.Value_StringValue) | |
| 		if stringVal.StringValue != "World" { | |
| 			t.Errorf("Expected 'World', got '%s'", stringVal.StringValue) | |
| 		} | |
| 	}) | |
| 
 | |
| 	t.Run("REVERSE function tests", func(t *testing.T) { | |
| 		result, err := engine.Reverse(&schema_pb.Value{Kind: &schema_pb.Value_StringValue{StringValue: "Hello"}}) | |
| 		if err != nil { | |
| 			t.Errorf("REVERSE failed: %v", err) | |
| 		} | |
| 		stringVal, _ := result.Kind.(*schema_pb.Value_StringValue) | |
| 		if stringVal.StringValue != "olleH" { | |
| 			t.Errorf("Expected 'olleH', got '%s'", stringVal.StringValue) | |
| 		} | |
| 
 | |
| 		// Test with Unicode | |
| 		result, err = engine.Reverse(&schema_pb.Value{Kind: &schema_pb.Value_StringValue{StringValue: "🙂👍"}}) | |
| 		if err != nil { | |
| 			t.Errorf("REVERSE failed: %v", err) | |
| 		} | |
| 		stringVal, _ = result.Kind.(*schema_pb.Value_StringValue) | |
| 		if stringVal.StringValue != "👍🙂" { | |
| 			t.Errorf("Expected '👍🙂', got '%s'", stringVal.StringValue) | |
| 		} | |
| 	}) | |
| } | |
| 
 | |
| // TestStringFunctionsSQL tests string functions through SQL execution | |
| func TestStringFunctionsSQL(t *testing.T) { | |
| 	engine := NewTestSQLEngine() | |
| 
 | |
| 	testCases := []struct { | |
| 		name        string | |
| 		sql         string | |
| 		expectError bool | |
| 		expectedVal string | |
| 	}{ | |
| 		{ | |
| 			name:        "UPPER function", | |
| 			sql:         "SELECT UPPER('hello world') AS upper_value FROM user_events LIMIT 1", | |
| 			expectError: false, | |
| 			expectedVal: "HELLO WORLD", | |
| 		}, | |
| 		{ | |
| 			name:        "LOWER function", | |
| 			sql:         "SELECT LOWER('HELLO WORLD') AS lower_value FROM user_events LIMIT 1", | |
| 			expectError: false, | |
| 			expectedVal: "hello world", | |
| 		}, | |
| 		{ | |
| 			name:        "LENGTH function", | |
| 			sql:         "SELECT LENGTH('hello') AS length_value FROM user_events LIMIT 1", | |
| 			expectError: false, | |
| 			expectedVal: "5", | |
| 		}, | |
| 		{ | |
| 			name:        "TRIM function", | |
| 			sql:         "SELECT TRIM('  hello world  ') AS trimmed_value FROM user_events LIMIT 1", | |
| 			expectError: false, | |
| 			expectedVal: "hello world", | |
| 		}, | |
| 		{ | |
| 			name:        "LTRIM function", | |
| 			sql:         "SELECT LTRIM('  hello world  ') AS ltrimmed_value FROM user_events LIMIT 1", | |
| 			expectError: false, | |
| 			expectedVal: "hello world  ", | |
| 		}, | |
| 		{ | |
| 			name:        "RTRIM function", | |
| 			sql:         "SELECT RTRIM('  hello world  ') AS rtrimmed_value FROM user_events LIMIT 1", | |
| 			expectError: false, | |
| 			expectedVal: "  hello world", | |
| 		}, | |
| 		{ | |
| 			name:        "Multiple string functions", | |
| 			sql:         "SELECT UPPER('hello') AS up, LOWER('WORLD') AS low, LENGTH('test') AS len FROM user_events LIMIT 1", | |
| 			expectError: false, | |
| 			expectedVal: "", // We'll check this separately | |
| 		}, | |
| 		{ | |
| 			name:        "String function with wrong argument count", | |
| 			sql:         "SELECT UPPER('hello', 'extra') FROM user_events LIMIT 1", | |
| 			expectError: true, | |
| 			expectedVal: "", | |
| 		}, | |
| 		{ | |
| 			name:        "String function with no arguments", | |
| 			sql:         "SELECT UPPER() FROM user_events LIMIT 1", | |
| 			expectError: true, | |
| 			expectedVal: "", | |
| 		}, | |
| 	} | |
| 
 | |
| 	for _, tc := range testCases { | |
| 		t.Run(tc.name, func(t *testing.T) { | |
| 			result, err := engine.ExecuteSQL(context.Background(), tc.sql) | |
| 
 | |
| 			if tc.expectError { | |
| 				if err == nil && result.Error == nil { | |
| 					t.Errorf("Expected error but got none") | |
| 				} | |
| 				return | |
| 			} | |
| 
 | |
| 			if err != nil { | |
| 				t.Errorf("Unexpected error: %v", err) | |
| 				return | |
| 			} | |
| 
 | |
| 			if result.Error != nil { | |
| 				t.Errorf("Query result has error: %v", result.Error) | |
| 				return | |
| 			} | |
| 
 | |
| 			if len(result.Rows) == 0 { | |
| 				t.Fatal("Expected at least one row") | |
| 			} | |
| 
 | |
| 			if tc.name == "Multiple string functions" { | |
| 				// Special case for multiple functions test | |
| 				if len(result.Rows[0]) != 3 { | |
| 					t.Fatalf("Expected 3 columns, got %d", len(result.Rows[0])) | |
| 				} | |
| 
 | |
| 				// Check UPPER('hello') -> 'HELLO' | |
| 				if result.Rows[0][0].ToString() != "HELLO" { | |
| 					t.Errorf("Expected 'HELLO', got '%s'", result.Rows[0][0].ToString()) | |
| 				} | |
| 
 | |
| 				// Check LOWER('WORLD') -> 'world' | |
| 				if result.Rows[0][1].ToString() != "world" { | |
| 					t.Errorf("Expected 'world', got '%s'", result.Rows[0][1].ToString()) | |
| 				} | |
| 
 | |
| 				// Check LENGTH('test') -> '4' | |
| 				if result.Rows[0][2].ToString() != "4" { | |
| 					t.Errorf("Expected '4', got '%s'", result.Rows[0][2].ToString()) | |
| 				} | |
| 			} else { | |
| 				actualVal := result.Rows[0][0].ToString() | |
| 				if actualVal != tc.expectedVal { | |
| 					t.Errorf("Expected '%s', got '%s'", tc.expectedVal, actualVal) | |
| 				} | |
| 			} | |
| 		}) | |
| 	} | |
| }
 |