velocity-python 0.0.131__py3-none-any.whl → 0.0.134__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 velocity-python might be problematic. Click here for more details.

Files changed (88) hide show
  1. velocity/__init__.py +1 -1
  2. velocity/app/tests/__init__.py +1 -0
  3. velocity/app/tests/test_email_processing.py +112 -0
  4. velocity/app/tests/test_payment_profile_sorting.py +191 -0
  5. velocity/app/tests/test_spreadsheet_functions.py +124 -0
  6. velocity/aws/tests/__init__.py +1 -0
  7. velocity/aws/tests/test_lambda_handler_json_serialization.py +120 -0
  8. velocity/aws/tests/test_response.py +163 -0
  9. velocity/db/core/decorators.py +20 -3
  10. velocity/db/core/engine.py +33 -7
  11. velocity/db/exceptions.py +7 -0
  12. velocity/db/servers/base/__init__.py +9 -0
  13. velocity/db/servers/base/initializer.py +70 -0
  14. velocity/db/servers/base/operators.py +98 -0
  15. velocity/db/servers/base/sql.py +503 -0
  16. velocity/db/servers/base/types.py +135 -0
  17. velocity/db/servers/mysql/__init__.py +73 -0
  18. velocity/db/servers/mysql/operators.py +54 -0
  19. velocity/db/servers/{mysql_reserved.py → mysql/reserved.py} +2 -14
  20. velocity/db/servers/mysql/sql.py +569 -0
  21. velocity/db/servers/mysql/types.py +107 -0
  22. velocity/db/servers/postgres/__init__.py +52 -2
  23. velocity/db/servers/postgres/operators.py +34 -0
  24. velocity/db/servers/postgres/sql.py +4 -3
  25. velocity/db/servers/postgres/types.py +88 -2
  26. velocity/db/servers/sqlite/__init__.py +61 -0
  27. velocity/db/servers/sqlite/operators.py +52 -0
  28. velocity/db/servers/sqlite/reserved.py +20 -0
  29. velocity/db/servers/sqlite/sql.py +530 -0
  30. velocity/db/servers/sqlite/types.py +92 -0
  31. velocity/db/servers/sqlserver/__init__.py +73 -0
  32. velocity/db/servers/sqlserver/operators.py +47 -0
  33. velocity/db/servers/sqlserver/reserved.py +32 -0
  34. velocity/db/servers/sqlserver/sql.py +625 -0
  35. velocity/db/servers/sqlserver/types.py +114 -0
  36. velocity/db/tests/__init__.py +1 -0
  37. velocity/db/tests/common_db_test.py +0 -0
  38. velocity/db/tests/postgres/__init__.py +1 -0
  39. velocity/db/tests/postgres/common.py +49 -0
  40. velocity/db/tests/postgres/test_column.py +29 -0
  41. velocity/db/tests/postgres/test_connections.py +25 -0
  42. velocity/db/tests/postgres/test_database.py +21 -0
  43. velocity/db/tests/postgres/test_engine.py +205 -0
  44. velocity/db/tests/postgres/test_general_usage.py +88 -0
  45. velocity/db/tests/postgres/test_imports.py +8 -0
  46. velocity/db/tests/postgres/test_result.py +19 -0
  47. velocity/db/tests/postgres/test_row.py +137 -0
  48. velocity/db/tests/postgres/test_schema_locking.py +335 -0
  49. velocity/db/tests/postgres/test_schema_locking_unit.py +115 -0
  50. velocity/db/tests/postgres/test_sequence.py +34 -0
  51. velocity/db/tests/postgres/test_table.py +101 -0
  52. velocity/db/tests/postgres/test_transaction.py +106 -0
  53. velocity/db/tests/sql/__init__.py +1 -0
  54. velocity/db/tests/sql/common.py +177 -0
  55. velocity/db/tests/sql/test_postgres_select_advanced.py +285 -0
  56. velocity/db/tests/sql/test_postgres_select_variances.py +517 -0
  57. velocity/db/tests/test_cursor_rowcount_fix.py +150 -0
  58. velocity/db/tests/test_db_utils.py +221 -0
  59. velocity/db/tests/test_postgres.py +212 -0
  60. velocity/db/tests/test_postgres_unchanged.py +81 -0
  61. velocity/db/tests/test_process_error_robustness.py +292 -0
  62. velocity/db/tests/test_result_caching.py +279 -0
  63. velocity/db/tests/test_result_sql_aware.py +117 -0
  64. velocity/db/tests/test_row_get_missing_column.py +72 -0
  65. velocity/db/tests/test_schema_locking_initializers.py +226 -0
  66. velocity/db/tests/test_schema_locking_simple.py +97 -0
  67. velocity/db/tests/test_sql_builder.py +165 -0
  68. velocity/db/tests/test_tablehelper.py +486 -0
  69. velocity/misc/tests/__init__.py +1 -0
  70. velocity/misc/tests/test_db.py +90 -0
  71. velocity/misc/tests/test_fix.py +78 -0
  72. velocity/misc/tests/test_format.py +64 -0
  73. velocity/misc/tests/test_iconv.py +203 -0
  74. velocity/misc/tests/test_merge.py +82 -0
  75. velocity/misc/tests/test_oconv.py +144 -0
  76. velocity/misc/tests/test_original_error.py +52 -0
  77. velocity/misc/tests/test_timer.py +74 -0
  78. {velocity_python-0.0.131.dist-info → velocity_python-0.0.134.dist-info}/METADATA +1 -1
  79. velocity_python-0.0.134.dist-info/RECORD +125 -0
  80. velocity/db/servers/mysql.py +0 -640
  81. velocity/db/servers/sqlite.py +0 -968
  82. velocity/db/servers/sqlite_reserved.py +0 -208
  83. velocity/db/servers/sqlserver.py +0 -921
  84. velocity/db/servers/sqlserver_reserved.py +0 -314
  85. velocity_python-0.0.131.dist-info/RECORD +0 -62
  86. {velocity_python-0.0.131.dist-info → velocity_python-0.0.134.dist-info}/WHEEL +0 -0
  87. {velocity_python-0.0.131.dist-info → velocity_python-0.0.134.dist-info}/licenses/LICENSE +0 -0
  88. {velocity_python-0.0.131.dist-info → velocity_python-0.0.134.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,517 @@
1
+ import unittest
2
+ from velocity.db.servers import postgres, tablehelper
3
+ from velocity.db.core.table import Query
4
+ from common import TestSQLModule, engine
5
+
6
+ DO_ALL = True
7
+
8
+ print(f"DO_ALL: {DO_ALL}")
9
+
10
+
11
+ @engine.transaction
12
+ class TestLocal(TestSQLModule):
13
+
14
+ def test_table_helper(self, tx):
15
+ if not DO_ALL:
16
+ return
17
+ th = tablehelper.TableHelper(tx, "fk_child")
18
+
19
+ expected_output = (
20
+ "column_name IN (SELECT COLUMN_NAME\nFROM TABLE_NAME\nWHERE COLUMN_NAME = 1)",
21
+ None,
22
+ )
23
+ t = th.make_predicate(
24
+ "column_name",
25
+ Query("select column_name from table_name where column_name = 1"),
26
+ )
27
+ self.assertEqual(t, expected_output)
28
+
29
+ expected_output = ("column_name = %s", "value")
30
+ t = th.make_predicate("column_name", "value")
31
+ self.assertEqual(t, expected_output)
32
+
33
+ expected_output = ("column_name > %s", "value")
34
+ t = th.make_predicate(">column_name", "value")
35
+ self.assertEqual(t, expected_output)
36
+
37
+ expected_output = ("column_name <> %s", "value")
38
+ t = th.make_predicate("<>column_name", "value")
39
+ self.assertEqual(t, expected_output)
40
+
41
+ expected_output = ("B.parent_name <> %s", "value")
42
+ t = th.make_predicate("<>parent_id>parent_name", "value")
43
+ self.assertEqual(t, expected_output)
44
+
45
+ expected_output = ('B."table" <> %s', "value")
46
+ t = th.make_predicate("<>parent_id>table", "value")
47
+ self.assertEqual(t, expected_output)
48
+
49
+ expected_output = ("B.parent_name IS NOT NULL", None)
50
+ t = th.make_predicate("<>parent_id>parent_name", None)
51
+ self.assertEqual(t, expected_output)
52
+
53
+ columns = [
54
+ ("parent_id", "parent_id"),
55
+ ("parent_name", "parent_name"),
56
+ ("parent_id>parent_name", "parent_id_parent_name"),
57
+ ("quijibo", "quijibo"),
58
+ ("doojibo", "doojibo"),
59
+ ("symmetric", '"symmetric"'),
60
+ ("table", '"table"'),
61
+ ("parent_id>is_valid", "parent_id_is_valid"),
62
+ ("sum_info", "sum_info"),
63
+ ("max_hours", "max_hours"),
64
+ ("parent_id>num_things", "parent_id_num_things"),
65
+ ("parent_id>symmetric", "parent_id_symmetric"),
66
+ ("parent_id>table", "parent_id_table"),
67
+ ]
68
+ for key, val in columns:
69
+ column = th.resolve_references(key, options={"alias_only": True})
70
+ self.assertEqual(column, val)
71
+ columns = [
72
+ ("parent_id", "parent_id"),
73
+ ("parent_name", "parent_name"),
74
+ ("parent_id>parent_name", "B.parent_name"),
75
+ ("quijibo", "quijibo"),
76
+ ("doojibo", "doojibo"),
77
+ ("symmetric", '"symmetric"'),
78
+ ("table", '"table"'),
79
+ ("parent_id>is_valid", "B.is_valid"),
80
+ ("sum_info", "sum_info"),
81
+ ("max_hours", "max_hours"),
82
+ ("parent_id>num_things", "B.num_things"),
83
+ ("parent_id>symmetric", 'B."symmetric"'),
84
+ ("parent_id>table", 'B."table"'),
85
+ ]
86
+ for key, val in columns:
87
+ column = th.resolve_references(key, options={"alias_table": True})
88
+ self.assertEqual(column, val)
89
+
90
+ columns = [
91
+ ("parent_id", "parent_id"),
92
+ ("parent_name", "parent_name"),
93
+ ("parent_id>parent_name", "B.parent_name as parent_id_parent_name"),
94
+ ("quijibo", "quijibo"),
95
+ ("doojibo", "doojibo"),
96
+ ("symmetric", '"symmetric"'),
97
+ ("table", '"table"'),
98
+ ("parent_id>is_valid", "B.is_valid as parent_id_is_valid"),
99
+ ("sum_info", "sum_info"),
100
+ ("max_hours", "max_hours"),
101
+ ("parent_id>num_things", "B.num_things as parent_id_num_things"),
102
+ ("parent_id>symmetric", 'B."symmetric" as parent_id_symmetric'),
103
+ ("parent_id>table", 'B."table" as parent_id_table'),
104
+ ]
105
+ for key, val in columns:
106
+ column = th.resolve_references(
107
+ key, options={"alias_table": True, "alias_column": True}
108
+ )
109
+ self.assertEqual(column, val)
110
+
111
+ columns = [
112
+ ("parent_id", "parent_id"),
113
+ ("parent_name", "parent_name"),
114
+ ("parent_id>parent_name", "fk_parent.parent_name as parent_id_parent_name"),
115
+ ("quijibo", "quijibo"),
116
+ ("doojibo", "doojibo"),
117
+ ("symmetric", '"symmetric"'),
118
+ ("table", '"table"'),
119
+ ("parent_id>is_valid", "fk_parent.is_valid as parent_id_is_valid"),
120
+ ("sum_info", "sum_info"),
121
+ ("max_hours", "max_hours"),
122
+ ("parent_id>num_things", "fk_parent.num_things as parent_id_num_things"),
123
+ ("parent_id>symmetric", 'fk_parent."symmetric" as parent_id_symmetric'),
124
+ ("parent_id>table", 'fk_parent."table" as parent_id_table'),
125
+ ]
126
+ for key, val in columns:
127
+ column = th.resolve_references(
128
+ key, options={"alias_table": False, "alias_column": True}
129
+ )
130
+ self.assertEqual(column, val)
131
+
132
+ def test_select_snuffleupagus(self, tx):
133
+ if not DO_ALL:
134
+ return
135
+
136
+ expected_sql = """SELECT parent_id,
137
+ parent_name,
138
+ B.parent_name AS parent_id_parent_name,
139
+ quijibo,
140
+ doojibo,
141
+ "symmetric",
142
+ "table",
143
+ B.is_valid AS parent_id_is_valid,
144
+ sum_info,
145
+ max_hours,
146
+ B.num_things AS parent_id_num_things,
147
+ B."symmetric" AS parent_id_symmetric,
148
+ B."table" AS parent_id_table
149
+ FROM fk_child AS A
150
+ LEFT JOIN fk_parent AS B ON A.parent_id = B.sys_id
151
+ WHERE B.parent_name = %s
152
+ AND B.is_valid IS TRUE
153
+ AND B.num_things = %s
154
+ AND B."symmetric" IS TRUE
155
+ AND sum_info = %s
156
+ AND max_hours = %s
157
+ GROUP BY parent_id_num_things,
158
+ parent_id_parent_name
159
+ HAVING B.num_things = %s
160
+ AND B."symmetric" IS TRUE
161
+ ORDER BY parent_id_num_things DESC,
162
+ parent_id_parent_name
163
+ OFFSET 99 ROWS FETCH NEXT 100 ROWS ONLY
164
+ FOR
165
+ UPDATE SKIP LOCKED"""
166
+ expected_vars = ("value", 1, "value", 1, 1)
167
+
168
+ sql, vars = tx.table("fk_child").select(
169
+ sql_only=True,
170
+ columns=[
171
+ "parent_id",
172
+ "parent_name",
173
+ "parent_id>parent_name",
174
+ "quijibo",
175
+ "doojibo",
176
+ "symmetric",
177
+ "table",
178
+ "parent_id>is_valid",
179
+ "sum_info",
180
+ "max_hours",
181
+ "parent_id>num_things",
182
+ "parent_id>symmetric",
183
+ "parent_id>table",
184
+ ],
185
+ where={
186
+ "parent_id>parent_name": "value",
187
+ "parent_id>is_valid": True,
188
+ "parent_id>num_things": 1,
189
+ "parent_id>symmetric": True,
190
+ "sum_info": "value",
191
+ "max_hours": 1,
192
+ },
193
+ having={"parent_id>num_things": 1, "parent_id>symmetric": True},
194
+ orderby=["parent_id>num_things desc", "parent_id>parent_name"],
195
+ groupby=["parent_id>num_things", "parent_id>parent_name"],
196
+ lock=True,
197
+ skip_locked=True,
198
+ start=99,
199
+ qty=100,
200
+ )
201
+
202
+ self.assertEqual(sql, expected_sql)
203
+ self.assertEqual(vars, expected_vars)
204
+
205
+ def test_create_index(self, tx):
206
+ if not DO_ALL:
207
+ return
208
+
209
+ sql, vars = tx.table("weird_names_table").create_index(
210
+ sql_only=True,
211
+ unique=True,
212
+ columns=[
213
+ "sum_info",
214
+ "max_hours",
215
+ "order",
216
+ ],
217
+ where={
218
+ "sum_info": "value",
219
+ "max_hours": 1,
220
+ },
221
+ )
222
+ expected_sql = """CREATE UNIQUE INDEX IDX__WEIRD_NAMES_TABLE__SUM_INFO_MAX_HOURS_ORDER ON weird_names_table (sum_info, max_hours, "order")
223
+ WHERE sum_info = %s
224
+ AND max_hours = %s
225
+ """
226
+ expected_vars = ("value", 1)
227
+ self.assertEqual(
228
+ sql.split(),
229
+ expected_sql.split(),
230
+ )
231
+ self.assertEqual(vars, expected_vars)
232
+
233
+ def test_delete(self, tx):
234
+ if not DO_ALL:
235
+ return
236
+
237
+ sql, vars = tx.table("weird_names_table").delete(
238
+ sql_only=True,
239
+ where={
240
+ "sum_info": "value",
241
+ "max_hours": 1,
242
+ },
243
+ )
244
+ expected_sql = """
245
+ DELETE FROM weird_names_table WHERE sum_info = %s AND max_hours = %s
246
+ """
247
+ expected_vars = ("value", 1)
248
+ self.assertEqual(
249
+ sql.split(),
250
+ expected_sql.split(),
251
+ )
252
+ self.assertEqual(vars, expected_vars)
253
+
254
+ def test_missing(self, tx):
255
+ if not DO_ALL:
256
+ return
257
+
258
+ sql, vars = tx.table("weird_names_table").missing(
259
+ sql_only=True,
260
+ list_=["a", "b", "c"],
261
+ where={
262
+ "sum_info": "value",
263
+ "max_hours": 1,
264
+ },
265
+ )
266
+ expected_sql = """
267
+ SELECT *
268
+ FROM UNNEST('{a,b,c}'::int[]) id
269
+ EXCEPT ALL
270
+ SELECT sys_id
271
+ FROM weird_names_table
272
+ WHERE sum_info = %s
273
+ AND max_hours = %s
274
+ """
275
+ expected_vars = ("value", 1)
276
+ self.assertEqual(
277
+ sql.split(),
278
+ expected_sql.split(),
279
+ )
280
+ self.assertEqual(vars, expected_vars)
281
+
282
+ def test_insert(self, tx):
283
+ if not DO_ALL:
284
+ return
285
+
286
+ sql, vars = tx.table("weird_names_table").insert(
287
+ sql_only=True,
288
+ data={
289
+ "sum_info": "value",
290
+ "max_hours": 1,
291
+ },
292
+ )
293
+ expected_sql = """
294
+ INSERT INTO weird_names_table (sum_info, max_hours)
295
+ VALUES (%s,%s)
296
+ """
297
+ expected_vars = ("value", 1)
298
+ self.assertEqual(
299
+ sql.split(),
300
+ expected_sql.split(),
301
+ )
302
+ self.assertEqual(vars, expected_vars)
303
+
304
+ def test_update(self, tx):
305
+ if not DO_ALL:
306
+ return
307
+
308
+ sql, vars = tx.table("weird_names_table").update(
309
+ sql_only=True,
310
+ data={
311
+ "sum_info": "value",
312
+ "max_hours": 1,
313
+ "order": None,
314
+ },
315
+ where={
316
+ "peekaboo": "value",
317
+ },
318
+ )
319
+ expected_sql = """
320
+ UPDATE weird_names_table
321
+ SET sum_info = %s,
322
+ max_hours = %s,
323
+ "order" = %s
324
+ WHERE peekaboo = %s
325
+ """
326
+ expected_vars = ("value", 1, None, "value")
327
+
328
+ self.assertEqual(
329
+ sql.split(),
330
+ expected_sql.split(),
331
+ )
332
+ self.assertEqual(vars, expected_vars)
333
+
334
+ def test_count(self, tx):
335
+ if not DO_ALL:
336
+ return
337
+
338
+ sql, vars = tx.table("weird_names_table").count(
339
+ sql_only=True,
340
+ where={
341
+ "peekaboo": "value",
342
+ },
343
+ )
344
+ expected_sql = """
345
+ SELECT count(*)
346
+ FROM weird_names_table
347
+ WHERE peekaboo = %s
348
+ """
349
+ expected_vars = ("value",)
350
+
351
+ self.assertEqual(
352
+ sql.split(),
353
+ expected_sql.split(),
354
+ )
355
+ self.assertEqual(vars, expected_vars)
356
+
357
+ def test_extract_column_name(self, tx):
358
+ if not DO_ALL:
359
+ return
360
+
361
+ th = tablehelper.TableHelper(tx, "fk_child")
362
+ column = th.extract_column_name("parent_id>parent_name")
363
+ self.assertEqual(column, "parent_id>parent_name")
364
+ column = th.extract_column_name("count(parent_id>parent_name)")
365
+ self.assertEqual(column, "parent_id>parent_name")
366
+ column = th.extract_column_name("sum(count(parent_id>parent_name))")
367
+ self.assertEqual(column, "parent_id>parent_name")
368
+ column = th.extract_column_name("coalesce(parent_id>parent_name, 0)")
369
+ self.assertEqual(column, "parent_id>parent_name")
370
+
371
+ column = th.extract_column_name("count(pArent_id>pArent_nAme)")
372
+ self.assertEqual(column, "pArent_id>pArent_nAme")
373
+
374
+ column = th.extract_column_name('count("pArent_id>pArent_nAme")')
375
+ self.assertEqual(column, "pArent_id>pArent_nAme")
376
+
377
+ # Ensure balanced parentheses
378
+
379
+ self.assertRaises(
380
+ ValueError,
381
+ th.extract_column_name,
382
+ "sum(count(coalesce(parent_id>parent_name, 0)",
383
+ )
384
+ column = th.extract_column_name(
385
+ "sum(count(coalesce(parent_id>parent_name, 0)))"
386
+ )
387
+ self.assertEqual(column, "parent_id>parent_name")
388
+
389
+ column = th.extract_column_name("!=sum(parent_id>parent_name, 0)")
390
+ self.assertEqual(column, "parent_id>parent_name")
391
+
392
+ for op in th.operators:
393
+ column = th.extract_column_name(f"{op}coalesce(parent_id>parent_name, 0)")
394
+ self.assertEqual(column, "parent_id>parent_name")
395
+
396
+ def test_resolve_references(self, tx):
397
+ if not DO_ALL:
398
+ return
399
+
400
+ th = tablehelper.TableHelper(tx, "fk_child")
401
+ column = th.resolve_references("parent_id>parent_name")
402
+ self.assertEqual(column, "fk_parent.parent_name as parent_id_parent_name")
403
+
404
+ column = th.resolve_references("sum(parent_id>parent_name)")
405
+ self.assertEqual(column, "sum(fk_parent.parent_name) as parent_id_parent_name")
406
+
407
+ column = th.resolve_references('coalesce(parent_id>parent_name, "account")')
408
+ self.assertEqual(
409
+ column,
410
+ 'coalesce(fk_parent.parent_name, "account") as parent_id_parent_name',
411
+ )
412
+
413
+ column = th.resolve_references('>coalesce(parent_id>parent_name, "account")')
414
+ self.assertEqual(
415
+ column,
416
+ 'coalesce(fk_parent.parent_name, "account") as parent_id_parent_name',
417
+ )
418
+
419
+ for op in th.operators:
420
+ column = th.resolve_references(f"{op}coalesce(parent_id>parent_name, 0)")
421
+ self.assertEqual(
422
+ column, f"coalesce(fk_parent.parent_name, 0) as parent_id_parent_name"
423
+ )
424
+
425
+ def test_duplicate_rows(self, tx):
426
+ if not DO_ALL:
427
+ return
428
+
429
+ sql, vars = tx.table("weird_names_table").duplicate_rows(
430
+ sql_only=True, columns=["peekaboo"]
431
+ )
432
+
433
+ expected_sql = """
434
+ SELECT t.*
435
+ FROM weird_names_table t
436
+ JOIN (SELECT peekaboo
437
+ FROM weird_names_table
438
+ GROUP BY peekaboo
439
+ HAVING count(*) > %s) dup
440
+ ON t.peekaboo = dup.peekaboo
441
+ ORDER BY peekaboo
442
+ """
443
+ expected_vars = (1,)
444
+
445
+ self.assertEqual(
446
+ sql.split(),
447
+ expected_sql.split(),
448
+ )
449
+ self.assertEqual(vars, expected_vars)
450
+
451
+ def test_select_strings(self, tx):
452
+ if not DO_ALL:
453
+ return
454
+
455
+ sql, vars = tx.table("weird_names_table").select(
456
+ sql_only=True,
457
+ columns="distinct parent_id, child_id",
458
+ where="where_value = 'value'",
459
+ having="having_value = 'value'",
460
+ )
461
+ expected_sql = """
462
+ SELECT DISTINCT parent_id, child_id
463
+ FROM weird_names_table
464
+ WHERE where_value = 'value'
465
+ HAVING having_value = 'value'
466
+ """
467
+ expected_vars = ()
468
+
469
+ self.assertEqual(
470
+ sql.split(),
471
+ expected_sql.split(),
472
+ )
473
+ self.assertEqual(vars, expected_vars)
474
+
475
+ def test_function_in_predicate(self, tx):
476
+ if not DO_ALL:
477
+ return
478
+ th = tablehelper.TableHelper(tx, "fk_child")
479
+ columns = [
480
+ ('coalesce(parent_id,"")', 'coalesce(parent_id,"")'),
481
+ ('coalesce(parent_name,"")', 'coalesce(parent_name,"")'),
482
+ (
483
+ 'coalesce(parent_id>parent_name,"")',
484
+ 'coalesce(B.parent_name,"") as parent_id_parent_name',
485
+ ),
486
+ ('coalesce(quijibo,"")', 'coalesce(quijibo,"")'),
487
+ ('coalesce(doojibo,"")', 'coalesce(doojibo,"")'),
488
+ ('coalesce(symmetric,"")', 'coalesce("symmetric","")'),
489
+ ('coalesce(table,"")', 'coalesce("table","")'),
490
+ (
491
+ 'coalesce(parent_id>is_valid,"")',
492
+ 'coalesce(B.is_valid,"") as parent_id_is_valid',
493
+ ),
494
+ ('coalesce(sum_info,"")', 'coalesce(sum_info,"")'),
495
+ ('coalesce(max_hours,"")', 'coalesce(max_hours,"")'),
496
+ (
497
+ 'coalesce(parent_id>num_things,"")',
498
+ 'coalesce(B.num_things,"") as parent_id_num_things',
499
+ ),
500
+ (
501
+ 'coalesce(parent_id>symmetric,"")',
502
+ 'coalesce(B."symmetric","") as parent_id_symmetric',
503
+ ),
504
+ (
505
+ 'sum(parent_id>table,"")',
506
+ 'sum(B."table","") as parent_id_table',
507
+ ),
508
+ ]
509
+ for key, val in columns:
510
+ column = th.resolve_references(
511
+ key, options={"alias_table": True, "alias_column": True}
512
+ )
513
+ self.assertEqual(column, val)
514
+
515
+
516
+ if __name__ == "__main__":
517
+ unittest.main()
@@ -0,0 +1,150 @@
1
+ import unittest
2
+ from unittest.mock import Mock, MagicMock
3
+ from velocity.db.core.table import Table
4
+ from velocity.db.core.result import Result
5
+
6
+
7
+ class TestCursorRowCountFix(unittest.TestCase):
8
+ """
9
+ Test cases to verify that table methods handle None cursors gracefully
10
+ when accessing result.cursor.rowcount.
11
+
12
+ This addresses the AttributeError: 'NoneType' object has no attribute 'rowcount'
13
+ issue that can occur when the cursor is None due to connection errors or
14
+ other exceptional conditions.
15
+ """
16
+
17
+ def setUp(self):
18
+ """Set up mock objects for testing."""
19
+ self.mock_tx = Mock()
20
+ self.mock_sql = Mock()
21
+ self.table = Table(self.mock_tx, "test_table")
22
+ self.table.sql = self.mock_sql
23
+
24
+ def test_insert_with_none_cursor(self):
25
+ """Test that insert() returns 0 when result.cursor is None."""
26
+ # Mock the SQL generation
27
+ self.mock_sql.insert.return_value = ("INSERT SQL", ["values"])
28
+
29
+ # Create a mock result with cursor = None
30
+ mock_result = Mock(spec=Result)
31
+ mock_result.cursor = None
32
+
33
+ # Mock the execute method to return our mock result
34
+ self.mock_tx.execute.return_value = mock_result
35
+
36
+ # Mock the cursor method
37
+ mock_cursor = Mock()
38
+ self.table.cursor = Mock(return_value=mock_cursor)
39
+
40
+ # Call insert and verify it returns 0 instead of raising AttributeError
41
+ result = self.table.insert({"test_field": "test_value"})
42
+ self.assertEqual(result, 0)
43
+
44
+ def test_update_with_none_cursor(self):
45
+ """Test that update() returns 0 when result.cursor is None."""
46
+ # Mock the SQL generation
47
+ self.mock_sql.update.return_value = ("UPDATE SQL", ["values"])
48
+
49
+ # Create a mock result with cursor = None
50
+ mock_result = Mock(spec=Result)
51
+ mock_result.cursor = None
52
+
53
+ # Mock the execute method to return our mock result
54
+ self.mock_tx.execute.return_value = mock_result
55
+
56
+ # Mock the cursor method
57
+ mock_cursor = Mock()
58
+ self.table.cursor = Mock(return_value=mock_cursor)
59
+
60
+ # Call update and verify it returns 0 instead of raising AttributeError
61
+ result = self.table.update({"test_field": "new_value"}, where={"id": 1})
62
+ self.assertEqual(result, 0)
63
+
64
+ def test_merge_with_none_cursor(self):
65
+ """Test that merge() returns 0 when result.cursor is None."""
66
+ # Mock the SQL generation
67
+ self.mock_sql.merge.return_value = ("MERGE SQL", ["values"])
68
+
69
+ # Create a mock result with cursor = None
70
+ mock_result = Mock(spec=Result)
71
+ mock_result.cursor = None
72
+
73
+ # Mock the execute method to return our mock result
74
+ self.mock_tx.execute.return_value = mock_result
75
+
76
+ # Mock the cursor method
77
+ mock_cursor = Mock()
78
+ self.table.cursor = Mock(return_value=mock_cursor)
79
+
80
+ # Call merge and verify it returns 0 instead of raising AttributeError
81
+ result = self.table.merge({"test_field": "test_value"})
82
+ self.assertEqual(result, 0)
83
+
84
+ def test_delete_with_none_cursor(self):
85
+ """Test that delete() returns 0 when result.cursor is None."""
86
+ # Mock the SQL generation
87
+ self.mock_sql.delete.return_value = ("DELETE SQL", ["values"])
88
+
89
+ # Create a mock result with cursor = None
90
+ mock_result = Mock(spec=Result)
91
+ mock_result.cursor = None
92
+
93
+ # Mock the execute method to return our mock result
94
+ self.mock_tx.execute.return_value = mock_result
95
+
96
+ # Call delete and verify it returns 0 instead of raising AttributeError
97
+ result = self.table.delete(where={"id": 1})
98
+ self.assertEqual(result, 0)
99
+
100
+ def test_insert_with_valid_cursor(self):
101
+ """Test that insert() returns rowcount when result.cursor is valid."""
102
+ # Mock the SQL generation
103
+ self.mock_sql.insert.return_value = ("INSERT SQL", ["values"])
104
+
105
+ # Create a mock cursor with rowcount
106
+ mock_cursor = Mock()
107
+ mock_cursor.rowcount = 1
108
+
109
+ # Create a mock result with valid cursor
110
+ mock_result = Mock(spec=Result)
111
+ mock_result.cursor = mock_cursor
112
+
113
+ # Mock the execute method to return our mock result
114
+ self.mock_tx.execute.return_value = mock_result
115
+
116
+ # Mock the cursor method
117
+ table_cursor = Mock()
118
+ self.table.cursor = Mock(return_value=table_cursor)
119
+
120
+ # Call insert and verify it returns the actual rowcount
121
+ result = self.table.insert({"test_field": "test_value"})
122
+ self.assertEqual(result, 1)
123
+
124
+ def test_update_with_valid_cursor(self):
125
+ """Test that update() returns rowcount when result.cursor is valid."""
126
+ # Mock the SQL generation
127
+ self.mock_sql.update.return_value = ("UPDATE SQL", ["values"])
128
+
129
+ # Create a mock cursor with rowcount
130
+ mock_cursor = Mock()
131
+ mock_cursor.rowcount = 2
132
+
133
+ # Create a mock result with valid cursor
134
+ mock_result = Mock(spec=Result)
135
+ mock_result.cursor = mock_cursor
136
+
137
+ # Mock the execute method to return our mock result
138
+ self.mock_tx.execute.return_value = mock_result
139
+
140
+ # Mock the cursor method
141
+ table_cursor = Mock()
142
+ self.table.cursor = Mock(return_value=table_cursor)
143
+
144
+ # Call update and verify it returns the actual rowcount
145
+ result = self.table.update({"test_field": "new_value"}, where={"id": 1})
146
+ self.assertEqual(result, 2)
147
+
148
+
149
+ if __name__ == "__main__":
150
+ unittest.main()