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
@@ -3,9 +3,31 @@
3
3
  """Basic SingleStoreDB connection testing."""
4
4
  import datetime
5
5
  import decimal
6
+ import math
6
7
  import os
7
8
  import unittest
8
9
 
10
+ from requests.exceptions import InvalidJSONError
11
+
12
+ try:
13
+ import numpy as np
14
+ has_numpy = True
15
+ except ImportError:
16
+ has_numpy = False
17
+
18
+ try:
19
+ import shapely.wkt
20
+ has_shapely = True
21
+ except ImportError:
22
+ has_shapely = False
23
+
24
+ try:
25
+ import pygeos
26
+ from pygeos.testing import assert_geometries_equal
27
+ has_pygeos = True
28
+ except ImportError:
29
+ has_pygeos = False
30
+
9
31
  import singlestoredb as s2
10
32
  from . import utils
11
33
  # import traceback
@@ -29,7 +51,6 @@ class TestBasics(unittest.TestCase):
29
51
  def setUp(self):
30
52
  self.conn = s2.connect(database=type(self).dbname)
31
53
  self.cur = self.conn.cursor()
32
- self.driver = self.conn._driver.dbapi.__name__
33
54
 
34
55
  def tearDown(self):
35
56
  try:
@@ -69,7 +90,7 @@ class TestBasics(unittest.TestCase):
69
90
  ('e', 'elephants', 0),
70
91
  ]), out
71
92
 
72
- assert rowcount == 5, rowcount
93
+ assert rowcount in (5, -1), rowcount
73
94
  assert rownumber == 5, rownumber
74
95
  assert lastrowid is None, lastrowid
75
96
  assert len(desc) == 3, desc
@@ -104,7 +125,7 @@ class TestBasics(unittest.TestCase):
104
125
  ('e', 'elephants', 0),
105
126
  ]), out
106
127
 
107
- assert rowcount == 5, rowcount
128
+ assert rowcount in (5, -1), rowcount
108
129
  assert rownumber == 5, rownumber
109
130
  assert lastrowid is None, lastrowid
110
131
  assert len(desc) == 3, desc
@@ -140,7 +161,7 @@ class TestBasics(unittest.TestCase):
140
161
  ('e', 'elephants', 0),
141
162
  ]), out
142
163
 
143
- assert rowcount == 5, rowcount
164
+ assert rowcount in (5, -1), rowcount
144
165
  assert rownumber == 5, rownumber
145
166
  assert lastrowid is None, lastrowid
146
167
  assert len(desc) == 3, desc
@@ -177,7 +198,7 @@ class TestBasics(unittest.TestCase):
177
198
  assert self.cur.rownumber == 5, self.cur.rownumber
178
199
 
179
200
  def test_execute_with_dict_params(self):
180
- self.cur.execute('select * from data where id < :name', dict(name='d'))
201
+ self.cur.execute('select * from data where id < %(name)s', dict(name='d'))
181
202
  out = self.cur.fetchall()
182
203
 
183
204
  desc = self.cur.description
@@ -190,7 +211,7 @@ class TestBasics(unittest.TestCase):
190
211
  ('c', 'cats', 5),
191
212
  ]), out
192
213
 
193
- assert rowcount == 3, rowcount
214
+ assert rowcount in (3, -1), rowcount
194
215
  assert lastrowid is None, lastrowid
195
216
  assert len(desc) == 3, desc
196
217
  assert desc[0].name == 'id', desc[0].name
@@ -200,8 +221,11 @@ class TestBasics(unittest.TestCase):
200
221
  assert desc[2].name == 'value', desc[2].name
201
222
  assert desc[2].type_code == 8, desc[2].type_code
202
223
 
224
+ with self.assertRaises(KeyError):
225
+ self.cur.execute('select * from data where id < %(name)s', dict(foo='d'))
226
+
203
227
  def test_execute_with_positional_params(self):
204
- self.cur.execute('select * from data where id < :1', ['d'])
228
+ self.cur.execute('select * from data where id < %s', ['d'])
205
229
  out = self.cur.fetchall()
206
230
 
207
231
  desc = self.cur.description
@@ -214,7 +238,7 @@ class TestBasics(unittest.TestCase):
214
238
  ('c', 'cats', 5),
215
239
  ]), out
216
240
 
217
- assert rowcount == 3, rowcount
241
+ assert rowcount in (3, -1), rowcount
218
242
  assert lastrowid is None, lastrowid
219
243
  assert len(desc) == 3, desc
