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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions Echo/Assets.xcassets/MicrosoftSQLServer.imageset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "sql25Icon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "sql25Icon 1.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "sql25Icon 2.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 23 additions & 0 deletions Echo/Assets.xcassets/MySQL.imageset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "mysql.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "mysql 1.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "mysql 2.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Echo/Assets.xcassets/MySQL.imageset/mysql.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 23 additions & 0 deletions Echo/Assets.xcassets/PostgreSQL.imageset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "PostgreSQL-Logo.wine.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "PostgreSQL-Logo.wine 1.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "PostgreSQL-Logo.wine 2.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 23 additions & 0 deletions Echo/Assets.xcassets/SQLite.imageset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "SQLite.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "SQLite 1.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "SQLite 2.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file added Echo/Assets.xcassets/SQLite.imageset/SQLite 1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Echo/Assets.xcassets/SQLite.imageset/SQLite 2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Echo/Assets.xcassets/SQLite.imageset/SQLite.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions Echo/Sources/Core/DatabaseEngine/DatabaseProtocols.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public protocol DatabaseSession: Sendable {
func getTableSchema(_ tableName: String, schemaName: String?) async throws -> [ColumnInfo]
func getObjectDefinition(objectName: String, schemaName: String, objectType: SchemaObjectInfo.ObjectType, database: String?) async throws -> String
func executeUpdate(_ sql: String) async throws -> Int
func executeUpdatesAtomically(_ statements: [String]) async throws
func renameTable(schema: String?, oldName: String, newName: String) async throws
func dropTable(schema: String?, name: String, ifExists: Bool) async throws
func truncateTable(schema: String?, name: String) async throws
Expand Down Expand Up @@ -159,6 +160,12 @@ public extension DatabaseSession {
try await simpleQuery(sql, progressHandler: progressHandler)
}

func executeUpdatesAtomically(_ statements: [String]) async throws {
for statement in statements {
_ = try await executeUpdate(statement)
}
}

func rebuildIndex(schema: String, table: String, index: String) async throws -> DatabaseMaintenanceResult {
throw DatabaseError.queryError("Index rebuild is not supported for this database type")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,14 @@ extension MSSQLDedicatedQuerySession {
if let raw = connection.decodeLastSensitivityClassification() {
queryResult.dataClassification = extractClassification(from: raw, columnCount: queryResult.columns.count)
}
queryResult.serverMessages = executionResult.messages
.filter { $0.kind == .info }
.map { message in
ServerMessage(
kind: .info,
number: message.number,
message: message.message,
state: message.state,
severity: message.severity
)
}
queryResult.serverMessages = executionResult.echoServerMessages()
return queryResult
}

func simpleQuery(_ sql: String, progressHandler: QueryProgressHandler?) async throws -> QueryResultSet {
if QueryStatementClassifier.isLikelyMessageOnlyStatement(sql, databaseType: .microsoftSQL) {
return try await simpleQuery(sql)
}
guard let progressHandler else {
return try await simpleQuery(sql)
}
Expand All @@ -60,6 +53,17 @@ extension MSSQLDedicatedQuerySession {
return Int(try await connection.execute(sql).rowCount ?? 0)
}

func executeUpdatesAtomically(_ statements: [String]) async throws {
guard !statements.isEmpty else { return }

let connection = try await readyConnection()
try await connection.withTransaction { transactionConnection in
for statement in statements {
_ = try await transactionConnection.execute(statement)
}
}
}

private func streamQueryWithProgress(
_ sql: String,
progressHandler: @escaping QueryProgressHandler
Expand Down Expand Up @@ -247,7 +251,15 @@ extension MSSQLDedicatedQuerySession {
number: message.number,
message: message.message,
state: message.state,
severity: message.severity
severity: message.severity,
serverName: message.serverName,
procedureName: message.procedureName,
lineNumber: message.lineNumber,
category: "Server Response",
metadata: [
"source": "sqlserver-nio",
"token": "INFO"
]
)
}
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import Foundation
import SQLServerKit

extension SQLServerExecutionResult {
func echoServerMessages() -> [ServerMessage] {
let infoAndErrorMessages = messages.map { message in
ServerMessage(
kind: message.kind == .error ? .error : .info,
number: message.number,
message: message.message,
state: message.state,
severity: message.severity,
serverName: message.serverName.isEmpty ? nil : message.serverName,
procedureName: message.procedureName.isEmpty ? nil : message.procedureName,
lineNumber: message.lineNumber,
category: "Server Response",
metadata: [
"source": "sqlserver-nio",
"token": message.kind == .error ? "ERROR" : "INFO"
]
)
}

let completionMessages = done.map { done in
let status = String(format: "0x%04X", done.status)
let curCmd = String(format: "0x%04X", done.curCmd)
let text = "DONE kind=\(done.kind.rawValue) status=\(status) curCmd=\(curCmd) rowCount=\(done.rowCount)"
return ServerMessage(
kind: .info,
number: 0,
message: text,
state: 0,
severity: 0,
category: "Driver Response",
metadata: [
"source": "sqlserver-nio",
"token": "DONE",
"kind": done.kind.rawValue,
"status": status,
"curCmd": curCmd,
"rowCount": "\(done.rowCount)"
]
)
}

return infoAndErrorMessages + completionMessages
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,14 @@ extension SQLServerSessionAdapter {
if let raw = result.classification {
queryResult.dataClassification = extractClassification(from: raw, columnCount: queryResult.columns.count)
}
queryResult.serverMessages = result.execResult.messages
.filter { $0.kind == .info }
.map { msg in
ServerMessage(
kind: .info,
number: msg.number,
message: msg.message,
state: msg.state,
severity: msg.severity
)
}
queryResult.serverMessages = result.execResult.echoServerMessages()
return queryResult
}

func simpleQuery(_ sql: String, progressHandler: QueryProgressHandler?) async throws -> QueryResultSet {
if QueryStatementClassifier.isLikelyMessageOnlyStatement(sql, databaseType: .microsoftSQL) {
return try await simpleQuery(sql)
}
guard let progressHandler else {
return try await simpleQuery(sql)
}
Expand All @@ -48,6 +41,16 @@ extension SQLServerSessionAdapter {
return Int(result.rowCount ?? 0)
}

func executeUpdatesAtomically(_ statements: [String]) async throws {
guard !statements.isEmpty else { return }

try await client.transactions.executeInTransaction {
for statement in statements {
_ = try await self.client.execute(statement)
}
}
}

func renameTable(schema: String?, oldName: String, newName: String) async throws {
try await client.admin.renameTable(
name: oldName,
Expand Down Expand Up @@ -258,7 +261,15 @@ extension SQLServerSessionAdapter {
number: msg.number,
message: msg.message,
state: msg.state,
severity: msg.severity
severity: msg.severity,
serverName: msg.serverName,
procedureName: msg.procedureName,
lineNumber: msg.lineNumber,
category: "Server Response",
metadata: [
"source": "sqlserver-nio",
"token": "INFO"
]
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ extension MySQLSession {
}

func simpleQuery(_ sql: String, progressHandler: QueryProgressHandler?) async throws -> QueryResultSet {
if QueryStatementClassifier.isLikelyMessageOnlyStatement(sql, databaseType: .mysql) {
return try await executeSimpleQuery(sql)
}

guard let progressHandler else {
return try await executeSimpleQuery(sql)
}
Expand Down Expand Up @@ -110,7 +114,7 @@ extension MySQLSession {
private func executeSimpleQuery(_ sql: String) async throws -> QueryResultSet {
do {
let result = try await client.query(sql)
return makeResultSet(from: result.rows)
return makeResultSet(from: result.rows, metadata: result.metadata)
} catch {
throw DatabaseError.queryError(error.localizedDescription)
}
Expand Down Expand Up @@ -165,7 +169,7 @@ extension MySQLSession {
}
}

private func makeResultSet(from rows: [MySQLRow]) -> QueryResultSet {
private func makeResultSet(from rows: [MySQLRow], metadata: MySQLWireQueryMetadata? = nil) -> QueryResultSet {
let columns = rows.first.map { makeColumnInfo(from: $0.columnDefinitions) } ?? []
let previewRows = rows.map { row in
row.values.indices.map { index in
Expand All @@ -174,9 +178,18 @@ extension MySQLSession {
}

return QueryResultSet(
columns: columns.isEmpty ? [ColumnInfo(name: "result", dataType: "text")] : columns,
columns: columns,
rows: previewRows,
totalRowCount: rows.count
totalRowCount: rows.count,
commandTag: metadata.map(commandResponse(from:))
)
}

private func commandResponse(from metadata: MySQLWireQueryMetadata) -> String {
var segments = ["affectedRows=\(metadata.affectedRows)"]
if let lastInsertID = metadata.lastInsertID {
segments.append("lastInsertID=\(lastInsertID)")
}
return segments.joined(separator: ", ")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,15 @@ nonisolated struct MySQLToolLocator {
}

private static func locateTool(name: String, customPath: String?) -> URL? {
for directory in searchDirectories(customPath: customPath) {
// When a custom path is explicitly provided, restrict the search to that
// directory only. The caller chose a specific tool location — do not fall
// through to system paths or `which`.
if let customPath, !customPath.isEmpty {
let tool = URL(fileURLWithPath: customPath).appendingPathComponent(name)
return FileManager.default.isExecutableFile(atPath: tool.path) ? tool : nil
}

for directory in searchDirectories() {
let tool = URL(fileURLWithPath: directory).appendingPathComponent(name)
if FileManager.default.isExecutableFile(atPath: tool.path) {
return tool
Expand Down Expand Up @@ -58,11 +66,8 @@ nonisolated struct MySQLToolLocator {
}
}

private static func searchDirectories(customPath: String?) -> [String] {
private static func searchDirectories() -> [String] {
var directories: [String] = []
if let customPath, !customPath.isEmpty {
directories.append(customPath)
}

let env = ProcessInfo.processInfo.environment
if let envPath = env["ECHO_MYSQL_TOOL_PATH"], !envPath.isEmpty {
Expand Down
Loading
Loading