singlestoredb 1.1.0__py3-none-any.whl → 1.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of singlestoredb might be problematic. Click here for more details.

Files changed (31) hide show
  1. singlestoredb/__init__.py +1 -1
  2. singlestoredb/config.py +6 -0
  3. singlestoredb/connection.py +23 -1
  4. singlestoredb/converters.py +390 -0
  5. singlestoredb/functions/ext/asgi.py +7 -1
  6. singlestoredb/fusion/handler.py +14 -8
  7. singlestoredb/fusion/handlers/stage.py +167 -84
  8. singlestoredb/fusion/handlers/workspace.py +250 -108
  9. singlestoredb/fusion/registry.py +27 -10
  10. singlestoredb/http/connection.py +18 -1
  11. singlestoredb/management/__init__.py +1 -0
  12. singlestoredb/management/organization.py +4 -0
  13. singlestoredb/management/utils.py +2 -2
  14. singlestoredb/management/workspace.py +79 -6
  15. singlestoredb/mysql/connection.py +92 -16
  16. singlestoredb/mysql/constants/EXTENDED_TYPE.py +3 -0
  17. singlestoredb/mysql/constants/FIELD_TYPE.py +16 -0
  18. singlestoredb/mysql/constants/VECTOR_TYPE.py +6 -0
  19. singlestoredb/mysql/cursors.py +13 -10
  20. singlestoredb/mysql/protocol.py +50 -1
  21. singlestoredb/notebook/__init__.py +15 -0
  22. singlestoredb/notebook/_objects.py +212 -0
  23. singlestoredb/tests/test.sql +49 -0
  24. singlestoredb/tests/test_connection.py +174 -0
  25. singlestoredb/utils/results.py +5 -1
  26. {singlestoredb-1.1.0.dist-info → singlestoredb-1.3.0.dist-info}/METADATA +1 -1
  27. {singlestoredb-1.1.0.dist-info → singlestoredb-1.3.0.dist-info}/RECORD +31 -27
  28. {singlestoredb-1.1.0.dist-info → singlestoredb-1.3.0.dist-info}/LICENSE +0 -0
  29. {singlestoredb-1.1.0.dist-info → singlestoredb-1.3.0.dist-info}/WHEEL +0 -0
  30. {singlestoredb-1.1.0.dist-info → singlestoredb-1.3.0.dist-info}/entry_points.txt +0 -0
  31. {singlestoredb-1.1.0.dist-info → singlestoredb-1.3.0.dist-info}/top_level.txt +0 -0
singlestoredb/__init__.py CHANGED
@@ -13,7 +13,7 @@ Examples
13
13
 
14
14
  """
15
15
 
16
- __version__ = '1.1.0'
16
+ __version__ = '1.3.0'
17
17
 
18
18
  from typing import Any
19
19
 
singlestoredb/config.py CHANGED
@@ -215,6 +215,12 @@ register_option(
215
215
  environ='SINGLESTOREDB_TRACK_ENV',
216
216
  )
217
217
 
218
+ register_option(
219
+ 'enable_extended_data_types', 'bool', check_bool, True,
220
+ 'Should extended data types (BSON, vector) be enabled?',
221
+ environ='SINGLESTOREDB_ENABLE_EXTENDED_DATA_TYPES',
222
+ )
223
+
218
224
  register_option(
219
225
  'fusion.enabled', 'bool', check_bool, False,
220
226
  'Should Fusion SQL queries be enabled?',
@@ -2,7 +2,10 @@
2
2
  """SingleStoreDB connections and cursors."""
3
3
  import abc
4
4
  import inspect
5
+ import io
6
+ import queue
5
7
  import re
8
+ import sys
6
9
  import warnings
7
10
  import weakref
8
11
  from collections.abc import Mapping
@@ -33,6 +36,11 @@ from .config import get_option
33
36
  from .utils.results import Description
34
37
  from .utils.results import Result
35
38
 
39
+ if sys.version_info < (3, 10):
40
+ InfileQueue = queue.Queue
41
+ else:
42
+ InfileQueue = queue.Queue[Union[bytes, str]]
43
+
36
44
 
37
45
  # DB-API settings
38
46
  apilevel = '2.0'
@@ -496,6 +504,14 @@ class Cursor(metaclass=abc.ABCMeta):
496
504
  def execute(
497
505
  self, query: str,
498
506
  args: Optional[Union[Sequence[Any], Dict[str, Any], Any]] = None,
507
+ infile_stream: Optional[ # type: ignore
508
+ Union[
509
+ io.RawIOBase,
510
+ io.TextIOBase,
511
+ Iterator[Union[bytes, str]],
512
+ InfileQueue,
513
+ ]
514
+ ] = None,
499
515
  ) -> int:
500
516
  """