220
244
  assert desc[0].name == 'id', desc[0].name
@@ -224,74 +248,67 @@ class TestBasics(unittest.TestCase):
224
248
  assert desc[2].name == 'value', desc[2].name
225
249
  assert desc[2].type_code == 8, desc[2].type_code
226
250
 
251
+ with self.assertRaises(TypeError):
252
+ self.cur.execute(
253
+ 'select * from data where id < %s and id > %s', ['d', 'e', 'f'],
254
+ )
255
+
256
+ with self.assertRaises(TypeError):
257
+ self.cur.execute('select * from data where id < %s and id > %s', ['d'])
258
+
227
259
  def test_execute_with_escaped_positional_substitutions(self):
228
260
  self.cur.execute(
229
- 'select `id`, `time` from alltypes where `time` = :1', ['00:07:00'],
261
+ 'select `id`, `time` from alltypes where `time` = %s', ['00:07:00'],
230
262
  )
231
263
  out = self.cur.fetchall()
232
- if self.driver == 'pyodbc':
233
- assert out[0] == (0, datetime.time(0, 7)), out[0]
234
- else:
235
- assert out[0] == (0, datetime.timedelta(seconds=420)), out[0]
264
+ assert out[0] == (0, datetime.timedelta(seconds=420)), out[0]
236
265
 
237
266
  self.cur.execute('select `id`, `time` from alltypes where `time` = "00:07:00"')
238
267
  out = self.cur.fetchall()
239
- if self.driver == 'pyodbc':
240
- assert out[0] == (0, datetime.time(0, 7)), out[0]
241
- else:
242
- assert out[0] == (0, datetime.timedelta(seconds=420)), out[0]
268
+ assert out[0] == (0, datetime.timedelta(seconds=420)), out[0]
243
269
 
244
- with self.assertRaises(IndexError):
245
- self.cur.execute(
246
- 'select `id`, `time` from alltypes where `id` = :1 '
247
- 'or `time` = "00:07:00"', [0],
248
- )
270
+ # with self.assertRaises(IndexError):
271
+ # self.cur.execute(
272
+ # 'select `id`, `time` from alltypes where `id` = %1s '
273
+ # 'or `time` = "00:07:00"', [0],
274
+ # )
249
275
 
250
276
  self.cur.execute(
251
- 'select `id`, `time` from alltypes where `id` = :1 '
252
- 'or `time` = "00::07::00"', [0],
277
+ 'select `id`, `time` from alltypes where `id` = %s '
278
+ 'or `time` = "00:07:00"', [0],
253
279
  )
254
280
  out = self.cur.fetchall()
255
- if self.driver == 'pyodbc':
256
- assert out[0] == (0, datetime.time(0, 7)), out[0]
257
- else:
258
- assert out[0] == (0, datetime.timedelta(seconds=420)), out[0]
281
+ assert out[0] == (0, datetime.timedelta(seconds=420)), out[0]
259
282
 
260
283
  def test_execute_with_escaped_substitutions(self):
261
284
  self.cur.execute(
262
- 'select `id`, `time` from alltypes where `time` = :time',
285
+ 'select `id`, `time` from alltypes where `time` = %(time)s',
263
286
  dict(time='00:07:00'),
264
287
  )
265
288
  out = self.cur.fetchall()
266
- if self.driver == 'pyodbc':
267
- assert out[0] == (0, datetime.time(0, 7)), out[0]
268
- else:
269
- assert out[0] == (0, datetime.timedelta(seconds=420)), out[0]
289
+ assert out[0] == (0, datetime.timedelta(seconds=420)), out[0]
270
290
 
271
291
  self.cur.execute(
272
- 'select `id`, `time` from alltypes where `time` = :time',
273
- dict(time='00::07::00'),
292
+ 'select `id`, `time` from alltypes where `time` = %(time)s',
293
+ dict(time='00:07:00'),
274
294
  )
275
295
  out = self.cur.fetchall()
276
- assert len(out) == 0, out
296
+ assert len(out) == 1, out
277
297
 
278
298
  with self.assertRaises(KeyError):
279
299
  self.cur.execute(
280
300
  'select `id`, `time`, `char_100` from alltypes '
281
- 'where `time` = :time or `char_100` like "foo:bar"',
282
- dict(time='00:07:00'),
301
+ 'where `time` = %(time)s or `char_100` like "foo:bar"',
302
+ dict(x='00:07:00'),
283
303
  )
284
304
 
285
305
  self.cur.execute(
286
306
  'select `id`, `time`, `char_100` from alltypes '
287
- 'where `time` = :time or `char_100` like "foo::bar"',
307
+ 'where `time` = %(time)s or `char_100` like "foo::bar"',
288
308
  dict(time='00:07:00'),
289
309
  )
290
310
  out = self.cur.fetchall()
291
- if self.driver == 'pyodbc':
292
- assert out[0][:2] == (0, datetime.time(0, 7)), out[0]
293
- else:
294
- assert out[0][:2] == (0, datetime.timedelta(seconds=420)), out[0]
311
+ assert out[0][:2] == (0, datetime.timedelta(seconds=420)), out[0]
295
312
 
296
313
  def test_is_connected(self):
297
314
  assert self.conn.is_connected()
@@ -311,7 +328,7 @@ class TestBasics(unittest.TestCase):
311
328
  def test_executemany(self):
312
329
  # NOTE: Doesn't actually do anything since no rows match
313
330
  self.cur.executemany(
314
- 'delete from data where id > :name',
331
+ 'delete from data where id > %(name)s',
315
332
  [dict(name='z'), dict(name='y')],
316
333
  )
317
334
 
@@ -482,10 +499,147 @@ class TestBasics(unittest.TestCase):
482
499
  self.cur.execute('garbage syntax')
483
500
 
484
501
  exc = cm.exception
485
- if self.driver != 'pyodbc':
486
- assert exc.errno == 1064, exc.errno
502
+ assert exc.errno == 1064, exc.errno
487
503
  assert 'You have an error in your SQL syntax' in exc.errmsg, exc.errmsg
488
504
 
505
+ def test_extended_types(self):
506
+ if not has_numpy or not has_pygeos or not has_shapely:
507
+ self.skipTest('Test requires numpy, pygeos, and shapely')
508
+
509
+ import uuid
510
+
511
+ key = str(uuid.uuid4())
512
+
513
+ # shapely data
514
+ data = [
515
+ (
516
+ 1, 'POLYGON((1 1, 2 1, 2 2, 1 2, 1 1))', 'POINT(1.5 1.5)',
517
+ [0.5, 0.6], datetime.datetime(1950, 1, 2, 12, 13, 14),
518
+ datetime.date(1950, 1, 2), datetime.time(12, 13, 14),
519
+ datetime.timedelta(seconds=123456), key,
520
+ ),
521
+ (
522
+ 2, 'POLYGON((5 1, 6 1, 6 2, 5 2, 5 1))', 'POINT(5.5 1.5)',
523
+ [1.3, 2.5], datetime.datetime(1960, 3, 4, 15, 16, 17),
524
+ datetime.date(1960, 3, 4), datetime.time(15, 16, 17),
525
+ datetime.timedelta(seconds=2), key,
526
+ ),
527
+ (
528
+ 3, 'POLYGON((5 5, 6 5, 6 6, 5 6, 5 5))', 'POINT(5.5 5.5)',
529
+ [10.3, 11.1], datetime.datetime(1970, 6, 7, 18, 19, 20),
530
+ datetime.date(1970, 5, 6), datetime.time(18, 19, 20),
531
+ datetime.timedelta(seconds=-2), key,
532
+ ),
533
+ (
534
+ 4, 'POLYGON((1 5, 2 5, 2 6, 1 6, 1 5))', 'POINT(1.5 5.5)',
535
+ [3.3, 3.4], datetime.datetime(1980, 8, 9, 21, 22, 23),
536
+ datetime.date(1980, 7, 8), datetime.time(21, 22, 23),
537
+ datetime.timedelta(seconds=-123456), key,
538
+ ),
539
+ (
540
+ 5, 'POLYGON((3 3, 4 3, 4 4, 3 4, 3 3))', 'POINT(3.5 3.5)',
541
+ [2.9, 9.5], datetime.datetime(2010, 10, 11, 1, 2, 3),
542
+ datetime.date(2010, 8, 9), datetime.time(1, 2, 3),
543
+ datetime.timedelta(seconds=0), key,
544
+ ),
545
+ ]
546
+
547
+ new_data = []
548
+ for i, row in enumerate(data):
549
+ row = list(row)
550
+ row[1] = shapely.wkt.loads(row[1])
551
+ row[2] = shapely.wkt.loads(row[2])
552
+ if 'http' in self.conn.driver:
553
+ row[3] = ''
554
+ else:
555
+ row[3] = np.array(row[3], dtype='<f4')
556
+ new_data.append(row)
557
+
558
+ self.cur.executemany(
559
+ 'INSERT INTO extended_types '
560
+ '(id, geography, geographypoint, vectors, dt, d, t, td, testkey) '
561
+ 'VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)', new_data,
562
+ )
563
+
564
+ self.cur.execute(
565
+ 'SELECT * FROM extended_types WHERE testkey = %s ORDER BY id', [key],
566
+ )
567
+
568
+ for data_row, row in zip(new_data, self.cur):
569
+ assert data_row[0] == row[0]
570
+ assert data_row[1].equals_exact(shapely.wkt.loads(row[1]), 1e-4)
571
+ assert data_row[2].equals_exact(shapely.wkt.loads(row[2]), 1e-4)
572
+ if 'http' in self.conn.driver:
573
+ assert row[3] == b''
574
+ else:
575
+ assert (data_row[3] == np.frombuffer(row[3], dtype='<f4')).all()
576
+
577
+ # pygeos data
578
+ data = [
579
+ (
580
+ 6, 'POLYGON((1 1, 2 1, 2 2, 1 2, 1 1))', 'POINT(1.5 1.5)',
581
+ [0.5, 0.6], datetime.datetime(1950, 1, 2, 12, 13, 14),
582
+ datetime.date(1950, 1, 2), datetime.time(12, 13, 14),
583
+ datetime.timedelta(seconds=123456), key,
584
+ ),
585
+ (
586
+ 7, 'POLYGON((5 1, 6 1, 6 2, 5 2, 5 1))', 'POINT(5.5 1.5)',
587
+ [1.3, 2.5], datetime.datetime(1960, 3, 4, 15, 16, 17),
588
+ datetime.date(1960, 3, 4), datetime.time(15, 16, 17),
589
+ datetime.timedelta(seconds=2), key,
590
+ ),
591
+ (
592
+ 8, 'POLYGON((5 5, 6 5, 6 6, 5 6, 5 5))', 'POINT(5.5 5.5)',
593
+ [10.3, 11.1], datetime.datetime(1970, 6, 7, 18, 19, 20),
594
+ datetime.date(1970, 5, 6), datetime.time(18, 19, 20),
595
+ datetime.timedelta(seconds=-2), key,
596
+ ),
597
+ (
598
+ 9, 'POLYGON((1 5, 2 5, 2 6, 1 6, 1 5))', 'POINT(1.5 5.5)',
599
+ [3.3, 3.4], datetime.datetime(1980, 8, 9, 21, 22, 23),
600
+ datetime.date(1980, 7, 8), datetime.time(21, 22, 23),
601
+ datetime.timedelta(seconds=-123456), key,
602
+ ),
603
+ (
604
+ 10, 'POLYGON((3 3, 4 3, 4 4, 3 4, 3 3))', 'POINT(3.5 3.5)',
605
+ [2.9, 9.5], datetime.datetime(2010, 10, 11, 1, 2, 3),
606
+ datetime.date(2010, 8, 9), datetime.time(1, 2, 3),
607
+ datetime.timedelta(seconds=0), key,
608
+ ),
609
+ ]
610
+
611
+ new_data = []
612
+ for i, row in enumerate(data):
613
+ row = list(row)
614
+ row[1] = pygeos.io.from_wkt(row[1])
615
+ row[2] = pygeos.io.from_wkt(row[2])
616
+ if 'http' in self.conn.driver:
617
+ row[3] = ''
618
+ else:
619
+ row[3] = np.array(row[3], dtype='<f4')
620
+ new_data.append(row)
621
+
622
+ self.cur.executemany(
623
+ 'INSERT INTO extended_types '
624
+ '(id, geography, geographypoint, vectors, dt, d, t, td, testkey) '
625
+ 'VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)', new_data,
626
+ )
627
+
628
+ self.cur.execute(
629
+ 'SELECT * FROM extended_types WHERE id >= 6 and testkey = %s ORDER BY id', [
630
+ key,
631
+ ],
632
+ )
633
+
634
+ for data_row, row in zip(new_data, self.cur):
635
+ assert data_row[0] == row[0]
636
+ assert_geometries_equal(data_row[1], pygeos.io.from_wkt(row[1]))
637
+ assert_geometries_equal(data_row[2], pygeos.io.from_wkt(row[2]))
638
+ if 'http' in self.conn.driver:
639
+ assert row[3] == b''
640
+ else:
641
+ assert (data_row[3] == np.frombuffer(row[3], dtype='<f4')).all()
642
+
489
643
  def test_alltypes(self):
