11import ssl
2+ from termios import CSTART
23from typing import Optional
4+ import lz4 .block
35from sqlitecloud .types import (
46 SQCLOUD_CMD ,
57 SQCLOUD_INTERNAL_ERRCODE ,
68 SQCloudConfig ,
79 SQCloudConnect ,
810 SQCloudException ,
911 SQCloudNumber ,
12+ SQCloudValue ,
1013)
1114import socket
1215
1316
1417class Driver :
15- def connect (self , hostname : str , port : int , config : SQCloudConfig ) -> SQCloudConnect :
18+ def connect (
19+ self , hostname : str , port : int , config : SQCloudConfig
20+ ) -> SQCloudConnect :
1621 """
1722 Connects to the SQLite Cloud server.
1823
@@ -150,24 +155,28 @@ def _internal_socket_read(self, connection: SQCloudConnect) -> any:
150155 data = connection .socket .recv (buffer_size )
151156 if not data :
152157 break
153-
158+
154159 # update buffers
155160 data = data .decode ()
156161 buffer += data
157162 nread += len (data )
158163
159164 c = buffer [0 ]
160165
161- if c == SQCLOUD_CMD .INT or c == SQCLOUD_CMD .FLOAT or c == SQCLOUD_CMD .NULL :
162- if buffer [nread - 1 ] != ' ' :
166+ if (
167+ c == SQCLOUD_CMD .INT .value
168+ or c == SQCLOUD_CMD .FLOAT .value
169+ or c == SQCLOUD_CMD .NULL .value
170+ ):
171+ if buffer [nread - 1 ] != " " :
163172 continue
164- elif c == SQCLOUD_CMD .ROWSET_CHUNK :
165- isEndOfChunk = buffer .endswith (SQCLOUD_CMD .CHUNKS_END )
173+ elif c == SQCLOUD_CMD .ROWSET_CHUNK . value :
174+ isEndOfChunk = buffer .endswith (SQCLOUD_CMD .CHUNKS_END . value )
166175 if not isEndOfChunk :
167176 continue
168177 else :
169178 n : SQCloudNumber = self ._internal_parse_number (buffer )
170- can_be_zerolength = c == SQCLOUD_CMD .BLOB or c == SQCLOUD_CMD .STRING
179+ can_be_zerolength = c == SQCLOUD_CMD .BLOB . value or c == SQCLOUD_CMD .STRING . value
171180 if n .value == 0 and not can_be_zerolength :
172181 continue
173182 if n .value + n .cstart != nread :
@@ -183,7 +192,7 @@ def _internal_socket_read(self, connection: SQCloudConnect) -> any:
183192 )
184193
185194 def _internal_parse_number (self , buffer : str , index : int = 1 ) -> SQCloudNumber :
186- sqlite_number = SQCloudNumber ()
195+ sqcloud_number = SQCloudNumber ()
187196 extvalue = 0
188197 isext = False
189198 blen = len (buffer )
@@ -193,24 +202,261 @@ def _internal_parse_number(self, buffer: str, index: int = 1) -> SQCloudNumber:
193202 c = buffer [i ]
194203
195204 # check for optional extended error code (ERRCODE:EXTERRCODE)
196- if c == ':' :
205+ if c == ":" :
197206 isext = True
198207 continue
199208
200209 # check for end of value
201- if c == ' ' :
202- sqlite_number .cstart = i + 1
203- sqlite_number .extcode = extvalue
204- return sqlite_number
210+ if c == " " :
211+ sqcloud_number .cstart = i + 1
212+ sqcloud_number .extcode = extvalue
213+ return sqcloud_number
214+
215+ v = int (c ) if c .isdigit () else 0
205216
206217 # compute numeric value
207218 if isext :
208- extvalue = (extvalue * 10 ) + int ( buffer [ i ])
219+ extvalue = (extvalue * 10 ) + v
209220 else :
210- sqlite_number .value = (sqlite_number .value * 10 ) + int (buffer [i ])
221+ sqcloud_number .value = (sqcloud_number .value * 10 ) + v
222+
223+ return sqcloud_number
211224
212- return 0
213-
214225 def _internal_parse_buffer (self , buffer : str , blen : int ) -> any :
215- # TODO
216- return
226+ # possible return values:
227+ # True => OK
228+ # False => error
229+ # integer
230+ # double
231+ # string
232+ # list
233+ # object
234+ # None
235+
236+ # check OK value
237+ if buffer == "+2 OK" :
238+ return True
239+
240+ cmd = buffer [0 ]
241+
242+ # check for compressed result
243+ if cmd == SQCLOUD_CMD .COMPRESSED .value :
244+ # TODO: use exception
245+ buffer = self ._internal_uncompress_data (buffer , blen )
246+ if buffer is None :
247+ raise SQCloudException (
248+ f"An error occurred while decompressing the input buffer of len { len } ." ,
249+ - 1 ,
250+ )
251+
252+ # first character contains command type
253+ if cmd in [
254+ SQCLOUD_CMD .ZEROSTRING .value ,
255+ SQCLOUD_CMD .RECONNECT .value ,
256+ SQCLOUD_CMD .PUBSUB .value ,
257+ SQCLOUD_CMD .COMMAND .value ,
258+ SQCLOUD_CMD .STRING .value ,
259+ SQCLOUD_CMD .ARRAY .value ,
260+ SQCLOUD_CMD .BLOB .value ,
261+ SQCLOUD_CMD .JSON .value ,
262+ ]:
263+ cstart = 0
264+ sqlite_number = self ._internal_parse_number (buffer , cstart )
265+ len = sqlite_number .value
266+ if len == 0 :
267+ return ""
268+
269+ if cmd == SQCLOUD_CMD .ZEROSTRING .value :
270+ len -= 1
271+ clone = buffer [cstart : cstart + len ]
272+
273+ if cmd == SQCLOUD_CMD .COMMAND .value :
274+ return self ._internal_run_command (clone )
275+ elif cmd == SQCLOUD_CMD .PUBSUB .value :
276+ # TODO
277+ return self ._internal_setup_pubsub (clone )
278+ elif cmd == SQCLOUD_CMD .RECONNECT .value :
279+ return self ._internal_reconnect (clone )
280+ elif cmd == SQCLOUD_CMD .ARRAY .value :
281+ return self ._internal_parse_array (clone )
282+
283+ return clone
284+
285+ elif cmd == SQCLOUD_CMD .ERROR .value :
286+ # -LEN ERRCODE:EXTCODE ERRMSG
287+ sqlite_number = self ._internal_parse_number (buffer )
288+ len = sqlite_number .value
289+ cstart = sqlite_number .cstart
290+ clone = buffer [cstart :]
291+
292+ sqlite_number = self ._internal_parse_number (clone , 0 )
293+ cstart2 = sqlite_number .cstart
294+
295+ errcode = sqlite_number .value
296+ xerrcode = sqlite_number .extcode
297+
298+ len -= cstart2
299+ errmsg = clone [cstart2 :]
300+
301+ raise SQCloudException (errmsg , errcode , xerrcode )
302+
303+ elif cmd in [SQCLOUD_CMD .ROWSET .value , SQCLOUD_CMD .ROWSET_CHUNK .value ]:
304+ # CMD_ROWSET: *LEN 0:VERSION ROWS COLS DATA
305+ # CMD_ROWSET_CHUNK: /LEN IDX:VERSION ROWS COLS DATA
306+ # TODO: return a custom object
307+ start = self ._internal_parse_rowset_signature (
308+ buffer , len , idx , version , nrows , ncols
309+ )
310+ if start < 0 :
311+ return False
312+
313+ # check for end-of-chunk condition
314+ if start == 0 and version == 0 :
315+ rowset = self .rowset
316+ self .rowset = None
317+ return rowset
318+
319+ rowset = self ._internal_parse_rowset (
320+ buffer , start , idx , version , nrows , ncols
321+ )
322+
323+ # continue parsing next chunk in the buffer
324+ buffer = buffer [len + len ("/{} " .format (len )) :]
325+ if buffer :
326+ return self .internal_parse_buffer (buffer , len (buffer ))
327+
328+ return rowset
329+
330+ elif cmd == SQCLOUD_CMD .NULL .value :
331+ return None
332+
333+ elif cmd in [SQCLOUD_CMD .INT .value , SQCLOUD_CMD .FLOAT .value ]:
334+ # TODO
335+ clone = self ._internal_parse_value (buffer , blen )
336+ if clone is None :
337+ return 0
338+ if cmd == SQCLOUD_CMD .INT .value :
339+ return int (clone )
340+ return float (clone )
341+
342+ elif cmd == SQCLOUD_CMD .RAWJSON .value :
343+ return None
344+
345+ return None
346+
347+ def _internal_uncompress_data (self , buffer : str , blen : int ) -> Optional [str ]:
348+ """
349+ %LEN COMPRESSED UNCOMPRESSED BUFFER
350+
351+ Args:
352+ buffer (str): The compressed data buffer.
353+ blen (int): The length of the buffer.
354+
355+ Returns:
356+ str: The uncompressed data.
357+
358+ Raises:
359+ None
360+ """
361+ tlen = 0 # total length
362+ clen = 0 # compressed length
363+ ulen = 0 # uncompressed length
364+ hlen = 0 # raw header length
365+ seek1 = 0
366+
367+ start = 1
368+ counter = 0
369+ for i in range (blen ):
370+ if buffer [i ] != " " :
371+ continue
372+ counter += 1
373+
374+ data = buffer [start :i ]
375+ start = i + 1
376+
377+ if counter == 1 :
378+ tlen = int (data )
379+ seek1 = start
380+ elif counter == 2 :
381+ clen = int (data )
382+ elif counter == 3 :
383+ ulen = int (data )
384+ break
385+
386+ # sanity check header values
387+ if tlen == 0 or clen == 0 or ulen == 0 or start == 1 or seek1 == 0 :
388+ return None
389+
390+ # copy raw header
391+ hlen = start - seek1
392+ header = buffer [start : start + hlen ]
393+
394+ # compute index of the first compressed byte
395+ start += hlen
396+
397+ # perform real decompression
398+ clone = header + str (lz4 .block .decompress (buffer [start :]))
399+
400+ # sanity check result
401+ if len (clone ) != ulen + hlen :
402+ return None
403+
404+ return clone
405+
406+ def _internal_reconnect (self , buffer : str ) -> bool :
407+ return True
408+
409+ def _internal_parse_array (self , buffer : str ) -> list :
410+ start = 0
411+ sqlite_number = self ._internal_parse_number (buffer , start )
412+ n = sqlite_number .value
413+ start = sqlite_number .cstart
414+
415+ r = []
416+ for i in range (n ):
417+ sqcloud_value = self ._internal_parse_value (buffer , start )
418+ start += sqcloud_value .cellsize
419+ r .append (sqcloud_value .value )
420+
421+ return r
422+
423+ def _internal_parse_value (self , buffer : str , index : int = 0 ) -> SQCloudValue :
424+ sqcloud_value = SQCloudValue ()
425+ len = 0
426+ cellsize = 0
427+
428+ # handle special NULL value case
429+ if buffer is None or buffer [index ] == SQCLOUD_CMD .NULL .value :
430+ len = 0
431+ if cellsize is not None :
432+ cellsize = 2
433+
434+ sqcloud_value .len = len
435+ sqcloud_value .cellsize = cellsize
436+
437+ return sqcloud_value
438+
439+ sqcloud_number = self ._internal_parse_number (buffer , index + 1 )
440+ blen = sqcloud_number .value
441+ cstart = sqcloud_number .cstart
442+
443+ # handle decimal/float cases
444+ if buffer [index ] == SQCLOUD_CMD .INT .value or buffer [index ] == SQCLOUD_CMD .FLOAT .value :
445+ nlen = cstart - index
446+ len = nlen - 2
447+ cellsize = nlen
448+
449+ sqcloud_value .value = buffer [index + 1 : index + 1 + len ]
450+ sqcloud_value .len
451+ sqcloud_value .cellsize = cellsize
452+
453+ return sqcloud_value
454+
455+ len = blen - 1 if buffer [index ] == SQCLOUD_CMD .ZEROSTRING .value else blen
456+ cellsize = blen + cstart - index
457+
458+ sqcloud_value .value = buffer [cstart : cstart + len ]
459+ sqcloud_value .len = len
460+ sqcloud_value .cellsize = cellsize
461+
462+ return sqcloud_value
0 commit comments