mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2025-09-19 23:49:23 +08:00
1016 lines
31 KiB
Go
1016 lines
31 KiB
Go
package engine
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/mq/schema"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/schema_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/query/sqltypes"
|
|
"github.com/seaweedfs/seaweedfs/weed/util/http"
|
|
"github.com/xwb1989/sqlparser"
|
|
)
|
|
|
|
// SQLEngine provides SQL query execution capabilities for SeaweedFS
|
|
// Assumptions:
|
|
// 1. MQ namespaces map directly to SQL databases
|
|
// 2. MQ topics map directly to SQL tables
|
|
// 3. Schema evolution is handled transparently with backward compatibility
|
|
// 4. Queries run against Parquet-stored MQ messages
|
|
type SQLEngine struct {
|
|
catalog *SchemaCatalog
|
|
}
|
|
|
|
// QueryResult represents the result of a SQL query execution
|
|
type QueryResult struct {
|
|
Columns []string `json:"columns"`
|
|
Rows [][]sqltypes.Value `json:"rows"`
|
|
Error error `json:"error,omitempty"`
|
|
}
|
|
|
|
// NewSQLEngine creates a new SQL execution engine
|
|
// Uses master address for service discovery and initialization
|
|
func NewSQLEngine(masterAddress string) *SQLEngine {
|
|
// Initialize global HTTP client if not already done
|
|
// This is needed for reading partition data from the filer
|
|
if http.GetGlobalHttpClient() == nil {
|
|
http.InitGlobalHttpClient()
|
|
}
|
|
|
|
return &SQLEngine{
|
|
catalog: NewSchemaCatalog(masterAddress),
|
|
}
|
|
}
|
|
|
|
// GetCatalog returns the schema catalog for external access
|
|
func (e *SQLEngine) GetCatalog() *SchemaCatalog {
|
|
return e.catalog
|
|
}
|
|
|
|
// ExecuteSQL parses and executes a SQL statement
|
|
// Assumptions:
|
|
// 1. All SQL statements are MySQL-compatible via xwb1989/sqlparser
|
|
// 2. DDL operations (CREATE/ALTER/DROP) modify underlying MQ topics
|
|
// 3. DML operations (SELECT) query Parquet files directly
|
|
// 4. Error handling follows MySQL conventions
|
|
func (e *SQLEngine) ExecuteSQL(ctx context.Context, sql string) (*QueryResult, error) {
|
|
// Handle DESCRIBE as a special case since it's not parsed as a standard statement
|
|
if strings.HasPrefix(strings.ToUpper(strings.TrimSpace(sql)), "DESCRIBE") {
|
|
return e.handleDescribeCommand(ctx, sql)
|
|
}
|
|
|
|
// Parse the SQL statement
|
|
stmt, err := sqlparser.Parse(sql)
|
|
if err != nil {
|
|
return &QueryResult{
|
|
Error: fmt.Errorf("SQL parse error: %v", err),
|
|
}, err
|
|
}
|
|
|
|
// Route to appropriate handler based on statement type
|
|
switch stmt := stmt.(type) {
|
|
case *sqlparser.Show:
|
|
return e.executeShowStatementWithDescribe(ctx, stmt)
|
|
case *sqlparser.DDL:
|
|
return e.executeDDLStatement(ctx, stmt)
|
|
case *sqlparser.Select:
|
|
return e.executeSelectStatement(ctx, stmt)
|
|
default:
|
|
err := fmt.Errorf("unsupported SQL statement type: %T", stmt)
|
|
return &QueryResult{Error: err}, err
|
|
}
|
|
}
|
|
|
|
// executeDDLStatement handles CREATE, ALTER, DROP operations
|
|
// Assumption: These operations modify the underlying MQ topic structure
|
|
func (e *SQLEngine) executeDDLStatement(ctx context.Context, stmt *sqlparser.DDL) (*QueryResult, error) {
|
|
switch stmt.Action {
|
|
case sqlparser.CreateStr:
|
|
return e.createTable(ctx, stmt)
|
|
case sqlparser.AlterStr:
|
|
return e.alterTable(ctx, stmt)
|
|
case sqlparser.DropStr:
|
|
return e.dropTable(ctx, stmt)
|
|
default:
|
|
err := fmt.Errorf("unsupported DDL action: %s", stmt.Action)
|
|
return &QueryResult{Error: err}, err
|
|
}
|
|
}
|
|
|
|
// executeSelectStatement handles SELECT queries
|
|
// Assumptions:
|
|
// 1. Queries run against Parquet files in MQ topics
|
|
// 2. Predicate pushdown is used for efficiency
|
|
// 3. Cross-topic joins are supported via partition-aware execution
|
|
func (e *SQLEngine) executeSelectStatement(ctx context.Context, stmt *sqlparser.Select) (*QueryResult, error) {
|
|
// Parse FROM clause to get table (topic) information
|
|
if len(stmt.From) != 1 {
|
|
err := fmt.Errorf("SELECT supports single table queries only")
|
|
return &QueryResult{Error: err}, err
|
|
}
|
|
|
|
// Extract table reference
|
|
var database, tableName string
|
|
switch table := stmt.From[0].(type) {
|
|
case *sqlparser.AliasedTableExpr:
|
|
switch tableExpr := table.Expr.(type) {
|
|
case sqlparser.TableName:
|
|
tableName = tableExpr.Name.String()
|
|
if tableExpr.Qualifier.String() != "" {
|
|
database = tableExpr.Qualifier.String()
|
|
}
|
|
default:
|
|
err := fmt.Errorf("unsupported table expression: %T", tableExpr)
|
|
return &QueryResult{Error: err}, err
|
|
}
|
|
default:
|
|
err := fmt.Errorf("unsupported FROM clause: %T", table)
|
|
return &QueryResult{Error: err}, err
|
|
}
|
|
|
|
// Use current database context if not specified
|
|
if database == "" {
|
|
database = e.catalog.GetCurrentDatabase()
|
|
if database == "" {
|
|
database = "default"
|
|
}
|
|
}
|
|
|
|
// Auto-discover and register topic if not already in catalog
|
|
if _, err := e.catalog.GetTableInfo(database, tableName); err != nil {
|
|
// Topic not in catalog, try to discover and register it
|
|
if regErr := e.discoverAndRegisterTopic(ctx, database, tableName); regErr != nil {
|
|
fmt.Printf("Warning: Failed to discover topic %s.%s: %v\n", database, tableName, regErr)
|
|
}
|
|
}
|
|
|
|
// Create HybridMessageScanner for the topic (reads both live logs + Parquet files)
|
|
// ✅ RESOLVED TODO: Get real filerClient from broker connection
|
|
var filerClient filer_pb.FilerClient
|
|
if e.catalog.brokerClient != nil {
|
|
var filerClientErr error
|
|
filerClient, filerClientErr = e.catalog.brokerClient.GetFilerClient()
|
|
if filerClientErr != nil {
|
|
// Log warning but continue with sample data fallback
|
|
fmt.Printf("Warning: Failed to get filer client: %v, using sample data\n", filerClientErr)
|
|
}
|
|
}
|
|
|
|
hybridScanner, err := NewHybridMessageScanner(filerClient, database, tableName)
|
|
if err != nil {
|
|
// Fallback to sample data if topic doesn't exist or filer unavailable
|
|
return e.executeSelectWithSampleData(ctx, stmt, database, tableName)
|
|
}
|
|
|
|
// Parse SELECT columns
|
|
var columns []string
|
|
selectAll := false
|
|
|
|
for _, selectExpr := range stmt.SelectExprs {
|
|
switch expr := selectExpr.(type) {
|
|
case *sqlparser.StarExpr:
|
|
selectAll = true
|
|
case *sqlparser.AliasedExpr:
|
|
switch col := expr.Expr.(type) {
|
|
case *sqlparser.ColName:
|
|
columns = append(columns, col.Name.String())
|
|
default:
|
|
err := fmt.Errorf("unsupported SELECT expression: %T", col)
|
|
return &QueryResult{Error: err}, err
|
|
}
|
|
default:
|
|
err := fmt.Errorf("unsupported SELECT expression: %T", expr)
|
|
return &QueryResult{Error: err}, err
|
|
}
|
|
}
|
|
|
|
// Parse WHERE clause for predicate pushdown
|
|
var predicate func(*schema_pb.RecordValue) bool
|
|
if stmt.Where != nil {
|
|
predicate, err = e.buildPredicate(stmt.Where.Expr)
|
|
if err != nil {
|
|
return &QueryResult{Error: err}, err
|
|
}
|
|
}
|
|
|
|
// Parse LIMIT clause
|
|
limit := 0
|
|
if stmt.Limit != nil && stmt.Limit.Rowcount != nil {
|
|
switch limitExpr := stmt.Limit.Rowcount.(type) {
|
|
case *sqlparser.SQLVal:
|
|
if limitExpr.Type == sqlparser.IntVal {
|
|
var parseErr error
|
|
limit64, parseErr := strconv.ParseInt(string(limitExpr.Val), 10, 64)
|
|
if parseErr != nil {
|
|
return &QueryResult{Error: parseErr}, parseErr
|
|
}
|
|
if limit64 > math.MaxInt32 || limit64 < 0 {
|
|
return &QueryResult{Error: fmt.Errorf("LIMIT value %d is out of valid range", limit64)}, fmt.Errorf("LIMIT value %d is out of valid range", limit64)
|
|
}
|
|
limit = int(limit64)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build hybrid scan options
|
|
// ✅ RESOLVED TODO: Extract from WHERE clause time filters
|
|
startTimeNs, stopTimeNs := int64(0), int64(0)
|
|
if stmt.Where != nil {
|
|
startTimeNs, stopTimeNs = e.extractTimeFilters(stmt.Where.Expr)
|
|
}
|
|
|
|
hybridScanOptions := HybridScanOptions{
|
|
StartTimeNs: startTimeNs, // Extracted from WHERE clause time comparisons
|
|
StopTimeNs: stopTimeNs, // Extracted from WHERE clause time comparisons
|
|
Limit: limit,
|
|
Predicate: predicate,
|
|
}
|
|
|
|
if !selectAll {
|
|
hybridScanOptions.Columns = columns
|
|
}
|
|
|
|
// Execute the hybrid scan (live logs + Parquet files)
|
|
results, err := hybridScanner.Scan(ctx, hybridScanOptions)
|
|
if err != nil {
|
|
return &QueryResult{Error: err}, err
|
|
}
|
|
|
|
// Convert to SQL result format
|
|
if selectAll {
|
|
columns = nil // Let converter determine all columns
|
|
}
|
|
|
|
return hybridScanner.ConvertToSQLResult(results, columns), nil
|
|
}
|
|
|
|
// executeSelectWithSampleData provides enhanced sample data that simulates both live and archived messages
|
|
func (e *SQLEngine) executeSelectWithSampleData(ctx context.Context, stmt *sqlparser.Select, database, tableName string) (*QueryResult, error) {
|
|
// Create a sample HybridMessageScanner to simulate both data sources
|
|
now := time.Now().UnixNano()
|
|
|
|
var sampleResults []HybridScanResult
|
|
|
|
switch tableName {
|
|
case "user_events":
|
|
sampleResults = []HybridScanResult{
|
|
// Live log data (recent)
|
|
{
|
|
Values: map[string]*schema_pb.Value{
|
|
"user_id": {Kind: &schema_pb.Value_Int32Value{Int32Value: 1003}},
|
|
"event_type": {Kind: &schema_pb.Value_StringValue{StringValue: "live_login"}},
|
|
"data": {Kind: &schema_pb.Value_StringValue{StringValue: `{"ip": "10.0.0.1", "live": true}`}},
|
|
},
|
|
Timestamp: now - 300000000000, // 5 minutes ago
|
|
Key: []byte("live-1003"),
|
|
Source: "live_log",
|
|
},
|
|
{
|
|
Values: map[string]*schema_pb.Value{
|
|
"user_id": {Kind: &schema_pb.Value_Int32Value{Int32Value: 1004}},
|
|
"event_type": {Kind: &schema_pb.Value_StringValue{StringValue: "live_click"}},
|
|
"data": {Kind: &schema_pb.Value_StringValue{StringValue: `{"button": "submit", "live": true}`}},
|
|
},
|
|
Timestamp: now - 120000000000, // 2 minutes ago
|
|
Key: []byte("live-1004"),
|
|
Source: "live_log",
|
|
},
|
|
// Archived Parquet data (older)
|
|
{
|
|
Values: map[string]*schema_pb.Value{
|
|
"user_id": {Kind: &schema_pb.Value_Int32Value{Int32Value: 1001}},
|
|
"event_type": {Kind: &schema_pb.Value_StringValue{StringValue: "archived_login"}},
|
|
"data": {Kind: &schema_pb.Value_StringValue{StringValue: `{"ip": "192.168.1.1", "archived": true}`}},
|
|
},
|
|
Timestamp: now - 3600000000000, // 1 hour ago
|
|
Key: []byte("archived-1001"),
|
|
Source: "parquet_archive",
|
|
},
|
|
{
|
|
Values: map[string]*schema_pb.Value{
|
|
"user_id": {Kind: &schema_pb.Value_Int32Value{Int32Value: 1002}},
|
|
"event_type": {Kind: &schema_pb.Value_StringValue{StringValue: "archived_logout"}},
|
|
"data": {Kind: &schema_pb.Value_StringValue{StringValue: `{"duration": 1800, "archived": true}`}},
|
|
},
|
|
Timestamp: now - 1800000000000, // 30 minutes ago
|
|
Key: []byte("archived-1002"),
|
|
Source: "parquet_archive",
|
|
},
|
|
}
|
|
case "system_logs":
|
|
sampleResults = []HybridScanResult{
|
|
// Live system logs
|
|
{
|
|
Values: map[string]*schema_pb.Value{
|
|
"level": {Kind: &schema_pb.Value_StringValue{StringValue: "INFO"}},
|
|
"message": {Kind: &schema_pb.Value_StringValue{StringValue: "Live service heartbeat"}},
|
|
"service": {Kind: &schema_pb.Value_StringValue{StringValue: "api-gateway"}},
|
|
},
|
|
Timestamp: now - 60000000000, // 1 minute ago
|
|
Key: []byte("live-log-001"),
|
|
Source: "live_log",
|
|
},
|
|
// Archived system logs
|
|
{
|
|
Values: map[string]*schema_pb.Value{
|
|
"level": {Kind: &schema_pb.Value_StringValue{StringValue: "ERROR"}},
|
|
"message": {Kind: &schema_pb.Value_StringValue{StringValue: "Database connection timeout"}},
|
|
"service": {Kind: &schema_pb.Value_StringValue{StringValue: "user-service"}},
|
|
},
|
|
Timestamp: now - 7200000000000, // 2 hours ago
|
|
Key: []byte("archived-error-001"),
|
|
Source: "parquet_archive",
|
|
},
|
|
}
|
|
default:
|
|
return &QueryResult{
|
|
Error: fmt.Errorf("table '%s.%s' not found", database, tableName),
|
|
}, fmt.Errorf("table '%s.%s' not found", database, tableName)
|
|
}
|
|
|
|
// Apply basic LIMIT if specified
|
|
if stmt.Limit != nil && stmt.Limit.Rowcount != nil {
|
|
if limitExpr, ok := stmt.Limit.Rowcount.(*sqlparser.SQLVal); ok && limitExpr.Type == sqlparser.IntVal {
|
|
if limit64, err := strconv.ParseInt(string(limitExpr.Val), 10, 64); err == nil {
|
|
if limit64 > math.MaxInt32 || limit64 < 0 {
|
|
return &QueryResult{
|
|
Error: fmt.Errorf("LIMIT value %d is out of valid range", limit64),
|
|
}, fmt.Errorf("LIMIT value %d is out of valid range", limit64)
|
|
}
|
|
limit := int(limit64)
|
|
if limit > 0 && limit < len(sampleResults) {
|
|
sampleResults = sampleResults[:limit]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Convert to SQL result format using hybrid scanner logic
|
|
return convertHybridResultsToSQL(sampleResults, nil), nil
|
|
}
|
|
|
|
// convertHybridResultsToSQL converts HybridScanResults to SQL format (helper function)
|
|
func convertHybridResultsToSQL(results []HybridScanResult, columns []string) *QueryResult {
|
|
if len(results) == 0 {
|
|
return &QueryResult{
|
|
Columns: columns,
|
|
Rows: [][]sqltypes.Value{},
|
|
}
|
|
}
|
|
|
|
// Determine columns if not specified
|
|
if len(columns) == 0 {
|
|
columnSet := make(map[string]bool)
|
|
for _, result := range results {
|
|
for columnName := range result.Values {
|
|
columnSet[columnName] = true
|
|
}
|
|
}
|
|
|
|
columns = make([]string, 0, len(columnSet))
|
|
for columnName := range columnSet {
|
|
columns = append(columns, columnName)
|
|
}
|
|
|
|
// Add metadata columns showing data source
|
|
columns = append(columns, "_source")
|
|
}
|
|
|
|
// Convert to SQL rows
|
|
rows := make([][]sqltypes.Value, len(results))
|
|
for i, result := range results {
|
|
row := make([]sqltypes.Value, len(columns))
|
|
for j, columnName := range columns {
|
|
if columnName == "_source" {
|
|
row[j] = sqltypes.NewVarChar(result.Source)
|
|
} else if value, exists := result.Values[columnName]; exists {
|
|
row[j] = convertSchemaValueToSQL(value)
|
|
} else {
|
|
row[j] = sqltypes.NULL
|
|
}
|
|
}
|
|
rows[i] = row
|
|
}
|
|
|
|
return &QueryResult{
|
|
Columns: columns,
|
|
Rows: rows,
|
|
}
|
|
}
|
|
|
|
// extractTimeFilters extracts time range filters from WHERE clause for optimization
|
|
// This allows push-down of time-based queries to improve scan performance
|
|
// Returns (startTimeNs, stopTimeNs) where 0 means unbounded
|
|
func (e *SQLEngine) extractTimeFilters(expr sqlparser.Expr) (int64, int64) {
|
|
startTimeNs, stopTimeNs := int64(0), int64(0)
|
|
|
|
// Recursively extract time filters from expression tree
|
|
e.extractTimeFiltersRecursive(expr, &startTimeNs, &stopTimeNs)
|
|
|
|
return startTimeNs, stopTimeNs
|
|
}
|
|
|
|
// extractTimeFiltersRecursive recursively processes WHERE expressions to find time comparisons
|
|
func (e *SQLEngine) extractTimeFiltersRecursive(expr sqlparser.Expr, startTimeNs, stopTimeNs *int64) {
|
|
switch exprType := expr.(type) {
|
|
case *sqlparser.ComparisonExpr:
|
|
e.extractTimeFromComparison(exprType, startTimeNs, stopTimeNs)
|
|
case *sqlparser.AndExpr:
|
|
// For AND expressions, combine time filters (intersection)
|
|
e.extractTimeFiltersRecursive(exprType.Left, startTimeNs, stopTimeNs)
|
|
e.extractTimeFiltersRecursive(exprType.Right, startTimeNs, stopTimeNs)
|
|
case *sqlparser.OrExpr:
|
|
// For OR expressions, we can't easily optimize time ranges
|
|
// Skip time filter extraction for OR clauses to avoid incorrect results
|
|
return
|
|
case *sqlparser.ParenExpr:
|
|
// Unwrap parentheses and continue
|
|
e.extractTimeFiltersRecursive(exprType.Expr, startTimeNs, stopTimeNs)
|
|
}
|
|
}
|
|
|
|
// extractTimeFromComparison extracts time bounds from comparison expressions
|
|
// Handles comparisons against timestamp columns (_timestamp_ns, timestamp, created_at, etc.)
|
|
func (e *SQLEngine) extractTimeFromComparison(comp *sqlparser.ComparisonExpr, startTimeNs, stopTimeNs *int64) {
|
|
// Check if this is a time-related column comparison
|
|
leftCol := e.getColumnName(comp.Left)
|
|
rightCol := e.getColumnName(comp.Right)
|
|
|
|
var valueExpr sqlparser.Expr
|
|
var reversed bool
|
|
|
|
// Determine which side is the time column
|
|
if e.isTimeColumn(leftCol) {
|
|
valueExpr = comp.Right
|
|
reversed = false
|
|
} else if e.isTimeColumn(rightCol) {
|
|
valueExpr = comp.Left
|
|
reversed = true
|
|
} else {
|
|
// Not a time comparison
|
|
return
|
|
}
|
|
|
|
// Extract the time value
|
|
timeValue := e.extractTimeValue(valueExpr)
|
|
if timeValue == 0 {
|
|
// Couldn't parse time value
|
|
return
|
|
}
|
|
|
|
// Apply the comparison operator to determine time bounds
|
|
operator := comp.Operator
|
|
if reversed {
|
|
// Reverse the operator if column and value are swapped
|
|
operator = e.reverseOperator(operator)
|
|
}
|
|
|
|
switch operator {
|
|
case sqlparser.GreaterThanStr: // timestamp > value
|
|
if *startTimeNs == 0 || timeValue > *startTimeNs {
|
|
*startTimeNs = timeValue
|
|
}
|
|
case sqlparser.GreaterEqualStr: // timestamp >= value
|
|
if *startTimeNs == 0 || timeValue >= *startTimeNs {
|
|
*startTimeNs = timeValue
|
|
}
|
|
case sqlparser.LessThanStr: // timestamp < value
|
|
if *stopTimeNs == 0 || timeValue < *stopTimeNs {
|
|
*stopTimeNs = timeValue
|
|
}
|
|
case sqlparser.LessEqualStr: // timestamp <= value
|
|
if *stopTimeNs == 0 || timeValue <= *stopTimeNs {
|
|
*stopTimeNs = timeValue
|
|
}
|
|
case sqlparser.EqualStr: // timestamp = value (point query)
|
|
// For exact matches, set both bounds to the same value
|
|
*startTimeNs = timeValue
|
|
*stopTimeNs = timeValue
|
|
}
|
|
}
|
|
|
|
// isTimeColumn checks if a column name refers to a timestamp field
|
|
func (e *SQLEngine) isTimeColumn(columnName string) bool {
|
|
if columnName == "" {
|
|
return false
|
|
}
|
|
|
|
// System timestamp columns
|
|
timeColumns := []string{
|
|
"_timestamp_ns", // SeaweedFS MQ system timestamp (nanoseconds)
|
|
"timestamp_ns", // Alternative naming
|
|
"timestamp", // Common timestamp field
|
|
"created_at", // Common creation time field
|
|
"updated_at", // Common update time field
|
|
"event_time", // Event timestamp
|
|
"log_time", // Log timestamp
|
|
"ts", // Short form
|
|
}
|
|
|
|
for _, timeCol := range timeColumns {
|
|
if strings.EqualFold(columnName, timeCol) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// getColumnName extracts column name from expression (handles ColName types)
|
|
func (e *SQLEngine) getColumnName(expr sqlparser.Expr) string {
|
|
switch exprType := expr.(type) {
|
|
case *sqlparser.ColName:
|
|
return exprType.Name.String()
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// extractTimeValue parses time values from SQL expressions
|
|
// Supports nanosecond timestamps, ISO dates, and relative times
|
|
func (e *SQLEngine) extractTimeValue(expr sqlparser.Expr) int64 {
|
|
switch exprType := expr.(type) {
|
|
case *sqlparser.SQLVal:
|
|
if exprType.Type == sqlparser.IntVal {
|
|
// Parse as nanosecond timestamp
|
|
if val, err := strconv.ParseInt(string(exprType.Val), 10, 64); err == nil {
|
|
return val
|
|
}
|
|
} else if exprType.Type == sqlparser.StrVal {
|
|
// Parse as ISO date or other string formats
|
|
timeStr := string(exprType.Val)
|
|
|
|
// Try parsing as RFC3339 (ISO 8601)
|
|
if t, err := time.Parse(time.RFC3339, timeStr); err == nil {
|
|
return t.UnixNano()
|
|
}
|
|
|
|
// Try parsing as RFC3339 with nanoseconds
|
|
if t, err := time.Parse(time.RFC3339Nano, timeStr); err == nil {
|
|
return t.UnixNano()
|
|
}
|
|
|
|
// Try parsing as date only (YYYY-MM-DD)
|
|
if t, err := time.Parse("2006-01-02", timeStr); err == nil {
|
|
return t.UnixNano()
|
|
}
|
|
|
|
// Try parsing as datetime (YYYY-MM-DD HH:MM:SS)
|
|
if t, err := time.Parse("2006-01-02 15:04:05", timeStr); err == nil {
|
|
return t.UnixNano()
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0 // Couldn't parse
|
|
}
|
|
|
|
// reverseOperator reverses comparison operators when column and value are swapped
|
|
func (e *SQLEngine) reverseOperator(op string) string {
|
|
switch op {
|
|
case sqlparser.GreaterThanStr:
|
|
return sqlparser.LessThanStr
|
|
case sqlparser.GreaterEqualStr:
|
|
return sqlparser.LessEqualStr
|
|
case sqlparser.LessThanStr:
|
|
return sqlparser.GreaterThanStr
|
|
case sqlparser.LessEqualStr:
|
|
return sqlparser.GreaterEqualStr
|
|
case sqlparser.EqualStr:
|
|
return sqlparser.EqualStr
|
|
case sqlparser.NotEqualStr:
|
|
return sqlparser.NotEqualStr
|
|
default:
|
|
return op
|
|
}
|
|
}
|
|
|
|
// buildPredicate creates a predicate function from a WHERE clause expression
|
|
// This is a simplified implementation - a full implementation would be much more complex
|
|
func (e *SQLEngine) buildPredicate(expr sqlparser.Expr) (func(*schema_pb.RecordValue) bool, error) {
|
|
switch exprType := expr.(type) {
|
|
case *sqlparser.ComparisonExpr:
|
|
return e.buildComparisonPredicate(exprType)
|
|
case *sqlparser.AndExpr:
|
|
leftPred, err := e.buildPredicate(exprType.Left)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rightPred, err := e.buildPredicate(exprType.Right)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return func(record *schema_pb.RecordValue) bool {
|
|
return leftPred(record) && rightPred(record)
|
|
}, nil
|
|
case *sqlparser.OrExpr:
|
|
leftPred, err := e.buildPredicate(exprType.Left)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rightPred, err := e.buildPredicate(exprType.Right)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return func(record *schema_pb.RecordValue) bool {
|
|
return leftPred(record) || rightPred(record)
|
|
}, nil
|
|
default:
|
|
return nil, fmt.Errorf("unsupported WHERE expression: %T", expr)
|
|
}
|
|
}
|
|
|
|
// buildComparisonPredicate creates a predicate for comparison operations (=, <, >, etc.)
|
|
func (e *SQLEngine) buildComparisonPredicate(expr *sqlparser.ComparisonExpr) (func(*schema_pb.RecordValue) bool, error) {
|
|
// Extract column name (left side)
|
|
colName, ok := expr.Left.(*sqlparser.ColName)
|
|
if !ok {
|
|
return nil, fmt.Errorf("unsupported comparison left side: %T", expr.Left)
|
|
}
|
|
|
|
columnName := colName.Name.String()
|
|
|
|
// Extract comparison value (right side)
|
|
var compareValue interface{}
|
|
switch val := expr.Right.(type) {
|
|
case *sqlparser.SQLVal:
|
|
switch val.Type {
|
|
case sqlparser.IntVal:
|
|
intVal, err := strconv.ParseInt(string(val.Val), 10, 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
compareValue = intVal
|
|
case sqlparser.StrVal:
|
|
compareValue = string(val.Val)
|
|
default:
|
|
return nil, fmt.Errorf("unsupported SQL value type: %v", val.Type)
|
|
}
|
|
case sqlparser.ValTuple:
|
|
// Handle IN expressions with multiple values: column IN (value1, value2, value3)
|
|
var inValues []interface{}
|
|
for _, tupleVal := range val {
|
|
switch v := tupleVal.(type) {
|
|
case *sqlparser.SQLVal:
|
|
switch v.Type {
|
|
case sqlparser.IntVal:
|
|
intVal, err := strconv.ParseInt(string(v.Val), 10, 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
inValues = append(inValues, intVal)
|
|
case sqlparser.StrVal:
|
|
inValues = append(inValues, string(v.Val))
|
|
}
|
|
}
|
|
}
|
|
compareValue = inValues
|
|
default:
|
|
return nil, fmt.Errorf("unsupported comparison right side: %T", expr.Right)
|
|
}
|
|
|
|
// Create predicate based on operator
|
|
operator := expr.Operator
|
|
|
|
return func(record *schema_pb.RecordValue) bool {
|
|
fieldValue, exists := record.Fields[columnName]
|
|
if !exists {
|
|
return false
|
|
}
|
|
|
|
return e.evaluateComparison(fieldValue, operator, compareValue)
|
|
}, nil
|
|
}
|
|
|
|
// evaluateComparison performs the actual comparison
|
|
func (e *SQLEngine) evaluateComparison(fieldValue *schema_pb.Value, operator string, compareValue interface{}) bool {
|
|
// This is a simplified implementation
|
|
// A full implementation would handle type coercion and all comparison operators
|
|
|
|
switch operator {
|
|
case "=":
|
|
return e.valuesEqual(fieldValue, compareValue)
|
|
case "<":
|
|
return e.valueLessThan(fieldValue, compareValue)
|
|
case ">":
|
|
return e.valueGreaterThan(fieldValue, compareValue)
|
|
case "<=":
|
|
return e.valuesEqual(fieldValue, compareValue) || e.valueLessThan(fieldValue, compareValue)
|
|
case ">=":
|
|
return e.valuesEqual(fieldValue, compareValue) || e.valueGreaterThan(fieldValue, compareValue)
|
|
case "!=", "<>":
|
|
return !e.valuesEqual(fieldValue, compareValue)
|
|
case "LIKE", "like":
|
|
return e.valueLike(fieldValue, compareValue)
|
|
case "IN", "in":
|
|
return e.valueIn(fieldValue, compareValue)
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Helper functions for value comparison (simplified implementation)
|
|
func (e *SQLEngine) valuesEqual(fieldValue *schema_pb.Value, compareValue interface{}) bool {
|
|
switch v := fieldValue.Kind.(type) {
|
|
case *schema_pb.Value_Int32Value:
|
|
if intVal, ok := compareValue.(int64); ok {
|
|
if intVal > math.MaxInt32 || intVal < math.MinInt32 {
|
|
return false // Value out of range for int32, cannot be equal
|
|
}
|
|
return v.Int32Value == int32(intVal)
|
|
}
|
|
case *schema_pb.Value_Int64Value:
|
|
if intVal, ok := compareValue.(int64); ok {
|
|
return v.Int64Value == intVal
|
|
}
|
|
case *schema_pb.Value_StringValue:
|
|
if strVal, ok := compareValue.(string); ok {
|
|
return v.StringValue == strVal
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (e *SQLEngine) valueLessThan(fieldValue *schema_pb.Value, compareValue interface{}) bool {
|
|
switch v := fieldValue.Kind.(type) {
|
|
case *schema_pb.Value_Int32Value:
|
|
if intVal, ok := compareValue.(int64); ok {
|
|
if intVal > math.MaxInt32 {
|
|
return true // int32 value is always less than values > MaxInt32
|
|
}
|
|
if intVal < math.MinInt32 {
|
|
return false // int32 value is always greater than values < MinInt32
|
|
}
|
|
return v.Int32Value < int32(intVal)
|
|
}
|
|
case *schema_pb.Value_Int64Value:
|
|
if intVal, ok := compareValue.(int64); ok {
|
|
return v.Int64Value < intVal
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (e *SQLEngine) valueGreaterThan(fieldValue *schema_pb.Value, compareValue interface{}) bool {
|
|
switch v := fieldValue.Kind.(type) {
|
|
case *schema_pb.Value_Int32Value:
|
|
if intVal, ok := compareValue.(int64); ok {
|
|
if intVal > math.MaxInt32 {
|
|
return false // int32 value is never greater than values > MaxInt32
|
|
}
|
|
if intVal < math.MinInt32 {
|
|
return true // int32 value is always greater than values < MinInt32
|
|
}
|
|
return v.Int32Value > int32(intVal)
|
|
}
|
|
case *schema_pb.Value_Int64Value:
|
|
if intVal, ok := compareValue.(int64); ok {
|
|
return v.Int64Value > intVal
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// valueLike implements SQL LIKE pattern matching with % and _ wildcards
|
|
func (e *SQLEngine) valueLike(fieldValue *schema_pb.Value, compareValue interface{}) bool {
|
|
// Only support LIKE for string values
|
|
stringVal, ok := fieldValue.Kind.(*schema_pb.Value_StringValue)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
pattern, ok := compareValue.(string)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
// Convert SQL LIKE pattern to Go regex pattern
|
|
// % matches any sequence of characters (.*), _ matches single character (.)
|
|
regexPattern := strings.ReplaceAll(pattern, "%", ".*")
|
|
regexPattern = strings.ReplaceAll(regexPattern, "_", ".")
|
|
regexPattern = "^" + regexPattern + "$" // Anchor to match entire string
|
|
|
|
// Compile and match regex
|
|
regex, err := regexp.Compile(regexPattern)
|
|
if err != nil {
|
|
return false // Invalid pattern
|
|
}
|
|
|
|
return regex.MatchString(stringVal.StringValue)
|
|
}
|
|
|
|
// valueIn implements SQL IN operator for checking if value exists in a list
|
|
func (e *SQLEngine) valueIn(fieldValue *schema_pb.Value, compareValue interface{}) bool {
|
|
// For now, handle simple case where compareValue is a slice of values
|
|
// In a full implementation, this would handle SQL IN expressions properly
|
|
values, ok := compareValue.([]interface{})
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
// Check if fieldValue matches any value in the list
|
|
for _, value := range values {
|
|
if e.valuesEqual(fieldValue, value) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// Helper methods for specific operations
|
|
|
|
func (e *SQLEngine) showDatabases(ctx context.Context) (*QueryResult, error) {
|
|
databases := e.catalog.ListDatabases()
|
|
|
|
result := &QueryResult{
|
|
Columns: []string{"Database"},
|
|
Rows: make([][]sqltypes.Value, len(databases)),
|
|
}
|
|
|
|
for i, db := range databases {
|
|
result.Rows[i] = []sqltypes.Value{
|
|
sqltypes.NewVarChar(db),
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (e *SQLEngine) showTables(ctx context.Context, dbName string) (*QueryResult, error) {
|
|
// Use current database context if no database specified
|
|
if dbName == "" {
|
|
dbName = e.catalog.GetCurrentDatabase()
|
|
if dbName == "" {
|
|
dbName = "default"
|
|
}
|
|
}
|
|
|
|
tables, err := e.catalog.ListTables(dbName)
|
|
if err != nil {
|
|
return &QueryResult{Error: err}, err
|
|
}
|
|
|
|
result := &QueryResult{
|
|
Columns: []string{"Tables_in_" + dbName},
|
|
Rows: make([][]sqltypes.Value, len(tables)),
|
|
}
|
|
|
|
for i, table := range tables {
|
|
result.Rows[i] = []sqltypes.Value{
|
|
sqltypes.NewVarChar(table),
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (e *SQLEngine) createTable(ctx context.Context, stmt *sqlparser.DDL) (*QueryResult, error) {
|
|
// Parse CREATE TABLE statement
|
|
// Assumption: Table name format is [database.]table_name
|
|
tableName := stmt.NewName.Name.String()
|
|
database := ""
|
|
|
|
// Check if database is specified in table name
|
|
if stmt.NewName.Qualifier.String() != "" {
|
|
database = stmt.NewName.Qualifier.String()
|
|
} else {
|
|
// Use current database context or default
|
|
database = e.catalog.GetCurrentDatabase()
|
|
if database == "" {
|
|
database = "default"
|
|
}
|
|
}
|
|
|
|
// Parse column definitions from CREATE TABLE
|
|
// Assumption: stmt.TableSpec contains column definitions
|
|
if stmt.TableSpec == nil || len(stmt.TableSpec.Columns) == 0 {
|
|
err := fmt.Errorf("CREATE TABLE requires column definitions")
|
|
return &QueryResult{Error: err}, err
|
|
}
|
|
|
|
// Convert SQL columns to MQ schema fields
|
|
fields := make([]*schema_pb.Field, len(stmt.TableSpec.Columns))
|
|
for i, col := range stmt.TableSpec.Columns {
|
|
fieldType, err := e.convertSQLTypeToMQ(col.Type)
|
|
if err != nil {
|
|
return &QueryResult{Error: err}, err
|
|
}
|
|
|
|
fields[i] = &schema_pb.Field{
|
|
Name: col.Name.String(),
|
|
Type: fieldType,
|
|
}
|
|
}
|
|
|
|
// Create record type for the topic
|
|
recordType := &schema_pb.RecordType{
|
|
Fields: fields,
|
|
}
|
|
|
|
// Create the topic via broker
|
|
partitionCount := int32(6) // Default partition count - TODO: make configurable
|
|
err := e.catalog.brokerClient.ConfigureTopic(ctx, database, tableName, partitionCount, recordType)
|
|
if err != nil {
|
|
return &QueryResult{Error: err}, err
|
|
}
|
|
|
|
// Register the new topic in catalog
|
|
mqSchema := &schema.Schema{
|
|
Namespace: database,
|
|
Name: tableName,
|
|
RecordType: recordType,
|
|
RevisionId: 1, // Initial revision
|
|
}
|
|
|
|
err = e.catalog.RegisterTopic(database, tableName, mqSchema)
|
|
if err != nil {
|
|
return &QueryResult{Error: err}, err
|
|
}
|
|
|
|
// Return success result
|
|
result := &QueryResult{
|
|
Columns: []string{"Result"},
|
|
Rows: [][]sqltypes.Value{
|
|
{sqltypes.NewVarChar(fmt.Sprintf("Table '%s.%s' created successfully", database, tableName))},
|
|
},
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (e *SQLEngine) alterTable(ctx context.Context, stmt *sqlparser.DDL) (*QueryResult, error) {
|
|
// TODO: Implement table alteration
|
|
// This will modify the MQ topic schema with versioning
|
|
err := fmt.Errorf("ALTER TABLE not yet implemented")
|
|
return &QueryResult{Error: err}, err
|
|
}
|
|
|
|
func (e *SQLEngine) dropTable(ctx context.Context, stmt *sqlparser.DDL) (*QueryResult, error) {
|
|
// Parse DROP TABLE statement
|
|
// Assumption: Table name is in stmt.NewName for DROP operations
|
|
tableName := stmt.NewName.Name.String()
|
|
database := ""
|
|
|
|
// Check if database is specified in table name
|
|
if stmt.NewName.Qualifier.String() != "" {
|
|
database = stmt.NewName.Qualifier.String()
|
|
} else {
|
|
// Use current database context or default
|
|
database = e.catalog.GetCurrentDatabase()
|
|
if database == "" {
|
|
database = "default"
|
|
}
|
|
}
|
|
|
|
// Delete the topic via broker
|
|
err := e.catalog.brokerClient.DeleteTopic(ctx, database, tableName)
|
|
if err != nil {
|
|
return &QueryResult{Error: err}, err
|
|
}
|
|
|
|
// Remove from catalog cache
|
|
// TODO: Implement catalog cache removal
|
|
|
|
// Return success result
|
|
result := &QueryResult{
|
|
Columns: []string{"Result"},
|
|
Rows: [][]sqltypes.Value{
|
|
{sqltypes.NewVarChar(fmt.Sprintf("Table '%s.%s' dropped successfully", database, tableName))},
|
|
},
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// discoverAndRegisterTopic attempts to discover an existing topic and register it in the SQL catalog
|
|
func (e *SQLEngine) discoverAndRegisterTopic(ctx context.Context, database, tableName string) error {
|
|
// First, check if topic exists by trying to get its schema from the broker/filer
|
|
recordType, err := e.catalog.brokerClient.GetTopicSchema(ctx, database, tableName)
|
|
if err != nil {
|
|
return fmt.Errorf("topic %s.%s not found or no schema available: %v", database, tableName, err)
|
|
}
|
|
|
|
// Create a schema object from the discovered record type
|
|
mqSchema := &schema.Schema{
|
|
Namespace: database,
|
|
Name: tableName,
|
|
RecordType: recordType,
|
|
RevisionId: 1, // Default to revision 1 for discovered topics
|
|
}
|
|
|
|
// Register the topic in the SQL catalog
|
|
err = e.catalog.RegisterTopic(database, tableName, mqSchema)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to register discovered topic %s.%s: %v", database, tableName, err)
|
|
}
|
|
|
|
fmt.Printf("Auto-discovered and registered topic: %s.%s\n", database, tableName)
|
|
return nil
|
|
}
|