velocity-python 0.0.137__tar.gz → 0.0.138__tar.gz

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 (134) hide show
  1. {velocity_python-0.0.137 → velocity_python-0.0.138}/PKG-INFO +1 -1
  2. {velocity_python-0.0.137 → velocity_python-0.0.138}/pyproject.toml +1 -1
  3. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/__init__.py +1 -1
  4. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/core/engine.py +23 -1
  5. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/servers/postgres/sql.py +41 -13
  6. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity_python.egg-info/PKG-INFO +1 -1
  7. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity_python.egg-info/SOURCES.txt +2 -1
  8. velocity_python-0.0.138/tests/test_where_clause_validation.py +198 -0
  9. {velocity_python-0.0.137 → velocity_python-0.0.138}/LICENSE +0 -0
  10. {velocity_python-0.0.137 → velocity_python-0.0.138}/README.md +0 -0
  11. {velocity_python-0.0.137 → velocity_python-0.0.138}/setup.cfg +0 -0
  12. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/app/__init__.py +0 -0
  13. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/app/invoices.py +0 -0
  14. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/app/orders.py +0 -0
  15. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/app/payments.py +0 -0
  16. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/app/purchase_orders.py +0 -0
  17. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/app/tests/__init__.py +0 -0
  18. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/app/tests/test_email_processing.py +0 -0
  19. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/app/tests/test_payment_profile_sorting.py +0 -0
  20. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/app/tests/test_spreadsheet_functions.py +0 -0
  21. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/aws/__init__.py +0 -0
  22. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/aws/amplify.py +0 -0
  23. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/aws/handlers/__init__.py +0 -0
  24. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/aws/handlers/base_handler.py +0 -0
  25. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/aws/handlers/context.py +0 -0
  26. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/aws/handlers/exceptions.py +0 -0
  27. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/aws/handlers/lambda_handler.py +0 -0
  28. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
  29. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/aws/handlers/mixins/activity_tracker.py +0 -0
  30. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/aws/handlers/mixins/error_handler.py +0 -0
  31. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/aws/handlers/mixins/legacy_mixin.py +0 -0
  32. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/aws/handlers/mixins/standard_mixin.py +0 -0
  33. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/aws/handlers/response.py +0 -0
  34. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  35. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/aws/tests/__init__.py +0 -0
  36. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
  37. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/aws/tests/test_response.py +0 -0
  38. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/__init__.py +0 -0
  39. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/core/__init__.py +0 -0
  40. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/core/column.py +0 -0
  41. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/core/database.py +0 -0
  42. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/core/decorators.py +0 -0
  43. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/core/result.py +0 -0
  44. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/core/row.py +0 -0
  45. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/core/sequence.py +0 -0
  46. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/core/table.py +0 -0
  47. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/core/transaction.py +0 -0
  48. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/exceptions.py +0 -0
  49. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/servers/__init__.py +0 -0
  50. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/servers/base/__init__.py +0 -0
  51. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/servers/base/initializer.py +0 -0
  52. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/servers/base/operators.py +0 -0
  53. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/servers/base/sql.py +0 -0
  54. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/servers/base/types.py +0 -0
  55. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/servers/mysql/__init__.py +0 -0
  56. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/servers/mysql/operators.py +0 -0
  57. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/servers/mysql/reserved.py +0 -0
  58. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/servers/mysql/sql.py +0 -0
  59. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/servers/mysql/types.py +0 -0
  60. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/servers/postgres/__init__.py +0 -0
  61. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/servers/postgres/operators.py +0 -0
  62. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/servers/postgres/reserved.py +0 -0
  63. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/servers/postgres/types.py +0 -0
  64. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/servers/sqlite/__init__.py +0 -0
  65. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/servers/sqlite/operators.py +0 -0
  66. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/servers/sqlite/reserved.py +0 -0
  67. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/servers/sqlite/sql.py +0 -0
  68. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/servers/sqlite/types.py +0 -0
  69. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
  70. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/servers/sqlserver/operators.py +0 -0
  71. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
  72. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/servers/sqlserver/sql.py +0 -0
  73. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/servers/sqlserver/types.py +0 -0
  74. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/servers/tablehelper.py +0 -0
  75. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/tests/__init__.py +0 -0
  76. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/tests/common_db_test.py +0 -0
  77. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/tests/postgres/__init__.py +0 -0
  78. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/tests/postgres/common.py +0 -0
  79. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/tests/postgres/test_column.py +0 -0
  80. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/tests/postgres/test_connections.py +0 -0
  81. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/tests/postgres/test_database.py +0 -0
  82. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/tests/postgres/test_engine.py +0 -0
  83. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
  84. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/tests/postgres/test_imports.py +0 -0
  85. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/tests/postgres/test_result.py +0 -0
  86. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/tests/postgres/test_row.py +0 -0
  87. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
  88. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
  89. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
  90. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
  91. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
  92. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/tests/postgres/test_table.py +0 -0
  93. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
  94. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
  95. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/tests/sql/__init__.py +0 -0
  96. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/tests/sql/common.py +0 -0
  97. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
  98. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
  99. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
  100. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/tests/test_db_utils.py +0 -0
  101. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/tests/test_postgres.py +0 -0
  102. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
  103. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
  104. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/tests/test_result_caching.py +0 -0
  105. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
  106. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
  107. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
  108. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
  109. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/tests/test_sql_builder.py +0 -0
  110. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/tests/test_tablehelper.py +0 -0
  111. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/db/utils.py +0 -0
  112. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/misc/__init__.py +0 -0
  113. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/misc/conv/__init__.py +0 -0
  114. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/misc/conv/iconv.py +0 -0
  115. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/misc/conv/oconv.py +0 -0
  116. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/misc/db.py +0 -0
  117. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/misc/export.py +0 -0
  118. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/misc/format.py +0 -0
  119. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/misc/mail.py +0 -0
  120. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/misc/merge.py +0 -0
  121. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/misc/tests/__init__.py +0 -0
  122. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/misc/tests/test_db.py +0 -0
  123. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/misc/tests/test_fix.py +0 -0
  124. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/misc/tests/test_format.py +0 -0
  125. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/misc/tests/test_iconv.py +0 -0
  126. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/misc/tests/test_merge.py +0 -0
  127. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/misc/tests/test_oconv.py +0 -0
  128. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/misc/tests/test_original_error.py +0 -0
  129. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/misc/tests/test_timer.py +0 -0
  130. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/misc/timer.py +0 -0
  131. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity/misc/tools.py +0 -0
  132. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  133. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity_python.egg-info/requires.txt +0 -0
  134. {velocity_python-0.0.137 → velocity_python-0.0.138}/src/velocity_python.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.0.137