490
644
  self.cur.execute('select * from alltypes where id = 0')
491
645
  names = [x[0] for x in self.cur.description]
@@ -496,30 +650,8 @@ class TestBasics(unittest.TestCase):
496
650
 
497
651
  bits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
498
652
 
499
- if self.driver == 'pyodbc':
500
- odbc_types = {
501
- # int -> bigint
502
- 3: 8, 1: 8, 2: 8, 9: 8,
503
- # float -> double
504
- 4: 5,
505
- # timestamp -> datetime
506
- 7: 12,
507
- # year -> bigint
508
- 13: 8,
509
- # char/binary -> varchar/varbinary
510
- 249: 15, 250: 15, 251: 15, 252: 15, 253: 15, 254: 15, 255: 15,
511
- # newdecimal -> decimal
512
- 246: 0,
513
- # json -> varchar
514
- 245: 15,
515
- # bit -> varchar
516
- 16: 15,
517
- }
518
- else:
519
- odbc_types = {}
520
-
521
653
  def otype(x):
522
- return odbc_types.get(x, x)
654
+ return x
523
655
 
524
656
  assert row['id'] == 0, row['id']
525
657
  assert typ['id'] == otype(3), typ['id']
@@ -575,21 +707,12 @@ class TestBasics(unittest.TestCase):
575
707
  assert row['date'] == datetime.date(8524, 11, 10), row['date']
576
708
  assert typ['date'] == 10, typ['date']
577
709
 
578
- # pyodbc uses time rather than timedelta. In addition, if you try to
579
- # put your own converter in, it changes the type code to 15!!!
580
- if self.driver == 'pyodbc':
581
- assert row['time'] == datetime.time(0, 7, 0), row['time']
582
- else:
583
- assert row['time'] == datetime.timedelta(minutes=7), row['time']
710
+ assert row['time'] == datetime.timedelta(minutes=7), row['time']
584
711
  assert typ['time'] == 11, typ['time']
585
712
 
586
- # Same as above
587
- if self.driver == 'pyodbc':
588
- assert row['time_6'] == datetime.time(0, 7, 0), row['time_6']
589
- else:
590
- assert row['time_6'] == datetime.timedelta(
591
- hours=1, minutes=10, microseconds=2,
592
- ), row['time_6']
713
+ assert row['time_6'] == datetime.timedelta(
714
+ hours=1, minutes=10, microseconds=2,
715
+ ), row['time_6']
593
716
  assert typ['time_6'] == 11, typ['time_6']
594
717
 
