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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Create a connection from a project folder. Pick one from the welcome screen or File > Open Project Folder..., and TablePro reads the database settings it finds in `.env` files, `wp-config.php`, `prisma/schema.prisma`, `config/database.yml`, `docker-compose.yml`, `application.properties`, `application.yml`, and `appsettings.json`. A project that uses more than one engine gets a row for each. Review what it found, pick one, and the connection form opens filled in. Nothing is saved or connected until you save it. (#1959)

### Changed

- The welcome screen groups the ways to add an existing connection into one **Add from Existing** menu, next to **Create Connection**. Import from URL now has a home in that menu and in the File menu, where it had none. **Try Sample Database** moved to the connection list, which already offers it when the list is empty.

### Fixed

- The license activation sheet now opens when you click **Activate License**. It was built and then failed to appear, and once that happened further clicks did nothing at all.
- File > Import from Other App..., Open Project Folder..., Import Connections... and Export Connections... now work when no welcome window is open. They used to do nothing.
- The tooltip on the welcome screen's **+** button shows the shortcut you actually have bound for New Connection instead of always claiming ⌘N.
- The AI chat panel stays inside the right panel when you drag it narrow. The model name, tool names, and code block headers now truncate, long chat messages and code wrap, wide tables scroll inside their own box, and the composer text follows the panel width instead of running under the editor. (#1956)

## [0.60.1] - 2026-07-25
Expand Down
17 changes: 17 additions & 0 deletions TablePro.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
5AEA8B472F6808CA0040461A /* EtcdHttpClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEA8B3C2F6808CA0040461A /* EtcdHttpClient.swift */; };
5AEA8B492F6808E90040461A /* TableProPluginKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A860000100000000 /* TableProPluginKit.framework */; };
5AEE5B362F5C9B7B00FA84D7 /* OracleNIO in Frameworks */ = {isa = PBXBuildFile; productRef = 5ACE00012F4F00000000000F /* OracleNIO */; };
5AYAMS00000000000000A003 /* Yams in Frameworks */ = {isa = PBXBuildFile; productRef = 5AYAMS00000000000000A002 /* Yams */; };
5E1A500100000000000000A1 /* ElasticsearchConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E1A500200000000000000A1 /* ElasticsearchConnection.swift */; };
5E1A500100000000000000A2 /* ElasticsearchConsoleParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E1A500200000000000000A2 /* ElasticsearchConsoleParser.swift */; };
5E1A500100000000000000A3 /* ElasticsearchMappingFlattener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E1A500200000000000000A3 /* ElasticsearchMappingFlattener.swift */; };
Expand Down Expand Up @@ -955,6 +956,7 @@
5A32BBFB2F9D5EAB00BAEB5F /* X509 in Frameworks */,
5ACE00012F4F000000000006 /* CodeEditTextView in Frameworks */,
5ACE00012F4F00000000000A /* Sparkle in Frameworks */,
5AYAMS00000000000000A003 /* Yams in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -1480,6 +1482,7 @@
5ACE00012F4F000000000010 /* TableProAnalytics */,
5A32BBFA2F9D5EAB00BAEB5F /* X509 */,
5A1MPORT000000000000A010 /* TableProImport */,
5AYAMS00000000000000A002 /* Yams */,
);
productName = TablePro;
productReference = 5A1091C72EF17EDC0055EA7C /* TablePro.app */;
Expand Down Expand Up @@ -2271,6 +2274,7 @@
5ACE00012F4F00000000000E /* XCRemoteSwiftPackageReference "oracle-nio" */,
5A0000012F4F000000000102 /* XCLocalSwiftPackageReference "Packages/TableProCore" */,
5A32BBF92F9D5EAB00BAEB5F /* XCRemoteSwiftPackageReference "swift-certificates" */,
5AYAMS00000000000000A001 /* XCRemoteSwiftPackageReference "Yams" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = 5A1091C82EF17EDC0055EA7C /* Products */;
Expand Down Expand Up @@ -5464,6 +5468,14 @@
revision = 23c77812ed90259243a761d7fdea19db47d3ca41;
};
};
5AYAMS00000000000000A001 /* XCRemoteSwiftPackageReference "Yams" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/jpsim/Yams";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 5.1.0;
};
};
/* End XCRemoteSwiftPackageReference section */

