@@ -193,9 +193,12 @@ final class QueryViewModel {
193 let toIndex = columnOrder.firstIndex(of: targetColumn),
193 let toIndex = columnOrder.firstIndex(of: targetColumn),
194 fromIndex != toIndex else { return }
194 fromIndex != toIndex else { return }
195 let item = columnOrder.remove(at: fromIndex)
195 let item = columnOrder.remove(at: fromIndex)
196+ // Match the drop-indicator semantics: a rightward move (fromIndex < toIndex)
197+ // lands the column *after* the target, a leftward move lands it *before*.
196− // Removing an element before the target shifts the target down by one, so
198+ // After removing an earlier element the target shifts down by one, so
197− // inserting at the original `toIndex` would land the column past its target.
199+ // inserting at `toIndex` lands after the target for rightward moves; for
200+ // leftward moves the target is unshifted and `toIndex` lands before it.
198− let adjusted = fromIndex < toIndex ? toIndex - 1 : toIndex
201+ let adjusted = toIndex
199 columnOrder.insert(item, at: adjusted)
202 columnOrder.insert(item, at: adjusted)
200 saveColumnConfig()
203 saveColumnConfig()
201 updateSQLColumns()
204 updateSQLColumns()
@@ -627,6 +630,9 @@ final class QueryViewModel {
627 var depth = 0
630 var depth = 0
628 var whereEnd: String.Index?
631 var whereEnd: String.Index?
629 var tailStart: String.Index?
632 var tailStart: String.Index?
633+ // Track whether the existing WHERE body has a top-level OR — only then does
634+ // AND-ing a new condition need parentheses to preserve precedence.
635+ var bodyHasTopLevelOr = false
630 for token in SQLTokenizer.tokenize(sql) {
636 for token in SQLTokenizer.tokenize(sql) {
631 switch token.kind {
637 switch token.kind {
632 case .leftParen:
638 case .leftParen:
@@ -638,6 +644,8 @@ final class QueryViewModel {
638 whereEnd = token.range.upperBound
644 whereEnd = token.range.upperBound
639 } else if tailKeywords.contains(kw), tailStart == nil {
645 } else if tailKeywords.contains(kw), tailStart == nil {
640 tailStart = token.range.lowerBound
646 tailStart = token.range.lowerBound
647+ } else if kw == "OR", whereEnd != nil, tailStart == nil {
648+ bodyHasTopLevelOr = true
@@ -646,16 +654,18 @@ final class QueryViewModel {
647 if let whereEnd {
655 if let whereEnd {
648 // Parenthesize the existing WHERE body before AND-ing the new
656 // Parenthesize the existing WHERE body before AND-ing the new
649− // condition, otherwise an existing top-level OR silently changes
657+ // condition only when it has a top-level OR, otherwise the OR
650− // meaning: `WHERE a = 1 OR b = 2` would become
658+ // silently changes meaning: `WHERE a = 1 OR b = 2` would become
651 // `WHERE a = 1 OR b = 2 AND c = 3`, which SQL binds as
659 // `WHERE a = 1 OR b = 2 AND c = 3`, which SQL binds as
652 // `a = 1 OR (b = 2 AND c = 3)` rather than the intended
660 // `a = 1 OR (b = 2 AND c = 3)` rather than the intended
653− // `(a = 1 OR b = 2) AND c = 3`.
661+ // `(a = 1 OR b = 2) AND c = 3`. A body without a top-level OR is
662+ // left un-parenthesized so simple filters stay readable.
654 let bodyEnd = tailStart ?? sql.endIndex
663 let bodyEnd = tailStart ?? sql.endIndex
655 let head = sql[..<whereEnd]
664 let head = sql[..<whereEnd]
656 let body = sql[whereEnd..<bodyEnd].trimmingCharacters(in: .whitespacesAndNewlines)
665 let body = sql[whereEnd..<bodyEnd].trimmingCharacters(in: .whitespacesAndNewlines)
657 let tail = sql[bodyEnd...].trimmingCharacters(in: .whitespacesAndNewlines)
666 let tail = sql[bodyEnd...].trimmingCharacters(in: .whitespacesAndNewlines)
667+ let wrappedBody = bodyHasTopLevelOr ? "(\(body))" : body
658− sql = "\(head) (\(body)) AND \(condition)"
668+ sql = "\(head) \(wrappedBody) AND \(condition)"
659 if !tail.isEmpty {
669 if !tail.isEmpty {
660 sql += " \(tail)"
670 sql += " \(tail)"