595
718
  assert row['datetime'] == datetime.datetime(
@@ -653,11 +776,7 @@ class TestBasics(unittest.TestCase):
653
776
  assert row['tinyblob'] == bytearray([10, 11, 12, 13, 14, 15]), row['tinyblob']
654
777
  assert typ['tinyblob'] == otype(249), typ['tinyblob']
655
778
 
656
- # pyodbc surfaces json as varchar
657
- if self.driver == 'pyodbc':
658
- assert row['json'] == '{"a":10,"b":2.75,"c":"hello world"}', row['json']
659
- else:
660
- assert row['json'] == {'a': 10, 'b': 2.75, 'c': 'hello world'}, row['json']
779
+ assert row['json'] == {'a': 10, 'b': 2.75, 'c': 'hello world'}, row['json']
661
780
  assert typ['json'] == otype(245), typ['json']
662
781
 
663
782
  assert row['enum'] == 'one', row['enum']
@@ -667,11 +786,7 @@ class TestBasics(unittest.TestCase):
667
786
  assert row['set'] in [{'two'}, 'two'], row['set']
668
787
  assert typ['set'] == otype(253), typ['set'] # mysql code: 248
669
788
 
670
- # pyodbc uses the opposite endianness of all other drivers
671
- if self.driver == 'pyodbc':
672
- assert row['bit'] == b'\x80\x00\x00\x00\x00\x00\x00\x00', row['bit']
673
- else:
674
- assert row['bit'] == b'\x00\x00\x00\x00\x00\x00\x00\x80', row['bit']
789
+ assert row['bit'] == b'\x00\x00\x00\x00\x00\x00\x00\x80', row['bit']
675
790
  assert typ['bit'] == otype(16), typ['bit']
676
791
 
677
792
  def test_alltypes_nulls(self):
@@ -682,30 +797,8 @@ class TestBasics(unittest.TestCase):
682
797
  row = dict(zip(names, out))
683
798
  typ = dict(zip(names, types))
684
799
 
685
- if self.driver == 'pyodbc':
686
- odbc_types = {
687
- # int -> bigint
688
- 3: 8, 1: 8, 2: 8, 9: 8,
689
- # float -> double
690
- 4: 5,
691
- # timestamp -> datetime
692
- 7: 12,
693
- # year -> bigint
694
- 13: 8,
695
- # char/binary -> varchar/varbinary
696
- 249: 15, 250: 15, 251: 15, 252: 15, 253: 15, 254: 15, 255: 15,
697
- # newdecimal -> decimal
698
- 246: 0,
699
- # json -> varchar
700
- 245: 15,
701
- # bit -> varchar
702
- 16: 15,
703
- }
704
- else:
705
- odbc_types = {}
706
-
707
800
  def otype(x):
708
- return odbc_types.get(x, x)
801
+ return x
709
802
 
710
803
  assert row['id'] == 1, row['id']
711
804
  assert typ['id'] == otype(3), typ['id']
@@ -830,6 +923,280 @@ class TestBasics(unittest.TestCase):
830
923
  assert row['bit'] is None, row['bit']
831
924
  assert typ['bit'] == otype(16), typ['bit']
832
925
 
926
+ def test_alltypes_mins(self):
927
+ self.cur.execute('select * from alltypes where id = 2')
928
+ names = [x[0] for x in self.cur.description]
929
+ out = self.cur.fetchone()
930
+ row = dict(zip(names, out))
931
+
932
+ expected = dict(
933
+ id=2,
934
+ tinyint=-128,
935
+ unsigned_tinyint=0,
936
+ bool=-128,
937
+ boolean=-128,
938
+ smallint=-32768,
939
+ unsigned_smallint=0,
940
+ mediumint=-8388608,
941
+ unsigned_mediumint=0,
942
+ int24=-8388608,
943
+ unsigned_int24=0,
944
+ int=-2147483648,
945
+ unsigned_int=0,
946
+ integer=-2147483648,
947
+ unsigned_integer=0,
948
+ bigint=-9223372036854775808,
949
+ unsigned_bigint=0,
950
+ float=0,
951
+ double=-1.7976931348623158e308,
952
+ real=-1.7976931348623158e308,
953
+ decimal=decimal.Decimal('-99999999999999.999999'),
954
+ dec=-decimal.Decimal('99999999999999.999999'),
955
+ fixed=decimal.Decimal('-99999999999999.999999'),
956
+ numeric=decimal.Decimal('-99999999999999.999999'),
957
+ date=datetime.date(1000, 1, 1),
958
+ time=-1 * datetime.timedelta(hours=838, minutes=59, seconds=59),
959
+ time_6=-1 * datetime.timedelta(hours=838, minutes=59, seconds=59),
960
+ datetime=datetime.datetime(1000, 1, 1, 0, 0, 0),
961
+ datetime_6=datetime.datetime(1000, 1, 1, 0, 0, 0, 0),
962
+ timestamp=datetime.datetime(1970, 1, 1, 0, 0, 1),
963
+ timestamp_6=datetime.datetime(1970, 1, 1, 0, 0, 1, 0),
964
+ year=1901,
965
+ char_100='',
966
+ binary_100=b'\x00' * 100,
967
+ varchar_200='',
968
+ varbinary_200=b'',
969
+ longtext='',
970
+ mediumtext='',
971
+ text='',
972
+ tinytext='',
973
+ longblob=b'',
974
+ mediumblob=b'',
975
+ blob=b'',
976
+ tinyblob=b'',
977
+ json={},
978
+ enum='one',
979
+ set='two',
980
+ bit=b'\x00\x00\x00\x00\x00\x00\x00\x00',
981
+ )
982
+
983
+ for k, v in sorted(row.items()):
984
+ assert v == expected[k], '{} != {} in key {}'.format(v, expected[k], k)
985
+
986
+ def test_alltypes_maxs(self):
987
+ self.cur.execute('select * from alltypes where id = 3')
988
+ names = [x[0] for x in self.cur.description]
989
+ out = self.cur.fetchone()
990
+ row = dict(zip(names, out))
991
+
992
+ expected = dict(
993
+ id=3,
994
+ tinyint=127,
995
+ unsigned_tinyint=255,
996
+ bool=127,
997
+ boolean=127,
998
+ smallint=32767,
999
+ unsigned_smallint=65535,
1000
+ mediumint=8388607,
1001
+ unsigned_mediumint=16777215,
1002
+ int24=8388607,
1003
+ unsigned_int24=16777215,
1004
+ int=2147483647,
1005
+ unsigned_int=4294967295,
1006
+ integer=2147483647,
1007
+ unsigned_integer=4294967295,
1008
+ bigint=9223372036854775807,
1009
+ unsigned_bigint=18446744073709551615,
1010
+ float=0,
1011
+ double=1.7976931348623158e308,
1012
+ real=1.7976931348623158e308,
1013
+ decimal=decimal.Decimal('99999999999999.999999'),
1014
+ dec=decimal.Decimal('99999999999999.999999'),
1015
+ fixed=decimal.Decimal('99999999999999.999999'),
1016
+ numeric=decimal.Decimal('99999999999999.999999'),
1017
+ date=datetime.date(9999, 12, 31),
1018
+ time=datetime.timedelta(hours=838, minutes=59, seconds=59),
1019
+ time_6=datetime.timedelta(hours=838, minutes=59, seconds=59),
1020
+ datetime=datetime.datetime(9999, 12, 31, 23, 59, 59),
1021
+ datetime_6=datetime.datetime(9999, 12, 31, 23, 59, 59, 999999),
1022
+ timestamp=datetime.datetime(2038, 1, 19, 3, 14, 7),
1023
+ timestamp_6=datetime.datetime(2038, 1, 19, 3, 14, 7, 999999),
1024
+ year=2155,
1025
+ char_100='',
1026
+ binary_100=b'\x00' * 100,
1027
+ varchar_200='',
1028
+ varbinary_200=b'',
1029
+ longtext='',
1030
+ mediumtext='',
1031
+ text='',
1032
+ tinytext='',
1033
+ longblob=b'',
1034
+ mediumblob=b'',
1035
+ blob=b'',
1036
+ tinyblob=b'',
1037
+ json={},
1038
+ enum='one',
1039
+ set='two',
1040
+ bit=b'\xff\xff\xff\xff\xff\xff\xff\xff',
1041
+ )
1042
+
1043
+ for k, v in sorted(row.items()):
1044
+ # TODO: Figure out how to get time zones working
1045
+ if 'timestamp' in k:
1046
+ continue
1047
+ assert v == expected[k], '{} != {} in key {}'.format(v, expected[k], k)
1048
+
1049
+ def test_alltypes_zeros(self):
1050
+ self.cur.execute('select * from alltypes where id = 4')
1051
+ names = [x[0] for x in self.cur.description]
1052
+ out = self.cur.fetchone()
1053
+ row = dict(zip(names, out))
1054
+
1055
+ expected = dict(
1056
+ id=4,
1057
+ tinyint=0,
1058
+ unsigned_tinyint=0,
1059
+ bool=0,
1060
+ boolean=0,
1061
+ smallint=0,
1062
+ unsigned_smallint=0,
1063
+ mediumint=0,
1064
+ unsigned_mediumint=0,
1065
+ int24=0,
1066
+ unsigned_int24=0,
1067
+ int=0,
1068
+ unsigned_int=0,
1069
+ integer=0,
1070
+ unsigned_integer=0,
1071
+ bigint=0,
1072
+ unsigned_bigint=0,
1073
+ float=0,
1074
+ double=0,
1075
+ real=0,
1076
+ decimal=decimal.Decimal('0.0'),
1077
+ dec=decimal.Decimal('0.0'),
1078
+ fixed=decimal.Decimal('0.0'),
1079
+ numeric=decimal.Decimal('0.0'),
1080
+ date=None,
1081
+ time=datetime.timedelta(hours=0, minutes=0, seconds=0),
1082
+ time_6=datetime.timedelta(hours=0, minutes=0, seconds=0, microseconds=0),
1083
+ datetime=None,
1084
+ datetime_6=None,
1085
+ timestamp=None,
1086
+ timestamp_6=None,
1087
+ year=None,
1088
+ char_100='',
1089
+ binary_100=b'\x00' * 100,
1090
+ varchar_200='',
1091
+ varbinary_200=b'',
1092
+ longtext='',
1093
+ mediumtext='',
1094
+ text='',
1095
+ tinytext='',
1096
+ longblob=b'',
1097
+ mediumblob=b'',
1098
+ blob=b'',
1099
+ tinyblob=b'',
1100
+ json={},
1101
+ enum='one',
1102
+ set='two',
1103
+ bit=b'\x00\x00\x00\x00\x00\x00\x00\x00',
1104
+ )
1105
+
1106
+ for k, v in sorted(row.items()):
1107
+ assert v == expected[k], '{} != {} in key {}'.format(v, expected[k], k)
1108
+
1109
+ def _test_MySQLdb(self):
1110
+ try:
1111
+ import json
1112
+ import MySQLdb
1113
+ except (ModuleNotFoundError, ImportError):
1114
+ self.skipTest('MySQLdb is not installed')
1115
+
1116
+ self.cur.execute('select * from alltypes order by id')
1117
+ s2_out = self.cur.fetchall()
1118
+
1119
+ port = self.conn.connection_params['port']
1120
+ if 'http' in self.conn.driver:
1121
+ port = 3306
1122
+
1123
+ args = dict(
1124
+ host=self.conn.connection_params['host'],
1125
+ port=port,
1126
+ user=self.conn.connection_params['user'],
1127
+ password=self.conn.connection_params['password'],
1128
+ database=type(self).dbname,
1129
+ )
1130
+
1131
+ with MySQLdb.connect(**args) as conn:
1132
+ conn.converter[245] = json.loads
1133
+ with conn.cursor() as cur:
1134
+ cur.execute('select * from alltypes order by id')
1135
+ mydb_out = cur.fetchall()
1136
+
1137
+ for a, b in zip(s2_out, mydb_out):
1138
+ assert a == b, (a, b)
1139
+
1140
+ def test_int_string(self):
1141
+ string = 'a' * 48
1142
+ self.cur.execute(f"SELECT 1, '{string}'")
1143
+ self.assertEqual((1, string), self.cur.fetchone())
1144
+
1145
+ def test_double_string(self):
1146
+ string = 'a' * 49
1147
+ self.cur.execute(f"SELECT 1.2 :> DOUBLE, '{string}'")
1148
+ self.assertEqual((1.2, string), self.cur.fetchone())
1149
+
1150
+ def test_year_string(self):
1151
+ string = 'a' * 49
1152
+ self.cur.execute(f"SELECT 1999 :> YEAR, '{string}'")
1153
+ self.assertEqual((1999, string), self.cur.fetchone())
1154
+
1155
+ def test_nan_as_null(self):
1156
+ with self.assertRaises((s2.ProgrammingError, InvalidJSONError)):
1157
+ self.cur.execute('SELECT %s :> DOUBLE AS X', [math.nan])
1158
+
1159
+ with s2.connect(database=type(self).dbname, nan_as_null=True) as conn:
1160
+ with conn.cursor() as cur:
1161
+ cur.execute('SELECT %s :> DOUBLE AS X', [math.nan])
1162
+ self.assertEqual(None, list(cur)[0][0])
1163
+
1164
+ with s2.connect(database=type(self).dbname, nan_as_null=True) as conn:
1165
+ with conn.cursor() as cur:
1166
+ cur.execute('SELECT %s :> DOUBLE AS X', [1.234])
1167
+ self.assertEqual(1.234, list(cur)[0][0])
1168
+
1169
+ def test_inf_as_null(self):
1170
+ with self.assertRaises((s2.ProgrammingError, InvalidJSONError)):
1171
+ self.cur.execute('SELECT %s :> DOUBLE AS X', [math.inf])
1172
+
1173
+ with s2.connect(database=type(self).dbname, inf_as_null=True) as conn:
1174
+ with conn.cursor() as cur:
1175
+ cur.execute('SELECT %s :> DOUBLE AS X', [math.inf])
1176
+ self.assertEqual(None, list(cur)[0][0])
1177
+
1178
+ with s2.connect(database=type(self).dbname, inf_as_null=True) as conn:
1179
+ with conn.cursor() as cur:
1180
+ cur.execute('SELECT %s :> DOUBLE AS X', [1.234])
1181
+ self.assertEqual(1.234, list(cur)[0][0])
1182
+
1183
+ def test_encoding_errors(self):
1184
+ with s2.connect(
1185
+ database=type(self).dbname,
1186
+ encoding_errors='strict',
1187
+ ) as conn:
1188
+ with conn.cursor() as cur:
1189
+ cur.execute('SELECT * FROM badutf8')
1190
+ list(cur)
1191
+
1192
+ with s2.connect(
1193
+ database=type(self).dbname,
1194
+ encoding_errors='backslashreplace',
1195
+ ) as conn:
1196
+ with conn.cursor() as cur:
1197
+ cur.execute('SELECT * FROM badutf8')
1198
+ list(cur)
1199
+
833
1200
 
834
1201
  if __name__ == '__main__':
835
1202
  import nose2