@@ -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``
279359public 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+
284382extension 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/// ```
302404public 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+ }
0 commit comments