velocity-python 0.0.35__py3-none-any.whl → 0.0.65__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.
@@ -1,13 +1,20 @@
1
- import psycopg2
2
1
  import re
3
- import os
4
2
  import hashlib
5
- import decimal
6
- import datetime
7
- from ..core import exceptions
8
- from ..core import engine
9
- from ..core.table import Query
10
- from .postgres_reserved import reserved_words
3
+ import sqlparse
4
+
5
+
6
+ from velocity.db.core import exceptions
7
+
8
+ from .reserved import reserved_words
9
+ from .types import TYPES
10
+ from .operators import OPERATORS
11
+ from ..tablehelper import TableHelper
12
+ from collections.abc import Mapping, Sequence
13
+
14
+
15
+ TableHelper.reserved = reserved_words
16
+ TableHelper.operators = OPERATORS
17
+
11
18
 
12
19
  system_fields = [
13
20
  "sys_id",
@@ -19,162 +26,8 @@ system_fields = [
19
26
  "description",
20
27
  ]
21
28
 
22
- default_config = {
23
- "database": os.environ["DBDatabase"],
24
- "host": os.environ["DBHost"],
25
- "port": os.environ["DBPort"],
26
- "user": os.environ["DBUser"],
27
- "password": os.environ["DBPassword"],
28
- }
29
-
30
- def initialize(config=None, **kwargs):
31
- if not config:
32
- config = default_config.copy()
33
- config.update(kwargs)
34
- return engine.Engine(psycopg2, config, SQL)
35
-
36
- def make_where(where, sql, vals, is_join=False):
37
- if not where:
38
- return
39
- sql.append("WHERE")
40
- if isinstance(where, str):
41
- sql.append(where)
42
- return
43
- if isinstance(where, dict):
44
- where = list(where.items())
45
- if not isinstance(where, list):
46
- raise Exception("Parameter `where` is not a valid datatype.")
47
- alias = "A"
48
- if is_join and isinstance(is_join, str):
49
- alias = is_join
50
- connect = ""
51
- for key, val in where:
52
- if connect:
53
- sql.append(connect)
54
- if is_join:
55
- if "." not in key:
56
- key = f"{alias}.{quote(key.lower())}"
57
- if val is None:
58
- if "!" in key:
59
- key = key.replace("!", "")
60
- sql.append(f"{key} is not NULL")
61
- else:
62
- sql.append(f"{key} is NULL")
63
- elif isinstance(val, (list, tuple)) and "><" not in key:
64
- if "!" in key:
65
- key = key.replace("!", "")
66
- sql.append(f"{key} not in %s")
67
- vals.append(tuple(val))
68
- else:
69
- sql.append(f"{key} in %s")
70
- vals.append(tuple(val))
71
- elif isinstance(val, Query):
72
- sql.append(f"{key} in ({val})")
73
- else:
74
- case = None
75
- if "<>" in key:
76
- key = key.replace("<>", "")
77
- op = "<>"
78
- elif "!=" in key:
79
- key = key.replace("!=", "")
80
- op = "<>"
81
- elif "!><" in key:
82
- key = key.replace("!><", "")
83
- op = "not between"
84
- elif "><" in key:
85
- key = key.replace("><", "")
86
- op = "between"
87
- elif "!%" in key:
88
- key = key.replace("!%", "")
89
- op = "not like"
90
- elif "%%" in key:
91
- key = key.replace("%%", "")
92
- op = "%"
93
- elif "%>" in key:
94
- key = key.replace("%>", "")
95
- op = "%>"
96
- elif "<%" in key:
97
- key = key.replace("<%", "")
98
- op = "<%"
99
- elif "==" in key:
100
- key = key.replace("==", "")
101
- op = "="
102
- elif "<=" in key:
103
- key = key.replace("<=", "")
104
- op = "<="
105
- elif ">=" in key:
106
- key = key.replace(">=", "")
107
- op = ">="
108
- elif "<" in key:
109
- key = key.replace("<", "")
110
- op = "<"
111
- elif ">" in key:
112
- key = key.replace(">", "")
113
- op = ">"
114
- elif "%" in key:
115
- key = key.replace("%", "")
116
- op = "ilike"
117
- elif "!~*" in key:
118
- key = key.replace("!~*", "")
119
- op = "!~*"
120
- elif "~*" in key:
121
- key = key.replace("~*", "")
122
- op = "~*"
123
- elif "!~" in key:
124
- key = key.replace("!~", "")
125
- op = "!~"
126
- elif "~" in key:
127
- key = key.replace("~", "")
128
- op = "~"
129
- elif "!" in key:
130
- key = key.replace("!", "")
131
- op = "<>"
132
- elif "=" in key:
133
- key = key.replace("=", "")
134
- op = "="
135
- else:
136
- op = "="
137
- if "#" in key:
138
- key = key.replace("#", "")
139
- op = "="
140
- case = "lower"
141
- if isinstance(val, str) and val[:2] == "@@" and val[2:]:
142
- sql.append(f"{key} {op} {val[2:]}")
143
- elif op in ["between", "not between"]:
144
- sql.append(f"{key} {op} %s and %s")
145
- vals.extend(val)
146
- else:
147
- if case:
148
- sql.append(f"{case}({key}) {op} {case}(%s)")
149
- else:
150
- sql.append(f"{key} {op} %s")
151
- vals.append(val)
152
- connect = "AND"
153
-
154
- def quote(data):
155
- if isinstance(data, list):
156
- new = []
157
- for item in data:
158
- if "@@" in item:
159
- new.append(item[2:])
160
- else:
161
- new.append(quote(item))
162
- return new
163
- else:
164
- parts = data.split(".")
165
- new = []
166
- for part in parts:
167
- if '"' in part:
168
- new.append(part)
169
- elif part.upper() in reserved_words:
170
- new.append(f'"{part}"')
171
- elif re.findall("[/]", part):
172
- new.append(f'"{part}"')
173
- else:
174
- new.append(part)
175
- return ".".join(new)
176
29
 
177
- class SQL(object):
30
+ class SQL:
178
31
  server = "PostGreSQL"
179
32
  type_column_identifier = "data_type"
180
33
  is_nullable = "is_nullable"
@@ -188,7 +41,7 @@ class SQL(object):
188
41
  ColumnMissingErrorCodes = ["42703"]
189
42
  ForeignKeyMissingErrorCodes = ["42704"]
190
43
 
191
- ConnectionErrorCodes = ["08001", "08S01","57P03", "08006", "53300"]
44
+ ConnectionErrorCodes = ["08001", "08S01", "57P03", "08006", "53300"]
192
45
  DuplicateKeyErrorCodes = [] # Handled in regex check.
193
46
  RetryTransactionCodes = []
194
47
  TruncationErrorCodes = ["22001"]
@@ -196,6 +49,388 @@ class SQL(object):
196
49
  DatabaseObjectExistsErrorCodes = ["42710", "42P07", "42P04"]
197
50
  DataIntegrityErrorCodes = ["23503"]
198
51
 
52
+ @classmethod
53
+ def get_error(self, e):
54
+ error_code = getattr(e, "pgcode", None)
55
+ error_mesg = getattr(e, "pgerror", None)
56
+ return error_code, error_mesg
57
+
58
+ types = TYPES
59
+
60
+ @classmethod
61
+ def select(
62
+ cls,
63
+ tx,
64
+ columns=None,
65
+ table=None,
66
+ where=None,
67
+ orderby=None,
68
+ groupby=None,
69
+ having=None,
70
+ start=None,
71
+ qty=None,
72
+ lock=None,
73
+ skip_locked=None,
74
+ ):
75
+
76
+ if not table:
77
+ raise ValueError("Table name is required.")
78
+
79
+ sql_parts = {
80
+ "SELECT": [],
81
+ "FROM": [],
82
+ "WHERE": [],
83
+ "GROUP BY": [],
84
+ "HAVING": [],
85
+ "ORDER BY": [],
86
+ }
87
+
88
+ sql = []
89
+ vals = []
90
+
91
+ # Assume these helpers and functions exist externally
92
+ th = TableHelper(tx, table)
93
+
94
+ # Handle columns and DISTINCT before aliasing
95
+ if columns is None:
96
+ # No columns specified - select all
97
+ columns = ["*"]
98
+ elif isinstance(columns, str):
99
+ columns = th.split_columns(columns)
100
+
101
+ if not isinstance(columns, Sequence):
102
+ raise Exception(
103
+ f"variable `columns` must be a sequence, but {type(columns)} was found"
104
+ )
105
+
106
+ columns = [c.strip() for c in columns] # Preserve original case
107
+ distinct = False
108
+
109
+ if any(
110
+ "distinct" in c.lower() for c in columns
111
+ ): # Check if "distinct" exists in any entry
112
+ distinct = True
113
+ columns = [
114
+ c.replace("distinct", "", 1).strip() if "distinct" in c.lower() else c
115
+ for c in columns
116
+ ]
117
+
118
+ processed_columns = []
119
+ for col in columns:
120
+ processed_columns.append(
121
+ th.resolve_references(
122
+ col, options={"alias_column": True, "alias_table": True}
123
+ )
124
+ )
125
+
126
+ columns = processed_columns
127
+
128
+ # Handle WHERE conditions
129
+ if isinstance(where, Mapping):
130
+ new_where = []
131
+ for key, val in where.items():
132
+ new_where.append(th.make_predicate(key, val))
133
+ where = new_where
134
+
135
+ new_orderby = []
136
+ if isinstance(orderby, str):
137
+ orderby = th.split_columns(orderby)
138
+ # Handle orderby references
139
+ if isinstance(orderby, (Sequence)):
140
+ for column in orderby:
141
+ if " " in column:
142
+ col_name, direction = column.split(" ", 1)
143
+ col_name = th.resolve_references(
144
+ col_name, options={"alias_only": True}
145
+ )
146
+ new_orderby.append(f"{col_name} {direction}")
147
+ else:
148
+ new_orderby.append(
149
+ th.resolve_references(
150
+ column.strip(), options={"alias_only": True}
151
+ )
152
+ )
153
+
154
+ if isinstance(orderby, Mapping):
155
+ for key, val in orderby.items():
156
+ parsed_key = th.resolve_references(key, options={"alias_only": True})
157
+ new_orderby.append(f"{parsed_key} {val}")
158
+ orderby = new_orderby
159
+
160
+ # Handle groupby
161
+ if isinstance(groupby, str):
162
+ groupby = th.split_columns(groupby)
163
+ if isinstance(groupby, (Sequence)):
164
+ new_groupby = []
165
+ for gcol in groupby:
166
+ new_groupby.append(
167
+ th.resolve_references(gcol, options={"alias_only": True})
168
+ )
169
+ groupby = new_groupby
170
+
171
+ # Handle having
172
+ if isinstance(having, Mapping):
173
+ new_having = []
174
+ for key, val in having.items():
175
+ new_having.append(th.make_predicate(key, val))
176
+ having = new_having
177
+
178
+ # SELECT clause
179
+ # columns is a list/tuple of already processed references
180
+ sql_parts["SELECT"].extend(columns)
181
+ alias = th.get_table_alias("current_table")
182
+ if not alias:
183
+ raise ValueError("Main table alias resolution failed.")
184
+
185
+ # FROM clause
186
+ if th.foreign_keys:
187
+ sql_parts["FROM"].append(f"{th.quote(table)} AS {th.quote(alias)}")
188
+ # Handle joins
189
+ done = []
190
+ for key, ref_info in th.foreign_keys.items():
191
+ ref_table = ref_info["ref_table"]
192
+ if ref_table in done:
193
+ continue
194
+ done.append(ref_table)
195
+ if not all(
196
+ k in ref_info
197
+ for k in ("alias", "local_column", "ref_table", "ref_column")
198
+ ):
199
+ raise ValueError(f"Invalid table alias info for {ref_table}.")
200
+ sql_parts["FROM"].append(
201
+ f"LEFT JOIN {th.quote(ref_table)} AS {th.quote(ref_info['alias'])} "
202
+ f"ON {th.quote(alias)}.{th.quote(ref_info['local_column'])} = {th.quote(ref_info['alias'])}.{th.quote(ref_info['ref_column'])}"
203
+ )
204
+ else:
205
+ sql_parts["FROM"].append(th.quote(table))
206
+
207
+ # WHERE
208
+ if where:
209
+ if isinstance(where, str):
210
+ sql_parts["WHERE"].append(where)
211
+ else:
212
+ for pred, val in where:
213
+ sql_parts["WHERE"].append(pred)
214
+ if val is None:
215
+ pass
216
+ elif isinstance(val, tuple):
217
+ vals.extend(val)
218
+ else:
219
+ vals.append(val)
220
+
221
+ # GROUP BY
222
+ if groupby:
223
+ sql_parts["GROUP BY"].append(",".join(groupby))
224
+
225
+ # HAVING
226
+ if having:
227
+ if isinstance(having, str):
228
+ sql_parts["HAVING"].append(having)
229
+ else:
230
+ for pred, val in having:
231
+ sql_parts["HAVING"].append(pred)
232
+ if val is None:
233
+ pass
234
+ elif isinstance(val, tuple):
235
+ vals.extend(val)
236
+ else:
237
+ vals.append(val)
238
+
239
+ # ORDER BY
240
+ if orderby:
241
+ sql_parts["ORDER BY"].append(",".join(orderby))
242
+
243
+ # Construct final SQL
244
+ if sql_parts["SELECT"]:
245
+ sql.append("SELECT")
246
+ if distinct:
247
+ sql.append("DISTINCT")
248
+ sql.append(", ".join(sql_parts["SELECT"]))
249
+
250
+ if sql_parts["FROM"]:
251
+ sql.append("FROM")
252
+ sql.append(" ".join(sql_parts["FROM"]))
253
+
254
+ if sql_parts["WHERE"]:
255
+ sql.append("WHERE " + " AND ".join(sql_parts["WHERE"]))
256
+
257
+ if sql_parts["GROUP BY"]:
258
+ sql.append("GROUP BY " + " ".join(sql_parts["GROUP BY"]))
259
+
260
+ if sql_parts["HAVING"]:
261
+ sql.append("HAVING " + " AND ".join(sql_parts["HAVING"]))
262
+
263
+ if sql_parts["ORDER BY"]:
264
+ sql.append("ORDER BY " + " ".join(sql_parts["ORDER BY"]))
265
+
266
+ # OFFSET/FETCH
267
+ if start is not None:
268
+ if not isinstance(start, int):
269
+ raise ValueError("Start (OFFSET) must be an integer.")
270
+ sql.append(f"OFFSET {start} ROWS")
271
+
272
+ if qty is not None:
273
+ if not isinstance(qty, int):
274
+ raise ValueError("Qty (FETCH) must be an integer.")
275
+ sql.append(f"FETCH NEXT {qty} ROWS ONLY")
276
+
277
+ # FOR UPDATE and SKIP LOCKED
278
+ if lock or skip_locked:
279
+ sql.append("FOR UPDATE")
280
+ if skip_locked:
281
+ sql.append("SKIP LOCKED")
282
+
283
+ sql = sqlparse.format(" ".join(sql), reindent=True, keyword_case="upper")
284
+ return sql, tuple(vals)
285
+
286
+ @classmethod
287
+ def update(cls, tx, table, data, where=None, pk=None, excluded=False):
288
+ if not table:
289
+ raise ValueError("Table name is required.")
290
+ if not pk and not where:
291
+ raise ValueError("Where clause (where) or primary key (pk) is required.")
292
+ if not isinstance(data, Mapping) or not data:
293
+ raise ValueError("data must be a non-empty mapping of column-value pairs.")
294
+
295
+ th = TableHelper(tx, table)
296
+
297
+ set_clauses = []
298
+ vals = []
299
+
300
+ if pk:
301
+ if where:
302
+ where.update(pk)
303
+ else:
304
+ where = pk
305
+
306
+ # Handle data columns (SET clause)
307
+ for col, val in data.items():
308
+ col = th.resolve_references(
309
+ col, options={"alias_column": False, "alias_table": False}
310
+ )
311
+
312
+ # Normal column
313
+ if excluded:
314
+ set_clauses.append(f"{col} = EXCLUDED.{col}")
315
+ else:
316
+ set_clauses.append(f"{col} = %s")
317
+ vals.append(val)
318
+
319
+ # Extract the final where conditions and values
320
+ where_clauses = []
321
+ if not excluded:
322
+ # First handle user-provided WHERE conditions
323
+ if isinstance(where, Mapping):
324
+ for key, val in where.items():
325
+ col, value = th.make_predicate(key, val)
326
+ where_clauses.append(col)
327
+ if isinstance(value, tuple):
328
+ vals.extend(value)
329
+ else:
330
+ vals.append(value)
331
+
332
+ # Final assembly of SQL
333
+ sql = []
334
+ sql.append("UPDATE")
335
+ if not excluded:
336
+ if th.foreign_keys:
337
+ sql.append(
338
+ f"{th.quote(table)} AS {th.quote(th.get_table_alias('current_table'))}"
339
+ )
340
+ else:
341
+ sql.append(TableHelper.quote(table))
342
+ sql.append("SET " + ", ".join(set_clauses))
343
+ if not excluded:
344
+ if th.foreign_keys:
345
+ for key, ref_info in th.foreign_keys.items():
346
+ ref_table = ref_info["ref_table"]
347
+ sql.append(
348
+ f"LEFT JOIN {th.quote(ref_table)} AS {th.quote(ref_info['alias'])} "
349
+ )
350
+ where_clauses.append(
351
+ f"{th.quote(th.get_table_alias('current_table'))}.{th.quote(ref_info['local_column'])} = {th.quote(ref_info['alias'])}.{th.quote(ref_info['ref_column'])}"
352
+ )
353
+
354
+ if not excluded:
355
+ if where_clauses:
356
+ sql.append("WHERE " + " AND ".join(where_clauses))
357
+ else:
358
+ # Without a WHERE, this updates all rows.
359
+ # If this is not desired, raise an error.
360
+ if not excluded:
361
+ raise ValueError(
362
+ "No WHERE clause could be constructed. Update would affect all rows."
363
+ )
364
+
365
+ # Final assembled query
366
+ sql = sqlparse.format(" ".join(sql), reindent=True, keyword_case="upper")
367
+ return sql, tuple(vals)
368
+
369
+ @classmethod
370
+ def insert(cls, table, data):
371
+ keys = []
372
+ vals = []
373
+ args = []
374
+ for key, val in data.items():
375
+ keys.append(TableHelper.quote(key.lower()))
376
+ if isinstance(val, str) and len(val) > 2 and val[:2] == "@@" and val[2:]:
377
+ vals.append(val[2:])
378
+ else:
379
+ vals.append("%s")
380
+ args.append(val)
381
+
382
+ sql = ["INSERT INTO"]
383
+ sql.append(TableHelper.quote(table))
384
+ sql.append("(")
385
+ sql.append(",".join(keys))
386
+ sql.append(")")
387
+ sql.append("VALUES")
388
+ sql.append("(")
389
+ sql.append(",".join(vals))
390
+ sql.append(")")
391
+ sql = sqlparse.format(" ".join(sql), reindent=True, keyword_case="upper")
392
+ return sql, tuple(args)
393
+
394
+ @classmethod
395
+ def merge(cls, tx, table, data, pk, on_conflict_do_nothing, on_conflict_update):
396
+ if pk is None:
397
+ pkeys = tx.table(table).primary_keys()
398
+ if not pkeys:
399
+ raise ValueError("Primary key required for merge.")
400
+ # If there are multiple primary keys, we need to use all of them
401
+ if len(pkeys) > 1:
402
+ pk = {pk: data[pk] for pk in pkeys}
403
+ else:
404
+ pk = {pkeys[0]: data[pkeys[0]]}
405
+ # remove primary key from data
406
+ data = {k: v for k, v in data.items() if k not in pk}
407
+
408
+ full_data = {}
409
+ full_data.update(data)
410
+ full_data.update(pk)
411
+
412
+ sql, vals = cls.insert(table, full_data)
413
+ sql = [sql]
414
+ vals = list(vals)
415
+ if on_conflict_do_nothing != on_conflict_update:
416
+ sql.append("ON CONFLICT")
417
+ sql.append("(")
418
+ sql.append(",".join(pk.keys()))
419
+ sql.append(")")
420
+ sql.append("DO")
421
+ if on_conflict_do_nothing:
422
+ sql.append("NOTHING")
423
+ elif on_conflict_update:
424
+ sql2, vals2 = cls.update(tx, table, data, pk, excluded=True)
425
+ sql.append(sql2)
426
+ vals.extend(vals2)
427
+ else:
428
+ raise Exception(
429
+ "Update on conflict must have one and only one option to complete on conflict."
430
+ )
431
+ sql = sqlparse.format(" ".join(sql), reindent=True, keyword_case="upper")
432
+ return sql, tuple(vals)
433
+
199
434
  @classmethod
200
435
  def version(cls):
201
436
  return "select version()", tuple()
@@ -250,143 +485,6 @@ class SQL(object):
250
485
  tuple(),
251
486
  )
