Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions TablePro/Core/Autocomplete/SQLCompletionProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,13 @@ final class SQLCompletionProvider {
"CONSTRAINT", "ENGINE", "CHARSET", "COLLATE"
])

case .alterTableColumn:
// After ALTER TABLE tablename DROP/MODIFY/CHANGE COLUMN - suggest column names
if let firstTable = context.tableReferences.first {
items = await schemaProvider.columnCompletionItems(for: firstTable.tableName)
}
items += filterKeywords(["COLUMN", "FIRST", "AFTER"])

case .createTable:
// Inside CREATE TABLE (...) - suggest constraints and data types
items = filterKeywords([
Expand Down
200 changes: 140 additions & 60 deletions TablePro/Core/Autocomplete/SQLContextAnalyzer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@ enum SQLClauseType {
case caseExpression // Inside CASE WHEN expression
case inList // Inside IN (...) list
case limit // After LIMIT/OFFSET
case alterTable // After ALTER TABLE tablename
case createTable // Inside CREATE TABLE definition
case columnDef // Typing column data type (after column name)
case unknown // Unknown or start of query
case alterTable // After ALTER TABLE tablename
case alterTableColumn // After DROP/MODIFY/CHANGE/RENAME COLUMN - need column names
case createTable // Inside CREATE TABLE definition
case columnDef // Typing column data type (after column name)
case unknown // Unknown or start of query
}

/// Represents a table reference with optional alias
Expand Down Expand Up @@ -89,6 +90,88 @@ struct SQLContext {
/// Analyzes SQL query to determine completion context
final class SQLContextAnalyzer {

// MARK: - Cached Regex Patterns (Compiled Once at Class Load)

/// Pre-compiled clause detection patterns for performance
/// ORDER MATTERS: More specific patterns must come before general ones
private static let clauseRegexes: [(regex: NSRegularExpression, clause: SQLClauseType)] = {
let patterns: [(String, SQLClauseType)] = [
// DDL patterns (most specific first)
("\\b(?:ADD|MODIFY|CHANGE)\\s+(?:COLUMN\\s+)?\\w+\\s+\\w*$", .columnDef),
("\\bALTER\\s+TABLE\\s+[`\"']?\\w+[`\"']?\\s+(?:DROP|MODIFY|CHANGE|RENAME)\\s+(?:COLUMN\\s+)?(?:[`\"']?\\w+[`\"']?)?\\s*$", .alterTableColumn),
("\\bALTER\\s+TABLE\\s+[^;]*\\bAFTER\\s+\\w*$", .alterTableColumn),
Comment thread
datlechin marked this conversation as resolved.
("\\bALTER\\s+TABLE\\s+[`\"']?\\w+[`\"']?\\s+\\w*$", .alterTable),
("\\bCREATE\\s+TABLE\\s+[^(]*\\([^)]*$", .createTable),
// Enhanced context patterns
("\\bIN\\s*\\([^)]*$", .inList),
("\\bCASE\\s+(?:WHEN\\s+[^;]*)?$", .caseExpression),
("\\b(LIMIT|OFFSET)\\s+\\d*$", .limit),
// Standard clause patterns
("\\bVALUES\\s*\\([^)]*$", .values),
("\\bINSERT\\s+INTO\\s+\\w+\\s*\\([^)]*$", .insertColumns),
("\\bINTO\\s+\\w*$", .into),
("\\bSET\\s+[^;]*$", .set),
("\\bHAVING\\s+[^;]*$", .having),
("\\bORDER\\s+BY\\s+[^;]*$", .orderBy),
("\\bGROUP\\s+BY\\s+[^;]*$", .groupBy),
("\\b(AND|OR)\\s+\\w*$", .and),
("\\bWHERE\\s+[^;]*$", .where_),
("\\bON\\s+[^;]*$", .on),
// JOIN patterns
("(?:LEFT|RIGHT|INNER|OUTER|FULL|CROSS)?\\s*(?:OUTER)?\\s*JOIN\\s+[`\"']?\\w+[`\"']?(?:\\s+(?:AS\\s+)?\\w+)?\\s*$", .join),
("\\bJOIN\\s+[`\"']?\\w*[`\"']?\\s*$", .join),
Comment thread
datlechin marked this conversation as resolved.
// FROM patterns
("\\bFROM\\s+[`\"']?\\w+[`\"']?(?:\\s+(?:AS\\s+)?\\w+)?\\s*$", .from),
("\\bFROM\\s+\\w*$", .from),
// SELECT is most general
("\\bSELECT\\s+[^;]*$", .select),
]
return patterns.compactMap { pattern, clause in
guard let regex = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive) else {
assertionFailure("Invalid SQL clause regex pattern: \(pattern)")
return nil
}
return (regex, clause)
}
}()

/// Pre-compiled regex for removing strings and comments (force-unwrap safe: simple patterns)
private static let singleQuoteStringRegex: NSRegularExpression = {
if let regex = try? NSRegularExpression(pattern: "'[^']*'") {
return regex
}
assertionFailure("Failed to compile singleQuoteStringRegex - invalid pattern")
// Fallback to a regex that matches nothing
return try! NSRegularExpression(pattern: "(?!)")
}()

private static let doubleQuoteStringRegex: NSRegularExpression = {
if let regex = try? NSRegularExpression(pattern: "\"[^\"]*\"") {
return regex
}
assertionFailure("Failed to compile doubleQuoteStringRegex - invalid pattern")
// Fallback to a regex that matches nothing
return try! NSRegularExpression(pattern: "(?!)")
}()

private static let blockCommentRegex: NSRegularExpression = {
if let regex = try? NSRegularExpression(pattern: "/\\*[\\s\\S]*?\\*/") {
return regex
}
assertionFailure("Failed to compile blockCommentRegex - invalid pattern")
// Fallback to a regex that matches nothing
return try! NSRegularExpression(pattern: "(?!)")
}()

private static let lineCommentRegex: NSRegularExpression = {
if let regex = try? NSRegularExpression(pattern: "--[^\n]*") {
return regex
}
assertionFailure("Failed to compile lineCommentRegex - invalid pattern")
// Fallback to a regex that matches nothing
return try! NSRegularExpression(pattern: "(?!)")
}()

// MARK: - Main Analysis

/// Analyze the query at the given cursor position
Expand Down Expand Up @@ -143,6 +226,14 @@ final class SQLContextAnalyzer {
}
}

// Extract ALTER TABLE table name and add to references
if let alterTableName = extractAlterTableName(from: currentStatement) {
let alterRef = TableReference(tableName: alterTableName, alias: nil)
if !tableReferences.contains(alterRef) {
tableReferences.append(alterRef)
}
}

// Calculate nesting level (subquery depth)
let nestingLevel = calculateNestingLevel(in: textBeforeCursor)

Expand Down Expand Up @@ -511,6 +602,26 @@ final class SQLContextAnalyzer {
return references
}

/// Pre-compiled regex for extracting table name from ALTER TABLE statements
private static let alterTableRegex: NSRegularExpression? = {
// Pattern: ALTER TABLE tablename (supports optional quoting and special characters)
let pattern = "(?i)\\bALTER\\s+TABLE\\s+[`\"']?([^`\"']+)[`\"']?"
Comment thread
datlechin marked this conversation as resolved.
return try? NSRegularExpression(pattern: pattern)
}()

/// Extract table name from ALTER TABLE statement
private func extractAlterTableName(from query: String) -> String? {
guard let regex = Self.alterTableRegex else { return nil }

let range = NSRange(query.startIndex..., in: query)
if let match = regex.firstMatch(in: query, range: range),
let tableRange = Range(match.range(at: 1), in: query) {
return String(query[tableRange])
}

return nil
}

/// Determine the clause type based on text before cursor
private func determineClauseType(textBeforeCursor: String, dotPrefix: String?, currentFunction: String? = nil) -> SQLClauseType {
// If we have a dot prefix, we're looking for columns
Expand All @@ -528,46 +639,10 @@ final class SQLContextAnalyzer {
// Remove string literals and comments for analysis
let cleaned = removeStringsAndComments(from: upper)

// Find the last keyword to determine context
// ORDER MATTERS: More specific patterns must come before general ones
let clausePatterns: [(pattern: String, clause: SQLClauseType)] = [
// DDL patterns (most specific first)
// After ADD/MODIFY COLUMN name - suggest data types
("\\b(?:ADD|MODIFY|CHANGE)\\s+(?:COLUMN\\s+)?\\w+\\s+\\w*$", .columnDef),
// After ALTER TABLE tablename - suggest ADD, DROP, MODIFY, etc.
("\\bALTER\\s+TABLE\\s+[`\"']?\\w+[`\"']?\\s+\\w*$", .alterTable),
// Inside CREATE TABLE (...) - suggest column definitions
("\\bCREATE\\s+TABLE\\s+[^(]*\\([^)]*$", .createTable),

// New patterns for enhanced context
("\\bIN\\s*\\([^)]*$", .inList),
("\\bCASE\\s+(?:WHEN\\s+[^;]*)?$", .caseExpression),
("\\b(LIMIT|OFFSET)\\s+\\d*$", .limit),

// Existing patterns
("\\bVALUES\\s*\\([^)]*$", .values),
("\\bINSERT\\s+INTO\\s+\\w+\\s*\\([^)]*$", .insertColumns),
("\\bINTO\\s+\\w*$", .into),
("\\bSET\\s+[^;]*$", .set),
("\\bHAVING\\s+[^;]*$", .having),
("\\bORDER\\s+BY\\s+[^;]*$", .orderBy),
("\\bGROUP\\s+BY\\s+[^;]*$", .groupBy),
("\\b(AND|OR)\\s+\\w*$", .and),
("\\bWHERE\\s+[^;]*$", .where_),
("\\bON\\s+[^;]*$", .on),
// JOIN: match various JOIN types followed by table [alias] - must come before FROM
("(?:LEFT|RIGHT|INNER|OUTER|FULL|CROSS)?\\s*(?:OUTER)?\\s*JOIN\\s+[`\"']?\\w+[`\"']?(?:\\s+(?:AS\\s+)?\\w+)?\\s*$", .join),
("\\bJOIN\\s+[`\"']?\\w*[`\"']?\\s*$", .join),
// FROM: match "FROM table" or "FROM table " (with or without trailing space) - NOT followed by WHERE/ORDER/etc.
("\\bFROM\\s+[`\"']?\\w+[`\"']?(?:\\s+(?:AS\\s+)?\\w+)?\\s*$", .from),
("\\bFROM\\s+\\w*$", .from),
// SELECT comes last as it's the most general
("\\bSELECT\\s+[^;]*$", .select),
]

for (pattern, clause) in clausePatterns {
if let regex = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive),
regex.firstMatch(in: cleaned, range: NSRange(cleaned.startIndex..., in: cleaned)) != nil {
// Use pre-compiled regex patterns for performance
let range = NSRange(cleaned.startIndex..., in: cleaned)
for (regex, clause) in Self.clauseRegexes {
if regex.firstMatch(in: cleaned, range: range) != nil {
return clause
}
}
Expand All @@ -579,25 +654,30 @@ final class SQLContextAnalyzer {
private func removeStringsAndComments(from text: String) -> String {
var result = text

// Remove single-quoted strings
if let regex = try? NSRegularExpression(pattern: "'[^']*'") {
result = regex.stringByReplacingMatches(in: result, range: NSRange(result.startIndex..., in: result), withTemplate: "''")
}
// Use pre-compiled regex patterns for performance
result = Self.singleQuoteStringRegex.stringByReplacingMatches(
in: result,
range: NSRange(result.startIndex..., in: result),
withTemplate: "''"
)

// Remove double-quoted strings
if let regex = try? NSRegularExpression(pattern: "\"[^\"]*\"") {
result = regex.stringByReplacingMatches(in: result, range: NSRange(result.startIndex..., in: result), withTemplate: "\"\"")
}
result = Self.doubleQuoteStringRegex.stringByReplacingMatches(
in: result,
range: NSRange(result.startIndex..., in: result),
withTemplate: "\"\""
)

// Remove block comments
if let regex = try? NSRegularExpression(pattern: "/\\*[\\s\\S]*?\\*/") {
result = regex.stringByReplacingMatches(in: result, range: NSRange(result.startIndex..., in: result), withTemplate: "")
}
result = Self.blockCommentRegex.stringByReplacingMatches(
in: result,
range: NSRange(result.startIndex..., in: result),
withTemplate: ""
)

// Remove line comments
if let regex = try? NSRegularExpression(pattern: "--[^\n]*") {
result = regex.stringByReplacingMatches(in: result, range: NSRange(result.startIndex..., in: result), withTemplate: "")
}
result = Self.lineCommentRegex.stringByReplacingMatches(
in: result,
range: NSRange(result.startIndex..., in: result),
withTemplate: ""
)

return result
}
Expand Down
4 changes: 1 addition & 3 deletions TablePro/Core/Utilities/SQLFileParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -247,9 +247,7 @@ final class SQLFileParser {
} catch {
// Log parsing errors - these should not fail silently
print("ERROR: SQL file parsing failed: \(error.localizedDescription)")
if let fileError = error as? NSError {
print("Error details: domain=\(fileError.domain), code=\(fileError.code)")
}
print("Error details: \(error)")
continuation.finish()
}
}
Expand Down
Loading