@@ -24,50 +24,7 @@ extension ClickHousePluginDriver {
2424
2525 var request = try buildRequest ( query: query, database: database, queryId: queryId)
2626 request. timeoutInterval = _queryTimeout. requestTimeoutInterval
27- let isSelect = Self . isSelectLikeQuery ( query)
28-
29- let ( data, response) = try await withTaskCancellationHandler {
30- try await withCheckedThrowingContinuation { ( continuation: CheckedContinuation < ( Data , URLResponse ) , Error > ) in
31- let task = session. dataTask ( with: request) { data, response, error in
32- if let error {
33- continuation. resume ( throwing: error)
34- return
35- }
36- guard let data, let response else {
37- continuation. resume ( throwing: ClickHouseError ( message: " Empty response from server " ) )
38- return
39- }
40- continuation. resume ( returning: ( data, response) )
41- }
42-
43- self . lock. lock ( )
44- self . currentTask = task
45- self . lock. unlock ( )
46-
47- task. resume ( )
48- }
49- } onCancel: {
50- self . lock. lock ( )
51- self . currentTask? . cancel ( )
52- self . currentTask = nil
53- self . lock. unlock ( )
54- }
55-
56- lock. lock ( )
57- currentTask = nil
58- lock. unlock ( )
59-
60- if let httpResponse = response as? HTTPURLResponse , httpResponse. statusCode >= 400 {
61- let body = String ( data: data, encoding: . utf8) ?? " Unknown error "
62- Self . logger. error ( " ClickHouse HTTP \( httpResponse. statusCode) : \( body) " )
63- throw ClickHouseError ( message: body. trimmingCharacters ( in: . whitespacesAndNewlines) )
64- }
65-
66- if isSelect {
67- return parseTabSeparatedResponse ( data)
68- }
69-
70- return CHQueryResult ( columns: [ ] , columnTypeNames: [ ] , rows: [ ] , affectedRows: 0 , isTruncated: false )
27+ return try await perform ( request: request, session: session)
7128 }
7229
7330 func executeRawWithParams( _ query: String , params: [ String : String ? ] , queryId: String ? = nil ) async throws -> CHQueryResult {
@@ -84,9 +41,39 @@ extension ClickHousePluginDriver {
8441
8542 var request = try buildRequest ( query: query, database: database, queryId: queryId, params: params)
8643 request. timeoutInterval = _queryTimeout. requestTimeoutInterval
87- let isSelect = Self . isSelectLikeQuery ( query)
44+ return try await perform ( request: request, session: session)
45+ }
46+
47+ private func perform( request: URLRequest , session: URLSession ) async throws -> CHQueryResult {
48+ let ( data, response) = try await send ( request: request, session: session)
49+
50+ lock. lock ( )
51+ currentTask = nil
52+ lock. unlock ( )
53+
54+ let httpResponse = response as? HTTPURLResponse
55+ if let httpResponse, httpResponse. statusCode >= 400 {
56+ let body = String ( data: data, encoding: . utf8) ?? " Unknown error "
57+ let exceptionCode = httpResponse. value ( forHTTPHeaderField: " X-ClickHouse-Exception-Code " ) ?? " none "
58+ Self . logger. error ( " ClickHouse HTTP \( httpResponse. statusCode) exception \( exceptionCode) : \( body) " )
59+ throw ClickHouseError ( message: body. trimmingCharacters ( in: . whitespacesAndNewlines) )
60+ }
61+
62+ let outcome = ClickHouseResponseClassifier . classify (
63+ headers: Self . headerFields ( httpResponse) ,
64+ body: data
65+ )
66+ return CHQueryResult (
67+ columns: outcome. columns,
68+ columnTypeNames: outcome. columnTypeNames,
69+ rows: outcome. rows,
70+ affectedRows: outcome. affectedRows,
71+ isTruncated: outcome. isTruncated
72+ )
73+ }
8874
89- let ( data, response) = try await withTaskCancellationHandler {
75+ private func send( request: URLRequest , session: URLSession ) async throws -> ( Data , URLResponse ) {
76+ try await withTaskCancellationHandler {
9077 try await withCheckedThrowingContinuation { ( continuation: CheckedContinuation < ( Data , URLResponse ) , Error > ) in
9178 let task = session. dataTask ( with: request) { data, response, error in
9279 if let error {
@@ -99,9 +86,11 @@ extension ClickHousePluginDriver {
9986 }
10087 continuation. resume ( returning: ( data, response) )
10188 }
89+
10290 self . lock. lock ( )
10391 self . currentTask = task
10492 self . lock. unlock ( )
93+
10594 task. resume ( )
10695 }
10796 } onCancel: {
@@ -110,22 +99,16 @@ extension ClickHousePluginDriver {
11099 self . currentTask = nil
111100 self . lock. unlock ( )
112101 }
102+ }
113103
114- lock. lock ( )
115- currentTask = nil
116- lock. unlock ( )
117-
118- if let httpResponse = response as? HTTPURLResponse , httpResponse. statusCode >= 400 {
119- let body = String ( data: data, encoding: . utf8) ?? " Unknown error "
120- Self . logger. error ( " ClickHouse HTTP \( httpResponse. statusCode) : \( body) " )
121- throw ClickHouseError ( message: body. trimmingCharacters ( in: . whitespacesAndNewlines) )
122- }
123-
124- if isSelect {
125- return parseTabSeparatedResponse ( data)
104+ private static func headerFields( _ response: HTTPURLResponse ? ) -> [ String : String ] {
105+ guard let response else { return [ : ] }
106+ var fields : [ String : String ] = [ : ]
107+ for (key, value) in response. allHeaderFields {
108+ guard let name = key as? String , let text = value as? String else { continue }
109+ fields [ name] = text
126110 }
127-
128- return CHQueryResult ( columns: [ ] , columnTypeNames: [ ] , rows: [ ] , affectedRows: 0 , isTruncated: false )
111+ return fields
129112 }
130113
131114 func buildRequest( query: String , database: String , queryId: String ? = nil , params: [ String : String ? ] ? = nil ) throws -> URLRequest {
@@ -145,14 +128,15 @@ extension ClickHousePluginDriver {
145128 queryItems. append ( URLQueryItem ( name: " query_id " , value: queryId) )
146129 }
147130 queryItems. append ( URLQueryItem ( name: " send_progress_in_http_headers " , value: " 1 " ) )
131+ queryItems. append ( contentsOf: ClickHouseResponseClassifier . transportQueryItems (
132+ supportsWriteExceptionSetting: ClickHouseCapabilities . parse ( serverVersion) . hasWriteExceptionInOutputFormatSetting
133+ ) )
148134 if let params {
149135 for (key, value) in params. sorted ( by: { $0. key < $1. key } ) {
150136 queryItems. append ( URLQueryItem ( name: " param_ \( key) " , value: value) )
151137 }
152138 }
153- if !queryItems. isEmpty {
154- components. queryItems = queryItems
155- }
139+ components. queryItems = queryItems
156140
157141 guard let url = components. url else {
158142 throw ClickHouseError ( message: " Failed to construct request URL " )
@@ -170,94 +154,11 @@ extension ClickHousePluginDriver {
170154
171155 let trimmedQuery = query. trimmingCharacters ( in: . whitespacesAndNewlines)
172156 . replacingOccurrences ( of: " ;+$ " , with: " " , options: . regularExpression)
173-
174- if Self . isSelectLikeQuery ( trimmedQuery) {
175- request. httpBody = ( trimmedQuery + " FORMAT TabSeparatedWithNamesAndTypes " ) . data ( using: . utf8)
176- } else {
177- request. httpBody = trimmedQuery. data ( using: . utf8)
178- }
157+ request. httpBody = trimmedQuery. data ( using: . utf8)
179158
180159 return request
181160 }
182161
183- static func isSelectLikeQuery( _ query: String ) -> Bool {
184- let trimmed = query. trimmingCharacters ( in: . whitespacesAndNewlines)
185- guard let firstWord = trimmed. split ( separator: " " , maxSplits: 1 ) . first else {
186- return false
187- }
188- return selectPrefixes. contains ( firstWord. uppercased ( ) )
189- }
190-
191- func parseTabSeparatedResponse( _ data: Data ) -> CHQueryResult {
192- guard let text = String ( data: data, encoding: . utf8) , !text. isEmpty else {
193- return CHQueryResult ( columns: [ ] , columnTypeNames: [ ] , rows: [ ] , affectedRows: 0 , isTruncated: false )
194- }
195-
196- let lines = text. components ( separatedBy: " \n " )
197-
198- guard lines. count >= 2 else {
199- return CHQueryResult ( columns: [ ] , columnTypeNames: [ ] , rows: [ ] , affectedRows: 0 , isTruncated: false )
200- }
201-
202- let columns = lines [ 0 ] . components ( separatedBy: " \t " )
203- let columnTypes = lines [ 1 ] . components ( separatedBy: " \t " )
204-
205- var rows : [ [ PluginCellValue ] ] = [ ]
206- var truncated = false
207- for i in 2 ..< lines. count {
208- let line = lines [ i]
209- if line. isEmpty { continue }
210-
211- let fields = line. components ( separatedBy: " \t " )
212- let row : [ PluginCellValue ] = fields. map { field in
213- if field == " \\ N " {
214- return . null
215- }
216- return . text( Self . unescapeTsvField ( field) )
217- }
218- rows. append ( row)
219- if rows. count >= PluginRowLimits . emergencyMax {
220- truncated = true
221- break
222- }
223- }
224-
225- return CHQueryResult (
226- columns: columns,
227- columnTypeNames: columnTypes,
228- rows: rows,
229- affectedRows: rows. count,
230- isTruncated: truncated
231- )
232- }
233-
234- static func unescapeTsvField( _ field: String ) -> String {
235- var result = " "
236- result. reserveCapacity ( ( field as NSString ) . length)
237- var iterator = field. makeIterator ( )
238-
239- while let char = iterator. next ( ) {
240- if char == " \\ " {
241- if let next = iterator. next ( ) {
242- switch next {
243- case " \\ " : result. append ( " \\ " )
244- case " t " : result. append ( " \t " )
245- case " n " : result. append ( " \n " )
246- default :
247- result. append ( " \\ " )
248- result. append ( next)
249- }
250- } else {
251- result. append ( " \\ " )
252- }
253- } else {
254- result. append ( char)
255- }
256- }
257-
258- return result
259- }
260-
261162 /// Convert `?` placeholders to `{p1:String}` and build parameter map for ClickHouse HTTP params.
262163 static func buildClickHouseParams(
263164 query: String ,
@@ -307,5 +208,4 @@ extension ClickHousePluginDriver {
307208
308209 return ( converted, paramMap)
309210 }
310-
311211}
0 commit comments