From 233ff2bf6411216b38b256134521bb08c82d0709 Mon Sep 17 00:00:00 2001 From: Mark Gregory Date: Tue, 20 Oct 2026 09:22:16 +0100 Subject: [PATCH] row editing --- DBSyn.xcodeproj/project.pbxproj | 8 ++--- DBSyn/MainView.swift | 37 ++++++++--------------- docs/DBSyn-1.0.5-build78-release-notes.md | 9 ++++++ 3 files changed, 25 insertions(+), 29 deletions(-) create mode 100644 docs/DBSyn-1.0.5-build78-release-notes.md diff --git a/DBSyn.xcodeproj/project.pbxproj b/DBSyn.xcodeproj/project.pbxproj index e59358c..46ac005 100644 --- a/DBSyn.xcodeproj/project.pbxproj +++ b/DBSyn.xcodeproj/project.pbxproj @@ -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."; diff --git a/DBSyn/MainView.swift b/DBSyn/MainView.swift index a8e209d..5b4ce56 100644 --- a/DBSyn/MainView.swift +++ b/DBSyn/MainView.swift @@ -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) diff --git a/docs/DBSyn-1.0.5-build78-release-notes.md b/docs/DBSyn-1.0.5-build78-release-notes.md new file mode 100644 index 0000000..7af7004 --- /dev/null +++ b/docs/DBSyn-1.0.5-build78-release-notes.md @@ -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.