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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Icon-only buttons announced as nothing by VoiceOver across the data grid, row inspector, editor find bar, filter bar, structure, dashboard and settings.
- Status icons that carried a result only as a symbol and a colour, silent to VoiceOver, in the AWS and app import steps and the plugin lists.
- Foreign key picker rows that could only be chosen with a mouse.
- No label and no working search in the foreign key picker on a SQLite table whose columns are declared without a type.
- SQLite foreign keys written `REFERENCES parent` with no column list pointing at a column the parent does not have, in the object browser, the ER diagram and the JSON inspector.
- Preview Referenced Row on iPhone and iPad building its filter by hand, so a value holding a backslash could reach the server as SQL. (#2996)
- The referenced key column offered as a label in the foreign key picker, where choosing it showed no label at all.
Expand Down
22 changes: 21 additions & 1 deletion TablePro/Models/Schema/ForeignKeyLookupColumn.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,31 @@ struct ForeignKeyLookupColumn: Equatable, Sendable, Identifiable {
/// question is asked of the raw type name and answered closed: a name that is not a known
/// character type carries no pattern predicate, which costs a search rather than an error on
/// every search.
///
/// A column that declares no type at all is the one open answer, and it is not a gap in the
/// list. Only a dynamically typed engine reports one: `create table t(a, b)` is legal SQLite
/// and `PRAGMA table_xinfo` gives back a zero-length type for it, measured on 3.54.0, while
/// every strict engine always names a type. `LIKE` is defined on every column there, measured
/// on the same build, so an undeclared column takes a predicate and can be a label. Reading it
/// as unknown instead left a hand-written SQLite database with no label anywhere and no way to
/// search one.
var supportsPatternMatch: Bool {
guard case .text = type, let base = Self.baseTypeName(of: type.rawType) else { return false }
guard case .text = type else { return false }
guard let base = Self.baseTypeName(of: type.rawType) else { return declaresNoType }
return Self.characterTypeNames.contains(base)
}

/// True when the engine answered with a type and that type was empty, which is how SQLite
/// reports a column declared without one.
///
/// A missing `rawType` is deliberately not this. That is the app having no type information at
/// all, which several of its own conversions produce, and reading it as "the engine declared
/// nothing" would hand a pattern predicate to a column nobody has typed.
var declaresNoType: Bool {
guard let rawType = type.rawType else { return false }
return rawType.trimmingCharacters(in: .whitespaces).isEmpty
}

/// The declared type as the reader sees it beside the column's name, or nothing when the
/// engine declares none: SQLite accepts `create table t(a, b)`, and an empty string reads as a
/// missing word rather than as a column with no type.
Expand Down
82 changes: 82 additions & 0 deletions TableProTests/Models/Schema/ForeignKeyLookupColumnTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
//
// ForeignKeyLookupColumnTests.swift
// TablePro
//

import Foundation
import Testing

@testable import TablePro

/// `supportsPatternMatch` decides both which columns the foreign key picker offers as a label on
/// its own and which ones carry the search. It answers a closed list of character type names,
/// because `ColumnTypeClassifier` files everything it does not recognise under `.text` and a `LIKE`
/// against a `uuid`, an enum or an array is an error on PostgreSQL rather than an empty result.
@Suite("ForeignKeyLookupColumn")
struct ForeignKeyLookupColumnTests {
private func column(_ rawType: String?) -> ForeignKeyLookupColumn {
ForeignKeyLookupColumn(name: "c", type: .text(rawType: rawType))
}

@Test("A character type carries a pattern predicate")
func characterTypesMatch() {
for rawType in ["TEXT", "VARCHAR(64)", "nvarchar(10)", "CITEXT", "LONGTEXT"] {
#expect(column(rawType).supportsPatternMatch, "\(rawType) should pattern match")
}
}

@Test("A type the classifier only guessed at carries none")
func guessedTypesDoNotMatch() {
for rawType in ["uuid", "inet", "money", "tsvector"] {
#expect(!column(rawType).supportsPatternMatch, "\(rawType) should not pattern match")
}
}

@Test("A type that is not text carries none")
func nonTextTypesDoNotMatch() {
#expect(!ForeignKeyLookupColumn(name: "c", type: .integer(rawType: "INTEGER")).supportsPatternMatch)
#expect(!ForeignKeyLookupColumn(name: "c", type: .date(rawType: "DATE")).supportsPatternMatch)
#expect(!ForeignKeyLookupColumn(name: "c", type: .decimal(rawType: "NUMERIC")).supportsPatternMatch)
}

/// `create table t(a, b)` is legal SQLite and common in hand-written databases. Measured on
/// 3.54.0: `PRAGMA table_xinfo` answers a zero-length type for such a column, and `LIKE`
/// against it works. Reading the empty type as unknown left every column of that table
/// unlabelled and unsearchable, with the picker listing bare keys and no way to say why.
@Test("A column the engine declared with no type carries a predicate")
func undeclaredTypeMatches() {
#expect(column("").supportsPatternMatch)
#expect(column(" ").supportsPatternMatch)
#expect(column("").declaresNoType)
#expect(column(" ").declaresNoType)
}

/// Several of the app's own conversions build a column with no type information at all. That
/// is not an engine saying "this column has no declared type", and it must not become a
/// predicate on a strict engine.
@Test("A column with no type information at all is left alone")
func missingTypeIsNotAnUndeclaredType() {
#expect(!column(nil).supportsPatternMatch)
#expect(!column(nil).declaresNoType)
}

@Test("A declared type is still reported for display, and an empty one is not")
func displayTypeNameFollowsTheDeclaration() {
#expect(column("TEXT").displayTypeName == "TEXT")
#expect(column("").displayTypeName == nil)
#expect(column(nil).displayTypeName == nil)
}

/// The reporter's shape: a parent whose every column is untyped had no label to offer.
@Test("An untyped table offers its first non-key column as a label")
func untypedTableStillGetsALabel() {
let columns = [
ForeignKeyLookupColumn(name: "marchio", type: .text(rawType: "")),
ForeignKeyLookupColumn(name: "nome", type: .text(rawType: "")),
]
let resolved = ForeignKeyLabelColumn.resolve(
columns: columns, keyColumn: "marchio", choice: .unset
)
#expect(resolved.map(\.name) == ["nome"])
}
}
Loading