Skip to content

Commit 2392447

Browse files
authored
Merge branch 'main' into feat/mssql-user-defined-types
Signed-off-by: Ngô Quốc Đạt <datlechin@gmail.com>
2 parents e7234e2 + 30286aa commit 2392447

62 files changed

Lines changed: 7398 additions & 1201 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎CHANGELOG.md‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2020
- Favorites and Recent sections, sorting, drag and drop into groups, inline rename and tag search tokens in the welcome window.
2121
- **File > New Group…**, **File > Rename** and **View > Sort Connections By** for the welcome window.
2222
- Favorites, Recent, nested groups, sorting and tag search tokens in the iOS connection list.
23+
- Per-table row filter, with an optional separate target filter, and row limit in data Compare & Sync. (#2537)
24+
- Row grid for data Compare & Sync with every column shown and each differing value marked. (#2537)
2325

2426
### Changed
2527

@@ -35,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3537
- Connection switcher lists Favorites, Recent and groups at every depth.
3638
- Connection rows without colored dots, on the Mac and on iOS.
3739
- SQL Server sessions open with the ANSI SET profile the server requires, matching every other client.
40+
- Compared columns in data Compare & Sync chosen per table, and saved with each table's key, filter and row limit. (#2537)
3841

3942
### Fixed
4043

@@ -44,6 +47,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4447
- SQL Server routines labelled encrypted when the account simply cannot read their source.
4548
- SQL Server version detection on a patched server, which left `CREATE OR ALTER` unused since 2016.
4649
- SQL editor jumping back while scrolling sideways near the start of a long line. (#2841)
50+
- Data sync scripts missing every UPDATE and DELETE. (#2537)
51+
- Data sync statements written to the source schema instead of the target.
52+
- Numeric-looking text such as `007` written unquoted by data sync, and key matches that hit extra rows.
53+
- Data sync pairing arbitrary rows on a key that is not unique.
54+
- Data sync inserts failing on SQL Server identity and PostgreSQL `GENERATED ALWAYS` columns.
55+
- Text columns compared as timestamps, and keys that differ only in case never synced.
56+
- Data sync scripts including tables never compared, or rows that changed after comparing.
57+
- Apply unavailable for a second sync in the same Compare & Sync window.
58+
- Choosing a source, target, mode or option during Apply cancelling the running sync.
59+
- Apply offered for a target switched to Read-Only after it was picked.
60+
- Compare & Sync reporting nothing written after a sync had written to the target.
61+
- Rolled-back data sync on MyISAM tables reported as leaving the target unchanged.
62+
- Cancelling a repeated data comparison clearing the previous results.
63+
- Table whose data comparison failed stuck included with no way to exclude it.
4764
- SSH settings dropped from a Mac connection after it synced from the iPhone app, turning off its tunnel or remote database file.
4865
- Remote database file path and access mode dropped when a connection was exported, shared as a link, or imported.
4966
- Remote database file connection hanging for minutes when its SSH connection dropped silently, with Cancel doing nothing.

‎TablePro/Core/Compare/CompareMetadataService.swift‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,13 +144,17 @@ internal struct CompareMetadataService {
144144
internal func bothSideTableReads(
145145
context: CompareRunner.Context,
146146
includeViews: Bool,
147-
profile: TableReadProfile
147+
profile: TableReadProfile,
148+
targetProfile: TableReadProfile? = nil
148149
) async throws -> (source: [TableStructureRead], target: [TableStructureRead]) {
149150
async let source = tableReads(
150151
for: context.source, connection: context.sourceConnection, includeViews: includeViews, profile: profile
151152
)
152153
async let target = tableReads(
153-
for: context.target, connection: context.targetConnection, includeViews: includeViews, profile: profile
154+
for: context.target,
155+
connection: context.targetConnection,
156+
includeViews: includeViews,
157+
profile: targetProfile ?? profile
154158
)
155159
return try await (source, target)
156160
}
@@ -555,9 +559,14 @@ internal struct TableReadProfile: Sendable {
555559
)
556560

557561
/// A data comparison pairs tables by name, reads the columns they share and walks their rows.
558-
/// It reads foreign keys to order the statements it writes, and it never looks at an index or
559-
/// at a storage engine.
562+
/// It reads foreign keys to order the statements it writes, and it never looks at an index.
560563
internal static let data = TableReadProfile(
561564
wantsIndexes: false, wantsForeignKeys: true, wantsTableMetadata: false
562565
)
566+
567+
/// A MySQL-family target also needs its storage engines, because a MyISAM table cannot roll
568+
/// back and a run that stops part way has to say so.
569+
internal static let dataWithStorageEngines = TableReadProfile(
570+
wantsIndexes: false, wantsForeignKeys: true, wantsTableMetadata: true
571+
)
563572
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
//
2+
// CompareRowFilter.swift
3+
// TablePro
4+
//
5+
6+
import Foundation
7+
8+
internal enum CompareRowFilter {
9+
internal static func normalized(_ text: String?) -> String? {
10+
guard let trimmed = text?.trimmingCharacters(in: .whitespacesAndNewlines), !trimmed.isEmpty else {
11+
return nil
12+
}
13+
return trimmed
14+
}
15+
16+
internal static func validationError(for text: String) -> String? {
17+
guard let filter = normalized(text) else { return nil }
18+
guard !filter.contains(";") else {
19+
return String(localized: "A filter is a single condition and cannot contain a semicolon.")
20+
}
21+
/// `SQLBoundaryValidator` anchors `--` to the start or to whitespace, and the filter is
22+
/// spliced into a single-line statement, so `id = 1--x` would comment out the ORDER BY and
23+
/// the row limit that follow it. `#` is MySQL's line comment and does the same.
24+
guard !filter.contains("--"), !filter.contains("#"),
25+
SQLBoundaryValidator.isRawFilterConditionSafe(filter) else {
26+
return String(localized: "A filter cannot contain a comment.")
27+
}
28+
/// Both readings of a backslash have to agree. MySQL, MariaDB and ClickHouse treat it as an
29+
/// escape inside a string literal and PostgreSQL does not, so a filter that is balanced
30+
/// under one reading and not the other closes the parenthesis this condition is wrapped in
31+
/// on one engine and not on the other.
32+
guard isBalanced(filter, backslashEscapes: true), isBalanced(filter, backslashEscapes: false) else {
33+
return String(localized: "A quote or parenthesis in this filter is not closed.")
34+
}
35+
return nil
36+
}
37+
38+
internal static func condition(for filter: String) -> String {
39+
"(\(filter))"
40+
}
41+
42+
private static func isBalanced(_ text: String, backslashEscapes: Bool) -> Bool {
43+
var depth = 0
44+
var closingQuote: Character?
45+
var characters = Array(text).makeIterator()
46+
var pending: Character?
47+
48+
while let character = pending ?? characters.next() {
49+
pending = nil
50+
if let quote = closingQuote {
51+
if backslashEscapes, character == "\\", quote != "]" {
52+
_ = characters.next()
53+
continue
54+
}
55+
guard character == quote else { continue }
56+
let next = characters.next()
57+
if next == quote, quote != "]" { continue }
58+
closingQuote = nil
59+
pending = next
60+
continue
61+
}
62+
switch character {
63+
case "'", "\"", "`":
64+
closingQuote = character
65+
case "[":
66+
closingQuote = "]"
67+
case "(":
68+
depth += 1
69+
case ")":
70+
depth -= 1
71+
guard depth >= 0 else { return false }
72+
default:
73+
break
74+
}
75+
}
76+
return depth == 0 && closingQuote == nil
77+
}
78+
}

0 commit comments

Comments
 (0)