/* Begin XCSwiftPackageProductDependency section */
Expand Down Expand Up @@ -5508,6 +5520,11 @@
package = 5A0000012F4F000000000102 /* XCLocalSwiftPackageReference "Packages/TableProCore" */;
productName = TableProMSSQLCore;
};
5AYAMS00000000000000A002 /* Yams */ = {
isa = XCSwiftPackageProductDependency;
package = 5AYAMS00000000000000A001 /* XCRemoteSwiftPackageReference "Yams" */;
productName = Yams;
};
AC1D0000000000000000F002 /* TableProTrinoCore */ = {
isa = XCSwiftPackageProductDependency;
package = 5A0000012F4F000000000102 /* XCLocalSwiftPackageReference "Packages/TableProCore" */;
Expand Down

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

2 changes: 2 additions & 0 deletions TablePro/Core/Events/AppCommands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ final class AppCommands {
let exportConnections = PassthroughSubject<Void, Never>()
let importConnections = PassthroughSubject<Void, Never>()
let importConnectionsFromApp = PassthroughSubject<Void, Never>()
let openProjectFolder = PassthroughSubject<Void, Never>()
let importConnectionFromURL = PassthroughSubject<Void, Never>()
let exportQueryResults = PassthroughSubject<Void, Never>()

// MARK: - Window / Sheet Commands
Expand Down
114 changes: 114 additions & 0 deletions TablePro/Core/Services/ProjectImport/AppSettingsJsonExtractor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
//
// AppSettingsJsonExtractor.swift
// TablePro
//

import Foundation

enum AppSettingsJsonExtractor {
static func extract(data: Data, relativePath: String) -> [ScannedConnectionCandidate] {
guard let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
return []
}
guard let strings = connectionStrings(in: root) else {
return []
}
return strings.sorted { $0.key < $1.key }.compactMap { name, value in
candidate(name: name, connectionString: value, relativePath: relativePath)
}
}

private static func connectionStrings(in root: [String: Any]) -> [String: String]? {
for (key, value) in root where key.lowercased() == "connectionstrings" {
guard let nested = value as? [String: Any] else {
continue
}
return nested.compactMapValues { $0 as? String }
}
return nil
}

static func candidate(
name: String,
connectionString: String,
relativePath: String
) -> ScannedConnectionCandidate? {
let keywords = parseKeywords(connectionString)
guard let type = provider(for: keywords) else {
return nil
}
var fields = ScannedConnectionFields(type: type)
fields.host = first(keywords, ["host", "server", "data source", "address", "addr", "network address"]) ?? ""
fields.database = first(keywords, ["database", "initial catalog"]) ?? ""
fields.username = first(keywords, ["username", "user id", "uid", "user"]) ?? ""
fields.password = first(keywords, ["password", "pwd"]) ?? ""
fields.connectionName = name
let address = splitHostPort(fields.host, type: type)
fields.host = address.host
fields.port = first(keywords, ["port"]).flatMap { Int($0) } ?? address.port ?? type.defaultPort
return ScannedConnectionCandidate(
parsedURL: fields.toParsedConnectionURL(),
sourceRelativePath: relativePath,
sourceKey: "ConnectionStrings:\(name)",
kind: .appSettingsJson,
tier: .configFile
)
}

static func parseKeywords(_ connectionString: String) -> [String: String] {
var keywords: [String: String] = [:]
for segment in connectionString.split(separator: ";") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor quoting when splitting appsettings connection strings

When an ADO.NET connection-string value contains a semicolon inside quotes, such as Password="pa;ss", splitting blindly at every semicolon records the password as "pa and discards the remainder. The candidate is still presented as having a password but cannot authenticate, so the keyword parser needs to recognize quoted or escaped delimiters.

Useful? React with 👍 / 👎.

guard let equals = segment.firstIndex(of: "=") else {
continue
}
let key = String(segment[segment.startIndex..<equals])
.trimmingCharacters(in: .whitespaces)
.lowercased()
let value = String(segment[segment.index(after: equals)...]).trimmingCharacters(in: .whitespaces)
guard !key.isEmpty, !value.isEmpty else {
continue
}
keywords[key] = value
}
return keywords
}

static func provider(for keywords: [String: String]) -> DatabaseType? {
if keywords["host"] != nil {
return .postgresql
}
if keywords["initial catalog"] != nil {
return .mssql
}
let sqlServerHints = ["trusted_connection", "encrypt", "trustservercertificate", "integrated security"]
if sqlServerHints.contains(where: { keywords[$0] != nil }) {
return .mssql
}
if keywords["uid"] != nil || keywords["pwd"] != nil {
return .mysql
}
return nil
Comment on lines +83 to +90

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Accept standard SQL Server connection strings using Database

When an appsettings entry uses the common SqlClient form Server=db;Database=app;User Id=sa;Password=..., none of these provider checks match because it uses neither Initial Catalog nor one of the listed SQL Server hints, and User Id/Password also do not satisfy the MySQL uid/pwd check. The connection is therefore omitted entirely even though all required SQL Server fields were parsed.

Useful? React with 👍 / 👎.

}

private static func first(_ keywords: [String: String], _ names: [String]) -> String? {
for name in names {
if let value = keywords[name] {
return value
}
}
return nil
}

private static func splitHostPort(_ value: String, type: DatabaseType) -> (host: String, port: Int?) {
let separator: Character = type == .mssql ? "," : ":"
guard let index = value.firstIndex(of: separator) else {
return (value, nil)
}
let head = String(value[value.startIndex..<index])
let tail = String(value[value.index(after: index)...])
guard let port = Int(tail) else {
return (value, nil)
}
return (head, port)
}
}
Loading
Loading