singlestoredb 0.4.0__py3-none-any.whl → 1.0.4__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 (120) hide show
  1. singlestoredb/__init__.py +33 -1
  2. singlestoredb/alchemy/__init__.py +90 -0
  3. singlestoredb/auth.py +5 -1
  4. singlestoredb/config.py +116 -14
  5. singlestoredb/connection.py +483 -516
  6. singlestoredb/converters.py +238 -135
  7. singlestoredb/exceptions.py +30 -2
  8. singlestoredb/functions/__init__.py +1 -0
  9. singlestoredb/functions/decorator.py +142 -0
  10. singlestoredb/functions/dtypes.py +1639 -0
  11. singlestoredb/functions/ext/__init__.py +2 -0
  12. singlestoredb/functions/ext/arrow.py +375 -0
  13. singlestoredb/functions/ext/asgi.py +661 -0
  14. singlestoredb/functions/ext/json.py +427 -0
  15. singlestoredb/functions/ext/mmap.py +306 -0
  16. singlestoredb/functions/ext/rowdat_1.py +744 -0
  17. singlestoredb/functions/signature.py +673 -0
  18. singlestoredb/fusion/__init__.py +11 -0
  19. singlestoredb/fusion/graphql.py +213 -0
  20. singlestoredb/fusion/handler.py +621 -0
  21. singlestoredb/fusion/handlers/stage.py +257 -0
  22. singlestoredb/fusion/handlers/utils.py +162 -0
  23. singlestoredb/fusion/handlers/workspace.py +412 -0
  24. singlestoredb/fusion/registry.py +164 -0
  25. singlestoredb/fusion/result.py +399 -0
  26. singlestoredb/http/__init__.py +27 -0
  27. singlestoredb/{http.py → http/connection.py} +555 -154
  28. singlestoredb/management/__init__.py +3 -0
  29. singlestoredb/management/billing_usage.py +148 -0
  30. singlestoredb/management/cluster.py +14 -6
  31. singlestoredb/management/manager.py +100 -38
  32. singlestoredb/management/organization.py +188 -0
  33. singlestoredb/management/region.py +5 -5
  34. singlestoredb/management/utils.py +281 -2
  35. singlestoredb/management/workspace.py +1344 -49
  36. singlestoredb/{clients/pymysqlsv → mysql}/__init__.py +16 -21
  37. singlestoredb/{clients/pymysqlsv → mysql}/_auth.py +39 -8
  38. singlestoredb/{clients/pymysqlsv → mysql}/charset.py +26 -23
  39. singlestoredb/{clients/pymysqlsv/connections.py → mysql/connection.py} +532 -165
  40. singlestoredb/{clients/pymysqlsv → mysql}/constants/CLIENT.py +0 -1
  41. singlestoredb/{clients/pymysqlsv → mysql}/constants/COMMAND.py +0 -1
  42. singlestoredb/{clients/pymysqlsv → mysql}/constants/CR.py +0 -2
  43. singlestoredb/{clients/pymysqlsv → mysql}/constants/ER.py +0 -1
  44. singlestoredb/{clients/pymysqlsv → mysql}/constants/FIELD_TYPE.py +1 -1
  45. singlestoredb/{clients/pymysqlsv → mysql}/constants/FLAG.py +0 -1
  46. singlestoredb/{clients/pymysqlsv → mysql}/constants/SERVER_STATUS.py +0 -1
  47. singlestoredb/mysql/converters.py +271 -0
  48. singlestoredb/{clients/pymysqlsv → mysql}/cursors.py +228 -112
  49. singlestoredb/mysql/err.py +92 -0
  50. singlestoredb/{clients/pymysqlsv → mysql}/optionfile.py +5 -4
  51. singlestoredb/{clients/pymysqlsv → mysql}/protocol.py +49 -20
  52. singlestoredb/mysql/tests/__init__.py +19 -0
  53. singlestoredb/{clients/pymysqlsv → mysql}/tests/base.py +32 -12
  54. singlestoredb/mysql/tests/conftest.py +37 -0
  55. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_DictCursor.py +11 -7
  56. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_SSCursor.py +17 -12
  57. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_basic.py +32 -24
  58. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_connection.py +130 -119
  59. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_converters.py +9 -7
  60. singlestoredb/mysql/tests/test_cursor.py +141 -0
  61. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_err.py +3 -2
  62. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_issues.py +35 -27
  63. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_load_local.py +13 -11
  64. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_nextset.py +7 -3
  65. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_optionfile.py +2 -1
  66. singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/__init__.py +1 -1
  67. singlestoredb/mysql/tests/thirdparty/test_MySQLdb/__init__.py +9 -0
  68. singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/capabilities.py +19 -17
  69. singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/dbapi20.py +31 -22
  70. singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/test_MySQLdb_capabilities.py +3 -4
  71. singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/test_MySQLdb_dbapi20.py +24 -20
  72. singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/test_MySQLdb_nonstandard.py +4 -4
  73. singlestoredb/{clients/pymysqlsv → mysql}/times.py +3 -4
  74. singlestoredb/pytest.py +283 -0
  75. singlestoredb/tests/empty.sql +0 -0
  76. singlestoredb/tests/ext_funcs/__init__.py +385 -0
  77. singlestoredb/tests/test.sql +210 -0
  78. singlestoredb/tests/test2.sql +1 -0
  79. singlestoredb/tests/test_basics.py +482 -115
  80. singlestoredb/tests/test_config.py +13 -13
  81. singlestoredb/tests/test_connection.py +241 -305
  82. singlestoredb/tests/test_dbapi.py +27 -0
  83. singlestoredb/tests/test_ext_func.py +1193 -0
  84. singlestoredb/tests/test_ext_func_data.py +1101 -0
  85. singlestoredb/tests/test_fusion.py +465 -0
  86. singlestoredb/tests/test_http.py +32 -26
  87. singlestoredb/tests/test_management.py +588 -8
  88. singlestoredb/tests/test_plugin.py +33 -0
  89. singlestoredb/tests/test_results.py +11 -12
  90. singlestoredb/tests/test_udf.py +687 -0
  91. singlestoredb/tests/utils.py +3 -2
  92. singlestoredb/utils/config.py +58 -0
  93. singlestoredb/utils/debug.py +13 -0
  94. singlestoredb/utils/mogrify.py +151 -0
  95. singlestoredb/utils/results.py +4 -1
  96. singlestoredb-1.0.4.dist-info/METADATA +139 -0
  97. singlestoredb-1.0.4.dist-info/RECORD +112 -0
  98. {singlestoredb-0.4.0.dist-info → singlestoredb-1.0.4.dist-info}/WHEEL +1 -1
  99. singlestoredb-1.0.4.dist-info/entry_points.txt +2 -0
  100. singlestoredb/clients/pymysqlsv/converters.py +0 -365
  101. singlestoredb/clients/pymysqlsv/err.py +0 -144
  102. singlestoredb/clients/pymysqlsv/tests/__init__.py +0 -19
  103. singlestoredb/clients/pymysqlsv/tests/test_cursor.py +0 -133
  104. singlestoredb/clients/pymysqlsv/tests/thirdparty/test_MySQLdb/__init__.py +0 -9
  105. singlestoredb/drivers/__init__.py +0 -45
  106. singlestoredb/drivers/base.py +0 -198
  107. singlestoredb/drivers/cymysql.py +0 -38
  108. singlestoredb/drivers/http.py +0 -47
  109. singlestoredb/drivers/mariadb.py +0 -40
  110. singlestoredb/drivers/mysqlconnector.py +0 -49
  111. singlestoredb/drivers/mysqldb.py +0 -60
  112. singlestoredb/drivers/pymysql.py +0 -37
  113. singlestoredb/drivers/pymysqlsv.py +0 -35
  114. singlestoredb/drivers/pyodbc.py +0 -65
  115. singlestoredb-0.4.0.dist-info/METADATA +0 -111
  116. singlestoredb-0.4.0.dist-info/RECORD +0 -86
  117. /singlestoredb/{clients → fusion/handlers}/__init__.py +0 -0
  118. /singlestoredb/{clients/pymysqlsv → mysql}/constants/__init__.py +0 -0
  119. {singlestoredb-0.4.0.dist-info → singlestoredb-1.0.4.dist-info}/LICENSE +0 -0
  120. {singlestoredb-0.4.0.dist-info → singlestoredb-1.0.4.dist-info}/top_level.txt +0 -0
