Skip to content

Commit 462e3f9

Browse files
committed
parse value, number, array and lz4 compression
1 parent 31342d4 commit 462e3f9

5 files changed

Lines changed: 356 additions & 30 deletions

File tree

requirements-dev.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ pytest==7.0.1
66
pytest-mock==3.6.1
77
black==22.8.0
88
python-dotenv==0.20.0
9+
lz4==3.1.10

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
lz4==3.1.10

src/sqlitecloud/driver.py

Lines changed: 265 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
import ssl
2+
from termios import CSTART
23
from typing import Optional
4+
import lz4.block
35
from sqlitecloud.types import (
46
SQCLOUD_CMD,
57
SQCLOUD_INTERNAL_ERRCODE,
68
SQCloudConfig,
79
SQCloudConnect,
810
SQCloudException,
911
SQCloudNumber,
12+
SQCloudValue,
1013
)
1114
import socket
1215

1316

1417
class 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

src/sqlitecloud/types.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
from typing import List, Optional
33
from enum import Enum
44

5+
from click import Option
6+
57

68
class SQCLOUD_VALUE_TYPE(Enum):
79
VALUE_INTEGER = 1
@@ -174,3 +176,14 @@ def __init__(self) -> None:
174176
self.value: int = 0
175177
self.cstart: int = 0
176178
self.extcode: int = None
179+
180+
181+
class SQCloudValue:
182+
"""
183+
Represents the parse value.
184+
"""
185+
186+
def __init__(self) -> None:
187+
self.value: Optional[str] = None
188+
self.len: int = 0
189+
self.cellsize: int = 0

0 commit comments

Comments
 (0)