501
517
  Execute a SQL statement.
@@ -510,6 +526,8 @@ class Cursor(metaclass=abc.ABCMeta):
510
526
  The SQL statement to execute
511
527
  args : Sequence or dict, optional
512
528
  Parameters to substitute into the SQL code
529
+ infile_stream : io.RawIOBase or io.TextIOBase or Iterator[bytes|str], optional
530
+ Data stream for ``LOCAL INFILE`` statement
513
531
 
514
532
  Examples
515
533
  --------
@@ -1285,6 +1303,7 @@ def connect(
1285
1303
  inf_as_null: Optional[bool] = None,
1286
1304
  encoding_errors: Optional[str] = None,
1287
1305
  track_env: Optional[bool] = None,
1306
+ enable_extended_data_types: Optional[bool] = None,
1288
1307
  ) -> Connection:
1289
1308
  """
1290
1309
  Return a SingleStoreDB connection.
@@ -1338,7 +1357,8 @@ def connect(
1338
1357
  autocommit : bool, optional
1339
1358
  Enable autocommits
1340
1359
  results_type : str, optional
1341
- The form of the query results: tuples, namedtuples, dicts
1360
+ The form of the query results: tuples, namedtuples, dicts,
1361
+ numpy, polars, pandas, arrow
1342
1362
  results_format : str, optional
1343
1363
  Deprecated. This option has been renamed to results_type.
1344
1364
  program_name : str, optional
@@ -1361,6 +1381,8 @@ def connect(
1361
1381
  The error handler name for value decoding errors
1362
1382
  track_env : bool, optional
1363
1383
  Should the connection track the SINGLESTOREDB_URL environment variable?
1384
+ enable_extended_data_types : bool, optional
1385
+ Should extended data types (BSON, vector) be enabled?
1364
1386
 
1365
1387
  Examples
1366
1388
  --------
@@ -2,6 +2,7 @@
2
2
  """Data value conversion utilities."""
3
3
  import datetime
4
4
  import re
5
+ import struct
5
6
  from base64 import b64decode
6
7
  from decimal import Decimal
7
8
  from json import loads as json_loads
@@ -25,7 +26,20 @@ try:
25
26
  except ImportError:
26
27
  has_pygeos = False
27
28
 
29
+ try:
30
+ import numpy
31
+ has_numpy = True
32
+ except ImportError:
33
+ has_numpy = False
34
+
35
+ try:
36
+ import bson
37
+ has_bson = True
38
+ except ImportError:
39
+ has_bson = False
40
+
28
41
 
42
+ # Cache fromisoformat methods if they exist
29
43
  # Cache fromisoformat methods if they exist
30
44
  _dt_datetime_fromisoformat = None
31
45
  if hasattr(datetime.datetime, 'fromisoformat'):
@@ -527,6 +541,369 @@ def geometry_or_none(x: Optional[str]) -> Optional[Any]:
527
541
  return x
528
542
 
529
543
 
544
+ def float32_vector_json_or_none(x: Optional[str]) -> Optional[Any]:
545
+ """
546
+ Covert value to float32 array.
547
+
548
+ Parameters
549
+ ----------
550
+ x : str or None
551
+ JSON array
552
+
553
+ Returns
554
+ -------
555
+ float32 numpy array
556
+ If input value is not None and numpy is installed
557
+ float Python list
558
+ If input value is not None and numpy is not installed
559
+ None
560
+ If input value is None
561
+
562
+ """
563
+ if x is None:
564
+ return None
565
+
566
+ if has_numpy:
567
+ return numpy.array(json_loads(x), dtype=numpy.float32)
568
+
569
+ return map(float, json_loads(x))
570
+
571
+
572
+ def float32_vector_or_none(x: Optional[bytes]) -> Optional[Any]:
573
+ """
574
+ Covert value to float32 array.
575
+
576
+ Parameters
577
+ ----------
578
+ x : bytes or None
579
+ Little-endian block of bytes.
580
+
581
+ Returns
582
+ -------
583
+ float32 numpy array
584
+ If input value is not None and numpy is installed
585
+ float Python list
586
+ If input value is not None and numpy is not installed
587
+ None
588
+ If input value is None
589
+
590
+ """
591
+ if x is None:
592
+ return None
593
+
594
+ if has_numpy:
595
+ return numpy.frombuffer(x, dtype=numpy.float32)
596
+
597
+ return struct.unpack(f'<{len(x)/4}f', x)
598
+
599
+
600
+ def float64_vector_json_or_none(x: Optional[str]) -> Optional[Any]:
601
+ """
602
+ Covert value to float64 array.
603
+
604
+ Parameters
605
+ ----------
606
+ x : str or None
607
+ JSON array
608
+
609
+ Returns
610
+ -------
611
+ float64 numpy array
612
+ If input value is not None and numpy is installed
613
+ float Python list
614
+ If input value is not None and numpy is not installed
615
+ None
616
+ If input value is None
617
+
618
+ """
619
+ if x is None:
620
+ return None
621
+
622
+ if has_numpy:
623
+ return numpy.array(json_loads(x), dtype=numpy.float64)
624
+
625
+ return map(float, json_loads(x))
626
+
627
+
628
+ def float64_vector_or_none(x: Optional[bytes]) -> Optional[Any]:
629
+ """
630
+ Covert value to float64 array.
631
+
632
+ Parameters
633
+ ----------
634
+ x : bytes or None
635
+ JSON array
636
+
637
+ Returns
638
+ -------
639
+ float64 numpy array
640
+ If input value is not None and numpy is installed
641
+ float Python list
642
+ If input value is not None and numpy is not installed
643
+ None
644
+ If input value is None
645
+
646
+ """
647
+ if x is None:
648
+ return None
649
+
650
+ if has_numpy:
651
+ return numpy.frombuffer(x, dtype=numpy.float64)
652
+
653
+ return struct.unpack(f'<{len(x)/8}d', x)
654
+
655
+
656
+ def int8_vector_json_or_none(x: Optional[str]) -> Optional[Any]:
657
+ """
658
+ Covert value to int8 array.
659
+
660
+ Parameters
661
+ ----------
662
+ x : str or None
663
+ JSON array
664
+
665
+ Returns
666
+ -------
667
+ int8 numpy array
668
+ If input value is not None and numpy is installed
669
+ int Python list
670
+ If input value is not None and numpy is not installed
671
+ None
672
+ If input value is None
673
+
674
+ """
675
+ if x is None:
676
+ return None
677
+
678
+ if has_numpy:
679
+ return numpy.array(json_loads(x), dtype=numpy.int8)
680
+
681
+ return map(int, json_loads(x))
682
+
683
+
684
+ def int8_vector_or_none(x: Optional[bytes]) -> Optional[Any]:
685
+ """
686
+ Covert value to int8 array.
687
+
688
+ Parameters
689
+ ----------
690
+ x : bytes or None
691
+ Little-endian block of bytes.
692
+
693
+ Returns
694
+ -------
695
+ int8 numpy array
696
+ If input value is not None and numpy is installed
697
+ int Python list
698
+ If input value is not None and numpy is not installed
699
+ None
700
+ If input value is None
701
+
702
+ """
703
+ if x is None:
704
+ return None
705
+
706
+ if has_numpy:
707
+ return numpy.frombuffer(x, dtype=numpy.int8)
708
+
709
+ return struct.unpack(f'<{len(x)}b', x)
710
+
711
+
712
+ def int16_vector_json_or_none(x: Optional[str]) -> Optional[Any]:
713
+ """
714
+ Covert value to int16 array.
715
+
716
+ Parameters
717
+ ----------
718
+ x : str or None
719
+ JSON array
720
+
721
+ Returns
722
+ -------
723
+ int16 numpy array
724
+ If input value is not None and numpy is installed
725
+ int Python list
726
+ If input value is not None and numpy is not installed
727
+ None
728
+ If input value is None
729
+
730
+ """
731
+ if x is None:
732
+ return None
733
+
734
+ if has_numpy:
735
+ return numpy.array(json_loads(x), dtype=numpy.int16)
736
+
737
+ return map(int, json_loads(x))
738
+
739
+
740
+ def int16_vector_or_none(x: Optional[bytes]) -> Optional[Any]:
741
+ """
742
+ Covert value to int16 array.
743
+
744
+ Parameters
745
+ ----------
746
+ x : bytes or None
747
+ Little-endian block of bytes.
748
+
749
+ Returns
750
+ -------
751
+ int16 numpy array
752
+ If input value is not None and numpy is installed
753
+ int Python list
754
+ If input value is not None and numpy is not installed
755
+ None
756
+ If input value is None
757
+
758
+ """
759
+ if x is None:
760
+ return None
761
+
762
+ if has_numpy:
763
+ return numpy.frombuffer(x, dtype=numpy.int16)
764
+
765
+ return struct.unpack(f'<{len(x)/2}h', x)
766
+
767
+
768
+ def int32_vector_json_or_none(x: Optional[str]) -> Optional[Any]:
769
+ """
770
+ Covert value to int32 array.
771
+
772
+ Parameters
773
+ ----------
774
+ x : str or None
775
+ JSON array
776
+
777
+ Returns
778
+ -------
779
+ int32 numpy array
780
+ If input value is not None and numpy is installed
781
+ int Python list
782
+ If input value is not None and numpy is not installed
783
+ None
784
+ If input value is None
785
+
786
+ """
787
+ if x is None:
788
+ return None
789
+
790
+ if has_numpy:
791
+ return numpy.array(json_loads(x), dtype=numpy.int32)
792
+
793
+ return map(int, json_loads(x))
794
+
795
+
796
+ def int32_vector_or_none(x: Optional[bytes]) -> Optional[Any]:
797
+ """
798
+ Covert value to int32 array.
799
+
800
+ Parameters
801
+ ----------
802
+ x : bytes or None
803
+ Little-endian block of bytes.
804
+
805
+ Returns
806
+ -------
807
+ int32 numpy array
808
+ If input value is not None and numpy is installed
809
+ int Python list
810
+ If input value is not None and numpy is not installed
811
+ None
812
+ If input value is None
813
+
814
+ """
815
+ if x is None:
816
+ return None
817
+
818
+ if has_numpy:
819
+ return numpy.frombuffer(x, dtype=numpy.int32)
820
+
821
+ return struct.unpack(f'<{len(x)/4}l', x)
822
+
823
+
824
+ def int64_vector_json_or_none(x: Optional[str]) -> Optional[Any]:
825
+ """
826
+ Covert value to int64 array.
827
+
828
+ Parameters
829
+ ----------
830
+ x : str or None
831
+ JSON array
832
+
833
+ Returns
834
+ -------
835
+ int64 numpy array
836
+ If input value is not None and numpy is installed
837
+ int Python list
838
+ If input value is not None and numpy is not installed
839
+ None
840
+ If input value is None
841
+
842
+ """
843
+ if x is None:
844
+ return None
845
+
846
+ if has_numpy:
847
+ return numpy.array(json_loads(x), dtype=numpy.int64)
848
+
849
+ return map(int, json_loads(x))
850
+
851
+
852
+ def int64_vector_or_none(x: Optional[bytes]) -> Optional[Any]:
853
+ """
854
+ Covert value to int64 array.
855
+
856
+ Parameters
857
+ ----------
858
+ x : bytes or None
859
+ Little-endian block of bytes.
860
+
861
+ Returns
862
+ -------
863
+ int64 numpy array
864
+ If input value is not None and numpy is installed
865
+ int Python list
866
+ If input value is not None and numpy is not installed
867
+ None
868
+ If input value is None
869
+
870
+ """
871
+ if x is None:
872
+ return None
873
+
874
+ # Bytes
875
+ if has_numpy:
876
+ return numpy.frombuffer(x, dtype=numpy.int64)
877
+
878
+ return struct.unpack(f'<{len(x)/8}l', x)
879
+
880
+
881
+ def bson_or_none(x: Optional[bytes]) -> Optional[Any]:
882
+ """
883
+ Convert a BSON value to a dictionary.
884
+
885
+ Parameters
886
+ ----------
887
+ x : bytes or None
888
+ BSON formatted bytes
889
+
890
+ Returns
891
+ -------
892
+ dict
893
+ If input value is not None and bson package is installed
894
+ bytes
895
+ If input value is not None and bson package is not installed
896
+ None
897
+ If input value is None
898
+
899
+ """
900
+ if x is None:
901
+ return None
902
+ if has_bson:
903
+ return bson.decode(x)
904
+ return x
905
+
906
+
530
907
  # Map of database types and conversion functions
531
908
  converters: Dict[int, Callable[..., Any]] = {
532
909
  0: decimal_or_none,
@@ -557,4 +934,17 @@ converters: Dict[int, Callable[..., Any]] = {
557
934
  # 253: identity,
558
935
  # 254: identity,
559
936
  255: geometry_or_none,
937
+ 1001: bson_or_none,
938
+ 2001: float32_vector_json_or_none,
939
+ 2002: float64_vector_json_or_none,
940
+ 2003: int8_vector_json_or_none,
941
+ 2004: int16_vector_json_or_none,
942
+ 2005: int32_vector_json_or_none,
943
+ 2006: int64_vector_json_or_none,
944
+ 3001: float32_vector_or_none,
945
+ 3002: float64_vector_or_none,
946
+ 3003: int8_vector_or_none,
947
+ 3004: int16_vector_or_none,
948
+ 3005: int32_vector_or_none,
949
+ 3006: int64_vector_or_none,
560
950
  }
@@ -455,10 +455,13 @@ class Application(object):
455
455
  endpoints[name.encode('utf-8')] = func, info
456
456
 
457
457
  # Fully qualified function name
458
- else:
458
+ elif '.' in funcs:
459
459
  pkg_path, func_names = funcs.rsplit('.', 1)
460
460
  pkg = importlib.import_module(pkg_path)
461
461
 
462
+ if pkg is None:
463
+ raise RuntimeError(f'Could not locate module: {pkg}')
464
+
462
465
  # Add endpoint for each exported function
463
466
  for name, alias in get_func_names(func_names):
464
467
  item = getattr(pkg, name)
@@ -466,6 +469,9 @@ class Application(object):
466
469
  func, info = make_func(alias, item)
467
470
  endpoints[alias.encode('utf-8')] = func, info
468
471
 
472
+ else:
473
+ raise RuntimeError(f'Could not locate module: {funcs}')
474
+
469
475
  elif isinstance(funcs, ModuleType):
470
476
  for x in vars(funcs).values():
471
477
  if not hasattr(x, '_singlestoredb_attrs'):
@@ -220,6 +220,7 @@ def _format_arguments(arg: str) -> str:
220
220
 
221
221
  def _to_markdown(txt: str) -> str:
222
222
  """Convert formatting to markdown."""
223
+ txt = re.sub(r'`([^`]+)\s+\<([^\>]+)>`_', r'[\1](\2)', txt)
223
224
  txt = txt.replace('``', '`')
224
225
 
225
226
  # Format code blocks
@@ -228,11 +229,12 @@ def _to_markdown(txt: str) -> str:
228
229
  while lines:
229
230
  line = lines.pop(0)
230
231
  if line.endswith('::'):
231
- out.append(line[:-2])
232
+ out.append(line[:-2] + '.')
232
233
  code = []
233
234
  while lines and (not lines[0].strip() or lines[0].startswith(' ')):
234
235
  code.append(lines.pop(0).rstrip())
235
- out.extend(['```sql', '\n'.join(code).rstrip(), '```\n'])
236
+ code_str = re.sub(r'^\s*\n', r'', '\n'.join(code).rstrip())
237
+ out.extend([f'```sql\n{code_str}\n```\n'])
236
238
  else:
237
239
  out.append(line)
238
240
 
@@ -241,9 +243,7 @@ def _to_markdown(txt: str) -> str:
241
243
 
242
244
  def build_help(syntax: str, grammar: str) -> str:
243
245
  """Build full help text."""
244
- syntax = re.sub(r'\s*\n+\s*', r' ', syntax.strip())
245
-
246
- cmd = re.match(r'([A-Z0-9_ ]+)', syntax)
246
+ cmd = re.match(r'([A-Z0-9_ ]+)', syntax.strip())
247
247
  if not cmd:
248
248
  raise ValueError(f'no command found: {syntax}')
249
249
 
@@ -264,7 +264,7 @@ def build_help(syntax: str, grammar: str) -> str:
264
264
  if 'description' in sections:
265
265
  out.extend([sections['description'], '\n\n'])
266
266
 
267
- out.extend(['## Syntax\n```sql\n', syntax, '\n```\n\n'])
267
+ out.append(f'## Syntax\n\n```sql{syntax}\n```\n\n')
268
268
 
269
269
  if 'arguments' in sections:
270
270
  out.extend([
@@ -272,9 +272,15 @@ def build_help(syntax: str, grammar: str) -> str:
272
272
  _format_arguments(sections['arguments']),
273
273
  '\n\n',
274
274
  ])
275
+ if 'argument' in sections:
276
+ out.extend([
277
+ '## Argument\n\n',
278
+ _format_arguments(sections['argument']),
279
+ '\n\n',
280
+ ])
275
281
 
276
282
  if 'remarks' in sections:
277
- out.extend(['## Remarks\n', sections['remarks'], '\n\n'])
283
+ out.extend(['## Remarks\n\n', sections['remarks'], '\n\n'])
278
284
 
279
285
  if 'examples' in sections:
280
286
  out.extend(['## Examples\n\n', _format_examples(sections['examples']), '\n\n'])
@@ -282,7 +288,7 @@ def build_help(syntax: str, grammar: str) -> str:
282
288
  out.extend(['## Example\n\n', _format_examples(sections['example']), '\n\n'])
283
289
 
284
290
  if 'see also' in sections:
285
- out.extend(['## See Also\n', sections['see also'], '\n\n'])
291
+ out.extend(['## See Also\n\n', sections['see also'], '\n\n'])
286
292
 
287
293
  return ''.join(out).rstrip() + '\n'
288
294