Skip to content

Commit a642ac5

Browse files
committed
AllColumnsExcluding
1 parent d03208b commit a642ac5

File tree

9 files changed

+301
-29
lines changed

9 files changed

+301
-29
lines changed

GRDB/QueryInterface/SQL/Column.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,13 @@ extension ColumnExpression {
6161

6262
extension ColumnExpression where Self == Column {
6363
/// The hidden rowID column.
64+
///
65+
/// For example:
66+
///
67+
/// ```swift
68+
/// // SELECT rowid FROM player
69+
/// let rowids = try Player.select(.rowID).fetchSet(db)
70+
/// ```
6471
public static var rowID: Self { Column.rowID }
6572
}
6673

GRDB/QueryInterface/SQL/SQLExpression.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2136,7 +2136,14 @@ public protocol SQLExpressible {
21362136
}
21372137

21382138
extension SQLExpressible where Self == Column {
2139-
/// The hidden rowID column
2139+
/// The hidden rowID column.
2140+
///
2141+
/// For example:
2142+
///
2143+
/// ```swift
2144+
/// // SELECT rowid FROM player
2145+
/// let rowids = try Player.select(.rowID).fetchSet(db)
2146+
/// ```
21402147
public static var rowID: Self { Column.rowID }
21412148
}
21422149

GRDB/QueryInterface/SQL/SQLSelection.swift

Lines changed: 198 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ public struct SQLSelection: Sendable {
2626
/// All columns, qualified: `player.*`
2727
case qualifiedAllColumns(TableAlias)
2828

29+
/// All columns but the specified ones
30+
case allColumnsExcluding(Set<CaseInsensitiveIdentifier>)
31+
32+
/// All columns but the specified ones, qualified.
33+
case qualifiedAllColumnsExcluding(TableAlias, Set<CaseInsensitiveIdentifier>)
34+
2935
/// An expression
3036
case expression(SQLExpression)
3137

@@ -41,11 +47,21 @@ public struct SQLSelection: Sendable {
4147
/// All columns: `*`
4248
static let allColumns = SQLSelection(impl: .allColumns)
4349

50+
/// All columns but the specified ones.
51+
static func allColumnsExcluding(_ excludedColumns: Set<CaseInsensitiveIdentifier>) -> Self {
52+
SQLSelection(impl: .allColumnsExcluding(excludedColumns))
53+
}
54+
4455
/// All columns, qualified: `player.*`
4556
static func qualifiedAllColumns(_ alias: TableAlias) -> Self {
4657
self.init(impl: .qualifiedAllColumns(alias))
4758
}
4859

60+
/// All columns but the specified ones, qualified.
61+
static func qualifiedAllColumnsExcluding(_ alias: TableAlias, _ excludedColumns: Set<CaseInsensitiveIdentifier>) -> Self {
62+
self.init(impl: .qualifiedAllColumnsExcluding(alias, excludedColumns))
63+
}
64+
4965
/// An expression
5066
static func expression(_ expression: SQLExpression) -> Self {
5167
self.init(impl: .expression(expression))
@@ -70,13 +86,16 @@ extension SQLSelection {
7086
/// Returns nil when the number of columns is unknown.
7187
func columnCount(_ context: SQLGenerationContext) throws -> Int? {
7288
switch impl {
73-
case .allColumns:
89+
case .allColumns, .allColumnsExcluding:
7490
// Likely a GRDB bug: we can't count the number of columns in an
7591
// unqualified table.
7692
return nil
7793

7894
case let .qualifiedAllColumns(alias):
79-
return try context.columnCount(in: alias.tableName)
95+
return try context.columnCount(in: alias.tableName, excluding: [])
96+
97+
case let .qualifiedAllColumnsExcluding(alias, excludedColumns):
98+
return try context.columnCount(in: alias.tableName, excluding: excludedColumns)
8099

81100
case .expression,
82101
.aliasedExpression:
@@ -104,7 +123,28 @@ extension SQLSelection {
104123
// SELECT COUNT(*) FROM tableName ...
105124
return .all
106125

107-
case .qualifiedAllColumns:
126+
case .allColumnsExcluding:
127+
// SELECT DISTINCT a, b, c FROM tableName ...
128+
if distinct {
129+
// TODO: if the selection were qualified, and if we had a
130+
// database connection, we could detect the case where there
131+
// remains only one column, and we could perform a
132+
// SELECT COUNT(DISTINCT remainingColumn) FROM tableName
133+
//
134+
// Since most people will not use `.allColumns(excluding:)`
135+
// when they want to select only one column, I guess that
136+
// this optimization has little chance to be needed.
137+
//
138+
// Can't count
139+
return nil
140+
}
141+
142+
// SELECT a, b, c FROM tableName ...
143+
// ->
144+
// SELECT COUNT(*) FROM tableName ...
145+
return .all
146+
147+
case .qualifiedAllColumns, .qualifiedAllColumnsExcluding:
108148
return nil
109149

110150
case let .expression(expression),
@@ -144,11 +184,38 @@ extension SQLSelection {
144184
case .allColumns:
145185
return "*"
146186

187+
case .allColumnsExcluding:
188+
// Likely a GRDB bug: we don't know the table name so we can't
189+
// load remaining columns. This selection should have been
190+
// turned into a `.qualifiedAllColumnsExcluding`.
191+
fatalError("Not implemented, or invalid query")
192+
147193
case let .qualifiedAllColumns(alias):
148194
if let qualifier = context.qualifier(for: alias) {
149195
return qualifier.quotedDatabaseIdentifier + ".*"
150196
}
151197
return "*"
198+
199+
case let .qualifiedAllColumnsExcluding(alias, excludedColumns):
200+
let columnsNames = try context.columnNames(in: alias.tableName)
201+
let remainingColumnsNames = if excludedColumns.isEmpty {
202+
columnsNames
203+
} else {
204+
columnsNames.filter {
205+
!excludedColumns.contains(CaseInsensitiveIdentifier(rawValue: $0))
206+
}
207+
}
208+
if columnsNames.count == remainingColumnsNames.count {
209+
// We're not excluding anything
210+
if let qualifier = context.qualifier(for: alias) {
211+
return qualifier.quotedDatabaseIdentifier + ".*"
212+
}
213+
return "*"
214+
} else {
215+
return try remainingColumnsNames
216+
.map { try SQLExpression.column($0).qualified(with: alias).sql(context) }
217+
.joined(separator: ", ")
218+
}
152219

153220
case let .expression(expression):
154221
return try expression.sql(context)
@@ -193,12 +260,15 @@ extension SQLSelection {
193260
/// Returns a qualified selection
194261
func qualified(with alias: TableAlias) -> SQLSelection {
195262
switch impl {
196-
case .qualifiedAllColumns:
263+
case .qualifiedAllColumns, .qualifiedAllColumnsExcluding:
197264
return self
198265

199266
case .allColumns:
200267
return .qualifiedAllColumns(alias)
201268

269+
case let .allColumnsExcluding(excludedColumns):
270+
return .qualifiedAllColumnsExcluding(alias, excludedColumns)
271+
202272
case let .expression(expression):
203273
return .expression(expression.qualified(with: alias))
204274

@@ -227,6 +297,8 @@ extension SQLSelection {
227297
return true
228298
case .allColumns, .qualifiedAllColumns:
229299
return false
300+
case .allColumnsExcluding, .qualifiedAllColumnsExcluding:
301+
return false
230302
case .expression:
231303
return false
232304
}
@@ -272,15 +344,41 @@ enum SQLCount {
272344
///
273345
/// ## Topics
274346
///
347+
/// ### Standard Selections
348+
///
349+
/// - ``rowID``
350+
/// - ``allColumns``
351+
/// - ``allColumns(excluding:)-3sg4w``
352+
/// - ``allColumns(excluding:)-3blq4``
353+
///
275354
/// ### Supporting Types
276355
///
277356
/// - ``AllColumns``
357+
/// - ``AllColumnsExcluding``
278358
/// - ``SQLSelection``
279359
public protocol SQLSelectable {
280360
/// Returns an SQL selection.
281361
var sqlSelection: SQLSelection { get }
282362
}
283363

364+
extension SQLSelectable where Self == Column {
365+
/// The hidden rowID column.
366+
///
367+
/// For example:
368+
///
369+
/// ```swift
370+
/// struct Player: FetchableRecord, TableRecord {
371+
/// static var databaseSelection: [any SQLSelectable] {
372+
/// [.allColumns, .rowID]
373+
/// }
374+
/// }
375+
///
376+
/// // SELECT *, rowid FROM player
377+
/// Player.fetchAll(db)
378+
/// ```
379+
public static var rowID: Self { Column.rowID }
380+
}
381+
284382
extension SQLSelection: SQLSelectable {
285383
// Not a real deprecation, just a usage warning
286384
@available(*, deprecated, message: "Already SQLSelection")
@@ -294,10 +392,14 @@ extension SQLSelection: SQLSelectable {
294392
/// For example:
295393
///
296394
/// ```swift
297-
/// try dbQueue.read { db in
298-
/// // SELECT * FROM player
299-
/// let players = try Player.select(AllColumns()).fetchAll(db)
395+
/// struct Player: FetchableRecord, TableRecord {
396+
/// static var databaseSelection: [any SQLSelectable] {
397+
/// [.allColumns, .rowID]
398+
/// }
300399
/// }
400+
///
401+
/// // SELECT *, rowid FROM player
402+
/// Player.fetchAll(db)
301403
/// ```
302404
public struct AllColumns: Sendable {
303405
/// The `*` selection.
@@ -309,3 +411,92 @@ extension AllColumns: SQLSelectable {
309411
.allColumns
310412
}
311413
}
414+
415+
extension SQLSelectable where Self == AllColumns {
416+
/// All columns of the requested table.
417+
///
418+
/// For example:
419+
///
420+
/// ```swift
421+
/// struct Player: FetchableRecord, TableRecord {
422+
/// static var databaseSelection: [any SQLSelectable] {
423+
/// [.allColumns, .rowID]
424+
/// }
425+
/// }
426+
///
427+
/// // SELECT *, rowid FROM player
428+
/// Player.fetchAll(db)
429+
/// ```
430+
public static var allColumns: AllColumns { AllColumns() }
431+
}
432+
433+
// MARK: - AllColumnsExcluding
434+
435+
/// `AllColumnsExcluding` selects all columns in a database table, but the
436+
/// ones you specify.
437+
///
438+
/// For example:
439+
///
440+
/// ```swift
441+
/// struct Player: TableRecord {
442+
/// static var databaseSelection: [any SQLSelectable] {
443+
/// [.allColumns(excluding: ["computedColumn"])]
444+
/// }
445+
/// }
446+
///
447+
/// // SELECT id, name, score FROM player
448+
/// Player.fetchAll(db)
449+
/// ```
450+
public struct AllColumnsExcluding: Sendable {
451+
var excludedColumns: Set<CaseInsensitiveIdentifier>
452+
453+
public init(_ excludedColumns: some Collection<String>) {
454+
self.excludedColumns = Set(excludedColumns.lazy.map {
455+
CaseInsensitiveIdentifier(rawValue: $0)
456+
})
457+
}
458+
}
459+
460+
extension AllColumnsExcluding: SQLSelectable {
461+
public var sqlSelection: SQLSelection {
462+
.allColumnsExcluding(excludedColumns)
463+
}
464+
}
465+
466+
extension SQLSelectable where Self == AllColumnsExcluding {
467+
/// All columns of the requested table, excluding the provided columns.
468+
///
469+
/// For example:
470+
///
471+
/// ```swift
472+
/// struct Player: TableRecord {
473+
/// static var databaseSelection: [any SQLSelectable] {
474+
/// [.allColumns(excluding: ["computedColumn"])]
475+
/// }
476+
/// }
477+
///
478+
/// // SELECT id, name, score FROM player
479+
/// Player.fetchAll(db)
480+
/// ```
481+
public static func allColumns(excluding excludedColumns: some Collection<String>) -> Self {
482+
AllColumnsExcluding(excludedColumns)
483+
}
484+
485+
/// All columns of the requested table, excluding the provided columns.
486+
///
487+
/// For example:
488+
///
489+
/// ```swift
490+
/// struct Player: TableRecord {
491+
/// static var databaseSelection: [any SQLSelectable] {
492+
/// [.allColumns(excluding: [Column("computedColumn")])]
493+
/// }
494+
/// }
495+
///
496+
/// // SELECT id, name, score FROM player
497+
/// Player.fetchAll(db)
498+
/// ```
499+
public static func allColumns(excluding excludedColumns: some Collection<any ColumnExpression>) -> Self {
500+
AllColumnsExcluding(excludedColumns.map(\.name))
501+
}
502+
}

GRDB/QueryInterface/SQLGeneration/SQLGenerationContext.swift

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,15 +153,50 @@ final class SQLGenerationContext {
153153
return nil
154154
}
155155

156-
func columnCount(in tableName: String) throws -> Int {
156+
/// Return the number of columns in the table or CTE identified
157+
/// by `tableName`.
158+
///
159+
/// - parameter tableName: The table name.
160+
/// - parameter excludedColumns: The eventual set of excluded columns.
161+
func columnCount(
162+
in tableName: String,
163+
excluding excludedColumns: Set<CaseInsensitiveIdentifier>
164+
) throws -> Int {
157165
if let cte = ownCTEs[tableName.lowercased()] {
158-
return try cte.columnCount(db)
166+
if excludedColumns.isEmpty {
167+
return try cte.columnCount(db)
168+
} else {
169+
fatalError("Not implemented: counting CTE columns with excluded columns")
170+
}
171+
}
172+
switch parent {
173+
case let .context(context):
174+
return try context.columnCount(in: tableName, excluding: excludedColumns)
175+
case let .none(db: db, argumentsSink: _):
176+
if excludedColumns.isEmpty {
177+
return try db.columns(in: tableName).count
178+
} else {
179+
return try db
180+
.columns(in: tableName)
181+
.filter { !excludedColumns.contains(CaseInsensitiveIdentifier(rawValue: $0.name)) }
182+
.count
183+
}
184+
}
185+
}
186+
187+
/// Return the names of the columns in the table or CTE identified
188+
/// by `tableName`.
189+
///
190+
/// - parameter tableName: The table name.
191+
func columnNames(in tableName: String) throws -> [String] {
192+
if ownCTEs[tableName.lowercased()] != nil {
193+
fatalError("Not implemented: extracing CTE column names")
159194
}
160195
switch parent {
161196
case let .context(context):
162-
return try context.columnCount(in: tableName)
197+
return try context.columnNames(in: tableName)
163198
case let .none(db: db, argumentsSink: _):
164-
return try db.columns(in: tableName).count
199+
return try db.columns(in: tableName).map(\.name)
165200
}
166201
}
167202
}

0 commit comments

Comments
 (0)