252
487
 
253
- @classmethod
254
- def __has_pointer(cls, columns):
255
- if isinstance(columns, str):
256
- columns = columns.split(",")
257
- if isinstance(columns, list):
258
- for column in columns:
259
- if "@@" in column:
260
- continue
261
- if ">" in column:
262
- return True
263
- return False
264
-
265
- @classmethod
266
- def select(
267
- cls,
268
- columns=None,
269
- table=None,
270
- where=None,
271
- orderby=None,
272
- groupby=None,
273
- having=None,
274
- start=None,
275
- qty=None,
276
- tbl=None,
277
- lock=None,
278
- skip_locked=None,
279
- ):
280
- if not table:
281
- raise Exception("Table name required")
282
- is_join = False
283
-
284
- if isinstance(columns, str) and "distinct" in columns.lower():
285
- sql = [
286
- "SELECT",
287
- columns,
288
- "FROM",
289
- quote(table),
290
- ]
291
- elif cls.__has_pointer(columns):
292
- is_join = True
293
- if isinstance(columns, str):
294
- columns = columns.split(",")
295
- letter = 65
296
- tables = {table: chr(letter)}
297
- letter += 1
298
- __select = []
299
- __from = [f"{quote(table)} AS {tables.get(table)}"]
300
- __left_join = []
301
-
302
- for column in columns:
303
- if "@@" in column:
304
- __select.append(column[2:])
305
- elif ">" in column:
306
- parts = column.split(">")
307
- foreign = tbl.foreign_key_info(parts[0])
308
- if not foreign:
309
- raise exceptions.DbApplicationError("Foreign key not defined")
310
- ref_table = foreign["referenced_table_name"]
311
- ref_schema = foreign["referenced_table_schema"]
312
- ref_column = foreign["referenced_column_name"]
313
- lookup = f"{ref_table}:{parts[0]}"
314
- if lookup in tables:
315
- __select.append(
316
- f'{tables.get(lookup)}."{parts[1]}" as "{'_'.join(parts)}"'
317
- )
318
- else:
319
- tables[lookup] = chr(letter)
320
- letter += 1
321
- __select.append(
322
- f'{tables.get(lookup)}."{parts[1]}" as "{'_'.join(parts)}"'
323
- )
324
- __left_join.append(
325
- f'LEFT OUTER JOIN "{ref_schema}"."{ref_table}" AS {tables.get(lookup)}'
326
- )
327
- __left_join.append(
328
- f'ON {tables.get(table)}."{parts[0]}" = {tables.get(lookup)}."{ref_column}"'
329
- )
330
- if orderby and column in orderby:
331
- orderby = orderby.replace(
332
- column, f"{tables.get(lookup)}.{parts[1]}"
333
- )
334
-
335
- else:
336
- if "(" in column:
337
- __select.append(column)
338
- else:
339
- __select.append(f"{tables.get(table)}.{column}")
340
- sql = ["SELECT"]
341
- sql.append(",".join(__select))
342
- sql.append("FROM")
343
- sql.extend(__from)
344
- sql.extend(__left_join)
345
- else:
346
- if columns:
347
- if isinstance(columns, str):
348
- columns = columns.split(",")
349
- if isinstance(columns, list):
350
- columns = quote(columns)
351
- columns = ",".join(columns)
352
- else:
353
- columns = "*"
354
- sql = [
355
- "SELECT",
356
- columns,
357
- "FROM",
358
- quote(table),
359
- ]
360
- vals = []
361
- make_where(where, sql, vals, is_join)
362
- if groupby:
363
- sql.append("GROUP BY")
364
- if isinstance(groupby, (list, tuple)):
365
- groupby = ",".join(groupby)
366
- sql.append(groupby)
367
- if having:
368
- sql.append("HAVING")
369
- if isinstance(having, (list, tuple)):
370
- having = ",".join(having)
371
- sql.append(having)
372
- if orderby:
373
- sql.append("ORDER BY")
374
- if isinstance(orderby, (list, tuple)):
375
- orderby = ",".join(orderby)
376
- sql.append(orderby)
377
- if start and qty:
378
- sql.append(f"OFFSET {start} ROWS FETCH NEXT {qty} ROWS ONLY")
379
- elif start:
380
- sql.append(f"OFFSET {start} ROWS")
381
- elif qty:
382
- sql.append(f"FETCH NEXT {qty} ROWS ONLY")
383
- if lock or skip_locked:
384
- sql.append("FOR UPDATE")
385
- if skip_locked:
386
- sql.append("SKIP LOCKED")
387
- sql = " ".join(sql)
388
- return sql, tuple(vals)
389
-
390
488
  @classmethod
391
489
  def create_database(cls, name):
392
490
  return f"create database {name}", tuple()
@@ -469,24 +567,26 @@ class SQL(object):
469
567
  if key in system_fields:
470
568
  continue
471
569
  sql.append(
472
- f"ALTER TABLE {quote(fqtn)} ADD COLUMN {quote(key)} {cls.get_type(val)};"
570
+ f"ALTER TABLE {TableHelper.quote(fqtn)} ADD COLUMN {TableHelper.quote(key)} {TYPES.get_type(val)};"
473
571
  )
474
- return "\n\t".join(sql), tuple()
572
+
573
+ sql = sqlparse.format(" ".join(sql), reindent=True, keyword_case="upper")
574
+ return sql, tuple()
475
575
 
476
576
  @classmethod
477
577
  def drop_table(cls, name):
478
- return f"drop table if exists {quote(name)} cascade;", tuple()
578
+ return f"drop table if exists {TableHelper.quote(name)} cascade;", tuple()
479
579
 
480
580
  @classmethod
481
581
  def drop_column(cls, table, name, cascade=True):
482
582
  if cascade:
483
583
  return (
484
- f"ALTER TABLE {quote(table)} DROP COLUMN {quote(name)} CASCADE",
584
+ f"ALTER TABLE {TableHelper.quote(table)} DROP COLUMN {TableHelper.quote(name)} CASCADE",
485
585
  tuple(),
486
586
  )
