velocity-python 0.0.105__py3-none-any.whl → 0.0.155__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.
- velocity/__init__.py +3 -1
- velocity/app/orders.py +3 -4
- velocity/app/tests/__init__.py +1 -0
- velocity/app/tests/test_email_processing.py +112 -0
- velocity/app/tests/test_payment_profile_sorting.py +191 -0
- velocity/app/tests/test_spreadsheet_functions.py +124 -0
- velocity/aws/__init__.py +3 -0
- velocity/aws/amplify.py +10 -6
- velocity/aws/handlers/__init__.py +2 -0
- velocity/aws/handlers/base_handler.py +248 -0
- velocity/aws/handlers/context.py +167 -2
- velocity/aws/handlers/exceptions.py +16 -0
- velocity/aws/handlers/lambda_handler.py +24 -85
- velocity/aws/handlers/mixins/__init__.py +16 -0
- velocity/aws/handlers/mixins/activity_tracker.py +181 -0
- velocity/aws/handlers/mixins/aws_session_mixin.py +192 -0
- velocity/aws/handlers/mixins/error_handler.py +192 -0
- velocity/aws/handlers/mixins/legacy_mixin.py +53 -0
- velocity/aws/handlers/mixins/standard_mixin.py +73 -0
- velocity/aws/handlers/response.py +1 -1
- velocity/aws/handlers/sqs_handler.py +28 -143
- velocity/aws/tests/__init__.py +1 -0
- velocity/aws/tests/test_lambda_handler_json_serialization.py +120 -0
- velocity/aws/tests/test_response.py +163 -0
- velocity/db/__init__.py +16 -4
- velocity/db/core/decorators.py +20 -4
- velocity/db/core/engine.py +185 -792
- velocity/db/core/result.py +36 -22
- velocity/db/core/row.py +15 -3
- velocity/db/core/table.py +283 -44
- velocity/db/core/transaction.py +19 -11
- velocity/db/exceptions.py +42 -18
- velocity/db/servers/base/__init__.py +9 -0
- velocity/db/servers/base/initializer.py +70 -0
- velocity/db/servers/base/operators.py +98 -0
- velocity/db/servers/base/sql.py +503 -0
- velocity/db/servers/base/types.py +135 -0
- velocity/db/servers/mysql/__init__.py +73 -0
- velocity/db/servers/mysql/operators.py +54 -0
- velocity/db/servers/{mysql_reserved.py → mysql/reserved.py} +2 -14
- velocity/db/servers/mysql/sql.py +718 -0
- velocity/db/servers/mysql/types.py +107 -0
- velocity/db/servers/postgres/__init__.py +59 -11
- velocity/db/servers/postgres/operators.py +34 -0
- velocity/db/servers/postgres/sql.py +474 -120
- velocity/db/servers/postgres/types.py +88 -2
- velocity/db/servers/sqlite/__init__.py +61 -0
- velocity/db/servers/sqlite/operators.py +52 -0
- velocity/db/servers/sqlite/reserved.py +20 -0
- velocity/db/servers/sqlite/sql.py +677 -0
- velocity/db/servers/sqlite/types.py +92 -0
- velocity/db/servers/sqlserver/__init__.py +73 -0
- velocity/db/servers/sqlserver/operators.py +47 -0
- velocity/db/servers/sqlserver/reserved.py +32 -0
- velocity/db/servers/sqlserver/sql.py +805 -0
- velocity/db/servers/sqlserver/types.py +114 -0
- velocity/db/servers/tablehelper.py +117 -91
- velocity/db/tests/__init__.py +1 -0
- velocity/db/tests/common_db_test.py +0 -0
- velocity/db/tests/postgres/__init__.py +1 -0
- velocity/db/tests/postgres/common.py +49 -0
- velocity/db/tests/postgres/test_column.py +29 -0
- velocity/db/tests/postgres/test_connections.py +25 -0
- velocity/db/tests/postgres/test_database.py +21 -0
- velocity/db/tests/postgres/test_engine.py +205 -0
- velocity/db/tests/postgres/test_general_usage.py +88 -0
- velocity/db/tests/postgres/test_imports.py +8 -0
- velocity/db/tests/postgres/test_result.py +19 -0
- velocity/db/tests/postgres/test_row.py +137 -0
- velocity/db/tests/postgres/test_row_comprehensive.py +720 -0
- velocity/db/tests/postgres/test_schema_locking.py +335 -0
- velocity/db/tests/postgres/test_schema_locking_unit.py +115 -0
- velocity/db/tests/postgres/test_sequence.py +34 -0
- velocity/db/tests/postgres/test_sql_comprehensive.py +462 -0
- velocity/db/tests/postgres/test_table.py +101 -0
- velocity/db/tests/postgres/test_table_comprehensive.py +646 -0
- velocity/db/tests/postgres/test_transaction.py +106 -0
- velocity/db/tests/sql/__init__.py +1 -0
- velocity/db/tests/sql/common.py +177 -0
- velocity/db/tests/sql/test_postgres_select_advanced.py +285 -0
- velocity/db/tests/sql/test_postgres_select_variances.py +517 -0
- velocity/db/tests/test_cursor_rowcount_fix.py +150 -0
- velocity/db/tests/test_db_utils.py +221 -0
- velocity/db/tests/test_postgres.py +448 -0
- velocity/db/tests/test_postgres_unchanged.py +81 -0
- velocity/db/tests/test_process_error_robustness.py +292 -0
- velocity/db/tests/test_result_caching.py +279 -0
- velocity/db/tests/test_result_sql_aware.py +117 -0
- velocity/db/tests/test_row_get_missing_column.py +72 -0
- velocity/db/tests/test_schema_locking_initializers.py +226 -0
- velocity/db/tests/test_schema_locking_simple.py +97 -0
- velocity/db/tests/test_sql_builder.py +165 -0
- velocity/db/tests/test_tablehelper.py +486 -0
- velocity/db/utils.py +62 -47
- velocity/misc/conv/__init__.py +2 -0
- velocity/misc/conv/iconv.py +5 -4
- velocity/misc/export.py +1 -4
- velocity/misc/merge.py +1 -1
- velocity/misc/tests/__init__.py +1 -0
- velocity/misc/tests/test_db.py +90 -0
- velocity/misc/tests/test_fix.py +78 -0
- velocity/misc/tests/test_format.py +64 -0
- velocity/misc/tests/test_iconv.py +203 -0
- velocity/misc/tests/test_merge.py +82 -0
- velocity/misc/tests/test_oconv.py +144 -0
- velocity/misc/tests/test_original_error.py +52 -0
- velocity/misc/tests/test_timer.py +74 -0
- velocity/misc/tools.py +0 -1
- {velocity_python-0.0.105.dist-info → velocity_python-0.0.155.dist-info}/METADATA +2 -2
- velocity_python-0.0.155.dist-info/RECORD +129 -0
- velocity/db/core/exceptions.py +0 -70
- velocity/db/servers/mysql.py +0 -641
- velocity/db/servers/sqlite.py +0 -968
- velocity/db/servers/sqlite_reserved.py +0 -208
- velocity/db/servers/sqlserver.py +0 -921
- velocity/db/servers/sqlserver_reserved.py +0 -314
- velocity_python-0.0.105.dist-info/RECORD +0 -56
- {velocity_python-0.0.105.dist-info → velocity_python-0.0.155.dist-info}/WHEEL +0 -0
- {velocity_python-0.0.105.dist-info → velocity_python-0.0.155.dist-info}/licenses/LICENSE +0 -0
- {velocity_python-0.0.105.dist-info → velocity_python-0.0.155.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(
|
|
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(
|
|
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
|
|
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[
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 (
|
|
584
|
-
|
|
585
|
-
|
|
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
|