@@ -66,7 +66,9 @@ import unittest
66
66
 
67
67
 
68
68
  class DatabaseAPI20Test(unittest.TestCase):
69
- """Test a database self.driver for DB API 2.0 compatibility.
69
+ """
70
+ Test a database self.driver for DB API 2.0 compatibility.
71
+
70
72
  This implementation tests Gadfly, but the TestCase
71
73
  is structured so that other self.drivers can subclass this
72
74
  test case to ensure compiliance with the DB-API. It is
@@ -85,6 +87,7 @@ class DatabaseAPI20Test(unittest.TestCase):
85
87
 
86
88
  Don't 'import DatabaseAPI20Test from dbapi20', or you will
87
89
  confuse the unit tester - just 'import dbapi20'.
90
+
88
91
  """
89
92
 
90
93
  # The self.driver module. This should be the module where the 'connect'
@@ -110,15 +113,23 @@ class DatabaseAPI20Test(unittest.TestCase):
110
113
  cursor.execute(self.ddl2)
111
114
 
112
115
  def setUp(self):
113
- """self.drivers should override this method to perform required setup
116
+ """
117
+ Setup the tests.
118
+
119
+ self.drivers should override this method to perform required setup
114
120
  if any is necessary, such as creating the database.
121
+
115
122
  """
