velocity-python 0.0.109__py3-none-any.whl → 0.0.161__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.
Files changed (120) hide show
  1. velocity/__init__.py +3 -1
  2. velocity/app/orders.py +3 -4
  3. velocity/app/tests/__init__.py +1 -0
  4. velocity/app/tests/test_email_processing.py +112 -0
  5. velocity/app/tests/test_payment_profile_sorting.py +191 -0
  6. velocity/app/tests/test_spreadsheet_functions.py +124 -0
  7. velocity/aws/__init__.py +3 -0
  8. velocity/aws/amplify.py +10 -6
  9. velocity/aws/handlers/__init__.py +2 -0
  10. velocity/aws/handlers/base_handler.py +248 -0
  11. velocity/aws/handlers/context.py +251 -2
  12. velocity/aws/handlers/exceptions.py +16 -0
  13. velocity/aws/handlers/lambda_handler.py +24 -85
  14. velocity/aws/handlers/mixins/__init__.py +16 -0
  15. velocity/aws/handlers/mixins/activity_tracker.py +181 -0
  16. velocity/aws/handlers/mixins/aws_session_mixin.py +192 -0
  17. velocity/aws/handlers/mixins/error_handler.py +192 -0
  18. velocity/aws/handlers/mixins/legacy_mixin.py +53 -0
  19. velocity/aws/handlers/mixins/standard_mixin.py +73 -0
  20. velocity/aws/handlers/response.py +1 -1
  21. velocity/aws/handlers/sqs_handler.py +28 -143
  22. velocity/aws/tests/__init__.py +1 -0
  23. velocity/aws/tests/test_lambda_handler_json_serialization.py +120 -0
  24. velocity/aws/tests/test_response.py +163 -0
  25. velocity/db/__init__.py +16 -4
  26. velocity/db/core/decorators.py +48 -13
  27. velocity/db/core/engine.py +187 -840
  28. velocity/db/core/result.py +33 -25
  29. velocity/db/core/row.py +15 -3
  30. velocity/db/core/table.py +493 -50
  31. velocity/db/core/transaction.py +28 -15
  32. velocity/db/exceptions.py +42 -18
  33. velocity/db/servers/base/__init__.py +9 -0
  34. velocity/db/servers/base/initializer.py +70 -0
  35. velocity/db/servers/base/operators.py +98 -0
  36. velocity/db/servers/base/sql.py +503 -0
  37. velocity/db/servers/base/types.py +135 -0
  38. velocity/db/servers/mysql/__init__.py +73 -0
  39. velocity/db/servers/mysql/operators.py +54 -0
  40. velocity/db/servers/{mysql_reserved.py → mysql/reserved.py} +2 -14
  41. velocity/db/servers/mysql/sql.py +718 -0
  42. velocity/db/servers/mysql/types.py +107 -0
  43. velocity/db/servers/postgres/__init__.py +59 -11
  44. velocity/db/servers/postgres/operators.py +34 -0
  45. velocity/db/servers/postgres/sql.py +474 -120
  46. velocity/db/servers/postgres/types.py +88 -2
  47. velocity/db/servers/sqlite/__init__.py +61 -0
  48. velocity/db/servers/sqlite/operators.py +52 -0
  49. velocity/db/servers/sqlite/reserved.py +20 -0
  50. velocity/db/servers/sqlite/sql.py +677 -0
  51. velocity/db/servers/sqlite/types.py +92 -0
  52. velocity/db/servers/sqlserver/__init__.py +73 -0
  53. velocity/db/servers/sqlserver/operators.py +47 -0
  54. velocity/db/servers/sqlserver/reserved.py +32 -0
  55. velocity/db/servers/sqlserver/sql.py +805 -0
  56. velocity/db/servers/sqlserver/types.py +114 -0
  57. velocity/db/servers/tablehelper.py +117 -91
  58. velocity/db/tests/__init__.py +1 -0
  59. velocity/db/tests/common_db_test.py +0 -0
  60. velocity/db/tests/postgres/__init__.py +1 -0
  61. velocity/db/tests/postgres/common.py +49 -0
  62. velocity/db/tests/postgres/test_column.py +29 -0
  63. velocity/db/tests/postgres/test_connections.py +25 -0
  64. velocity/db/tests/postgres/test_database.py +21 -0
  65. velocity/db/tests/postgres/test_engine.py +205 -0
  66. velocity/db/tests/postgres/test_general_usage.py +88 -0
  67. velocity/db/tests/postgres/test_imports.py +8 -0
  68. velocity/db/tests/postgres/test_result.py +19 -0
  69. velocity/db/tests/postgres/test_row.py +137 -0
  70. velocity/db/tests/postgres/test_row_comprehensive.py +720 -0
  71. velocity/db/tests/postgres/test_schema_locking.py +335 -0
  72. velocity/db/tests/postgres/test_schema_locking_unit.py +115 -0
  73. velocity/db/tests/postgres/test_sequence.py +34 -0
  74. velocity/db/tests/postgres/test_sql_comprehensive.py +462 -0
  75. velocity/db/tests/postgres/test_table.py +101 -0
  76. velocity/db/tests/postgres/test_table_comprehensive.py +646 -0
  77. velocity/db/tests/postgres/test_transaction.py +106 -0
  78. velocity/db/tests/sql/__init__.py +1 -0
  79. velocity/db/tests/sql/common.py +177 -0
  80. velocity/db/tests/sql/test_postgres_select_advanced.py +285 -0
  81. velocity/db/tests/sql/test_postgres_select_variances.py +517 -0
  82. velocity/db/tests/test_cursor_rowcount_fix.py +150 -0
  83. velocity/db/tests/test_db_utils.py +270 -0
  84. velocity/db/tests/test_postgres.py +448 -0
  85. velocity/db/tests/test_postgres_unchanged.py +81 -0
  86. velocity/db/tests/test_process_error_robustness.py +292 -0
  87. velocity/db/tests/test_result_caching.py +279 -0
  88. velocity/db/tests/test_result_sql_aware.py +117 -0
  89. velocity/db/tests/test_row_get_missing_column.py +72 -0
  90. velocity/db/tests/test_schema_locking_initializers.py +226 -0
  91. velocity/db/tests/test_schema_locking_simple.py +97 -0
  92. velocity/db/tests/test_sql_builder.py +165 -0
  93. velocity/db/tests/test_tablehelper.py +486 -0
  94. velocity/db/utils.py +129 -51
  95. velocity/misc/conv/__init__.py +2 -0
  96. velocity/misc/conv/iconv.py +5 -4
  97. velocity/misc/export.py +1 -4
  98. velocity/misc/merge.py +1 -1
  99. velocity/misc/tests/__init__.py +1 -0
  100. velocity/misc/tests/test_db.py +90 -0
  101. velocity/misc/tests/test_fix.py +78 -0
  102. velocity/misc/tests/test_format.py +64 -0
  103. velocity/misc/tests/test_iconv.py +203 -0
  104. velocity/misc/tests/test_merge.py +82 -0
  105. velocity/misc/tests/test_oconv.py +144 -0
  106. velocity/misc/tests/test_original_error.py +52 -0
  107. velocity/misc/tests/test_timer.py +74 -0
  108. velocity/misc/tools.py +0 -1
  109. {velocity_python-0.0.109.dist-info → velocity_python-0.0.161.dist-info}/METADATA +2 -2
  110. velocity_python-0.0.161.dist-info/RECORD +129 -0
  111. velocity/db/core/exceptions.py +0 -70
  112. velocity/db/servers/mysql.py +0 -641
  113. velocity/db/servers/sqlite.py +0 -968
  114. velocity/db/servers/sqlite_reserved.py +0 -208
  115. velocity/db/servers/sqlserver.py +0 -921
  116. velocity/db/servers/sqlserver_reserved.py +0 -314
  117. velocity_python-0.0.109.dist-info/RECORD +0 -56
  118. {velocity_python-0.0.109.dist-info → velocity_python-0.0.161.dist-info}/WHEEL +0 -0
  119. {velocity_python-0.0.109.dist-info → velocity_python-0.0.161.dist-info}/licenses/LICENSE +0 -0
  120. {velocity_python-0.0.109.dist-info → velocity_python-0.0.161.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,114 @@
1
+ import decimal
2
+ import datetime
3
+ from ..base.types import BaseTypes
4
+
5
+
6
+ class TYPES(BaseTypes):
7
+ """
8
+ SQL Server-specific type mapping implementation.
9
+ """
10
+
11
+ TEXT = "NVARCHAR(MAX)"
12
+ VARCHAR = "VARCHAR"
13
+ NVARCHAR = "NVARCHAR"
14
+ INTEGER = "INT"
15
+ BIGINT = "BIGINT"
16
+ SMALLINT = "SMALLINT"
17
+ TINYINT = "TINYINT"
18
+ NUMERIC = "DECIMAL"
19
+ DECIMAL = "DECIMAL"
20
+ FLOAT = "FLOAT"
21
+ REAL = "REAL"
22
+ MONEY = "MONEY"
23
+ DATETIME = "DATETIME"
24
+ DATETIME2 = "DATETIME2"
25
+ DATE = "DATE"
26
+ TIME = "TIME"
27
+ TIMESTAMP = "ROWVERSION"
28
+ BOOLEAN = "BIT"
29
+ BINARY = "VARBINARY(MAX)"
30
+ UNIQUEIDENTIFIER = "UNIQUEIDENTIFIER"
31
+
32
+ @classmethod
33
+ def get_type(cls, v):
34
+ """
35
+ Returns a suitable SQL type string for a Python value/object (SQL Server).
36
+ """
37
+ is_special, special_val = cls._handle_special_values(v)
38
+ if is_special:
39
+ return special_val
40
+
41
+ if isinstance(v, str) or v is str:
42
+ return cls.TEXT
43
+ if isinstance(v, bool) or v is bool:
44
+ return cls.BOOLEAN
45
+ if isinstance(v, int) or v is int:
46
+ return cls.BIGINT
47
+ if isinstance(v, float) or v is float:
48
+ return f"{cls.DECIMAL}(19, 6)"
49
+ if isinstance(v, decimal.Decimal) or v is decimal.Decimal:
50
+ return f"{cls.DECIMAL}(19, 6)"
51
+ if isinstance(v, datetime.datetime) or v is datetime.datetime:
52
+ return cls.DATETIME2
53
+ if isinstance(v, datetime.date) or v is datetime.date:
54
+ return cls.DATE
55
+ if isinstance(v, datetime.time) or v is datetime.time:
56
+ return cls.TIME
57
+ if isinstance(v, bytes) or v is bytes:
58
+ return cls.BINARY
59
+ return cls.TEXT
60
+
61
+ @classmethod
62
+ def get_conv(cls, v):
63
+ """
64
+ Returns a base SQL type for expression usage (SQL Server).
65
+ """
66
+ is_special, special_val = cls._handle_special_values(v)
67
+ if is_special:
68
+ return special_val
69
+
70
+ if isinstance(v, str) or v is str:
71
+ return cls.NVARCHAR
72
+ if isinstance(v, bool) or v is bool:
73
+ return cls.BOOLEAN
74
+ if isinstance(v, int) or v is int:
75
+ return cls.BIGINT
76
+ if isinstance(v, float) or v is float:
77
+ return cls.DECIMAL
78
+ if isinstance(v, decimal.Decimal) or v is decimal.Decimal:
79
+ return cls.DECIMAL
80
+ if isinstance(v, datetime.datetime) or v is datetime.datetime:
81
+ return cls.DATETIME2
82
+ if isinstance(v, datetime.date) or v is datetime.date:
83
+ return cls.DATE
84
+ if isinstance(v, datetime.time) or v is datetime.time:
85
+ return cls.TIME
86
+ if isinstance(v, bytes) or v is bytes:
87
+ return cls.BINARY
88
+ return cls.NVARCHAR
89
+
90
+ @classmethod
91
+ def py_type(cls, v):
92
+ """
93
+ Returns the Python type that corresponds to an SQL type string (SQL Server).
94
+ """
95
+ v = str(v).upper()
96
+ if v in (cls.INTEGER, cls.SMALLINT, cls.BIGINT, cls.TINYINT):
97
+ return int
98
+ if v in (cls.NUMERIC, cls.DECIMAL, cls.MONEY) or "DECIMAL" in v:
99
+ return decimal.Decimal
100
+ if v in (cls.FLOAT, cls.REAL):
101
+ return float
102
+ if v in (cls.TEXT, cls.VARCHAR, cls.NVARCHAR) or "VARCHAR" in v or "CHAR" in v:
103
+ return str
104
+ if v == cls.BOOLEAN or v == "BIT":
105
+ return bool
106
+ if v == cls.DATE:
107
+ return datetime.date
108
+ if v == cls.TIME:
109
+ return datetime.time
110
+ if v in (cls.DATETIME, cls.DATETIME2):
111
+ return datetime.datetime
112
+ if v == cls.BINARY or "BINARY" in v:
113
+ return bytes
114
+ raise Exception(f"Unmapped SQL Server type {v}")
@@ -9,21 +9,21 @@ class TableHelper:
9
9
  """
10
10
  A helper class used to build SQL queries with joined/aliased tables,
11
11
  including foreign key expansions, pointer syntax, etc.
12
-
12
+
13
13
  This class is database-agnostic. Database-specific reserved words and operators
14
14
  should be set as class attributes by the database implementation modules.
15
15
  """
16
16
 
17
17
  # Reserved words that need quoting - set by database implementation
18
18
  reserved = []
19
-
20
- # SQL operators with their symbols - set by database implementation
19
+
20
+ # SQL operators with their symbols - set by database implementation
21
21
  operators = {}
22
22
 
23
23
  def __init__(self, tx, table: str):
24
24
  """
25
25
  Initialize TableHelper.
26
-
26
+
27
27
  Args:
28
28
  tx: Database transaction object
29
29
  table: The main table name for this query
@@ -44,20 +44,20 @@ class TableHelper:
44
44
  def split_columns(self, query: str) -> List[str]:
45
45
  """
46
46
  Splits a string of comma-separated column expressions into a list, keeping parentheses balanced.
47
-
47
+
48
48
  Args:
49
49
  query: Comma-separated column expression string
50
-
50
+
51
51
  Returns:
52
52
  List of individual column expressions
53
53
  """
54
54
  if not isinstance(query, str):
55
55
  raise TypeError(f"Query must be a string, got {type(query)}")
56
-
56
+
57
57
  columns = []
58
58
  balance = 0
59
59
  current = []
60
-
60
+
61
61
  for char in query:
62
62
  if char == "," and balance == 0:
63
63
  column = "".join(current).strip()
@@ -70,13 +70,13 @@ class TableHelper:
70
70
  elif char == ")":
71
71
  balance -= 1
72
72
  current.append(char)
73
-
73
+
74
74
  # Add the last column
75
75
  if current:
76
76
  column = "".join(current).strip()
77
77
  if column:
78
78
  columns.append(column)
79
-
79
+
80
80
  return columns
81
81
 
82
82
  def requires_joins(self) -> bool:
@@ -86,65 +86,67 @@ class TableHelper:
86
86
  def has_pointer(self, column: str) -> bool:
87
87
  """
88
88
  Checks if there's an '>' in the column that indicates a pointer reference, e.g. 'local_column>foreign_column'.
89
-
89
+
90
90
  Args:
91
91
  column: The column string to check
92
-
92
+
93
93
  Returns:
94
94
  bool: True if column contains pointer syntax
95
-
95
+
96
96
  Raises:
97
97
  ValueError: If column format is invalid
98
98
  """
99
99
  if not isinstance(column, str):
100
100
  raise ValueError(f"Column must be a string, got {type(column)}")
101
-
101
+
102
102
  if not re.search(r"^[a-zA-Z0-9_>*]", column):
103
103
  raise ValueError(f"Invalid column specified: {column}")
104
-
104
+
105
105
  return bool(re.search(r"[a-zA-Z0-9_]+>[a-zA-Z0-9_]+", column))
106
106
 
107
107
  def __fetch_foreign_data(self, key: str) -> Dict[str, Any]:
108
108
  """
109
109
  Fetch foreign key information for a given key.
110
-
110
+
111
111
  Args:
112
112
  key: The foreign key string in format 'local_column>foreign_column'
113
-
113
+
114
114
  Returns:
115
115
  Dict containing foreign key metadata
116
-
116
+
117
117
  Raises:
118
118
  ValueError: If foreign key is not properly defined
119
119
  """
120
120
  if key in self.foreign_keys:
121
121
  return self.foreign_keys[key]
122
-
122
+
123
123
  if ">" not in key:
124
- raise ValueError(f"Invalid foreign key format: {key}. Expected 'local>foreign'")
125
-
124
+ raise ValueError(
125
+ f"Invalid foreign key format: {key}. Expected 'local>foreign'"
126
+ )
127
+
126
128
  local_column, foreign_column = key.split(">", 1) # Split only on first >
127
-
129
+
128
130
  try:
129
131
  foreign = self.tx.table(self.current_table).foreign_key_info(local_column)
130
132
  except Exception as e:
131
133
  raise ValueError(f"Error fetching foreign key info for {local_column}: {e}")
132
-
134
+
133
135
  if not foreign:
134
136
  raise ValueError(
135
137
  f"Foreign key `{self.current_table}.{local_column}>{foreign_column}` not defined."
136
138
  )
137
-
139
+
138
140
  ref_table = foreign["referenced_table_name"]
139
141
  ref_schema = foreign["referenced_table_schema"]
140
142
  ref_column = foreign["referenced_column_name"]
141
-
143
+
142
144
  if ref_table not in self.table_aliases:
143
145
  if self.letter > 90: # Z is ASCII 90
144
146
  raise ValueError("Too many table aliases - limit of 26 tables exceeded")
145
147
  self.table_aliases[ref_table] = chr(self.letter)
146
148
  self.letter += 1
147
-
149
+
148
150
  alias = self.table_aliases[ref_table]
149
151
  data = {
150
152
  "alias": alias,
@@ -156,21 +158,23 @@ class TableHelper:
156
158
  self.foreign_keys[key] = data
157
159
  return data
158
160
 
159
- def resolve_references(self, key: str, options: Optional[Dict[str, Any]] = None) -> str:
161
+ def resolve_references(
162
+ self, key: str, options: Optional[Dict[str, Any]] = None
163
+ ) -> str:
160
164
  """
161
165
  Resolves pointer syntax or table alias references.
162
-
166
+
163
167
  Args:
164
168
  key: The column key that may contain pointer syntax (e.g., 'local>foreign')
165
169
  options: Dictionary controlling aliasing behavior:
166
170
  - alias_column: Whether to add column aliases
167
- - alias_table: Whether to prefix with table aliases
171
+ - alias_table: Whether to prefix with table aliases
168
172
  - alias_only: Whether to return only the alias name
169
173
  - bypass_on_error: Whether to return original key on errors
170
-
174
+
171
175
  Returns:
172
176
  Resolved column reference with appropriate aliasing
173
-
177
+
174
178
  Raises:
175
179
  ValueError: If key is invalid and bypass_on_error is False
176
180
  """
@@ -178,10 +182,10 @@ class TableHelper:
178
182
  if options and options.get("bypass_on_error"):
179
183
  return key or ""
180
184
  raise ValueError(f"Invalid key: {key}")
181
-
185
+
182
186
  if options is None:
183
187
  options = {"alias_column": True, "alias_table": False, "alias_only": False}
184
-
188
+
185
189
  # Remove operator first, then extract column name
186
190
  key_without_operator = self.remove_operator(key)
187
191
  column = self.extract_column_name(key_without_operator)
@@ -193,7 +197,11 @@ class TableHelper:
193
197
  alias = self.get_table_alias("current_table")
194
198
  if not self.has_pointer(column):
195
199
  # Standard column - no pointer syntax
196
- if options.get("alias_table") and alias and alias != self.table_aliases.get("current_table", "A"):
200
+ if (
201
+ options.get("alias_table")
202
+ and alias
203
+ and alias != self.table_aliases.get("current_table", "A")
204
+ ):
197
205
  name = f"{alias}.{self.quote(column)}"
198
206
  else:
199
207
  name = self.quote(column)
@@ -206,33 +214,35 @@ class TableHelper:
206
214
  if options.get("bypass_on_error"):
207
215
  return key
208
216
  raise ValueError(f"Invalid pointer syntax in column: {column}")
209
-
217
+
210
218
  local_column, foreign_column = pointer_parts
211
219
  local_column = local_column.strip()
212
220
  foreign_column = foreign_column.strip()
213
-
221
+
214
222
  if not local_column or not foreign_column:
215
223
  if options.get("bypass_on_error"):
216
224
  return key
217
225
  raise ValueError(f"Invalid pointer syntax - empty parts in: {column}")
218
-
226
+
219
227
  if options.get("alias_only"):
220
228
  return f"{local_column}_{foreign_column}"
221
-
229
+
222
230
  try:
223
231
  data = self.__fetch_foreign_data(column)
224
232
  except Exception as e:
225
233
  if options.get("bypass_on_error"):
226
234
  return key
227
235
  raise ValueError(f"Failed to resolve foreign key reference '{column}': {e}")
228
-
236
+
229
237
  # Build the foreign table reference
230
238
  if options.get("alias_table"):
231
- foreign_alias = self.get_table_alias(data['ref_table'])
239
+ foreign_alias = self.get_table_alias(data["ref_table"])
232
240
  if not foreign_alias:
233
241
  if options.get("bypass_on_error"):
234
242
  return key
235
- raise ValueError(f"No alias found for foreign table: {data['ref_table']}")
243
+ raise ValueError(
244
+ f"No alias found for foreign table: {data['ref_table']}"
245
+ )
236
246
  name = f"{foreign_alias}.{self.quote(foreign_column)}"
237
247
  else:
238
248
  name = f"{data['ref_table']}.{self.quote(foreign_column)}"
@@ -241,33 +251,33 @@ class TableHelper:
241
251
  result = self.remove_operator(key).replace(column, name, 1)
242
252
  if options.get("alias_column"):
243
253
  result += f" AS {local_column}_{foreign_column}"
244
-
254
+
245
255
  return result
246
256
 
247
257
  def get_operator(self, key: str, val: Any) -> str:
248
258
  """
249
259
  Determines the SQL operator from the start of `key` or defaults to '='.
250
-
260
+
251
261
  Args:
252
262
  key: The key string that may contain an operator prefix
253
263
  val: The value (used for context in operator determination)
254
-
264
+
255
265
  Returns:
256
266
  str: The SQL operator to use
257
-
267
+
258
268
  Raises:
259
269
  ValueError: If key is invalid or operator is unsafe
260
270
  """
261
271
  if not isinstance(key, str):
262
272
  raise ValueError(f"Key must be a string, got {type(key)}")
263
-
273
+
264
274
  # Sanitize the key to prevent injection
265
275
  sanitized_key = " ".join(key.replace('"', "").split())
266
-
276
+
267
277
  for symbol, operator in self.operators.items():
268
278
  if sanitized_key.startswith(symbol):
269
279
  # Basic validation that the operator is safe
270
- if not re.match(r'^[A-Z\s<>=!]+$', operator):
280
+ if not re.match(r"^[A-Z\s<>=!]+$", operator):
271
281
  raise ValueError(f"Unsafe operator detected: {operator}")
272
282
  return operator
273
283
  return "="
@@ -275,16 +285,16 @@ class TableHelper:
275
285
  def remove_operator(self, key: str) -> str:
276
286
  """
277
287
  Strips recognized operator symbols from the start of `key`.
278
-
288
+
279
289
  Args:
280
290
  key: The key string that may contain an operator prefix
281
-
291
+
282
292
  Returns:
283
293
  Key with operator prefix removed
284
294
  """
285
295
  if not isinstance(key, str):
286
296
  return key
287
-
297
+
288
298
  for symbol in self.operators.keys():
289
299
  if key.startswith(symbol):
290
300
  return key.replace(symbol, "", 1)
@@ -344,14 +354,14 @@ class TableHelper:
344
354
  # Handle asterisk separately since \b doesn't work with non-word characters
345
355
  if expr.strip() == "*":
346
356
  return "*"
347
-
357
+
348
358
  # Check for pointer syntax (>)
349
359
  if ">" in expr:
350
360
  # For pointer syntax, return the whole expression
351
361
  pointer_match = re.search(r"([a-zA-Z_][\w]*>[a-zA-Z_][\w]*)", expr)
352
362
  if pointer_match:
353
363
  return pointer_match.group(1)
354
-
364
+
355
365
  match = re.search(
356
366
  r"\b([a-zA-Z_][\w]*\.\*|[a-zA-Z_][\w]*(?:\.[a-zA-Z_][\w]*)?)\b", expr
357
367
  )
@@ -360,10 +370,10 @@ class TableHelper:
360
370
  def are_parentheses_balanced(self, expression: str) -> bool:
361
371
  """
362
372
  Checks if parentheses in `expression` are balanced.
363
-
373
+
364
374
  Args:
365
375
  expression: The expression to check
366
-
376
+
367
377
  Returns:
368
378
  bool: True if parentheses are balanced
369
379
  """
@@ -371,7 +381,7 @@ class TableHelper:
371
381
  opening = "({["
372
382
  closing = ")}]"
373
383
  matching = {")": "(", "}": "{", "]": "["}
374
-
384
+
375
385
  for char in expression:
376
386
  if char in opening:
377
387
  stack.append(char)
@@ -383,30 +393,32 @@ class TableHelper:
383
393
  def get_table_alias(self, table: str) -> Optional[str]:
384
394
  """
385
395
  Get the alias for a table.
386
-
396
+
387
397
  Args:
388
398
  table: The table name
389
-
399
+
390
400
  Returns:
391
401
  The table alias or None if not found
392
402
  """
393
403
  return self.table_aliases.get(table)
394
404
 
395
- def make_predicate(self, key: str, val: Any, options: Optional[Dict[str, Any]] = None) -> Tuple[str, Any]:
405
+ def make_predicate(
406
+ self, key: str, val: Any, options: Optional[Dict[str, Any]] = None
407
+ ) -> Tuple[str, Any]:
396
408
  """
397
409
  Builds a piece of SQL and corresponding parameters for a WHERE/HAVING predicate based on `key`, `val`.
398
-
410
+
399
411
  Args:
400
412
  key: The column key (may include operator prefix)
401
413
  val: The value to compare against
402
414
  options: Dictionary of options for reference resolution
403
-
415
+
404
416
  Returns:
405
417
  Tuple of (sql_string, parameters)
406
418
  """
407
419
  if options is None:
408
420
  options = {"alias_table": True, "alias_column": False}
409
-
421
+
410
422
  column = self.resolve_references(key, options=options)
411
423
  op = self.get_operator(key, val)
412
424
 
@@ -434,16 +446,16 @@ class TableHelper:
434
446
  val = tuple(int(v) for v in val)
435
447
  except ValueError:
436
448
  pass # Keep as strings if conversion fails
437
-
449
+
438
450
  # Convert to tuple for better parameter handling
439
451
  val_tuple = tuple(val)
440
-
452
+
441
453
  if not val_tuple: # Empty list/tuple
442
454
  if "!" in key:
443
455
  return "1=1", None # Empty NOT IN is always true
444
456
  else:
445
457
  return "1=0", None # Empty IN is always false
446
-
458
+
447
459
  # Use IN/NOT IN for better type compatibility
448
460
  if "!" in key:
449
461
  placeholders = ",".join(["%s"] * len(val_tuple))
@@ -459,50 +471,56 @@ class TableHelper:
459
471
  # Between operators
460
472
  if op in ["BETWEEN", "NOT BETWEEN"]:
461
473
  if not isinstance(val, (list, tuple)) or len(val) != 2:
462
- raise ValueError(f"BETWEEN operator requires exactly 2 values, got {val}")
474
+ raise ValueError(
475
+ f"BETWEEN operator requires exactly 2 values, got {val}"
476
+ )
463
477
  return f"{column} {op} %s and %s", tuple(val)
464
478
 
465
479
  # Default single-parameter predicate
466
480
  return f"{column} {op} %s", val
467
481
 
468
- def make_where(self, where: Union[Dict[str, Any], List[Tuple[str, Any]], str, None]) -> Tuple[str, Tuple[Any, ...]]:
482
+ def make_where(
483
+ self, where: Union[Dict[str, Any], List[Tuple[str, Any]], str, None]
484
+ ) -> Tuple[str, Tuple[Any, ...]]:
469
485
  """
470
486
  Converts various WHERE clause formats into SQL string and parameter values.
471
-
487
+
472
488
  Args:
473
489
  where: WHERE conditions in one of these formats:
474
490
  - Dict: {column: value} pairs that become "column = value" predicates
475
491
  - List of tuples: [(predicate_sql, params), ...] for pre-built predicates
476
492
  - String: Raw SQL WHERE clause (parameters not extracted)
477
493
  - None: No WHERE clause
478
-
494
+
479
495
  Returns:
480
496
  Tuple of (sql_string, parameter_tuple):
481
497
  - sql_string: Complete WHERE clause including "WHERE" keyword, or empty string
482
498
  - parameter_tuple: Tuple of parameter values for placeholder substitution
483
-
499
+
484
500
  Raises:
485
501
  ValueError: If where format is invalid or predicate generation fails
486
502
  TypeError: If where is an unsupported type
487
503
  """
488
504
  if not where:
489
505
  return "", tuple()
490
-
506
+
491
507
  # Convert dict to list of predicates
492
508
  if isinstance(where, Mapping):
493
509
  if not where: # Empty dict
494
510
  return "", tuple()
495
-
511
+
496
512
  predicate_list = []
497
513
  for key, val in where.items():
498
514
  if not isinstance(key, str):
499
- raise ValueError(f"WHERE clause keys must be strings, got {type(key)}: {key}")
515
+ raise ValueError(
516
+ f"WHERE clause keys must be strings, got {type(key)}: {key}"
517
+ )
500
518
  try:
501
519
  predicate_list.append(self.make_predicate(key, val))
502
520
  except Exception as e:
503
521
  raise ValueError(f"Failed to create predicate for '{key}': {e}")
504
522
  where = predicate_list
505
-
523
+
506
524
  # Handle string WHERE clause (pass through as-is)
507
525
  elif isinstance(where, str):
508
526
  where_clause = where.strip()
@@ -512,39 +530,45 @@ class TableHelper:
512
530
  if not where_clause.upper().startswith("WHERE"):
513
531
  where_clause = f"WHERE {where_clause}"
514
532
  return where_clause, tuple()
515
-
533
+
516
534
  # Validate list format
517
535
  elif isinstance(where, (list, tuple)):
518
536
  if not where: # Empty list
519
537
  return "", tuple()
520
-
538
+
521
539
  # Validate each predicate tuple
522
540
  for i, item in enumerate(where):
523
541
  if not isinstance(item, (list, tuple)) or len(item) != 2:
524
- raise ValueError(f"WHERE predicate {i} must be a 2-element tuple (sql, params), got: {item}")
542
+ raise ValueError(
543
+ f"WHERE predicate {i} must be a 2-element tuple (sql, params), got: {item}"
544
+ )
525
545
  sql_part, params = item
526
546
  if not isinstance(sql_part, str):
527
- raise ValueError(f"WHERE predicate {i} SQL must be a string, got {type(sql_part)}: {sql_part}")
547
+ raise ValueError(
548
+ f"WHERE predicate {i} SQL must be a string, got {type(sql_part)}: {sql_part}"
549
+ )
528
550
  else:
529
- raise TypeError(f"WHERE clause must be dict, list, string, or None, got {type(where)}: {where}")
551
+ raise TypeError(
552
+ f"WHERE clause must be dict, list, string, or None, got {type(where)}: {where}"
553
+ )
530
554
 
531
555
  # Build final SQL and collect parameters
532
556
  sql_parts = ["WHERE"]
533
557
  vals = []
534
-
558
+
535
559
  for i, (pred_sql, pred_val) in enumerate(where):
536
560
  if i > 0: # Add AND between predicates
537
561
  sql_parts.append("AND")
538
-
562
+
539
563
  sql_parts.append(pred_sql)
540
-
564
+
541
565
  # Handle parameter values
542
566
  if pred_val is not None:
543
567
  if isinstance(pred_val, tuple):
544
568
  vals.extend(pred_val)
545
569
  else:
546
570
  vals.append(pred_val)
547
-
571
+
548
572
  return " ".join(sql_parts), tuple(vals)
549
573
 
550
574
  @classmethod
@@ -552,10 +576,10 @@ class TableHelper:
552
576
  """
553
577
  Class method version of quote for backward compatibility.
554
578
  Quotes identifiers (columns/tables) if needed, especially if they match reserved words or contain special chars.
555
-
579
+
556
580
  Args:
557
581
  data: String identifier or list of identifiers to quote
558
-
582
+
559
583
  Returns:
560
584
  Quoted identifier(s)
561
585
  """
@@ -571,22 +595,24 @@ class TableHelper:
571
595
 
572
596
  parts = data.split(".")
573
597
  quoted_parts = []
574
-
598
+
575
599
  for part in parts:
576
600
  if not part: # Skip empty parts
577
601
  continue
578
-
602
+
579
603
  # Skip if already quoted
580
604
  if part.startswith('"') and part.endswith('"'):
581
605
  quoted_parts.append(part)
582
606
  # Quote if reserved word, contains special chars, or starts with digit
583
- elif (part.upper() in cls.reserved or
584
- re.search(r'[/\-\s]', part) or
585
- (part and part[0].isdigit())):
607
+ elif (
608
+ part.upper() in cls.reserved
609
+ or re.search(r"[/\-\s]", part)
610
+ or (part and part[0].isdigit())
611
+ ):
586
612
  # Escape any existing quotes in the identifier
587
613
  escaped_part = part.replace('"', '""')
588
614
  quoted_parts.append(f'"{escaped_part}"')
589
615
  else:
590
616
  quoted_parts.append(part)
591
-
617
+
592
618
  return ".".join(quoted_parts)
@@ -0,0 +1 @@
1
+ # Database module tests
File without changes
@@ -0,0 +1 @@
1
+ # PostgreSQL tests