5 changed files with 398 additions and 46 deletions
-
1weed/query/engine/broker_client.go
-
28weed/query/engine/catalog.go
-
38weed/query/engine/engine.go
-
154weed/query/engine/mock_test.go
-
223weed/query/engine/mocks_test.go
@ -0,0 +1,154 @@ |
|||||
|
package engine |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"testing" |
||||
|
) |
||||
|
|
||||
|
func TestMockBrokerClient_BasicFunctionality(t *testing.T) { |
||||
|
mockBroker := NewMockBrokerClient() |
||||
|
|
||||
|
// Test ListNamespaces
|
||||
|
namespaces, err := mockBroker.ListNamespaces(context.Background()) |
||||
|
if err != nil { |
||||
|
t.Fatalf("Expected no error, got %v", err) |
||||
|
} |
||||
|
if len(namespaces) != 2 { |
||||
|
t.Errorf("Expected 2 namespaces, got %d", len(namespaces)) |
||||
|
} |
||||
|
|
||||
|
// Test ListTopics
|
||||
|
topics, err := mockBroker.ListTopics(context.Background(), "default") |
||||
|
if err != nil { |
||||
|
t.Fatalf("Expected no error, got %v", err) |
||||
|
} |
||||
|
if len(topics) != 2 { |
||||
|
t.Errorf("Expected 2 topics in default namespace, got %d", len(topics)) |
||||
|
} |
||||
|
|
||||
|
// Test GetTopicSchema
|
||||
|
schema, err := mockBroker.GetTopicSchema(context.Background(), "default", "user_events") |
||||
|
if err != nil { |
||||
|
t.Fatalf("Expected no error, got %v", err) |
||||
|
} |
||||
|
if len(schema.Fields) != 3 { |
||||
|
t.Errorf("Expected 3 fields in user_events schema, got %d", len(schema.Fields)) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestMockBrokerClient_FailureScenarios(t *testing.T) { |
||||
|
mockBroker := NewMockBrokerClient() |
||||
|
|
||||
|
// Configure mock to fail
|
||||
|
mockBroker.SetFailure(true, "simulated broker failure") |
||||
|
|
||||
|
// Test that operations fail as expected
|
||||
|
_, err := mockBroker.ListNamespaces(context.Background()) |
||||
|
if err == nil { |
||||
|
t.Error("Expected error when mock is configured to fail") |
||||
|
} |
||||
|
|
||||
|
_, err = mockBroker.ListTopics(context.Background(), "default") |
||||
|
if err == nil { |
||||
|
t.Error("Expected error when mock is configured to fail") |
||||
|
} |
||||
|
|
||||
|
_, err = mockBroker.GetTopicSchema(context.Background(), "default", "user_events") |
||||
|
if err == nil { |
||||
|
t.Error("Expected error when mock is configured to fail") |
||||
|
} |
||||
|
|
||||
|
// Test that filer client also fails
|
||||
|
_, err = mockBroker.GetFilerClient() |
||||
|
if err == nil { |
||||
|
t.Error("Expected error when mock is configured to fail") |
||||
|
} |
||||
|
|
||||
|
// Reset mock to working state
|
||||
|
mockBroker.SetFailure(false, "") |
||||
|
|
||||
|
// Test that operations work again
|
||||
|
namespaces, err := mockBroker.ListNamespaces(context.Background()) |
||||
|
if err != nil { |
||||
|
t.Errorf("Expected no error after resetting mock, got %v", err) |
||||
|
} |
||||
|
if len(namespaces) == 0 { |
||||
|
t.Error("Expected namespaces after resetting mock") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestMockBrokerClient_TopicManagement(t *testing.T) { |
||||
|
mockBroker := NewMockBrokerClient() |
||||
|
|
||||
|
// Test ConfigureTopic (add a new topic)
|
||||
|
err := mockBroker.ConfigureTopic(context.Background(), "test", "new-topic", 1, nil) |
||||
|
if err != nil { |
||||
|
t.Fatalf("Expected no error, got %v", err) |
||||
|
} |
||||
|
|
||||
|
// Verify the topic was added
|
||||
|
topics, err := mockBroker.ListTopics(context.Background(), "test") |
||||
|
if err != nil { |
||||
|
t.Fatalf("Expected no error, got %v", err) |
||||
|
} |
||||
|
|
||||
|
foundNewTopic := false |
||||
|
for _, topic := range topics { |
||||
|
if topic == "new-topic" { |
||||
|
foundNewTopic = true |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
if !foundNewTopic { |
||||
|
t.Error("Expected new-topic to be in the topics list") |
||||
|
} |
||||
|
|
||||
|
// Test DeleteTopic
|
||||
|
err = mockBroker.DeleteTopic(context.Background(), "test", "new-topic") |
||||
|
if err != nil { |
||||
|
t.Fatalf("Expected no error, got %v", err) |
||||
|
} |
||||
|
|
||||
|
// Verify the topic was removed
|
||||
|
topics, err = mockBroker.ListTopics(context.Background(), "test") |
||||
|
if err != nil { |
||||
|
t.Fatalf("Expected no error, got %v", err) |
||||
|
} |
||||
|
|
||||
|
for _, topic := range topics { |
||||
|
if topic == "new-topic" { |
||||
|
t.Error("Expected new-topic to be removed from topics list") |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestSQLEngineWithMockBrokerClient_ErrorHandling(t *testing.T) { |
||||
|
// Create an engine with a failing mock broker
|
||||
|
mockBroker := NewMockBrokerClient() |
||||
|
mockBroker.SetFailure(true, "mock broker unavailable") |
||||
|
|
||||
|
catalog := &SchemaCatalog{ |
||||
|
databases: make(map[string]*DatabaseInfo), |
||||
|
currentDatabase: "default", |
||||
|
brokerClient: mockBroker, |
||||
|
} |
||||
|
|
||||
|
engine := &SQLEngine{catalog: catalog} |
||||
|
|
||||
|
// Test that queries fail gracefully with proper error messages
|
||||
|
result, err := engine.ExecuteSQL(context.Background(), "SELECT * FROM nonexistent_topic") |
||||
|
|
||||
|
// ExecuteSQL itself should not return an error, but the result should contain an error
|
||||
|
if err != nil { |
||||
|
// If ExecuteSQL returns an error, that's also acceptable for this test
|
||||
|
t.Logf("ExecuteSQL returned error (acceptable): %v", err) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Should have an error in the result when broker is unavailable
|
||||
|
if result.Error == nil { |
||||
|
t.Error("Expected error in query result when broker is unavailable") |
||||
|
} else { |
||||
|
t.Logf("Got expected error in result: %v", result.Error) |
||||
|
} |
||||
|
} |
@ -0,0 +1,223 @@ |
|||||
|
package engine |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"fmt" |
||||
|
|
||||
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/pb/schema_pb" |
||||
|
util_http "github.com/seaweedfs/seaweedfs/weed/util/http" |
||||
|
) |
||||
|
|
||||
|
// NewTestSchemaCatalog creates a schema catalog for testing with sample data
|
||||
|
// Uses mock clients instead of real service connections
|
||||
|
func NewTestSchemaCatalog() *SchemaCatalog { |
||||
|
catalog := &SchemaCatalog{ |
||||
|
databases: make(map[string]*DatabaseInfo), |
||||
|
currentDatabase: "default", |
||||
|
brokerClient: NewMockBrokerClient(), // Use mock instead of nil
|
||||
|
} |
||||
|
|
||||
|
// Pre-populate with sample data to avoid service discovery requirements
|
||||
|
catalog.initSampleData() |
||||
|
return catalog |
||||
|
} |
||||
|
|
||||
|
// NewTestSQLEngine creates a new SQL execution engine for testing
|
||||
|
// Does not attempt to connect to real SeaweedFS services
|
||||
|
func NewTestSQLEngine() *SQLEngine { |
||||
|
// Initialize global HTTP client if not already done
|
||||
|
// This is needed for reading partition data from the filer
|
||||
|
if util_http.GetGlobalHttpClient() == nil { |
||||
|
util_http.InitGlobalHttpClient() |
||||
|
} |
||||
|
|
||||
|
return &SQLEngine{ |
||||
|
catalog: NewTestSchemaCatalog(), |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MockBrokerClient implements BrokerClient interface for testing
|
||||
|
type MockBrokerClient struct { |
||||
|
namespaces []string |
||||
|
topics map[string][]string // namespace -> topics
|
||||
|
schemas map[string]*schema_pb.RecordType // "namespace.topic" -> schema
|
||||
|
shouldFail bool |
||||
|
failMessage string |
||||
|
} |
||||
|
|
||||
|
// NewMockBrokerClient creates a new mock broker client with sample data
|
||||
|
func NewMockBrokerClient() *MockBrokerClient { |
||||
|
client := &MockBrokerClient{ |
||||
|
namespaces: []string{"default", "test"}, |
||||
|
topics: map[string][]string{ |
||||
|
"default": {"user_events", "system_logs"}, |
||||
|
"test": {"test-topic"}, |
||||
|
}, |
||||
|
schemas: make(map[string]*schema_pb.RecordType), |
||||
|
} |
||||
|
|
||||
|
// Add sample schemas
|
||||
|
client.schemas["default.user_events"] = &schema_pb.RecordType{ |
||||
|
Fields: []*schema_pb.Field{ |
||||
|
{Name: "user_id", Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_STRING}}}, |
||||
|
{Name: "event_type", Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_STRING}}}, |
||||
|
{Name: "data", Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_STRING}}}, |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
client.schemas["default.system_logs"] = &schema_pb.RecordType{ |
||||
|
Fields: []*schema_pb.Field{ |
||||
|
{Name: "level", Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_STRING}}}, |
||||
|
{Name: "message", Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_STRING}}}, |
||||
|
{Name: "service", Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_STRING}}}, |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
client.schemas["test.test-topic"] = &schema_pb.RecordType{ |
||||
|
Fields: []*schema_pb.Field{ |
||||
|
{Name: "id", Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_INT32}}}, |
||||
|
{Name: "name", Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_STRING}}}, |
||||
|
{Name: "value", Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_DOUBLE}}}, |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
return client |
||||
|
} |
||||
|
|
||||
|
// SetFailure configures the mock to fail with the given message
|
||||
|
func (m *MockBrokerClient) SetFailure(shouldFail bool, message string) { |
||||
|
m.shouldFail = shouldFail |
||||
|
m.failMessage = message |
||||
|
} |
||||
|
|
||||
|
// ListNamespaces returns the mock namespaces
|
||||
|
func (m *MockBrokerClient) ListNamespaces(ctx context.Context) ([]string, error) { |
||||
|
if m.shouldFail { |
||||
|
return nil, fmt.Errorf("mock broker failure: %s", m.failMessage) |
||||
|
} |
||||
|
return m.namespaces, nil |
||||
|
} |
||||
|
|
||||
|
// ListTopics returns the mock topics for a namespace
|
||||
|
func (m *MockBrokerClient) ListTopics(ctx context.Context, namespace string) ([]string, error) { |
||||
|
if m.shouldFail { |
||||
|
return nil, fmt.Errorf("mock broker failure: %s", m.failMessage) |
||||
|
} |
||||
|
|
||||
|
if topics, exists := m.topics[namespace]; exists { |
||||
|
return topics, nil |
||||
|
} |
||||
|
return []string{}, nil |
||||
|
} |
||||
|
|
||||
|
// GetTopicSchema returns the mock schema for a topic
|
||||
|
func (m *MockBrokerClient) GetTopicSchema(ctx context.Context, namespace, topic string) (*schema_pb.RecordType, error) { |
||||
|
if m.shouldFail { |
||||
|
return nil, fmt.Errorf("mock broker failure: %s", m.failMessage) |
||||
|
} |
||||
|
|
||||
|
key := fmt.Sprintf("%s.%s", namespace, topic) |
||||
|
if schema, exists := m.schemas[key]; exists { |
||||
|
return schema, nil |
||||
|
} |
||||
|
return nil, fmt.Errorf("topic %s not found", key) |
||||
|
} |
||||
|
|
||||
|
// GetFilerClient returns a mock filer client
|
||||
|
func (m *MockBrokerClient) GetFilerClient() (filer_pb.FilerClient, error) { |
||||
|
if m.shouldFail { |
||||
|
return nil, fmt.Errorf("mock broker failure: %s", m.failMessage) |
||||
|
} |
||||
|
return NewMockFilerClient(), nil |
||||
|
} |
||||
|
|
||||
|
// MockFilerClient implements filer_pb.FilerClient interface for testing
|
||||
|
type MockFilerClient struct { |
||||
|
shouldFail bool |
||||
|
failMessage string |
||||
|
} |
||||
|
|
||||
|
// NewMockFilerClient creates a new mock filer client
|
||||
|
func NewMockFilerClient() *MockFilerClient { |
||||
|
return &MockFilerClient{} |
||||
|
} |
||||
|
|
||||
|
// SetFailure configures the mock to fail with the given message
|
||||
|
func (m *MockFilerClient) SetFailure(shouldFail bool, message string) { |
||||
|
m.shouldFail = shouldFail |
||||
|
m.failMessage = message |
||||
|
} |
||||
|
|
||||
|
// WithFilerClient executes a function with a mock filer client
|
||||
|
func (m *MockFilerClient) WithFilerClient(followRedirect bool, fn func(client filer_pb.SeaweedFilerClient) error) error { |
||||
|
if m.shouldFail { |
||||
|
return fmt.Errorf("mock filer failure: %s", m.failMessage) |
||||
|
} |
||||
|
|
||||
|
// For testing, we can just return success since the actual filer operations
|
||||
|
// are not critical for SQL engine unit tests
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// AdjustedUrl implements the FilerClient interface (mock implementation)
|
||||
|
func (m *MockFilerClient) AdjustedUrl(location *filer_pb.Location) string { |
||||
|
if location != nil && location.Url != "" { |
||||
|
return location.Url |
||||
|
} |
||||
|
return "mock://localhost:8080" |
||||
|
} |
||||
|
|
||||
|
// GetDataCenter implements the FilerClient interface (mock implementation)
|
||||
|
func (m *MockFilerClient) GetDataCenter() string { |
||||
|
return "mock-datacenter" |
||||
|
} |
||||
|
|
||||
|
// ConfigureTopic creates or updates a topic configuration (mock implementation)
|
||||
|
func (m *MockBrokerClient) ConfigureTopic(ctx context.Context, namespace, topicName string, partitionCount int32, recordType *schema_pb.RecordType) error { |
||||
|
if m.shouldFail { |
||||
|
return fmt.Errorf("mock broker failure: %s", m.failMessage) |
||||
|
} |
||||
|
|
||||
|
// Store the schema in our mock data
|
||||
|
key := fmt.Sprintf("%s.%s", namespace, topicName) |
||||
|
m.schemas[key] = recordType |
||||
|
|
||||
|
// Add to topics list if not already present
|
||||
|
if topics, exists := m.topics[namespace]; exists { |
||||
|
for _, topic := range topics { |
||||
|
if topic == topicName { |
||||
|
return nil // Already exists
|
||||
|
} |
||||
|
} |
||||
|
m.topics[namespace] = append(topics, topicName) |
||||
|
} else { |
||||
|
m.topics[namespace] = []string{topicName} |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// DeleteTopic removes a topic and all its data (mock implementation)
|
||||
|
func (m *MockBrokerClient) DeleteTopic(ctx context.Context, namespace, topicName string) error { |
||||
|
if m.shouldFail { |
||||
|
return fmt.Errorf("mock broker failure: %s", m.failMessage) |
||||
|
} |
||||
|
|
||||
|
// Remove from schemas
|
||||
|
key := fmt.Sprintf("%s.%s", namespace, topicName) |
||||
|
delete(m.schemas, key) |
||||
|
|
||||
|
// Remove from topics list
|
||||
|
if topics, exists := m.topics[namespace]; exists { |
||||
|
newTopics := make([]string, 0, len(topics)) |
||||
|
for _, topic := range topics { |
||||
|
if topic != topicName { |
||||
|
newTopics = append(newTopics, topic) |
||||
|
} |
||||
|
} |
||||
|
m.topics[namespace] = newTopics |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue