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.
- singlestoredb/__init__.py +33 -1
- singlestoredb/alchemy/__init__.py +90 -0
- singlestoredb/auth.py +5 -1
- singlestoredb/config.py +116 -14
- singlestoredb/connection.py +483 -516
- singlestoredb/converters.py +238 -135
- singlestoredb/exceptions.py +30 -2
- singlestoredb/functions/__init__.py +1 -0
- singlestoredb/functions/decorator.py +142 -0
- singlestoredb/functions/dtypes.py +1639 -0
- singlestoredb/functions/ext/__init__.py +2 -0
- singlestoredb/functions/ext/arrow.py +375 -0
- singlestoredb/functions/ext/asgi.py +661 -0
- singlestoredb/functions/ext/json.py +427 -0
- singlestoredb/functions/ext/mmap.py +306 -0
- singlestoredb/functions/ext/rowdat_1.py +744 -0
- singlestoredb/functions/signature.py +673 -0
- singlestoredb/fusion/__init__.py +11 -0
- singlestoredb/fusion/graphql.py +213 -0
- singlestoredb/fusion/handler.py +621 -0
- singlestoredb/fusion/handlers/stage.py +257 -0
- singlestoredb/fusion/handlers/utils.py +162 -0
- singlestoredb/fusion/handlers/workspace.py +412 -0
- singlestoredb/fusion/registry.py +164 -0
- singlestoredb/fusion/result.py +399 -0
- singlestoredb/http/__init__.py +27 -0
- singlestoredb/{http.py → http/connection.py} +555 -154
- singlestoredb/management/__init__.py +3 -0
- singlestoredb/management/billing_usage.py +148 -0
- singlestoredb/management/cluster.py +14 -6
- singlestoredb/management/manager.py +100 -38
- singlestoredb/management/organization.py +188 -0
- singlestoredb/management/region.py +5 -5
- singlestoredb/management/utils.py +281 -2
- singlestoredb/management/workspace.py +1344 -49
- singlestoredb/{clients/pymysqlsv → mysql}/__init__.py +16 -21
- singlestoredb/{clients/pymysqlsv → mysql}/_auth.py +39 -8
- singlestoredb/{clients/pymysqlsv → mysql}/charset.py +26 -23
- singlestoredb/{clients/pymysqlsv/connections.py → mysql/connection.py} +532 -165
- singlestoredb/{clients/pymysqlsv → mysql}/constants/CLIENT.py +0 -1
- singlestoredb/{clients/pymysqlsv → mysql}/constants/COMMAND.py +0 -1
- singlestoredb/{clients/pymysqlsv → mysql}/constants/CR.py +0 -2
- singlestoredb/{clients/pymysqlsv → mysql}/constants/ER.py +0 -1
- singlestoredb/{clients/pymysqlsv → mysql}/constants/FIELD_TYPE.py +1 -1
- singlestoredb/{clients/pymysqlsv → mysql}/constants/FLAG.py +0 -1
- singlestoredb/{clients/pymysqlsv → mysql}/constants/SERVER_STATUS.py +0 -1
- singlestoredb/mysql/converters.py +271 -0
- singlestoredb/{clients/pymysqlsv → mysql}/cursors.py +228 -112
- singlestoredb/mysql/err.py +92 -0
- singlestoredb/{clients/pymysqlsv → mysql}/optionfile.py +5 -4
- singlestoredb/{clients/pymysqlsv → mysql}/protocol.py +49 -20
- singlestoredb/mysql/tests/__init__.py +19 -0
- singlestoredb/{clients/pymysqlsv → mysql}/tests/base.py +32 -12
- singlestoredb/mysql/tests/conftest.py +37 -0
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_DictCursor.py +11 -7
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_SSCursor.py +17 -12
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_basic.py +32 -24
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_connection.py +130 -119
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_converters.py +9 -7
- singlestoredb/mysql/tests/test_cursor.py +141 -0
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_err.py +3 -2
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_issues.py +35 -27
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_load_local.py +13 -11
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_nextset.py +7 -3
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_optionfile.py +2 -1
- singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/__init__.py +1 -1
- singlestoredb/mysql/tests/thirdparty/test_MySQLdb/__init__.py +9 -0
- singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/capabilities.py +19 -17
- singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/dbapi20.py +31 -22
- singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/test_MySQLdb_capabilities.py +3 -4
- singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/test_MySQLdb_dbapi20.py +24 -20
- singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/test_MySQLdb_nonstandard.py +4 -4
- singlestoredb/{clients/pymysqlsv → mysql}/times.py +3 -4
- singlestoredb/pytest.py +283 -0
- singlestoredb/tests/empty.sql +0 -0
- singlestoredb/tests/ext_funcs/__init__.py +385 -0
- singlestoredb/tests/test.sql +210 -0
- singlestoredb/tests/test2.sql +1 -0
- singlestoredb/tests/test_basics.py +482 -115
- singlestoredb/tests/test_config.py +13 -13
- singlestoredb/tests/test_connection.py +241 -305
- singlestoredb/tests/test_dbapi.py +27 -0
- singlestoredb/tests/test_ext_func.py +1193 -0
- singlestoredb/tests/test_ext_func_data.py +1101 -0
- singlestoredb/tests/test_fusion.py +465 -0
- singlestoredb/tests/test_http.py +32 -26
- singlestoredb/tests/test_management.py +588 -8
- singlestoredb/tests/test_plugin.py +33 -0
- singlestoredb/tests/test_results.py +11 -12
- singlestoredb/tests/test_udf.py +687 -0
- singlestoredb/tests/utils.py +3 -2
- singlestoredb/utils/config.py +58 -0
- singlestoredb/utils/debug.py +13 -0
- singlestoredb/utils/mogrify.py +151 -0
- singlestoredb/utils/results.py +4 -1
- singlestoredb-1.0.4.dist-info/METADATA +139 -0
- singlestoredb-1.0.4.dist-info/RECORD +112 -0
- {singlestoredb-0.4.0.dist-info → singlestoredb-1.0.4.dist-info}/WHEEL +1 -1
- singlestoredb-1.0.4.dist-info/entry_points.txt +2 -0
- singlestoredb/clients/pymysqlsv/converters.py +0 -365
- singlestoredb/clients/pymysqlsv/err.py +0 -144
- singlestoredb/clients/pymysqlsv/tests/__init__.py +0 -19
- singlestoredb/clients/pymysqlsv/tests/test_cursor.py +0 -133
- singlestoredb/clients/pymysqlsv/tests/thirdparty/test_MySQLdb/__init__.py +0 -9
- singlestoredb/drivers/__init__.py +0 -45
- singlestoredb/drivers/base.py +0 -198
- singlestoredb/drivers/cymysql.py +0 -38
- singlestoredb/drivers/http.py +0 -47
- singlestoredb/drivers/mariadb.py +0 -40
- singlestoredb/drivers/mysqlconnector.py +0 -49
- singlestoredb/drivers/mysqldb.py +0 -60
- singlestoredb/drivers/pymysql.py +0 -37
- singlestoredb/drivers/pymysqlsv.py +0 -35
- singlestoredb/drivers/pyodbc.py +0 -65
- singlestoredb-0.4.0.dist-info/METADATA +0 -111
- singlestoredb-0.4.0.dist-info/RECORD +0 -86
- /singlestoredb/{clients → fusion/handlers}/__init__.py +0 -0
- /singlestoredb/{clients/pymysqlsv → mysql}/constants/__init__.py +0 -0
- {singlestoredb-0.4.0.dist-info → singlestoredb-1.0.4.dist-info}/LICENSE +0 -0
- {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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
815
|
-
|
|
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
|
-
|
|
821
|
-
|
|
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
|
-
|
|
827
|
-
|
|
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
|
-
|
|
835
|
-
|
|
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')
|
singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/test_MySQLdb_capabilities.py
RENAMED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
# type: ignore
|
|
2
|
-
import unittest
|
|
3
2
|
import warnings
|
|
4
3
|
|
|
5
|
-
import singlestoredb.
|
|
4
|
+
import singlestoredb.mysql as sv
|
|
6
5
|
from . import capabilities
|
|
7
|
-
from singlestoredb.
|
|
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.
|
|
93
|
+
from singlestoredb.mysql.constants import ER
|
|
95
94
|
|
|
96
95
|
try:
|
|
97
96
|
self.cursor.execute('describe some_non_existent_table')
|
singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/test_MySQLdb_dbapi20.py
RENAMED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
# type: ignore
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
import singlestoredb.clients.pymysqlsv as sv
|
|
2
|
+
import singlestoredb.mysql as sv
|
|
5
3
|
from . import dbapi20
|
|
6
|
-
from singlestoredb.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
145
|
-
|
|
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
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
|
214
|
-
#
|
|
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)
|
singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/test_MySQLdb_nonstandard.py
RENAMED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# type: ignore
|
|
2
|
-
import sys
|
|
3
2
|
import unittest
|
|
4
3
|
|
|
5
|
-
import singlestoredb.
|
|
6
|
-
from singlestoredb.
|
|
7
|
-
from singlestoredb.
|
|
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])
|
singlestoredb/pytest.py
ADDED
|
@@ -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
|