3
+ Version: 0.0.138
4
4
  Summary: A rapid application development library for interfacing with data storage
5
5
  Author-email: Velocity Team <info@codeclubs.org>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "velocity-python"
7
- version = "0.0.137"
7
+ version = "0.0.138"
8
8
  authors = [
9
9
  { name="Velocity Team", email="info@codeclubs.org" },
10
10
  ]
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.0.137"
1
+ __version__ = version = "0.0.138"
2
2
 
3
3
  from . import aws
4
4
  from . import db
@@ -367,12 +367,34 @@ class Engine:
367
367
 
368
368
  msg = str(exception).strip().lower()
369
369
 
370
- # Create enhanced error message with SQL query
370
+ # Create enhanced error message with SQL query and context
371
371
  enhanced_message = str(exception)
372
+
373
+ # Add specific guidance for common WHERE clause errors
374
+ exception_str_lower = str(exception).lower()
375
+ if "argument of where must be type boolean" in exception_str_lower:
376
+ enhanced_message += (
377
+ "\n\n*** WHERE CLAUSE ERROR ***\n"
378
+ "This error typically occurs when a WHERE clause contains a bare value "
379
+ "instead of a proper boolean expression.\n"
380
+ "Common fixes:\n"
381
+ " - Change WHERE 1001 to WHERE sys_id = 1001\n"
382
+ " - Change WHERE {'column': value} format in dictionaries\n"
383
+ " - Ensure string WHERE clauses are complete SQL expressions"
384
+ )
385
+
372
386
  if sql:
373
387
  enhanced_message += (
374
388
  f"\n\nSQL Query:\n{self._format_sql_with_params(sql, parameters)}"
375
389
  )
390
+
391
+ # Add call stack context for better debugging
392
+ import traceback
393
+ stack_trace = traceback.format_stack()
394
+ # Get the last few frames that aren't in the error handling itself
395
+ relevant_frames = [frame for frame in stack_trace if 'process_error' not in frame and 'logging' not in frame][-3:]
396
+ if relevant_frames:
397
+ enhanced_message += "\n\nCall Context:\n" + "".join(relevant_frames)
376
398
 
377
399
  # Format SQL for logging
378
400
  formatted_sql_info = ""
@@ -69,7 +69,7 @@ class SQL(BaseSQLDialect):
69
69
 
70
70
  default_schema = "public"
71
71
 
72
- ApplicationErrorCodes = ["22P02", "42883", "42501", "42601", "25P01", "25P02"]
72
+ ApplicationErrorCodes = ["22P02", "42883", "42501", "42601", "25P01", "25P02", "42804"] # Added 42804 for datatype mismatch
73
73
 
74
74
  DatabaseMissingErrorCodes = ["3D000"]
75
75
  TableMissingErrorCodes = ["42P01"]
@@ -296,23 +296,37 @@ class SQL(BaseSQLDialect):
296
296
  else:
297
297
  sql_parts["FROM"].append(TableHelper.quote(table))
298
298
 
299
- # WHERE
299
+ # WHERE - Enhanced validation to prevent malformed SQL
300
300
  if where:
301
301
  if isinstance(where, str):
302
302
  # Validate string WHERE clauses to prevent malformed SQL
303
- if where.strip().isdigit() or where.strip() in ('True', 'False'):
303
+ where_stripped = where.strip()
304
+ if not where_stripped:
305
+ raise ValueError("WHERE clause cannot be empty string.")
306
+ # Check for boolean literals first (includes '1' and '0')
307
+ if where_stripped in ('True', 'False', '1', '0'):
304
308
  raise ValueError(
305
309
  f"Invalid WHERE clause: '{where}'. "
306
- "String WHERE clauses must be complete SQL expressions, "
307
- "not bare values. Use a dictionary like {{'column': value}} instead."
310
+ "Boolean literals alone are not valid WHERE clauses. "
311
+ "Use complete SQL expressions like 'sys_active = true' instead."
312
+ )
313
+ # Then check for other numeric values (excluding '1' and '0' already handled above)
314
+ elif where_stripped.isdigit():
315
+ raise ValueError(
316
+ f"Invalid WHERE clause: '{where}'. "
317
+ "Bare integers are not valid WHERE clauses. "
318
+ "Use a dictionary like {{'sys_id': {where_stripped}}} or "
319
+ f"a complete SQL expression like 'sys_id = {where_stripped}' instead."
308
320
  )
309
321
  sql_parts["WHERE"].append(where)
310
322
  elif isinstance(where, (int, float, bool)):
311
323
  # Handle primitive types that should be converted to proper WHERE clauses
324
+ suggested_fix = "{'sys_id': " + str(where) + "}" if isinstance(where, int) else "complete SQL expression"
312
325
  raise ValueError(
313
326
  f"Invalid WHERE clause: {where} (type: {type(where).__name__}). "
314
- "WHERE clauses must be dictionaries like {{'column': value}}, "
315
- "lists of tuples, or complete SQL strings."
327
+ f"Primitive values cannot be WHERE clauses directly. "
328
+ f"Use a dictionary like {suggested_fix} or a complete SQL string instead. "
329
+ f"This error prevents PostgreSQL 'argument of WHERE must be type boolean' errors."
316
330
  )
317
331
  elif isinstance(where, Mapping):
318
332
  # Convert dictionary to predicate list
@@ -473,20 +487,34 @@ class SQL(BaseSQLDialect):
473
487
  new_where.append(th.make_predicate(key, val))
474
488
  where = new_where
475
489
  elif isinstance(where, str):
476
- # Validate string WHERE clauses to prevent malformed SQL
477
- if where.strip().isdigit() or where.strip() in ('True', 'False'):
490
+ # Enhanced validation for string WHERE clauses
491
+ where_stripped = where.strip()
492
+ if not where_stripped:
493
+ raise ValueError("WHERE clause cannot be empty string.")
494
+ # Check for boolean literals first (includes '1' and '0')
495
+ if where_stripped in ('True', 'False', '1', '0'):
496
+ raise ValueError(
497
+ f"Invalid WHERE clause: '{where}'. "
498
+ "Boolean literals alone are not valid WHERE clauses. "
499
+ "Use complete SQL expressions like 'sys_active = true' instead."
500
+ )
501
+ # Then check for other numeric values (excluding '1' and '0' already handled above)
502
+ elif where_stripped.isdigit():
478
503
  raise ValueError(
479
504
  f"Invalid WHERE clause: '{where}'. "
480
- "String WHERE clauses must be complete SQL expressions, "
481
- "not bare values. Use a dictionary like {{'column': value}} instead."
505
+ "Bare integers are not valid WHERE clauses. "
506
+ f"Use a dictionary like {{'sys_id': {where_stripped}}} or "
507
+ f"a complete SQL expression like 'sys_id = {where_stripped}' instead."
482
508
  )
483
509
  where_clauses.append(where)
484
510
  elif isinstance(where, (int, float, bool)):
485
511
  # Handle primitive types that should be converted to proper WHERE clauses
512
+ suggested_fix = "{'sys_id': " + str(where) + "}" if isinstance(where, int) else "complete SQL expression"
486
513
  raise ValueError(
487
514
  f"Invalid WHERE clause: {where} (type: {type(where).__name__}). "
488
- "WHERE clauses must be dictionaries like {{'column': value}}, "
489
- "lists of tuples, or complete SQL strings."
515
+ f"Primitive values cannot be WHERE clauses directly. "
516
+ f"Use a dictionary like {suggested_fix} or a complete SQL string instead. "
517
+ f"This error prevents PostgreSQL 'argument of WHERE must be type boolean' errors."
490
518
  )
491
519
 
492
520
  # Process the where clause if it's a list of tuples
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.0.137
3
+ Version: 0.0.138
4
4
  Summary: A rapid application development library for interfacing with data storage
5
5
  Author-email: Velocity Team <info@codeclubs.org>
6
6
  License-Expression: MIT
@@ -128,4 +128,5 @@ src/velocity_python.egg-info/PKG-INFO
128
128
  src/velocity_python.egg-info/SOURCES.txt
129
129
  src/velocity_python.egg-info/dependency_links.txt
130
130
  src/velocity_python.egg-info/requires.txt
