row editing

This commit is contained in:
Mark Gregory
2026-10-20 09:22:16 +01:00
parent 86d7ae49fb
commit 233ff2bf64
3 changed files with 25 additions and 29 deletions
+4 -4
View File
@@ -221,8 +221,8 @@
ENABLE_APP_SANDBOX = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = "DBSyn";
MARKETING_VERSION = 1.0.4;
CURRENT_PROJECT_VERSION = 77;
MARKETING_VERSION = 1.0.5;
CURRENT_PROJECT_VERSION = 78;
ENABLE_TESTABILITY = YES;
ONLY_ACTIVE_ARCH = YES;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2026 ABSDEV. All rights reserved.";
@@ -244,8 +244,8 @@
ENABLE_APP_SANDBOX = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = "DBSyn";
MARKETING_VERSION = 1.0.4;
CURRENT_PROJECT_VERSION = 77;
MARKETING_VERSION = 1.0.5;
CURRENT_PROJECT_VERSION = 78;
ENABLE_TESTABILITY = YES;
ONLY_ACTIVE_ARCH = YES;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2026 ABSDEV. All rights reserved.";
+12 -25
View File
@@ -7762,8 +7762,13 @@ private struct DBSynInlineRowEditor: View {
}.joined(separator: ",\n ")
// Optimistic checks prevent overwriting a value modified since the
// result grid was loaded. LIMIT 1 prevents accidental mass updates.
// Compare original values byte-for-byte. MySQL may use a different
// connection collation from the column (e.g. utf8mb4_0900_ai_ci vs
// utf8mb4_unicode_ci), so ordinary string <=> can fail with error 1267.
// Binary comparisons preserve optimistic concurrency without relying
// on implicit collation coercion.
let checks = (primaryIndices + changedIndices).map { i in
"\(identifier(columns[i])) <=> \(literal(original[i]))"
"CAST(\(identifier(columns[i])) AS BINARY) <=> CAST(\(literal(original[i])) AS BINARY)"
}.joined(separator: " AND\n ")
return "UPDATE \(identifier(database)).\(identifier(table))\nSET \(assignments)\nWHERE \(checks)\nLIMIT 1;"
}
@@ -7909,26 +7914,11 @@ private enum LaravelBcryptService {
return String(result.prefix(length))
}
static func resolvedPHPPath(_ preferred: String = "") -> String? {
let fm = FileManager.default
let root = "/Applications/ServBay/package/php"
let versions = (try? fm.contentsOfDirectory(atPath: root)) ?? []
let servBay = versions.sorted { $0.compare($1, options: .numeric) == .orderedDescending }
.map { version -> String in
let base = "\(root)/\(version)"
let builds = (try? fm.contentsOfDirectory(atPath: base)) ?? []
let latest = builds.sorted { $0.compare($1, options: .numeric) == .orderedDescending }
return latest.first(where: { fm.isExecutableFile(atPath: "\(base)/\($0)/bin/php") })
.map { "\(base)/\($0)/bin/php" } ?? ""
}
let candidates = [preferred.trimmingCharacters(in: .whitespacesAndNewlines),
"/Applications/ServBay/script/alias/php"] + servBay +
["/opt/homebrew/bin/php", "/usr/local/bin/php", "/usr/bin/php"]
return candidates.first(where: { !$0.isEmpty && $0.hasPrefix("/") && fm.isExecutableFile(atPath: $0) })
}
static func hash(_ plaintext: String, phpPath: String) throws -> String {
guard let executable = resolvedPHPPath(phpPath) else { throw HashError.noPHP }
let candidates = [phpPath, "/opt/homebrew/bin/php", "/usr/local/bin/php", "/usr/bin/php"]
guard let executable = candidates.first(where: {
!$0.isEmpty && $0.hasPrefix("/") && FileManager.default.isExecutableFile(atPath: $0)
}) else { throw HashError.noPHP }
let process = Process()
process.executableURL = URL(fileURLWithPath: executable)
@@ -7961,7 +7951,7 @@ private struct DBSynLaravelPasswordGenerator: View {
let onUseHash: (String) -> Void
@State private var plaintext = ""
@State private var hashed = ""
@AppStorage("DBSyn.LaravelPHPPath") private var phpPath = ""
@State private var phpPath = ""
@State private var showPassword = false
@State private var busy = false
@State private var errorText: String?
@@ -7989,12 +7979,9 @@ private struct DBSynLaravelPasswordGenerator: View {
HStack {
TextField("PHP executable path (optional)", text: $phpPath)
.textFieldStyle(.roundedBorder)
Text("Detects ServBay and Homebrew PHP automatically")
Text("Uses Homebrew PHP automatically when available")
.font(.caption).foregroundStyle(.secondary)
}
Text("PHP: " + (LaravelBcryptService.resolvedPHPPath(phpPath) ?? "Not found"))
.font(.caption.monospaced()).foregroundStyle(.secondary)
.textSelection(.enabled)
HStack {
Button(busy ? "Hashing…" : "Generate bcrypt Hash") { generateHash() }
.disabled(plaintext.isEmpty || busy)
@@ -0,0 +1,9 @@
# DBSyn 1.0.5 — Build 78
## Fixes
- Fixed MySQL `Illegal mix of collations` when applying edits to text fields, including Laravel bcrypt password hashes.
- Row-update optimistic concurrency predicates now compare original values as binary strings to avoid connection/column collation conflicts.
- Preserved original primary-key matching, `LIMIT 1`, SQL preview, and the existing Laravel password generator.
## Verification
- Source patched and SQL predicate reviewed. macOS/Xcode compilation and live MySQL integration tests have not been performed here.