116
123
  pass
117
124
 
118
125
  def tearDown(self):
119
- """self.drivers should override this method to perform required cleanup
126
+ """
127
+ Tear down the tests.
128
+
129
+ self.drivers should override this method to perform required cleanup
120
130
  if any is necessary, such as deleting the test database.
121
131
  The default drops the tables that may be created.
132
+
122
133
  """
123
134
  con = self._connect()
124
135
  try:
@@ -226,7 +237,7 @@ class DatabaseAPI20Test(unittest.TestCase):
226
237
  def test_cursor(self):
227
238
  con = self._connect()
228
239
  try:
229
- cur = con.cursor()
240
+ cur = con.cursor() # noqa: F841
230
241
  finally:
231
242
  con.close()
232
243
 
@@ -266,7 +277,7 @@ class DatabaseAPI20Test(unittest.TestCase):
266
277
  )
267
278
  self.assertEqual(
268
279
  len(cur.description[0]),
269
- 7,
280
+ 9,
270
281
  'cursor.description[x] tuples must have 7 elements',
271
282
  )
272
283
  self.assertEqual(
@@ -280,6 +291,7 @@ class DatabaseAPI20Test(unittest.TestCase):
280
291
  'cursor.description[x][1] must return column type. Got %r'
281
292
  % cur.description[0][1],
282
293
  )
294
+ cur.fetchall()
283
295
 
284
296
  # Make sure self.description gets reset
285
297
  self.executeDDL2(cur)
@@ -684,7 +696,7 @@ class DatabaseAPI20Test(unittest.TestCase):
684
696
  for sql in self._populate():
685
697
  cur.execute(sql)
686
698
 
687
- cur.execute('select name from %sbooze' % self.table_prefix)
699
+ cur.execute('select name from %sbooze order by name' % self.table_prefix)
688
700
  rows1 = cur.fetchone()
689
701
  rows23 = cur.fetchmany(2)
690
702
  rows4 = cur.fetchone()
@@ -750,17 +762,14 @@ class DatabaseAPI20Test(unittest.TestCase):
750
762
  assert cur.nextset()
751
763
  names = cur.fetchall()
752
764
  assert len(names) == len(self.samples)
753
- s = cur.nextset()
754
- assert s == None, 'No more return sets, should return None'
765
+ s = cur.nextset() # noqa: F841
766
+ assert s is None, 'No more return sets, should return None'
755
767
  finally:
756
768
  self.help_nextset_tearDown(cur)
757
769
 
758
770
  finally:
759
771
  con.close()
760
772
 
761
- def test_nextset(self):
762
- raise NotImplementedError('Drivers need to override this test')
763
-
764
773
  def test_arraysize(self):
765
774
  # Not much here - rest of the tests for this are in test_fetchmany
766
775
  con = self._connect()
@@ -811,28 +820,28 @@ class DatabaseAPI20Test(unittest.TestCase):
811
820
  con.close()
812
821
 
813
822
  def test_Date(self):
814
- d1 = self.driver.Date(2002, 12, 25)
815
- d2 = self.driver.DateFromTicks(time.mktime((2002, 12, 25, 0, 0, 0, 0, 0, 0)))
823
+ self.driver.Date(2002, 12, 25)
824
+ self.driver.DateFromTicks(time.mktime((2002, 12, 25, 0, 0, 0, 0, 0, 0)))
816
825
  # Can we assume this? API doesn't specify, but it seems implied
817
- # self.assertEqual(str(d1),str(d2))
826
+ # self.assertEqual(str(d1), str(d2))
818
827
 
819
828
  def test_Time(self):
820
- t1 = self.driver.Time(13, 45, 30)
821
- t2 = self.driver.TimeFromTicks(time.mktime((2001, 1, 1, 13, 45, 30, 0, 0, 0)))
829
+ self.driver.Time(13, 45, 30)
830
+ self.driver.TimeFromTicks(time.mktime((2001, 1, 1, 13, 45, 30, 0, 0, 0)))
822
831
  # Can we assume this? API doesn't specify, but it seems implied
823
- # self.assertEqual(str(t1),str(t2))
832
+ # self.assertEqual(str(t1), str(t2))
824
833
 
825
834
  def test_Timestamp(self):
826
- t1 = self.driver.Timestamp(2002, 12, 25, 13, 45, 30)
827
- t2 = self.driver.TimestampFromTicks(
835
+ self.driver.Timestamp(2002, 12, 25, 13, 45, 30)
836
+ self.driver.TimestampFromTicks(
828
837
  time.mktime((2002, 12, 25, 13, 45, 30, 0, 0, 0)),
829
838
  )
830
839
  # Can we assume this? API doesn't specify, but it seems implied
831
- # self.assertEqual(str(t1),str(t2))
840
+ # self.assertEqual(str(t1), str(t2))
832
841
 
833
842
  def test_Binary(self):
834
- b = self.driver.Binary(b'Something')
835
- b = self.driver.Binary(b'')
843
+ self.driver.Binary(b'Something')
844
+ self.driver.Binary(b'')
836
845
 
837
846
  def test_STRING(self):
838
847
  self.assertTrue(hasattr(self.driver, 'STRING'), 'module.STRING must be defined')
@@ -1,10 +1,9 @@
1
1
  # type: ignore
2
- import unittest
3
2
  import warnings
4
3
 
5
- import singlestoredb.clients.pymysqlsv as sv
4
+ import singlestoredb.mysql as sv
6
5
  from . import capabilities
7
- from singlestoredb.clients.pymysqlsv.tests import base
6
+ from singlestoredb.mysql.tests import base
8
7
 
9
8
  warnings.filterwarnings('error')
10
9
 
@@ -91,7 +90,7 @@ class test_MySQLdb(capabilities.DatabaseTest):
91
90
  self.check_data_integrity(('col1 char(1)', 'col2 char(1)'), generator)
92
91
 
93
92
  def test_bug_2671682(self):
94
- from singlestoredb.clients.pymysqlsv.constants import ER
93
+ from singlestoredb.mysql.constants import ER
95
94
 
96
95
  try:
97
96
  self.cursor.execute('describe some_non_existent_table')
@@ -1,12 +1,11 @@
1
1
  # type: ignore
2
- import unittest
3
-
4
- import singlestoredb.clients.pymysqlsv as sv
2
+ import singlestoredb.mysql as sv
5
3
  from . import dbapi20
6
- from singlestoredb.clients.pymysqlsv.tests import base
4
+ from singlestoredb.mysql.tests import base
7
5
 
8
6
 
9
7
  class test_MySQLdb(dbapi20.DatabaseAPI20Test):
8
+
10
9
  driver = sv
11
10
  connect_args = ()
12
11
  connect_kw_args = base.PyMySQLTestCase.databases[0].copy()
@@ -24,10 +23,10 @@ class test_MySQLdb(dbapi20.DatabaseAPI20Test):
24
23
  def test_setoutputsize_basic(self):
25
24
  pass
26
25
 
27
- """The tests on fetchone and fetchall and rowcount bogusly
28
- test for an exception if the statement cannot return a
29
- result set. MySQL always returns a result set; it's just that
30
- some things return empty result sets."""
26
+ # The tests on fetchone and fetchall and rowcount bogusly
27
+ # test for an exception if the statement cannot return a
28
+ # result set. MySQL always returns a result set; it's just that
29
+ # some things return empty result sets.
31
30
 
32
31
  def test_fetchall(self):
33
32
  con = self._connect()
@@ -120,8 +119,9 @@ class test_MySQLdb(dbapi20.DatabaseAPI20Test):
120
119
  self.assertEqual(
121
120
  r[0], 'Victoria Bitter', 'cursor.fetchone retrieved incorrect data',
122
121
  )
123
- # self.assertEqual(cur.fetchone(),None,
124
- ## 'cursor.fetchone should return None if no more rows available'
122
+ # self.assertEqual(cur.fetchone(), None,
123
+ # 'cursor.fetchone should return None if '
124
+ # 'no more rows available'
125
125
  # )
126
126
  self.assertTrue(cur.rowcount in (-1, 1))
127
127
  finally:
@@ -134,15 +134,15 @@ class test_MySQLdb(dbapi20.DatabaseAPI20Test):
134
134
  cur = con.cursor()
135
135
  self.executeDDL1(cur)
136
136
  # self.assertEqual(cur.rowcount,-1,
137
- ## 'cursor.rowcount should be -1 after executing no-result '
137
+ # 'cursor.rowcount should be -1 after executing no-result '
138
138
  # 'statements'
139
139
  # )
140
140
  cur.execute(
141
141
  "insert into %sbooze values ('Victoria Bitter')" % (self.table_prefix),
142
142
  )
143
143
  # self.assertTrue(cur.rowcount in (-1,1),
144
- ## 'cursor.rowcount should == number or rows inserted, or '
145
- ## 'set to -1 after executing an insert statement'
144
+ # 'cursor.rowcount should == number or rows inserted, or '
145
+ # 'set to -1 after executing an insert statement'
146
146
  # )
147
147
  cur.execute('select name from %sbooze' % self.table_prefix)
148
148
  self.assertTrue(
@@ -150,10 +150,11 @@ class test_MySQLdb(dbapi20.DatabaseAPI20Test):
150
150
  'cursor.rowcount should == number of rows returned, or '
151
151
  'set to -1 after executing a select statement',
152
152
  )
153
+ cur.fetchall()
153
154
  self.executeDDL2(cur)
154
- # self.assertEqual(cur.rowcount,-1,
155
- ## 'cursor.rowcount not being reset to -1 after executing '
156
- ## 'no-result statements'
155
+ # self.assertEqual(cur.rowcount, -1,
156
+ # 'cursor.rowcount not being reset to -1 after executing '
157
+ # 'no-result statements'
157
158
  # )
158
159
  finally:
159
160
  con.close()
@@ -178,11 +179,11 @@ class test_MySQLdb(dbapi20.DatabaseAPI20Test):
178
179
  cur.execute(sql)
179
180
 
180
181
  def help_nextset_tearDown(self, cur):
181
- 'If cleaning up is needed after nextSetTest'
182
+ # If cleaning up is needed after nextSetTest
182
183
  cur.execute('drop procedure deleteme')
183
184
 
184
185
  def test_nextset(self):
185
- from warnings import warn
186
+ # from warnings import warn
186
187
 
187
188
  con = self._connect()
188
189
  try:
@@ -201,6 +202,8 @@ class test_MySQLdb(dbapi20.DatabaseAPI20Test):
201
202
  cur.callproc('deleteme')
202
203
  numberofrows = cur.fetchone()
203
204
  assert numberofrows[0] == len(self.samples)
205
+ if cur._result.unbuffered_active:
206
+ assert len(cur.fetchall()) == 0
204
207
  assert cur.nextset()
205
208
  names = cur.fetchall()
206
209
  assert len(names) == len(self.samples)
@@ -210,8 +213,9 @@ class test_MySQLdb(dbapi20.DatabaseAPI20Test):
210
213
  self.assertEqual(
211
214
  len(empty), 0, 'non-empty result set after other result sets',
212
215
  )
213
- # warn("Incompatibility: MySQL returns an empty result set for the CALL itself",
214
- # Warning)
216
+ # warn("Incompatibility: MySQL returns an empty result set "
217
+ # "for the CALL itself",
218
+ # Warning)
215
219
  # assert s == None,'No more return sets, should return None'
216
220
  finally:
217
221
  self.help_nextset_tearDown(cur)
@@ -1,15 +1,15 @@
1
1
  # type: ignore
2
- import sys
3
2
  import unittest
4
3
 
5
- import singlestoredb.clients.pymysqlsv as sv
6
- from singlestoredb.clients.pymysqlsv.constants import FIELD_TYPE
7
- from singlestoredb.clients.pymysqlsv.tests import base
4
+ import singlestoredb.mysql as sv
5
+ from singlestoredb.mysql.constants import FIELD_TYPE
6
+ from singlestoredb.mysql.tests import base
8
7
 
9
8
  _mysql = sv
10
9
 
11
10
 
12
11
  class TestDBAPISet(unittest.TestCase):
12
+
13
13
  def test_set_equality(self):
14
14
  self.assertTrue(sv.STRING == sv.STRING)
15
15
 
@@ -1,4 +1,3 @@
1
- # type: ignore
2
1
  from datetime import date
3
2
  from datetime import datetime
4
3
  from datetime import time
@@ -12,13 +11,13 @@ TimeDelta = timedelta
12
11
  Timestamp = datetime
13
12
 
14
13
 
15
- def DateFromTicks(ticks):
14
+ def DateFromTicks(ticks: int) -> date:
16
15
  return date(*localtime(ticks)[:3])
17
16
 
18
17
 
19
- def TimeFromTicks(ticks):
18
+ def TimeFromTicks(ticks: int) -> time:
20
19
  return time(*localtime(ticks)[3:6])
21
20
 
22
21
 
23
- def TimestampFromTicks(ticks):
22
+ def TimestampFromTicks(ticks: int) -> datetime:
24
23
  return datetime(*localtime(ticks)[:6])
@@ -0,0 +1,283 @@
1
+ #!/usr/bin/env python
2
+ """Pytest plugin"""
3
+ import logging
4
+ import os
5
+ import subprocess
6
+ import time
7
+ from enum import Enum
8
+ from typing import Iterator
9
+ from typing import Optional
10
+
11
+ import pytest
12
+
13
+ from . import connect
14
+ from .connection import Connection
15
+ from .connection import Cursor
16
+
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ # How many times to attempt to connect to the container
22
+ STARTUP_CONNECT_ATTEMPTS = 10
23
+ # How long to wait between connection attempts
24
+ STARTUP_CONNECT_TIMEOUT_SECONDS = 2
25
+ # How many times to check if all connections are closed
26
+ TEARDOWN_WAIT_ATTEMPTS = 20
27
+ # How long to wait between checking connections
28
+ TEARDOWN_WAIT_SECONDS = 2
29
+
30
+
31
+ class ExecutionMode(Enum):
32
+ SEQUENTIAL = 1
33
+ LEADER = 2
34
+ FOLLOWER = 3
35
+
36
+
37
+ @pytest.fixture(scope='session')
38
+ def execution_mode() -> ExecutionMode:
39
+ """Determine the pytest mode for this process"""
40
+
41
+ worker = os.environ.get('PYTEST_XDIST_WORKER')
42
+ worker_count = os.environ.get('PYTEST_XDIST_WORKER_COUNT')
43
+
44
+ # If we're not in pytest-xdist, the mode is Sequential
45
+ if worker is None or worker_count is None:
46
+ logger.debug('XDIST environment vars not found')
47
+ return ExecutionMode.SEQUENTIAL
48
+
49
+ logger.debug(f'PYTEST_XDIST_WORKER == {worker}')
50
+ logger.debug(f'PYTEST_XDIST_WORKER_COUNT == {worker_count}')
51
+
52
+ # If we're the only worker, than the mode is Sequential
53
+ if worker_count == '1':
54
+ return ExecutionMode.SEQUENTIAL
55
+ else:
56
+ # The first worker (named "gw0") is the leader
57
+ # if there are multiple workers
58
+ if worker == 'gw0':
59
+ return ExecutionMode.LEADER
60
+ else:
61
+ return ExecutionMode.FOLLOWER
62
+
63
+
64
+ @pytest.fixture(scope='session')
65
+ def node_name() -> Iterator[str]:
66
+ """Determine the name of this worker node"""
67
+
68
+ worker = os.environ.get('PYTEST_XDIST_WORKER')
69
+
70
+ if worker is None:
71
+ logger.debug('XDIST environment vars not found')
72
+ yield 'master'
73
+ else:
74
+ logger.debug(f'PYTEST_XDIST_WORKER == {worker}')
75
+ yield worker
76
+
77
+
78
+ class _TestContainerManager():
79
+ """Manages the setup and teardown of a SingleStoreDB Dev Container"""
80
+
81
+ def __init__(self) -> None:
82
+ self.container_name = 'singlestoredb-test-container'
83
+ self.dev_image_name = 'ghcr.io/singlestore-labs/singlestoredb-dev'
84
+
85
+ assert 'SINGLESTORE_LICENSE' in os.environ, 'SINGLESTORE_LICENSE not set'
86
+
87
+ self.root_password = 'Q8r4D7yXR8oqn'
88
+ self.environment_vars = {
89
+ 'SINGLESTORE_LICENSE': None,
90
+ 'ROOT_PASSWORD': f"\"{self.root_password}\"",
91
+ 'SINGLESTORE_SET_GLOBAL_DEFAULT_PARTITIONS_PER_LEAF': '1',
92
+ }
93
+
94
+ self.ports = ['3306', '8080', '9000']
95
+
96
+ self.url = f'root:{self.root_password}@127.0.0.1:3306'
97
+
98
+ def start(self) -> None:
99
+ command = ' '.join(self._start_command())
100
+
101
+ logger.info(f'Starting container {self.container_name}')
102
+ try:
103
+ license = os.environ['SINGLESTORE_LICENSE']
104
+ env = {
105
+ 'SINGLESTORE_LICENSE': license,
106
+ }
107
+ subprocess.check_call(command, shell=True, env=env)
108
+ except Exception as e:
109
+ logger.exception(e)
110
+ raise RuntimeError(
111
+ 'Failed to start container. '
112
+ 'Is one already running?',
113
+ ) from e
114
+ logger.debug('Container started')
115
+
116
+ def _start_command(self) -> Iterator[str]:
117
+ yield 'docker run -d --name'
118
+ yield self.container_name
119
+ for key, value in self.environment_vars.items():
120
+ yield '-e'
121
+ if value is None:
122
+ yield key
123
+ else:
124
+ yield f'{key}={value}'
125
+
126
+ for port in self.ports:
127
+ yield '-p'
128
+ yield f'{port}:{port}'
129
+
130
+ yield self.dev_image_name
131
+
132
+ def print_logs(self) -> None:
133
+ logs_command = ['docker', 'logs', self.container_name]
134
+ logger.info('Getting logs')
135
+ logger.info(subprocess.check_output(logs_command))
136
+
137
+ def connect(self) -> Connection:
138
+ # Run all but one attempts trying again if they fail
139
+ for i in range(STARTUP_CONNECT_ATTEMPTS - 1):
140
+ try:
141
+ return connect(self.url)
142
+ except Exception:
143
+ logger.debug(f'Database not available yet (attempt #{i}).')
144
+ time.sleep(STARTUP_CONNECT_TIMEOUT_SECONDS)
145
+ else:
146
+ # Try one last time and report error if it fails
147
+ try:
148
+ return connect(self.url)
149
+ except Exception as e:
150
+ logger.error('Timed out while waiting to connect to database.')
151
+ logger.exception(e)
152
+ self.print_logs()
153
+ raise RuntimeError('Failed to connect to database') from e
154
+
155
+ def wait_till_connections_closed(self) -> None:
156
+ heart_beat = connect(self.url)
157
+ for i in range(TEARDOWN_WAIT_ATTEMPTS):
158
+ connections = self.get_open_connections(heart_beat)
159
+ if connections is None:
160
+ raise RuntimeError('Could not determine the number of open connections.')
161
+ logger.debug(
162
+ f'Waiting for other connections (n={connections-1}) '
163
+ f'to close (attempt #{i})',
164
+ )
165
+ time.sleep(TEARDOWN_WAIT_SECONDS)
166
+ else:
167
+ logger.warning('Timed out while waiting for other connections to close')
168
+ self.print_logs()
169
+
170
+ def get_open_connections(self, conn: Connection) -> Optional[int]:
171
+ for row in conn.show.status(extended=True):
172
+ name = row['Name']
173
+ value = row['Value']
174
+ logger.info(f'{name} = {value}')
175
+ if name == 'Threads_connected':
176
+ return int(value)
177
+
178
+ return None
179
+
180
+ def stop(self) -> None:
181
+ logger.info('Cleaning up SingleStore DB dev container')
182
+ logger.debug('Stopping container')
183
+ try:
184
+ subprocess.check_call(f'docker stop {self.container_name}', shell=True)
185
+ except Exception as e:
186
+ logger.exception(e)
187
+ raise RuntimeError('Failed to stop container.') from e
188
+
189
+ logger.debug('Removing container')
190
+ try:
191
+ subprocess.check_call(f'docker rm {self.container_name}', shell=True)
192
+ except Exception as e:
193
+ logger.exception(e)
194
+ raise RuntimeError('Failed to stop container.') from e
195
+
196
+
197
+ @pytest.fixture(scope='session')
198
+ def singlestoredb_test_container(
199
+ execution_mode: ExecutionMode,
200
+ ) -> Iterator[_TestContainerManager]:
201
+ """Sets up and tears down the test container"""
202
+
203
+ if not isinstance(execution_mode, ExecutionMode):
204
+ raise TypeError(f"Invalid execution mode '{execution_mode}'")
205
+
206
+ container_manager = _TestContainerManager()
207
+
208
+ # In sequential operation do all the steps
209
+ if execution_mode == ExecutionMode.SEQUENTIAL:
210
+ logger.debug('Not distributed')
211
+ container_manager.start()
212
+ yield container_manager
213
+ container_manager.stop()
214
+
215
+ # In distributed execution as leader,
216
+ # do the steps but wait for other workers before stopping
217
+ elif execution_mode == ExecutionMode.LEADER:
218
+ logger.debug('Distributed leader')
219
+ container_manager.start()
220
+ yield container_manager
221
+ container_manager.wait_till_connections_closed()
222
+ container_manager.stop()
223
+
224
+ # In distributed exeuction as a non-leader,
225
+ # don't worry about the container lifecycle
226
+ elif execution_mode == ExecutionMode.FOLLOWER:
227
+ logger.debug('Distributed follower')
228
+ yield container_manager
229
+
230
+
231
+ @pytest.fixture(scope='session')
232
+ def singlestoredb_connection(
233
+ singlestoredb_test_container: _TestContainerManager,
234
+ ) -> Iterator[Connection]:
235
+ """Creates and closes the connection"""
236
+
237
+ connection = singlestoredb_test_container.connect()
238
+ logger.debug('Connected to database.')
239
+
240
+ yield connection
241
+
242
+ logger.debug('Closing connection')
243
+ connection.close()
244
+
245
+
246
+ class _NameAllocator():
247
+ """Generates unique names for each database"""
248
+
249
+ def __init__(self, id: str) -> None:
250
+ self.id = id
251
+ self.names = 0
252
+
253
+ def get_name(self) -> str:
254
+ name = f'x_db_{self.id}_{self.names}'
255
+ self.names += 1
256
+ return name
257
+
258
+
259
+ @pytest.fixture(scope='session')
260
+ def name_allocator(node_name: str) -> Iterator[_NameAllocator]:
261
+ """Makes a worker-local name allocator using the node name"""
262
+
263
+ yield _NameAllocator(node_name)
264
+
265
+
266
+ @pytest.fixture
267
+ def singlestoredb_tempdb(
268
+ singlestoredb_connection: Connection, name_allocator: _NameAllocator,
269
+ ) -> Iterator[Cursor]:
270
+ """Provides a connection to a unique temporary test database"""
271
+
272
+ assert singlestoredb_connection.is_connected(), 'Database is no longer connected'
273
+ db = name_allocator.get_name()
274
+
275
+ with singlestoredb_connection.cursor() as cursor:
276
+ logger.debug(f"Creating temporary DB \"{db}\"")
277
+ cursor.execute(f'CREATE DATABASE {db}')
278
+ cursor.execute(f'USE {db}')
279
+
280
+ yield cursor
281
+
282
+ logger.debug(f"Dropping temporary DB \"{db}\"")
283
+ cursor.execute(f'DROP DATABASE {db}')
File without changes