131
- src/velocity_python.egg-info/top_level.txt
131
+ src/velocity_python.egg-info/top_level.txt
132
+ tests/test_where_clause_validation.py
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test cases for WHERE clause validation improvements.
4
+ Tests the fixes for the PostgreSQL 'argument of WHERE must be type boolean' error.
5
+ """
6
+
7
+ import unittest
8
+ from unittest.mock import Mock, MagicMock
9
+ from velocity.db.servers.postgres.sql import SQL
10
+ from velocity.db import exceptions
11
+
12
+
13
+ class TestWhereClauseValidation(unittest.TestCase):
14
+ """Test WHERE clause validation improvements."""
15
+
16
+ def setUp(self):
17
+ """Set up test fixtures."""
18
+ self.mock_tx = Mock()
19
+ self.mock_tx.table.return_value.primary_keys.return_value = ['sys_id']
20
+
21
+ # Mock TableHelper methods
22
+ self.mock_helper = Mock()
23
+ self.mock_helper.resolve_references.return_value = "test_column"
24
+ self.mock_helper.make_predicate.return_value = ("test_column = %s", 123)
25
+ self.mock_helper.get_table_alias.return_value = "t1"
26
+ self.mock_helper.foreign_keys = {}
27
+ self.mock_helper.split_columns.return_value = ["column1", "column2"]
28
+
29
+ # Patch TableHelper creation
30
+ import velocity.db.servers.postgres.sql as sql_module
31
+ sql_module._get_table_helper = Mock(return_value=self.mock_helper)
32
+
33
+ def test_invalid_where_bare_integer(self):
34
+ """Test that bare integers in WHERE clauses are rejected with helpful error."""
35
+ with self.assertRaises(ValueError) as cm:
36
+ SQL.select(self.mock_tx, table="test_table", where=1001)
37
+
38
+ error_msg = str(cm.exception)
39
+ self.assertIn("Invalid WHERE clause: 1001", error_msg)
40
+ self.assertIn("Primitive values cannot be WHERE clauses directly", error_msg)
41
+ self.assertIn("{'sys_id': 1001}", error_msg)
42
+ self.assertIn("PostgreSQL 'argument of WHERE must be type boolean' errors", error_msg)
43
+
44
+ def test_invalid_where_string_integer(self):
45
+ """Test that string integers in WHERE clauses are rejected."""
46
+ with self.assertRaises(ValueError) as cm:
47
+ SQL.select(self.mock_tx, table="test_table", where="1001")
48
+
49
+ error_msg = str(cm.exception)
50
+ self.assertIn("Invalid WHERE clause: '1001'", error_msg)
51
+ self.assertIn("Bare integers are not valid WHERE clauses", error_msg)
52
+ self.assertIn("sys_id = 1001", error_msg)
53
+
54
+ def test_invalid_where_boolean_literal(self):
55
+ """Test that boolean literals in WHERE clauses are rejected."""
56
+ test_cases = ["True", "False", "1", "0"]
57
+
58
+ for bool_val in test_cases:
59
+ with self.subTest(bool_val=bool_val):
60
+ with self.assertRaises(ValueError) as cm:
61
+ SQL.select(self.mock_tx, table="test_table", where=bool_val)
62
+
63
+ error_msg = str(cm.exception)
64
+ self.assertIn(f"Invalid WHERE clause: '{bool_val}'", error_msg)
65
+ self.assertIn("Boolean literals alone are not valid WHERE clauses", error_msg)
66
+
67
+ def test_invalid_where_empty_string(self):
68
+ """Test that empty string WHERE clauses are rejected."""
69
+ with self.assertRaises(ValueError) as cm:
70
+ SQL.select(self.mock_tx, table="test_table", where=" ")
71
+
72
+ error_msg = str(cm.exception)
73
+ self.assertIn("WHERE clause cannot be empty string", error_msg)
74
+
75
+ def test_invalid_where_float(self):
76
+ """Test that float values in WHERE clauses are rejected."""
77
+ with self.assertRaises(ValueError) as cm:
78
+ SQL.select(self.mock_tx, table="test_table", where=123.45)
79
+
80
+ error_msg = str(cm.exception)
81
+ self.assertIn("Invalid WHERE clause: 123.45", error_msg)
82
+ self.assertIn("type: float", error_msg)
83
+
84
+ def test_invalid_where_boolean(self):
85
+ """Test that boolean values in WHERE clauses are rejected."""
86
+ with self.assertRaises(ValueError) as cm:
87
+ SQL.select(self.mock_tx, table="test_table", where=True)
88
+
89
+ error_msg = str(cm.exception)
90
+ self.assertIn("Invalid WHERE clause: True", error_msg)
91
+ self.assertIn("type: bool", error_msg)
92
+
93
+ def test_valid_where_dictionary(self):
94
+ """Test that dictionary WHERE clauses work correctly."""
95
+ try:
96
+ sql, params = SQL.select(
97
+ self.mock_tx,
98
+ table="test_table",
99
+ where={"sys_id": 1001}
100
+ )
101
+ # Should not raise an exception
102
+ self.assertIsInstance(sql, str)
103
+ self.assertIsInstance(params, tuple)
104
+ except ValueError as e:
105
+ self.fail(f"Valid dictionary WHERE clause raised ValueError: {e}")
106
+
107
+ def test_valid_where_complete_sql_string(self):
108
+ """Test that complete SQL string WHERE clauses work correctly."""
109
+ try:
110
+ sql, params = SQL.select(
111
+ self.mock_tx,
112
+ table="test_table",
113
+ where="sys_id = 1001 AND sys_active = true"
114
+ )
115
+ # Should not raise an exception
116
+ self.assertIsInstance(sql, str)
117
+ self.assertIsInstance(params, tuple)
118
+ except ValueError as e:
119
+ self.fail(f"Valid SQL string WHERE clause raised ValueError: {e}")
120
+
121
+ def test_update_where_validation(self):
122
+ """Test that UPDATE method has the same WHERE validation."""
123
+ with self.assertRaises(ValueError) as cm:
124
+ SQL.update(
125
+ self.mock_tx,
126
+ table="test_table",
127
+ data={"name": "test"},
128
+ where=1001
129
+ )
130
+
131
+ error_msg = str(cm.exception)
132
+ self.assertIn("Invalid WHERE clause: 1001", error_msg)
133
+ self.assertIn("PostgreSQL 'argument of WHERE must be type boolean' errors", error_msg)
134
+
135
+ def test_update_where_string_integer(self):
136
+ """Test UPDATE method rejects string integers."""
137
+ with self.assertRaises(ValueError) as cm:
138
+ SQL.update(
139
+ self.mock_tx,
140
+ table="test_table",
141
+ data={"name": "test"},
142
+ where="999"
143
+ )
144
+
145
+ error_msg = str(cm.exception)
146
+ self.assertIn("Invalid WHERE clause: '999'", error_msg)
147
+ self.assertIn("Bare integers are not valid WHERE clauses", error_msg)
148
+
149
+
150
+ class TestEnhancedErrorMessages(unittest.TestCase):
151
+ """Test enhanced error message functionality."""
152
+
153
+ def test_datatype_mismatch_error_enhancement(self):
154
+ """Test that datatype mismatch errors get enhanced messages."""
155
+ from velocity.db.core.engine import Engine
156
+
157
+ # Create a mock exception that simulates the PostgreSQL error
158
+ mock_exception = Exception("argument of WHERE must be type boolean, not type integer")
159
+
160
+ # Create an engine instance with mocked dependencies
161
+ mock_driver = Mock()
162
+ mock_config = Mock()
163
+ mock_sql = Mock()
164
+ # Set up the error code to trigger ApplicationError path
165
+ mock_sql.get_error.return_value = ("42804", "datatype mismatch")
166
+ mock_sql.ApplicationErrorCodes = ["42804"]
167
+ # Add all the other error code lists that the engine checks
168
+ mock_sql.ColumnMissingErrorCodes = []
169
+ mock_sql.TableMissingErrorCodes = []
170
+ mock_sql.DatabaseMissingErrorCodes = []
171
+ mock_sql.ForeignKeyMissingErrorCodes = []
172
+ mock_sql.TruncationErrorCodes = []
173
+ mock_sql.DataIntegrityErrorCodes = []
174
+ mock_sql.ConnectionErrorCodes = []
175
+ mock_sql.DuplicateKeyErrorCodes = []
176
+ mock_sql.DatabaseObjectExistsErrorCodes = []
177
+ mock_sql.LockTimeoutErrorCodes = []
178
+ mock_sql.RetryTransactionCodes = []
179
+
180
+ engine = Engine(mock_driver, mock_config, mock_sql)
181
+
182
+ # Mock the _format_sql_with_params method
183
+ engine._format_sql_with_params = Mock(return_value="SELECT * FROM test WHERE 1001")
184
+
185
+ # Test that the error gets enhanced
186
+ with self.assertRaises(exceptions.DbApplicationError) as cm:
187
+ engine.process_error(mock_exception, "SELECT * FROM test WHERE 1001", [])
188
+
189
+ error_msg = str(cm.exception)
190
+ # Verify the enhanced error message contains our WHERE clause help
191
+ self.assertIn("*** WHERE CLAUSE ERROR ***", error_msg)
192
+ self.assertIn("WHERE 1001 to WHERE sys_id = 1001", error_msg)
193
+ self.assertIn("SQL Query:", error_msg)
194
+ self.assertIn("Call Context:", error_msg)
195
+
196
+
197
+ if __name__ == '__main__':
198
+ unittest.main()