487
587
  else:
488
588
  return (
489
- f"ALTER TABLE {quote(table)} DROP COLUMN {quote(name)}",
589
+ f"ALTER TABLE {TableHelper.quote(table)} DROP COLUMN {TableHelper.quote(name)}",
490
590
  tuple(),
491
591
  )
492
592
 
@@ -616,8 +716,17 @@ class SQL(object):
616
716
  where["LOWER(KCU1.TABLE_NAME)"] = table.lower()
617
717
  if column:
618
718
  where["LOWER(KCU1.COLUMN_NAME)"] = column.lower()
619
- make_where(where, sql, vals)
620
- return " ".join(sql), tuple(vals)
719
+ sql.append("WHERE")
720
+ connect = ""
721
+ for key, val in where.items():
722
+ if connect:
723
+ sql.append(connect)
724
+ sql.append(f"{key} = %s")
725
+ vals.append(val)
726
+ connect = "AND"
727
+
728
+ sql = sqlparse.format(" ".join(sql), reindent=True, keyword_case="upper")
729
+ return sql, tuple(vals)
621
730
 
622
731
  @classmethod
623
732
  def create_foreign_key(
@@ -637,7 +746,7 @@ class SQL(object):
637
746
  m.update(" ".join(key_to_columns).encode("utf-8"))
638
747
  name = f"FK_{m.hexdigest()}"
639
748
  sql = f"ALTER TABLE {table} ADD CONSTRAINT {name} FOREIGN KEY ({','.join(columns)}) REFERENCES {key_to_table} ({','.join(key_to_columns)});"
640
-
749
+ sql = sqlparse.format(sql, reindent=True, keyword_case="upper")
641
750
  return sql, tuple()
642
751
 
643
752
  @classmethod
@@ -669,6 +778,7 @@ class SQL(object):
669
778
  @classmethod
670
779
  def create_index(
671
780
  cls,
781
+ tx,
672
782
  table=None,
673
783
  columns=None,
674
784
  unique=False,
@@ -677,7 +787,6 @@ class SQL(object):
677
787
  name=None,
678
788
  schema=None,
679
789
  trigram=None,
680
- tbl=None,
681
790
  lower=None,
682
791
  ):
683
792
  """
@@ -688,14 +797,14 @@ class SQL(object):
688
797
  if "." not in table and schema:
689
798
  table = f"{schema}.{table}"
690
799
  if isinstance(columns, (list, set)):
691
- columns = ",".join([quote(c.lower()) for c in columns])
800
+ columns = ",".join([TableHelper.quote(c.lower()) for c in columns])
692
801
  else:
693
- columns = quote(columns)
802
+ columns = TableHelper.quote(columns)
694
803
  sql = ["CREATE"]
695
804
  if unique:
696
805
  sql.append("UNIQUE")
697
806
  sql.append("INDEX")
698
- tablename = quote(table)
807
+ tablename = TableHelper.quote(table)
699
808
  if not name:
700
809
  name = re.sub(
701
810
  r"\([^)]*\)",
@@ -703,54 +812,58 @@ class SQL(object):
703
812
  columns.replace(" ", "").replace(",", "_").replace('"', ""),
704
813
  )
705
814
  if trigram:
706
- sql.append(
707
- f"IDX__TRGM_{table.replace('.', '_')}_{trigram.upper()}__{name}"
708
- )
815
+ sql.append(f"IDX__TRGM_{table.replace('.', '_')}_{trigram}__{name}".upper())
709
816
  else:
710
- sql.append(f"IDX__{table.replace('.', '_')}__{name}")
817
+ sql.append(f"IDX__{table.replace('.', '_')}__{name}".upper())
711
818
  sql.append("ON")
712
- sql.append(quote(tablename))
819
+ sql.append(TableHelper.quote(tablename))
713
820
 
714
821
  if trigram:
715
822
  sql.append("USING")
716
823
  sql.append(trigram)
717
824
  sql.append("(")
718
- if tbl:
719
- join = ""
720
- for column_name in columns.split(","):
721
- column_name = column_name.replace('"', "")
722
- if join:
723
- sql.append(join)
724
- column = tbl.column(column_name)
725
- if column.py_type == str:
726
- if lower:
727
- sql.append(f"lower({quote(column_name)})")
728
- else:
729
- sql.append(quote(column_name))
825
+ join = ""
826
+ for column_name in columns.split(","):
827
+ column_name = column_name.replace('"', "")
828
+ if join:
829
+ sql.append(join)
830
+ column = tx.table(table).column(column_name)
831
+ print(column)
832
+ if not column.exists():
833
+ raise Exception(
834
+ f"Column {column_name} does not exist in table {table}."
835
+ )
836
+ if column.py_type == str:
837
+ if lower:
838
+ sql.append(f"lower({TableHelper.quote(column_name)})")
730
839
  else:
731
- sql.append(quote(column_name))
732
- join = ","
733
- else:
734
- sql.append(columns)
840
+ sql.append(TableHelper.quote(column_name))
841
+ else:
842
+ sql.append(TableHelper.quote(column_name))
843
+ join = ","
844
+
735
845
  if trigram:
736
846
  sql.append(f"{trigram.lower()}_trgm_ops")
737
847
  sql.append(")")
738
848
  vals = []
849
+ s, v = TableHelper(tx, table).make_where(where)
850
+ sql.append(s)
851
+ vals.extend(v)
739
852
 
740
- make_where(where, sql, vals)
741
- return " ".join(sql), tuple(vals)
853
+ sql = sqlparse.format(" ".join(sql), reindent=True, keyword_case="upper")
854
+ return sql, tuple(vals)
742
855
 
743
856
  @classmethod
744
857
  def drop_index(cls, table=None, columns=None, name=None, schema=None, trigram=None):
745
858
  if "." not in table and schema:
746
859
  table = f"{schema}.{table}"
747
860
  if isinstance(columns, (list, set)):
748
- columns = ",".join([quote(c.lower()) for c in columns])
861
+ columns = ",".join([TableHelper.quote(c.lower()) for c in columns])
749
862
  else:
750
- columns = quote(columns)
863
+ columns = TableHelper.quote(columns)
751
864
  sql = ["DROP"]
752
865
  sql.append("INDEX IF EXISTS")
753
- tablename = quote(table)
866
+ tablename = TableHelper.quote(table)
754
867
  if not name:
755
868
  name = re.sub(
756
869
  r"\([^)]*\)",
@@ -758,209 +871,12 @@ class SQL(object):
758
871
  columns.replace(" ", "").replace(",", "_").replace('"', ""),
759
872
  )
760
873
  if trigram:
761
- sql.append(
762
- f"IDX__TRGM_{table.replace('.', '_')}_{trigram.upper()}__{name}"
763
- )
874
+ sql.append(f"IDX__TRGM_{table.replace('.', '_')}_{trigram.upper()}__{name}")
764
875
  else:
765
876
  sql.append(f"IDX__{table.replace('.', '_')}__{name}")
766
- return " ".join(sql), tuple()
767
877
 
768
- @classmethod
769
- def merge(cls, table, data, pk, on_conflict_do_nothing, on_conflict_update):
770
- d = {}
771
- d.update(data)
772
- d.update(pk)
773
- sql, vals = cls.insert(table, d)
774
- sql = [sql]
775
- vals = list(vals)
776
- if on_conflict_do_nothing != on_conflict_update:
777
- sql.append("ON CONFLICT")
778
- sql.append("(")
779
- sql.append(",".join(pk.keys()))
780
- sql.append(")")
781
- sql.append("DO")
782
- if on_conflict_do_nothing:
783
- sql.append("NOTHING")
784
- elif on_conflict_update:
785
- sql2, vals2 = cls.update(table, data, pk, excluded=True)
786
- sql.append(sql2)
787
- vals.extend(vals2)
788
- else:
789
- raise Exception(
790
- "Update on conflict must have one and only one option to complete on conflict."
791
- )
792
- return " ".join(sql), tuple(vals)
793
-
794
- @classmethod
795
- def insert(cls, table, data):
796
- keys = []
797
- vals = []
798
- args = []
799
- for key, val in data.items():
800
- keys.append(quote(key.lower()))
801
- if isinstance(val, str) and len(val) > 2 and val[:2] == "@@" and val[2:]:
802
- vals.append(val[2:])
803
- else:
804
- vals.append("%s")
805
- args.append(val)
806
-
807
- sql = ["INSERT INTO"]
808
- sql.append(quote(table))
809
- sql.append("(")
810
- sql.append(",".join(keys))
811
- sql.append(")")
812
- sql.append("VALUES")
813
- sql.append("(")
814
- sql.append(",".join(vals))
815
- sql.append(")")
816
- sql = " ".join(sql)
817
- return sql, tuple(args)
818
-
819
- @classmethod
820
- def update(
821
- cls,
822
- table,
823
- data,
824
- pk,
825
- left_join=None,
826
- inner_join=None,
827
- outer_join=None,
828
- excluded=False,
829
- ):
830
- alias = "A"
831
- if " " in table:
832
- alias, table = table.split(" ")
833
- is_join = bool(left_join or inner_join or outer_join)
834
- sql = ["UPDATE"]
835
- if not excluded:
836
- sql.append(quote(table))
837
- sql.append("SET")
838
- vals = []
839
- connect = ""
840
- for key, val in data.items():
841
- if connect:
842
- sql.append(connect)
843
- if isinstance(val, str) and val[:2] == "@@" and val[2:]:
844
- sql.append(f"{key} = {val[2:]}")
845
- else:
846
- if excluded:
847
- sql.append(f"{key} = EXCLUDED.{key}")
848
- else:
849
- sql.append(f"{key} = %s")
850
- vals.append(val)
851
- connect = ","
852
- if is_join:
853
- sql.append("FROM")
854
- sql.append(table)
855
- sql.append("AS")
856
- sql.append(alias)
857
- if left_join:
858
- for k, v in left_join.items():
859
- sql.append("LEFT JOIN")
860
- sql.append(k)
861
- sql.append("ON")
862
- sql.append(v)
863
- if outer_join:
864
- for k, v in outer_join.items():
865
- sql.append("OUTER JOIN")
866
- sql.append(k)
867
- sql.append("ON")
868
- sql.append(v)
869
- if inner_join:
870
- for k, v in inner_join.items():
871
- sql.append("INNER JOIN")
872
- sql.append(k)
873
- sql.append("ON")
874
- sql.append(v)
875
- if not excluded:
876
- make_where(pk, sql, vals, is_join)
877
- return " ".join(sql), tuple(vals)
878
-
879
- @classmethod
880
- def get_type(cls, v):
881
- if isinstance(v, str):
882
- if v[:2] == "@@":
883
- return v[2:] or cls.TYPES.TEXT
884
- elif isinstance(v, str) or v is str:
885
- return cls.TYPES.TEXT
886
- elif isinstance(v, bool) or v is bool:
887
- return cls.TYPES.BOOLEAN
888
- elif isinstance(v, int) or v is int:
889
- return cls.TYPES.BIGINT
890
- elif isinstance(v, float) or v is float:
891
- return f"{cls.TYPES.NUMERIC}(19, 6)"
892
- elif isinstance(v, decimal.Decimal) or v is decimal.Decimal:
893
- return f"{cls.TYPES.NUMERIC}(19, 6)"
894
- elif isinstance(v, datetime.datetime) or v is datetime.datetime:
895
- return cls.TYPES.DATETIME
896
- elif isinstance(v, datetime.date) or v is datetime.date:
897
- return cls.TYPES.DATE
898
- elif isinstance(v, datetime.time) or v is datetime.time:
899
- return cls.TYPES.TIME
900
- elif isinstance(v, datetime.timedelta) or v is datetime.timedelta:
901
- return cls.TYPES.INTERVAL
902
- elif isinstance(v, bytes) or v is bytes:
903
- return cls.TYPES.BINARY
904
- return cls.TYPES.TEXT
905
-
906
- @classmethod
907
- def get_conv(cls, v):
908
- if isinstance(v, str):
909
- if v[:2] == "@@":
910
- return v[2:] or cls.TYPES.TEXT
911
- elif isinstance(v, str) or v is str:
912
- return cls.TYPES.TEXT
913
- elif isinstance(v, bool) or v is bool:
914
- return cls.TYPES.BOOLEAN
915
- elif isinstance(v, int) or v is int:
916
- return cls.TYPES.BIGINT
917
- elif isinstance(v, float) or v is float:
918
- return cls.TYPES.NUMERIC
919
- elif isinstance(v, decimal.Decimal) or v is decimal.Decimal:
920
- return cls.TYPES.NUMERIC
921
- elif isinstance(v, datetime.datetime) or v is datetime.datetime:
922
- return cls.TYPES.DATETIME
923
- elif isinstance(v, datetime.date) or v is datetime.date:
924
- return cls.TYPES.DATE
925
- elif isinstance(v, datetime.time) or v is datetime.time:
926
- return cls.TYPES.TIME
927
- elif isinstance(v, datetime.timedelta) or v is datetime.timedelta:
928
- return cls.TYPES.INTERVAL
929
- elif isinstance(v, bytes) or v is bytes:
930
- return cls.TYPES.BINARY
931
- return cls.TYPES.TEXT
932
-
933
- @classmethod
934
- def py_type(cls, v):
935
- v = str(v).upper()
936
- if v == cls.TYPES.INTEGER:
937
- return int
938
- elif v == cls.TYPES.SMALLINT:
939
- return int
940
- elif v == cls.TYPES.BIGINT:
941
- return int
942
- elif v == cls.TYPES.NUMERIC:
943
- return decimal.Decimal
944
- elif v == cls.TYPES.TEXT:
945
- return str
946
- elif v == cls.TYPES.BOOLEAN:
947
- return bool
948
- elif v == cls.TYPES.DATE:
949
- return datetime.date
950
- elif v == cls.TYPES.TIME:
951
- return datetime.time
952
- elif v == cls.TYPES.TIME_TZ:
953
- return datetime.time
954
- elif v == cls.TYPES.DATETIME:
955
- return datetime.datetime
956
- elif v == cls.TYPES.INTERVAL:
957
- return datetime.timedelta
958
- elif v == cls.TYPES.DATETIME_TZ:
959
- return datetime.datetime
960
- elif v == cls.TYPES.INTERVAL_TZ:
961
- return datetime.timedelta
962
- else:
963
- raise Exception(f"unmapped type {v}")
878
+ sql = sqlparse.format(" ".join(sql), reindent=True, keyword_case="upper")
879
+ return sql, tuple()
964
880
 
965
881
  @classmethod
966
882
  def massage_data(cls, data):
@@ -985,46 +901,53 @@ class SQL(object):
985
901
  for key, val in columns.items():
986
902
  key = re.sub("<>!=%", "", key.lower())
987
903
  sql.append(
988
- f"ALTER TABLE {quote(table)} ADD {quote(key)} {cls.get_type(val)} {null};"
904
+ f"ALTER TABLE {TableHelper.quote(table)} ADD {TableHelper.quote(key)} {TYPES.get_type(val)} {null};"
989
905
  )
990
- return "\n\t".join(sql), tuple()
906
+ sql = sqlparse.format(" ".join(sql), reindent=True, keyword_case="upper")
907
+ return sql, tuple()
991
908
 
992
909
  @classmethod
993
910
  def alter_drop(cls, table, columns):
994
- sql = [f"ALTER TABLE {quote(table)} DROP COLUMN"]
911
+ sql = [f"ALTER TABLE {TableHelper.quote(table)} DROP COLUMN"]
995
912
  if isinstance(columns, dict):
996
913
  for key, val in columns.items():
997
914
  key = re.sub("<>!=%", "", key.lower())
998
915
  sql.append(f"{key},")
999
916
  if sql[-1][-1] == ",":
1000
917
  sql[-1] = sql[-1][:-1]
1001
- return "\n\t".join(sql), tuple()
918
+ sql = sqlparse.format(" ".join(sql), reindent=True, keyword_case="upper")
919
+ return sql, tuple()
1002
920
 
1003
921
  @classmethod
1004
922
  def alter_column_by_type(cls, table, column, value, nullable=True):
1005
- sql = [f"ALTER TABLE {quote(table)} ALTER COLUMN"]
1006
- sql.append(f"{quote(column)} TYPE {cls.get_type(value)}")
1007
- sql.append(f"USING {quote(column)}::{cls.get_conv(value)}")
923
+ sql = [f"ALTER TABLE {TableHelper.quote(table)} ALTER COLUMN"]
924
+ sql.append(f"{TableHelper.quote(column)} TYPE {TYPES.get_type(value)}")
925
+ sql.append(f"USING {TableHelper.quote(column)}::{TYPES.get_conv(value)}")
1008
926
  if not nullable:
1009
927
  sql.append("NOT NULL")
1010
- return "\n\t".join(sql), tuple()
928
+ sql = sqlparse.format(" ".join(sql), reindent=True, keyword_case="upper")
929
+ return sql, tuple()
1011
930
 
1012
931
  @classmethod
1013
932
  def alter_column_by_sql(cls, table, column, value):
1014
- sql = [f"ALTER TABLE {quote(table)} ALTER COLUMN"]
1015
- sql.append(f"{quote(column)} {value}")
1016
- return " ".join(sql), tuple()
933
+ sql = [f"ALTER TABLE {TableHelper.quote(table)} ALTER COLUMN"]
934
+ sql.append(f"{TableHelper.quote(column)} {value}")
935
+ sql = sqlparse.format(" ".join(sql), reindent=True, keyword_case="upper")
936
+ return sql, tuple()
1017
937
 
1018
938
  @classmethod
1019
939
  def rename_column(cls, table, orig, new):
1020
940
  return (
1021
- f"ALTER TABLE {quote(table)} RENAME COLUMN {quote(orig)} TO {quote(new)};",
941
+ f"ALTER TABLE {TableHelper.quote(table)} RENAME COLUMN {TableHelper.quote(orig)} TO {TableHelper.quote(new)};",
1022
942
  tuple(),
1023
943
  )
1024
944
 
1025
945
  @classmethod
1026
946
  def rename_table(cls, table, new):
1027
- return f"ALTER TABLE {quote(table)} RENAME TO {quote(new)};", tuple()
947
+ return (
948
+ f"ALTER TABLE {TableHelper.quote(table)} RENAME TO {TableHelper.quote(new)};",
949
+ tuple(),
950
+ )
1028
951
 
1029
952
  @classmethod
1030
953
  def create_savepoint(cls, sp):
@@ -1039,26 +962,19 @@ class SQL(object):
1039
962
  return f'ROLLBACK TO SAVEPOINT "{sp}"', tuple()
1040
963
 
1041
964
  @classmethod
1042
- def duplicate_rows(cls, table, columns, where={}):
1043
- return cls.select(
1044
- columns,
1045
- table,
1046
- where,
1047
- orderby=columns,
1048
- groupby=columns,
1049
- having="count(*) > 2",
1050
- )
1051
-
1052
- @classmethod
1053
- def delete(cls, table, where):
965
+ def delete(cls, tx, table, where):
1054
966
  sql = [f"DELETE FROM {table}"]
1055
967
  vals = []
1056
- make_where(where, sql, vals)
1057
- return " ".join(sql), tuple(vals)
968
+ if where:
969
+ s, v = TableHelper(tx, table).make_where(where)
970
+ sql.append(s)
971
+ vals.extend(v)
972
+ sql = sqlparse.format(" ".join(sql), reindent=True, keyword_case="upper")
973
+ return sql, tuple(vals)
1058
974
 
1059
975
  @classmethod
1060
976
  def truncate(cls, table):
1061
- return f"truncate table {quote(table)}", tuple()
977
+ return f"truncate table {TableHelper.quote(table)}", tuple()
1062
978
 
1063
979
  @classmethod
1064
980
  def create_view(cls, name, query, temp=False, silent=True):
@@ -1071,7 +987,8 @@ class SQL(object):
1071
987
  sql.append(name)
1072
988
  sql.append("AS")
1073
989
  sql.append(query)
1074
- return " ".join(sql), tuple()
990
+ sql = sqlparse.format(" ".join(sql), reindent=True, keyword_case="upper")
991
+ return sql, tuple()
1075
992
 
1076
993
  @classmethod
1077
994
  def drop_view(cls, name, silent=True):
@@ -1079,7 +996,8 @@ class SQL(object):
1079
996
  if silent:
1080
997
  sql.append("IF EXISTS")
1081
998
  sql.append(name)
1082
- return " ".join(sql), tuple()
999
+ sql = sqlparse.format(" ".join(sql), reindent=True, keyword_case="upper")
1000
+ return sql, tuple()
1083
1001
 
1084
1002
  @classmethod
1085
1003
  def alter_trigger(cls, table, state="ENABLE", name="USER"):
@@ -1093,7 +1011,7 @@ class SQL(object):
1093
1011
  )
1094
1012
 
1095
1013
  @classmethod
1096
- def missing(cls, table, list, column="SYS_ID", where=None):
1014
+ def missing(cls, tx, table, list, column="SYS_ID", where=None):
1097
1015
  sql = [
1098
1016
  f"SELECT * FROM",
1099
1017
  f"UNNEST('{{{','.join([str(x) for x in list])}}}'::int[]) id",
@@ -1101,22 +1019,23 @@ class SQL(object):
1101
1019
  f"SELECT {column} FROM {table}",
1102
1020
  ]
1103
1021
  vals = []
1104
- make_where(where, sql, vals)
1105
- return " ".join(sql), tuple(vals)
1106
-
1107
- class TYPES(object):
1108
- TEXT = "TEXT"
1109
- INTEGER = "INTEGER"
1110
- NUMERIC = "NUMERIC"
1111
- DATETIME_TZ = "TIMESTAMP WITH TIME ZONE"
1112
- TIMESTAMP_TZ = "TIMESTAMP WITH TIME ZONE"
1113
- DATETIME = "TIMESTAMP WITHOUT TIME ZONE"
1114
- TIMESTAMP = "TIMESTAMP WITHOUT TIME ZONE"
1115
- DATE = "DATE"
1116
- TIME_TZ = "TIME WITH TIME ZONE"
1117
- TIME = "TIME WITHOUT TIME ZONE"
1118
- BIGINT = "BIGINT"
1119
- SMALLINT = "SMALLINT"
1120
- BOOLEAN = "BOOLEAN"
1121
- BINARY = "BYTEA"
1122
- INTERVAL = "INTERVAL"
1022
+ if where:
1023
+ s, v = TableHelper(tx, table).make_where(where)
1024
+ sql.append(s)
1025
+ vals.extend(v)
1026
+ sql = sqlparse.format(" ".join(sql), reindent=True, keyword_case="upper")
1027
+ return sql, tuple(vals)
1028
+
1029
+ @classmethod
1030
+ def indexes(cls, table):
1031
+ """
1032
+ Returns SQL for retrieving all indexes on a given table with detailed attributes.
1033
+ """
1034
+ return (
1035
+ """
1036
+ SELECT indexname, tablename, schemaname, indexdef
1037
+ FROM pg_indexes
1038
+ WHERE tablename = %s
1039
+ """,
1040
+ (table,),
1041
+ )