velocity-python 0.0.34__py3-none-any.whl → 0.0.64__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 (33) hide show
  1. velocity/__init__.py +1 -1
  2. velocity/db/core/column.py +25 -105
  3. velocity/db/core/database.py +79 -23
  4. velocity/db/core/decorators.py +84 -47
  5. velocity/db/core/engine.py +179 -184
  6. velocity/db/core/result.py +94 -49
  7. velocity/db/core/row.py +81 -46
  8. velocity/db/core/sequence.py +112 -22
  9. velocity/db/core/table.py +660 -243
  10. velocity/db/core/transaction.py +75 -77
  11. velocity/db/servers/mysql.py +5 -237
  12. velocity/db/servers/mysql_reserved.py +237 -0
  13. velocity/db/servers/postgres/__init__.py +19 -0
  14. velocity/db/servers/postgres/operators.py +23 -0
  15. velocity/db/servers/postgres/reserved.py +254 -0
  16. velocity/db/servers/postgres/sql.py +1041 -0
  17. velocity/db/servers/postgres/types.py +109 -0
  18. velocity/db/servers/sqlite.py +1 -210
  19. velocity/db/servers/sqlite_reserved.py +208 -0
  20. velocity/db/servers/sqlserver.py +1 -316
  21. velocity/db/servers/sqlserver_reserved.py +314 -0
  22. velocity/db/servers/tablehelper.py +277 -0
  23. velocity/misc/conv/iconv.py +277 -91
  24. velocity/misc/conv/oconv.py +5 -4
  25. velocity/misc/db.py +2 -2
  26. velocity/misc/format.py +2 -2
  27. {velocity_python-0.0.34.dist-info → velocity_python-0.0.64.dist-info}/METADATA +6 -6
  28. velocity_python-0.0.64.dist-info/RECORD +47 -0
  29. {velocity_python-0.0.34.dist-info → velocity_python-0.0.64.dist-info}/WHEEL +1 -1
  30. velocity/db/servers/postgres.py +0 -1396
  31. velocity_python-0.0.34.dist-info/RECORD +0 -39
  32. {velocity_python-0.0.34.dist-info → velocity_python-0.0.64.dist-info}/LICENSE +0 -0
  33. {velocity_python-0.0.34.dist-info → velocity_python-0.0.64.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,277 @@
1
+ import pprint
2
+ import re
3
+ from collections.abc import Mapping
4
+ from ..core.table import Query
5
+
6
+
7
+ class TableHelper:
8
+ """
9
+ A helper class used to build SQL queries with joined/aliased tables,
10
+ including foreign key expansions, pointer syntax, etc.
11
+ """
12
+
13
+ reserved = []
14
+ operators = {}
15
+
16
+ def __init__(self, tx, table):
17
+ self.tx = tx
18
+ self.letter = 65
19
+ self.table_aliases = {}
20
+ self.foreign_keys = {}
21
+ self.current_table = table
22
+ self.table_aliases["current_table"] = chr(self.letter)
23
+ self.letter += 1
24
+
25
+ def __str__(self):
26
+ return "\n".join(
27
+ f"{key}: {pprint.pformat(value)}" for key, value in vars(self).items()
28
+ )
29
+
30
+ def split_columns(self, query):
31
+ """
32
+ Splits a string of comma-separated column expressions into a list, keeping parentheses balanced.
33
+ """
34
+ columns = []
35
+ balance = 0
36
+ current = []
37
+ for char in query:
38
+ if char == "," and balance == 0:
39
+ columns.append("".join(current).strip())
40
+ current = []
41
+ else:
42
+ if char == "(":
43
+ balance += 1
44
+ elif char == ")":
45
+ balance -= 1
46
+ current.append(char)
47
+ if current:
48
+ columns.append("".join(current).strip())
49
+ return columns
50
+
51
+ def requires_joins(self):
52
+ return len(self.table_aliases) > 1
53
+
54
+ def has_pointer(self, column):
55
+ """
56
+ Checks if there's an '>' in the column that indicates a pointer reference, e.g. 'local_column>foreign_column'.
57
+ """
58
+ if not isinstance(column, str) or not re.search(r"^[a-zA-Z0-9_>*]", column):
59
+ raise Exception(f"Invalid column specified: {column}")
60
+ return bool(re.search(r"[a-zA-Z0-9_]+>[a-zA-Z0-9_]+", column))
61
+
62
+ def __fetch_foreign_data(self, key):
63
+ if key in self.foreign_keys:
64
+ return self.foreign_keys[key]
65
+ local_column, foreign_column = key.split(">")
66
+ foreign = self.tx.table(self.current_table).foreign_key_info(local_column)
67
+ if not foreign:
68
+ raise Exception(
69
+ f"Foreign key `{self.current_table}.{local_column}>{foreign_column}` not defined."
70
+ )
71
+ ref_table = foreign["referenced_table_name"]
72
+ ref_schema = foreign["referenced_table_schema"]
73
+ ref_column = foreign["referenced_column_name"]
74
+ if ref_table not in self.table_aliases:
75
+ self.table_aliases[ref_table] = chr(self.letter)
76
+ self.letter += 1
77
+ alias = self.table_aliases[ref_table]
78
+ data = {
79
+ "alias": alias,
80
+ "ref_table": ref_table,
81
+ "ref_schema": ref_schema,
82
+ "local_column": local_column,
83
+ "ref_column": ref_column,
84
+ }
85
+ self.foreign_keys[key] = data
86
+ return data
87
+
88
+ def resolve_references(self, key, options=None):
89
+ """
90
+ Resolves pointer syntax or table alias references.
91
+ `options` can control whether to alias columns and/or tables.
92
+ """
93
+ if options is None:
94
+ options = {"alias_column": True, "alias_table": False, "alias_only": False}
95
+
96
+ column = self.extract_column_name(key)
97
+ alias = self.get_table_alias("current_table")
98
+ if not key or not column:
99
+ raise Exception(f"Invalid key or column: key={key}, column={column}")
100
+
101
+ if not self.has_pointer(column):
102
+ # Standard column
103
+ if options.get("alias_table") and alias != "A":
104
+ name = alias + "." + self.quote(column)
105
+ else:
106
+ name = self.quote(column)
107
+ return self.remove_operator(key).replace(column, name)
108
+
109
+ local_column, foreign_column = column.split(">")
110
+ if options.get("alias_only"):
111
+ return f"{local_column}_{foreign_column}"
112
+ data = self.__fetch_foreign_data(column)
113
+ if options.get("alias_table"):
114
+ name = f"{self.get_table_alias(data['ref_table'])}.{self.quote(foreign_column)}"
115
+ else:
116
+ name = f"{data['ref_table']}.{self.quote(foreign_column)}"
117
+
118
+ result = self.remove_operator(key).replace(column, name)
119
+ if options.get("alias_column"):
120
+ result += f" as {local_column}_{foreign_column}"
121
+ return result
122
+
123
+ def get_operator(self, key, val):
124
+ """
125
+ Determines the SQL operator from the start of `key` or defaults to '='.
126
+ """
127
+ key = " ".join(key.replace('"', "").split())
128
+ for symbol, operator in self.operators.items():
129
+ if key.startswith(symbol):
130
+ return operator
131
+ return "="
132
+
133
+ def remove_operator(self, key):
134
+ """
135
+ Strips recognized operator symbols from the start of `key`.
136
+ """
137
+ for symbol in self.operators.keys():
138
+ if key.startswith(symbol):
139
+ return key.replace(symbol, "", 1)
140
+ return key
141
+
142
+ def extract_column_name(self, sql_expression):
143
+ """
144
+ Extracts the 'bare' column name from an expression, ignoring function calls, operators, etc.
145
+ """
146
+ expr = sql_expression.replace('"', "")
147
+ expr = self.remove_operator(expr)
148
+ if not self.are_parentheses_balanced(expr):
149
+ raise ValueError(f"Unbalanced parentheses in expression: {expr}")
150
+
151
+ # Remove outer function calls, preserving inside parentheses.
152
+ while "(" in expr or ")" in expr:
153
+ expr = re.sub(r"\b\w+\s*\((.*?)\)", r"\1", expr)
154
+
155
+ if "," in expr:
156
+ expr = expr.split(",")[0].strip()
157
+
158
+ pattern = r"^([a-zA-Z0-9_]+\.\*|\*|[a-zA-Z0-9_>]+(?:\.[a-zA-Z0-9_]+)?)$"
159
+ match = re.search(pattern, expr)
160
+ return match.group(1) if match else None
161
+
162
+ def are_parentheses_balanced(self, expression):
163
+ """
164
+ Checks if parentheses in `expression` are balanced.
165
+ """
166
+ stack = []
167
+ opening = "({["
168
+ closing = ")}]"
169
+ matching = {")": "(", "}": "{", "]": "["}
170
+ for char in expression:
171
+ if char in opening:
172
+ stack.append(char)
173
+ elif char in closing:
174
+ if not stack or stack.pop() != matching[char]:
175
+ return False
176
+ return not stack
177
+
178
+ def get_table_alias(self, table):
179
+ return self.table_aliases.get(table)
180
+
181
+ def make_predicate(self, key, val, options=None):
182
+ """
183
+ Builds a piece of SQL and corresponding parameters for a WHERE/HAVING predicate based on `key`, `val`.
184
+ """
185
+ if options is None:
186
+ options = {"alias_table": True, "alias_column": False}
187
+ case = None
188
+ column = self.resolve_references(key, options=options)
189
+ op = self.get_operator(key, val)
190
+
191
+ # Subquery?
192
+ if isinstance(val, Query):
193
+ if op in ("<>"):
194
+ return f"{column} NOT IN ({val})", val.params or None
195
+ return f"{column} IN ({val})", val.params or None
196
+
197
+ # Null / special markers
198
+ if val is None or isinstance(val, bool) or val in ("@@INFINITY", "@@UNKNOWN"):
199
+ if isinstance(val, str) and val.startswith("@@"):
200
+ val = val[2:]
201
+ if val is None:
202
+ val = "NULL"
203
+ if op == "<>":
204
+ return f"{column} IS NOT {str(val).upper()}", None
205
+ return f"{column} IS {str(val).upper()}", None
206
+
207
+ # Lists / tuples => IN / NOT IN
208
+ if isinstance(val, (list, tuple)) and "><" not in key:
209
+ if "!" in key:
210
+ return f"{column} NOT IN %s", list(val)
211
+ return f"{column} IN %s", list(val)
212
+
213
+ # "@@" => pass as literal
214
+ if isinstance(val, str) and val.startswith("@@") and val[2:]:
215
+ return f"{column} {op} {val[2:]}", None
216
+
217
+ # Between operators
218
+ if op in ["BETWEEN", "NOT BETWEEN"]:
219
+ return f"{column} {op} %s and %s", tuple(val)
220
+
221
+ if case:
222
+ return f"{case}({column}) {op} {case}(%s)", val
223
+
224
+ # Default single-parameter predicate
225
+ return f"{column} {op} %s", val
226
+
227
+ def make_where(self, where):
228
+ """
229
+ Converts a dict-based WHERE into a list of predicate strings + param values.
230
+ """
231
+ if isinstance(where, Mapping):
232
+ new_where = []
233
+ for key, val in where.items():
234
+ new_where.append(self.make_predicate(key, val))
235
+ where = new_where
236
+
237
+ sql = []
238
+ vals = []
239
+ if where:
240
+ sql.append("WHERE")
241
+ join = ""
242
+ for pred, val in where:
243
+ if join:
244
+ sql.append(join)
245
+ sql.append(pred)
246
+ join = "AND"
247
+ if val is not None:
248
+ if isinstance(val, tuple):
249
+ vals.extend(val)
250
+ else:
251
+ vals.append(val)
252
+ return " ".join(sql), tuple(vals)
253
+
254
+ @classmethod
255
+ def quote(cls, data):
256
+ """
257
+ Quotes identifiers (columns/tables) if needed, especially if they match reserved words or contain special chars.
258
+ """
259
+ if isinstance(data, list):
260
+ new_list = []
261
+ for item in data:
262
+ if item.startswith("@@"):
263
+ new_list.append(item[2:])
264
+ else:
265
+ new_list.append(cls.quote(item))
266
+ return new_list
267
+
268
+ parts = data.split(".")
269
+ quoted_parts = []
270
+ for part in parts:
271
+ if '"' in part:
272
+ quoted_parts.append(part)
273
+ elif part.upper() in cls.reserved or re.findall(r"[/]", part):
274
+ quoted_parts.append(f'"{part}"')
275
+ else:
276
+ quoted_parts.append(part)
277
+ return ".".join(quoted_parts)