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.
- velocity/__init__.py +1 -1
- velocity/db/core/column.py +25 -105
- velocity/db/core/database.py +79 -23
- velocity/db/core/decorators.py +84 -47
- velocity/db/core/engine.py +190 -189
- velocity/db/core/result.py +94 -49
- velocity/db/core/row.py +81 -46
- velocity/db/core/sequence.py +112 -22
- velocity/db/core/table.py +660 -243
- velocity/db/core/transaction.py +75 -77
- velocity/db/servers/mysql.py +4 -0
- velocity/db/servers/postgres/__init__.py +19 -0
- velocity/db/servers/postgres/operators.py +23 -0
- velocity/db/servers/{postgres.py → postgres/sql.py} +508 -589
- velocity/db/servers/postgres/types.py +109 -0
- velocity/db/servers/tablehelper.py +277 -0
- velocity/misc/conv/iconv.py +277 -91
- velocity/misc/conv/oconv.py +5 -4
- velocity/misc/db.py +2 -2
- velocity/misc/format.py +2 -2
- {velocity_python-0.0.35.dist-info → velocity_python-0.0.65.dist-info}/METADATA +7 -6
- velocity_python-0.0.65.dist-info/RECORD +47 -0
- {velocity_python-0.0.35.dist-info → velocity_python-0.0.65.dist-info}/WHEEL +1 -1
- velocity_python-0.0.35.dist-info/RECORD +0 -43
- /velocity/db/servers/{postgres_reserved.py → postgres/reserved.py} +0 -0
- {velocity_python-0.0.35.dist-info → velocity_python-0.0.65.dist-info/licenses}/LICENSE +0 -0
- {velocity_python-0.0.35.dist-info → velocity_python-0.0.65.dist-info}/top_level.txt +0 -0
velocity/db/core/table.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import sqlparse
|
|
2
2
|
from velocity.db import exceptions
|
|
3
3
|
from velocity.db.core.row import Row
|
|
4
4
|
from velocity.db.core.result import Result
|
|
@@ -11,34 +11,34 @@ from velocity.db.core.decorators import (
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class Query:
|
|
14
|
+
"""
|
|
15
|
+
A utility class to store raw SQL and parameters without immediately executing.
|
|
16
|
+
"""
|
|
17
|
+
|
|
14
18
|
def __init__(self, sql, params=()):
|
|
15
|
-
self.sql = sql
|
|
16
|
-
self.params = params
|
|
19
|
+
self.sql = sqlparse.format(sql, reindent=True, keyword_case="upper")
|
|
20
|
+
self.params = tuple(params)
|
|
17
21
|
|
|
18
22
|
def __str__(self):
|
|
19
23
|
return self.sql
|
|
20
24
|
|
|
21
25
|
|
|
22
|
-
class Table
|
|
26
|
+
class Table:
|
|
27
|
+
"""
|
|
28
|
+
Provides an interface for performing CRUD and metadata operations on a DB table.
|
|
29
|
+
"""
|
|
30
|
+
|
|
23
31
|
def __init__(self, tx, name):
|
|
24
32
|
self.tx = tx
|
|
25
|
-
assert self.tx
|
|
26
33
|
self.name = name.lower()
|
|
27
|
-
assert self.name
|
|
28
34
|
self.sql = tx.engine.sql
|
|
29
|
-
assert self.sql
|
|
30
35
|
|
|
31
36
|
def __str__(self):
|
|
32
|
-
return
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
""" % (
|
|
38
|
-
self.name,
|
|
39
|
-
self.exists(),
|
|
40
|
-
len(self.columns),
|
|
41
|
-
len(self),
|
|
37
|
+
return (
|
|
38
|
+
f"Table: {self.name}\n"
|
|
39
|
+
f"(table exists) {self.exists()}\n"
|
|
40
|
+
f"Columns: {len(self.columns())}\n"
|
|
41
|
+
f"Rows: {len(self)}\n"
|
|
42
42
|
)
|
|
43
43
|
|
|
44
44
|
def __enter__(self):
|
|
@@ -49,12 +49,14 @@ class Table(object):
|
|
|
49
49
|
self.close()
|
|
50
50
|
|
|
51
51
|
def close(self):
|
|
52
|
+
"""
|
|
53
|
+
Closes the internal cursor if we have one open.
|
|
54
|
+
"""
|
|
52
55
|
try:
|
|
53
56
|
self._cursor.close()
|
|
54
57
|
except:
|
|
55
58
|
pass
|
|
56
59
|
|
|
57
|
-
@property
|
|
58
60
|
def cursor(self):
|
|
59
61
|
try:
|
|
60
62
|
return self._cursor
|
|
@@ -63,75 +65,128 @@ class Table(object):
|
|
|
63
65
|
return self._cursor
|
|
64
66
|
|
|
65
67
|
def __call__(self, where=None):
|
|
66
|
-
|
|
68
|
+
"""
|
|
69
|
+
Generator: SELECT rows matching the `where` condition.
|
|
70
|
+
"""
|
|
71
|
+
sql, val = self.sql.select(self.tx, table=self.name, where=where)
|
|
67
72
|
for data in self.tx.execute(sql, val):
|
|
68
73
|
yield self.row(data)
|
|
69
74
|
|
|
70
75
|
def __iter__(self):
|
|
71
|
-
|
|
76
|
+
"""
|
|
77
|
+
Iterate over all rows in ascending order by sys_id.
|
|
78
|
+
"""
|
|
79
|
+
sql, val = self.sql.select(self.tx, table=self.name, orderby="sys_id")
|
|
72
80
|
for data in self.tx.execute(sql, val):
|
|
73
81
|
yield self.row(data)
|
|
74
82
|
|
|
75
|
-
|
|
76
|
-
|
|
83
|
+
def sys_columns(self, **kwds):
|
|
84
|
+
"""
|
|
85
|
+
Returns the raw list of columns, possibly skipping any additional system columns logic.
|
|
86
|
+
"""
|
|
77
87
|
sql, vals = self.sql.columns(self.name)
|
|
78
|
-
|
|
88
|
+
if kwds.get("sql_only", False):
|
|
89
|
+
return sql, vals
|
|
90
|
+
result = self.tx.execute(sql, vals, cursor=self.cursor())
|
|
79
91
|
return [x[0] for x in result.as_tuple()]
|
|
80
92
|
|
|
81
|
-
@property
|
|
82
93
|
def columns(self):
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return columns
|
|
94
|
+
"""
|
|
95
|
+
Returns column names, excluding columns that start with 'sys_'.
|
|
96
|
+
"""
|
|
97
|
+
return [col for col in self.sys_columns() if not col.startswith("sys_")]
|
|
88
98
|
|
|
89
99
|
@return_default(None, (exceptions.DbObjectExistsError,))
|
|
90
|
-
def create_index(
|
|
100
|
+
def create_index(
|
|
101
|
+
self, columns, unique=False, direction=None, where=None, lower=None, **kwds
|
|
102
|
+
):
|
|
103
|
+
"""
|
|
104
|
+
Creates an index on the given columns. Returns None on success, or `None` if the index already exists.
|
|
105
|
+
If the object already exists, this function will return without raising an error, but the exisitng
|
|
106
|
+
index may have been created with different parameters.
|
|
107
|
+
"""
|
|
91
108
|
sql, vals = self.sql.create_index(
|
|
92
|
-
self.
|
|
109
|
+
self.tx,
|
|
110
|
+
table=self.name,
|
|
111
|
+
columns=columns,
|
|
112
|
+
unique=unique,
|
|
113
|
+
direction=direction,
|
|
114
|
+
where=where,
|
|
115
|
+
lower=lower,
|
|
93
116
|
)
|
|
94
|
-
|
|
117
|
+
if kwds.get("sql_only", False):
|
|
118
|
+
return sql, vals
|
|
119
|
+
self.tx.execute(sql, vals, cursor=self.cursor())
|
|
95
120
|
|
|
96
121
|
@return_default(None)
|
|
97
122
|
def drop_index(self, columns, **kwds):
|
|
98
|
-
|
|
99
|
-
|
|
123
|
+
"""
|
|
124
|
+
Drops an index for the specified columns.
|
|
125
|
+
"""
|
|
126
|
+
sql, vals = self.sql.drop_index(self.name, columns)
|
|
127
|
+
if kwds.get("sql_only", False):
|
|
128
|
+
return sql, vals
|
|
129
|
+
self.tx.execute(sql, vals, cursor=self.cursor())
|
|
100
130
|
|
|
101
131
|
@return_default(None)
|
|
102
132
|
def drop_column(self, column):
|
|
133
|
+
"""
|
|
134
|
+
Drops a column from this table.
|
|
135
|
+
"""
|
|
103
136
|
sql, vals = self.sql.drop_column(self.name, column)
|
|
104
|
-
self.tx.execute(sql, vals, cursor=self.cursor)
|
|
105
|
-
|
|
106
|
-
def create(self, columns=
|
|
137
|
+
self.tx.execute(sql, vals, cursor=self.cursor())
|
|
138
|
+
|
|
139
|
+
def create(self, columns=None, drop=False):
|
|
140
|
+
"""
|
|
141
|
+
Creates this table with system columns plus the given `columns` dictionary.
|
|
142
|
+
Optionally drops any existing table first.
|
|
143
|
+
"""
|
|
144
|
+
columns = columns or {}
|
|
107
145
|
sql, vals = self.sql.create_table(self.name, columns, drop)
|
|
108
|
-
self.tx.execute(sql, vals, cursor=self.cursor)
|
|
146
|
+
self.tx.execute(sql, vals, cursor=self.cursor())
|
|
109
147
|
|
|
110
148
|
def drop(self):
|
|
149
|
+
"""
|
|
150
|
+
Drops this table if it exists.
|
|
151
|
+
"""
|
|
111
152
|
sql, vals = self.sql.drop_table(self.name)
|
|
112
|
-
self.tx.execute(sql, vals, cursor=self.cursor)
|
|
153
|
+
self.tx.execute(sql, vals, cursor=self.cursor())
|
|
113
154
|
|
|
114
155
|
def exists(self):
|
|
156
|
+
"""
|
|
157
|
+
Returns True if this table already exists in the DB.
|
|
158
|
+
"""
|
|
115
159
|
sql, vals = self.sql.tables()
|
|
116
|
-
result = self.tx.execute(sql, vals, cursor=self.cursor)
|
|
160
|
+
result = self.tx.execute(sql, vals, cursor=self.cursor())
|
|
117
161
|
if "." in self.name:
|
|
118
|
-
return
|
|
119
|
-
|
|
120
|
-
return bool(self.name in ["%s" % x[1] for x in result.as_tuple()])
|
|
162
|
+
return self.name in [f"{x[0]}.{x[1]}" for x in result.as_tuple()]
|
|
163
|
+
return self.name in [x[1] for x in result.as_tuple()]
|
|
121
164
|
|
|
122
165
|
def column(self, name):
|
|
166
|
+
"""
|
|
167
|
+
Returns a Column object for the given column name.
|
|
168
|
+
"""
|
|
123
169
|
return Column(self, name)
|
|
124
170
|
|
|
125
171
|
def row(self, key=None, lock=None):
|
|
126
|
-
|
|
172
|
+
"""
|
|
173
|
+
Retrieves a Row instance for the given primary key or conditions dict. If `key` is None, returns a new row.
|
|
174
|
+
"""
|
|
175
|
+
if key is None:
|
|
127
176
|
return self.new(lock=lock)
|
|
128
|
-
return Row(self, key)
|
|
177
|
+
return Row(self, key, lock=lock)
|
|
129
178
|
|
|
130
179
|
def dict(self, key):
|
|
131
|
-
|
|
132
|
-
|
|
180
|
+
"""
|
|
181
|
+
Returns a row as a dictionary or empty dict if not found.
|
|
182
|
+
"""
|
|
183
|
+
r = self.find(key)
|
|
184
|
+
return r.to_dict() if r else {}
|
|
133
185
|
|
|
134
186
|
def rows(self, where=None, orderby=None, qty=None, lock=None, skip_locked=None):
|
|
187
|
+
"""
|
|
188
|
+
Generator that yields Row objects matching `where`, up to `qty`.
|
|
189
|
+
"""
|
|
135
190
|
for key in self.ids(
|
|
136
191
|
where=where, orderby=orderby, qty=qty, lock=lock, skip_locked=skip_locked
|
|
137
192
|
):
|
|
@@ -148,7 +203,10 @@ class Table(object):
|
|
|
148
203
|
lock=None,
|
|
149
204
|
skip_locked=None,
|
|
150
205
|
):
|
|
151
|
-
|
|
206
|
+
"""
|
|
207
|
+
Returns a generator of sys_id values for rows matching `where`.
|
|
208
|
+
"""
|
|
209
|
+
results = self.select(
|
|
152
210
|
"sys_id",
|
|
153
211
|
where=where,
|
|
154
212
|
orderby=orderby,
|
|
@@ -158,63 +216,76 @@ class Table(object):
|
|
|
158
216
|
qty=qty,
|
|
159
217
|
lock=lock,
|
|
160
218
|
skip_locked=skip_locked,
|
|
161
|
-
)
|
|
219
|
+
)
|
|
220
|
+
for key in results:
|
|
162
221
|
yield key["sys_id"]
|
|
163
222
|
|
|
164
223
|
def set_id(self, start):
|
|
224
|
+
"""
|
|
225
|
+
Sets the sequence for this table's sys_id to the given start value.
|
|
226
|
+
"""
|
|
165
227
|
sql, vals = self.sql.set_id(self.name, start)
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
def new(self, data=
|
|
228
|
+
self.tx.execute(sql, vals, cursor=self.cursor())
|
|
229
|
+
|
|
230
|
+
def new(self, data=None, lock=None):
|
|
231
|
+
"""
|
|
232
|
+
Inserts a new row with the given data and returns a Row object. If data is None, sets sys_modified automatically.
|
|
233
|
+
"""
|
|
234
|
+
if data is None:
|
|
235
|
+
data = {"sys_modified": "@@CURRENT_TIMESTAMP"}
|
|
169
236
|
if len(data) == 1 and "sys_id" in data:
|
|
170
237
|
return self.row(data, lock=lock).touch()
|
|
171
|
-
|
|
238
|
+
self.insert(data)
|
|
172
239
|
sql, vals = self.sql.last_id(self.name)
|
|
173
240
|
sys_id = self.tx.execute(sql, vals).scalar()
|
|
174
241
|
return self.row(sys_id, lock=lock)
|
|
175
242
|
|
|
176
243
|
def get(self, where, lock=None, use_where=False):
|
|
244
|
+
"""
|
|
245
|
+
Gets or creates a row matching `where`. If multiple rows match, raises DuplicateRowsFoundError.
|
|
246
|
+
If none match, a new row is created with the non-operator aspects of `where`.
|
|
247
|
+
"""
|
|
177
248
|
if where is None:
|
|
178
|
-
raise Exception("None is not allowed as a primary key")
|
|
249
|
+
raise Exception("None is not allowed as a primary key.")
|
|
179
250
|
if isinstance(where, int):
|
|
180
251
|
where = {"sys_id": where}
|
|
181
252
|
result = self.select("sys_id", where=where, lock=lock).all()
|
|
182
253
|
if len(result) > 1:
|
|
183
|
-
sql = self.
|
|
254
|
+
sql = self.select("sys_id", sql_only=True, where=where, lock=lock)
|
|
184
255
|
raise exceptions.DuplicateRowsFoundError(
|
|
185
|
-
"More than one entry found. {}"
|
|
256
|
+
f"More than one entry found. {sql}"
|
|
186
257
|
)
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
where.pop(key)
|
|
194
|
-
return self.new(where, lock=lock)
|
|
258
|
+
if not result:
|
|
259
|
+
new_data = where.copy()
|
|
260
|
+
for k in list(new_data.keys()):
|
|
261
|
+
if set("<>!=%").intersection(k):
|
|
262
|
+
new_data.pop(k)
|
|
263
|
+
return self.new(new_data, lock=lock)
|
|
195
264
|
if use_where:
|
|
196
265
|
return Row(self, where, lock=lock)
|
|
197
|
-
|
|
198
|
-
return Row(self, result[0]["sys_id"], lock=lock)
|
|
266
|
+
return Row(self, result[0]["sys_id"], lock=lock)
|
|
199
267
|
|
|
200
268
|
@return_default(None)
|
|
201
269
|
def find(self, where, lock=None, use_where=False):
|
|
270
|
+
"""
|
|
271
|
+
Finds a single row matching `where`, or returns None if none found.
|
|
272
|
+
Raises DuplicateRowsFoundError if multiple rows match.
|
|
273
|
+
"""
|
|
202
274
|
if where is None:
|
|
203
|
-
raise Exception("None is not allowed as a primary key")
|
|
275
|
+
raise Exception("None is not allowed as a primary key.")
|
|
204
276
|
if isinstance(where, int):
|
|
205
277
|
where = {"sys_id": where}
|
|
206
278
|
result = self.select("sys_id", where=where, lock=lock).all()
|
|
279
|
+
if not result:
|
|
280
|
+
return None
|
|
207
281
|
if len(result) > 1:
|
|
208
|
-
sql = self.
|
|
282
|
+
sql = self.select("sys_id", sql_only=True, where=where, lock=lock)
|
|
209
283
|
raise exceptions.DuplicateRowsFoundError(
|
|
210
|
-
"More than one entry found. {}"
|
|
284
|
+
f"More than one entry found. {sql}"
|
|
211
285
|
)
|
|
212
|
-
elif len(result) < 1:
|
|
213
|
-
return None
|
|
214
286
|
if use_where:
|
|
215
287
|
return Row(self, where, lock=lock)
|
|
216
|
-
|
|
217
|
-
return Row(self, result[0]["sys_id"], lock=lock)
|
|
288
|
+
return Row(self, result[0]["sys_id"], lock=lock)
|
|
218
289
|
|
|
219
290
|
@return_default(None)
|
|
220
291
|
def first(
|
|
@@ -226,185 +297,222 @@ class Table(object):
|
|
|
226
297
|
skip_locked=None,
|
|
227
298
|
use_where=False,
|
|
228
299
|
):
|
|
300
|
+
"""
|
|
301
|
+
Finds the first matching row (by `orderby`) or creates one if `create_new=True` and none found.
|
|
302
|
+
"""
|
|
229
303
|
if where is None:
|
|
230
|
-
raise Exception("None is not allowed as a where clause")
|
|
304
|
+
raise Exception("None is not allowed as a where clause.")
|
|
231
305
|
if isinstance(where, int):
|
|
232
306
|
where = {"sys_id": where}
|
|
233
|
-
|
|
307
|
+
results = self.select(
|
|
234
308
|
"sys_id", where=where, orderby=orderby, skip_locked=skip_locked
|
|
235
309
|
).all()
|
|
236
|
-
if
|
|
310
|
+
if not results:
|
|
237
311
|
if create_new:
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
where.pop(key)
|
|
244
|
-
return self.new(where, lock=lock)
|
|
312
|
+
new_data = where.copy()
|
|
313
|
+
for k in list(new_data.keys()):
|
|
314
|
+
if set("<>!=%").intersection(k):
|
|
315
|
+
new_data.pop(k)
|
|
316
|
+
return self.new(new_data, lock=lock)
|
|
245
317
|
return None
|
|
246
318
|
if use_where:
|
|
247
319
|
return Row(self, where, lock=lock)
|
|
248
|
-
|
|
249
|
-
return Row(self, result[0]["sys_id"], lock=lock)
|
|
320
|
+
return Row(self, results[0]["sys_id"], lock=lock)
|
|
250
321
|
|
|
251
322
|
@return_default(None)
|
|
252
323
|
def one(self, where=None, orderby=None, lock=None, use_where=False):
|
|
324
|
+
"""
|
|
325
|
+
Returns exactly one row matching `where`, or None if none found.
|
|
326
|
+
"""
|
|
253
327
|
if isinstance(where, int):
|
|
254
328
|
where = {"sys_id": where}
|
|
255
|
-
|
|
256
|
-
if
|
|
329
|
+
results = self.select("sys_id", where=where, orderby=orderby).all()
|
|
330
|
+
if not results:
|
|
257
331
|
return None
|
|
258
332
|
if use_where:
|
|
259
333
|
return Row(self, where, lock=lock)
|
|
260
|
-
|
|
261
|
-
return Row(self, result[0]["sys_id"], lock=lock)
|
|
334
|
+
return Row(self, results[0]["sys_id"], lock=lock)
|
|
262
335
|
|
|
263
|
-
@property
|
|
264
336
|
def primary_keys(self):
|
|
337
|
+
"""
|
|
338
|
+
Returns the list of primary key columns for this table.
|
|
339
|
+
"""
|
|
265
340
|
sql, vals = self.sql.primary_keys(self.name)
|
|
266
|
-
result = self.tx.execute(sql, vals, cursor=self.cursor)
|
|
341
|
+
result = self.tx.execute(sql, vals, cursor=self.cursor())
|
|
267
342
|
return [x[0] for x in result.as_tuple()]
|
|
268
343
|
|
|
269
|
-
@property
|
|
270
344
|
def foreign_keys(self):
|
|
345
|
+
"""
|
|
346
|
+
Returns the list of foreign key columns for this table (may be incomplete).
|
|
347
|
+
"""
|
|
271
348
|
sql, vals = self.sql.primary_keys(self.name)
|
|
272
|
-
result = self.tx.execute(sql, vals, cursor=self.cursor)
|
|
349
|
+
result = self.tx.execute(sql, vals, cursor=self.cursor())
|
|
273
350
|
return [x[0] for x in result.as_tuple()]
|
|
274
351
|
|
|
275
|
-
def foreign_key_info(self, column):
|
|
352
|
+
def foreign_key_info(self, column, **kwds):
|
|
353
|
+
"""
|
|
354
|
+
Returns info about a foreign key for the specified column.
|
|
355
|
+
"""
|
|
276
356
|
sql, vals = self.sql.foreign_key_info(table=self.name, column=column)
|
|
277
|
-
|
|
357
|
+
if kwds.get("sql_only", False):
|
|
358
|
+
return sql, vals
|
|
359
|
+
return self.tx.execute(sql, vals, cursor=self.cursor()).one()
|
|
278
360
|
|
|
279
361
|
@return_default()
|
|
280
|
-
def create_foreign_key(
|
|
362
|
+
def create_foreign_key(
|
|
363
|
+
self, columns, key_to_table, key_to_columns="sys_id", **kwds
|
|
364
|
+
):
|
|
365
|
+
"""
|
|
366
|
+
Creates a foreign key referencing `key_to_table(key_to_columns)`.
|
|
367
|
+
"""
|
|
281
368
|
sql, vals = self.sql.create_foreign_key(
|
|
282
369
|
self.name, columns, key_to_table, key_to_columns
|
|
283
370
|
)
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
371
|
+
if kwds.get("sql_only", False):
|
|
372
|
+
return sql, vals
|
|
373
|
+
return self.tx.execute(sql, vals, cursor=self.cursor())
|
|
374
|
+
|
|
375
|
+
def drop_foreign_key(self, columns, key_to_table, key_to_columns="sys_id", **kwds):
|
|
376
|
+
"""
|
|
377
|
+
Drops the specified foreign key constraint.
|
|
378
|
+
"""
|
|
287
379
|
sql, vals = self.sql.create_foreign_key(
|
|
288
380
|
self.name, columns, key_to_table, key_to_columns
|
|
289
381
|
)
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
382
|
+
if kwds.get("sql_only", False):
|
|
383
|
+
return sql, vals
|
|
384
|
+
return self.tx.execute(sql, vals, cursor=self.cursor())
|
|
385
|
+
|
|
386
|
+
def rename(self, name, **kwds):
|
|
387
|
+
"""
|
|
388
|
+
Renames this table.
|
|
389
|
+
"""
|
|
293
390
|
sql, vals = self.sql.rename_table(self.name, name)
|
|
294
|
-
|
|
391
|
+
if kwds.get("sql_only", False):
|
|
392
|
+
return sql, vals
|
|
393
|
+
self.tx.execute(sql, vals, cursor=self.cursor())
|
|
295
394
|
self.name = name
|
|
296
395
|
|
|
297
396
|
def lower_keys(self, arg):
|
|
397
|
+
"""
|
|
398
|
+
Returns a copy of the dict `arg` with all keys lowercased.
|
|
399
|
+
"""
|
|
298
400
|
new = {}
|
|
299
401
|
if isinstance(arg, dict):
|
|
300
|
-
for key in
|
|
301
|
-
new[key.lower()] =
|
|
402
|
+
for key, val in arg.items():
|
|
403
|
+
new[key.lower()] = val
|
|
302
404
|
return new
|
|
303
405
|
|
|
304
406
|
@create_missing
|
|
305
|
-
def alter(self, columns):
|
|
407
|
+
def alter(self, columns, **kwds):
|
|
408
|
+
"""
|
|
409
|
+
Adds any missing columns (based on the provided dict) to the table.
|
|
410
|
+
"""
|
|
411
|
+
if not isinstance(columns, dict):
|
|
412
|
+
raise Exception("Columns must be a dict.")
|
|
413
|
+
columns = self.lower_keys(columns)
|
|
306
414
|
diff = []
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
for k in list(columns.keys()):
|
|
311
|
-
diff.append(k) if k not in self.sys_columns else None
|
|
312
|
-
else:
|
|
313
|
-
raise Exception(
|
|
314
|
-
"I don't know how to handle columns data type in this context"
|
|
315
|
-
)
|
|
415
|
+
for k in columns.keys():
|
|
416
|
+
if k not in self.sys_columns():
|
|
417
|
+
diff.append(k)
|
|
316
418
|
if diff:
|
|
317
|
-
|
|
318
|
-
sql, vals = self.sql.alter_add(self.name,
|
|
319
|
-
|
|
419
|
+
newcols = {key: columns[key] for key in diff}
|
|
420
|
+
sql, vals = self.sql.alter_add(self.name, newcols)
|
|
421
|
+
if kwds.get("sql_only", False):
|
|
422
|
+
return sql, vals
|
|
423
|
+
self.tx.execute(sql, vals, cursor=self.cursor())
|
|
320
424
|
|
|
321
425
|
@create_missing
|
|
322
|
-
def alter_type(self, column, type_or_value, nullable=True):
|
|
426
|
+
def alter_type(self, column, type_or_value, nullable=True, **kwds):
|
|
427
|
+
"""
|
|
428
|
+
Alters the specified column to match a new SQL type (inferred from `type_or_value`).
|
|
429
|
+
"""
|
|
323
430
|
sql, vals = self.sql.alter_column_by_type(
|
|
324
431
|
self.name, column, type_or_value, nullable
|
|
325
432
|
)
|
|
326
|
-
|
|
433
|
+
if kwds.get("sql_only", False):
|
|
434
|
+
return sql, vals
|
|
435
|
+
self.tx.execute(sql, vals, cursor=self.cursor())
|
|
327
436
|
|
|
328
437
|
@create_missing
|
|
329
|
-
def update(self, data,
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
438
|
+
def update(self, data, where=None, pk=None, **kwds):
|
|
439
|
+
"""
|
|
440
|
+
Performs an UPDATE of rows matching `where` or `pk` with `data`.
|
|
441
|
+
"""
|
|
442
|
+
sql, vals = self.sql.update(self.tx, self.name, data, where, pk)
|
|
443
|
+
if kwds.get("sql_only", False):
|
|
444
|
+
return sql, vals
|
|
445
|
+
result = self.tx.execute(sql, vals, cursor=self.cursor())
|
|
334
446
|
return result.cursor.rowcount
|
|
335
447
|
|
|
336
448
|
@reset_id_on_dup_key
|
|
337
449
|
@create_missing
|
|
338
|
-
def insert(self, data):
|
|
450
|
+
def insert(self, data, **kwds):
|
|
451
|
+
"""
|
|
452
|
+
Performs an INSERT of the given data into this table. Resets sys_id on duplicate keys if needed.
|
|
453
|
+
"""
|
|
339
454
|
sql, vals = self.sql.insert(self.name, data)
|
|
340
|
-
|
|
455
|
+
if kwds.get("sql_only", False):
|
|
456
|
+
return sql, vals
|
|
457
|
+
result = self.tx.execute(sql, vals, cursor=self.cursor())
|
|
341
458
|
return result.cursor.rowcount
|
|
342
459
|
|
|
343
460
|
@reset_id_on_dup_key
|
|
344
461
|
@create_missing
|
|
345
|
-
def merge(self, data, pk):
|
|
462
|
+
def merge(self, data, pk=None, **kwds):
|
|
463
|
+
"""
|
|
464
|
+
Implements an UPSERT (merge) with conflict handling on pk columns.
|
|
465
|
+
"""
|
|
346
466
|
sql, vals = self.sql.merge(
|
|
347
|
-
self.
|
|
467
|
+
self.tx,
|
|
468
|
+
self.name,
|
|
469
|
+
data,
|
|
470
|
+
pk,
|
|
471
|
+
on_conflict_do_nothing=False,
|
|
472
|
+
on_conflict_update=True,
|
|
348
473
|
)
|
|
349
|
-
|
|
474
|
+
if kwds.get("sql_only", False):
|
|
475
|
+
return sql, vals
|
|
476
|
+
result = self.tx.execute(sql, vals, cursor=self.cursor())
|
|
350
477
|
return result.cursor.rowcount
|
|
351
478
|
|
|
352
479
|
upsert = merge
|
|
353
480
|
indate = merge
|
|
354
481
|
|
|
355
|
-
def mergeSQL(self, data, pk):
|
|
356
|
-
return self.sql.merge(
|
|
357
|
-
self.name, data, pk, on_conflict_do_nothing=False, on_conflict_update=True
|
|
358
|
-
)
|
|
359
|
-
|
|
360
|
-
def insertSQL(self, data):
|
|
361
|
-
return self.sql.insert(self.name, data)
|
|
362
|
-
|
|
363
|
-
def updateSQL(self, data, pk, left_join=None, inner_join=None, outer_join=None):
|
|
364
|
-
return self.sql.update(self.name, data, pk, left_join, inner_join, outer_join)
|
|
365
|
-
|
|
366
482
|
@return_default(0)
|
|
367
|
-
def count(self, where=None):
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
@return_default(0)
|
|
372
|
-
def sum(self, column, where=None):
|
|
483
|
+
def count(self, where=None, **kwds):
|
|
484
|
+
"""
|
|
485
|
+
Returns the count of rows matching `where`.
|
|
486
|
+
"""
|
|
373
487
|
sql, vals = self.sql.select(
|
|
374
|
-
columns="
|
|
375
|
-
table=self.name,
|
|
376
|
-
where=where,
|
|
488
|
+
self.tx, columns="count(*)", table=self.name, where=where
|
|
377
489
|
)
|
|
378
|
-
|
|
490
|
+
if kwds.get("sql_only", False):
|
|
491
|
+
return sql, vals
|
|
492
|
+
return self.tx.execute(sql, vals, cursor=self.cursor()).scalar()
|
|
379
493
|
|
|
380
494
|
@return_default(0)
|
|
381
|
-
def
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
@return_default(None)
|
|
386
|
-
def oldest(self, where={}, field="sys_modified", columns="sys_id", lock=None):
|
|
495
|
+
def sum(self, column, where=None, **kwds):
|
|
496
|
+
"""
|
|
497
|
+
Returns the sum of the given column across rows matching `where`.
|
|
498
|
+
"""
|
|
387
499
|
sql, vals = self.sql.select(
|
|
388
|
-
|
|
500
|
+
self.tx,
|
|
501
|
+
columns=f"coalesce(sum(coalesce({column},0)),0)",
|
|
389
502
|
table=self.name,
|
|
390
503
|
where=where,
|
|
391
|
-
orderby=field + " asc",
|
|
392
|
-
qty=1,
|
|
393
|
-
lock=lock,
|
|
394
504
|
)
|
|
395
|
-
|
|
505
|
+
if kwds.get("sql_only", False):
|
|
506
|
+
return sql, vals
|
|
507
|
+
return self.tx.execute(sql, vals, cursor=self.cursor()).scalar()
|
|
396
508
|
|
|
397
|
-
@return_default(
|
|
398
|
-
def
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
qty=1,
|
|
405
|
-
lock=lock,
|
|
406
|
-
)
|
|
407
|
-
return self.tx.execute(sql, vals, cursor=self.cursor).scalar()
|
|
509
|
+
@return_default(0)
|
|
510
|
+
def __len__(self):
|
|
511
|
+
"""
|
|
512
|
+
Returns count of all rows in the table.
|
|
513
|
+
"""
|
|
514
|
+
sql, vals = self.sql.select(self.tx, columns="count(*)", table=self.name)
|
|
515
|
+
return self.tx.execute(sql, vals, cursor=self.cursor()).scalar()
|
|
408
516
|
|
|
409
517
|
@return_default(Result())
|
|
410
518
|
def select(
|
|
@@ -418,8 +526,13 @@ class Table(object):
|
|
|
418
526
|
qty=None,
|
|
419
527
|
lock=None,
|
|
420
528
|
skip_locked=None,
|
|
529
|
+
**kwds,
|
|
421
530
|
):
|
|
531
|
+
"""
|
|
532
|
+
Performs a SELECT query, returning a Result object (defaults to as_dict() transform).
|
|
533
|
+
"""
|
|
422
534
|
sql, vals = self.sql.select(
|
|
535
|
+
self.tx,
|
|
423
536
|
columns=columns,
|
|
424
537
|
table=self.name,
|
|
425
538
|
where=where,
|
|
@@ -428,41 +541,21 @@ class Table(object):
|
|
|
428
541
|
having=having,
|
|
429
542
|
start=start,
|
|
430
543
|
qty=qty,
|
|
431
|
-
tbl=self,
|
|
432
544
|
lock=lock,
|
|
433
545
|
skip_locked=skip_locked,
|
|
434
546
|
)
|
|
547
|
+
if kwds.get("sql_only", False):
|
|
548
|
+
return sql, vals
|
|
435
549
|
return self.tx.execute(sql, vals)
|
|
436
550
|
|
|
437
551
|
def list(self, *args, **kwds):
|
|
552
|
+
"""
|
|
553
|
+
Shortcut to run a SELECT and retrieve .all() in a single call.
|
|
554
|
+
"""
|
|
555
|
+
if kwds.get("sql_only", False):
|
|
556
|
+
raise Exception("sql_only is not supported for list queries")
|
|
438
557
|
return self.select(*args, **kwds).all()
|
|
439
558
|
|
|
440
|
-
def selectSQL(
|
|
441
|
-
self,
|
|
442
|
-
columns=None,
|
|
443
|
-
where=None,
|
|
444
|
-
orderby=None,
|
|
445
|
-
groupby=None,
|
|
446
|
-
having=None,
|
|
447
|
-
start=None,
|
|
448
|
-
qty=None,
|
|
449
|
-
lock=None,
|
|
450
|
-
skip_locked=None,
|
|
451
|
-
):
|
|
452
|
-
return self.sql.select(
|
|
453
|
-
columns=columns,
|
|
454
|
-
table=self.name,
|
|
455
|
-
where=where,
|
|
456
|
-
orderby=orderby,
|
|
457
|
-
groupby=groupby,
|
|
458
|
-
having=having,
|
|
459
|
-
start=start,
|
|
460
|
-
qty=qty,
|
|
461
|
-
tbl=self,
|
|
462
|
-
lock=lock,
|
|
463
|
-
skip_locked=skip_locked,
|
|
464
|
-
)
|
|
465
|
-
|
|
466
559
|
def query(
|
|
467
560
|
self,
|
|
468
561
|
columns=None,
|
|
@@ -475,7 +568,11 @@ class Table(object):
|
|
|
475
568
|
lock=None,
|
|
476
569
|
skip_locked=None,
|
|
477
570
|
):
|
|
571
|
+
"""
|
|
572
|
+
Returns a Query object suitable for usage in sub-queries, etc.
|
|
573
|
+
"""
|
|
478
574
|
sql, vals = self.sql.select(
|
|
575
|
+
self.tx,
|
|
479
576
|
columns=columns,
|
|
480
577
|
table=self.name,
|
|
481
578
|
where=where,
|
|
@@ -484,14 +581,12 @@ class Table(object):
|
|
|
484
581
|
having=having,
|
|
485
582
|
start=start,
|
|
486
583
|
qty=qty,
|
|
487
|
-
tbl=self,
|
|
488
584
|
lock=lock,
|
|
489
585
|
skip_locked=skip_locked,
|
|
490
586
|
)
|
|
491
587
|
if vals:
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
)
|
|
588
|
+
# Not supporting dictionary-based 'where' in raw Query usage.
|
|
589
|
+
raise Exception("A query generator does not support dictionary-type WHERE.")
|
|
495
590
|
return Query(sql)
|
|
496
591
|
|
|
497
592
|
@return_default(Result())
|
|
@@ -506,8 +601,13 @@ class Table(object):
|
|
|
506
601
|
qty=None,
|
|
507
602
|
lock=None,
|
|
508
603
|
skip_locked=None,
|
|
604
|
+
**kwds,
|
|
509
605
|
):
|
|
606
|
+
"""
|
|
607
|
+
Similar to select(), but calls server_execute() instead of execute().
|
|
608
|
+
"""
|
|
510
609
|
sql, vals = self.sql.select(
|
|
610
|
+
self.tx,
|
|
511
611
|
columns=columns,
|
|
512
612
|
table=self.name,
|
|
513
613
|
where=where,
|
|
@@ -516,14 +616,20 @@ class Table(object):
|
|
|
516
616
|
having=having,
|
|
517
617
|
start=start,
|
|
518
618
|
qty=qty,
|
|
519
|
-
tbl=self,
|
|
520
619
|
lock=lock,
|
|
521
620
|
skip_locked=skip_locked,
|
|
522
621
|
)
|
|
622
|
+
if kwds.get("sql_only", False):
|
|
623
|
+
return sql, vals
|
|
523
624
|
return self.tx.server_execute(sql, vals)
|
|
524
625
|
|
|
525
626
|
@return_default(Result())
|
|
526
627
|
def batch(self, size=100, *args, **kwds):
|
|
628
|
+
"""
|
|
629
|
+
Generator that yields batches of rows (lists) of size `size`.
|
|
630
|
+
"""
|
|
631
|
+
if kwds.get("sql_only", False):
|
|
632
|
+
raise Exception("sql_only is not supported for batch queries")
|
|
527
633
|
current = 0
|
|
528
634
|
while True:
|
|
529
635
|
kwds["start"] = current
|
|
@@ -536,93 +642,404 @@ class Table(object):
|
|
|
536
642
|
raise StopIteration
|
|
537
643
|
|
|
538
644
|
def get_value(self, key, pk):
|
|
645
|
+
"""
|
|
646
|
+
Returns a single scalar value of `key` for the row matching `pk`.
|
|
647
|
+
"""
|
|
539
648
|
return self.select(columns=key, where=pk).scalar()
|
|
540
649
|
|
|
541
650
|
@return_default({})
|
|
542
|
-
def get_row(self, where, lock=None):
|
|
651
|
+
def get_row(self, where, lock=None, **kwds):
|
|
652
|
+
"""
|
|
653
|
+
Retrieves a single row as dict or an empty dict if none found.
|
|
654
|
+
"""
|
|
543
655
|
if not where:
|
|
544
656
|
raise Exception("Unique key for the row to be retrieved is required.")
|
|
545
657
|
sql, vals = self.sql.select(
|
|
546
|
-
columns="*", table=self.name, where=where, lock=lock
|
|
658
|
+
self.tx, columns="*", table=self.name, where=where, lock=lock
|
|
547
659
|
)
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
660
|
+
if kwds.get("sql_only", False):
|
|
661
|
+
return sql, vals
|
|
662
|
+
return self.tx.execute(sql, vals, cursor=self.cursor()).one()
|
|
663
|
+
|
|
664
|
+
def delete(self, where, **kwds):
|
|
665
|
+
"""
|
|
666
|
+
Deletes rows matching `where`. Raises Exception if `where` is falsy.
|
|
667
|
+
"""
|
|
551
668
|
if not where:
|
|
552
669
|
raise Exception(
|
|
553
|
-
"You just tried to delete an entire table. Use truncate instead."
|
|
670
|
+
"You just tried to delete an entire table. Use `truncate` instead."
|
|
554
671
|
)
|
|
555
|
-
sql, vals = self.sql.delete(table=self.name, where=where)
|
|
672
|
+
sql, vals = self.sql.delete(tx=self.tx, table=self.name, where=where)
|
|
673
|
+
if kwds.get("sql_only", False):
|
|
674
|
+
return sql, vals
|
|
556
675
|
result = self.tx.execute(sql, vals)
|
|
557
676
|
return result.cursor.rowcount
|
|
558
677
|
|
|
559
|
-
def truncate(self):
|
|
678
|
+
def truncate(self, **kwds):
|
|
679
|
+
"""
|
|
680
|
+
Truncates this table (removes all rows).
|
|
681
|
+
"""
|
|
560
682
|
sql, vals = self.sql.truncate(table=self.name)
|
|
683
|
+
if kwds.get("sql_only", False):
|
|
684
|
+
return sql, vals
|
|
561
685
|
self.tx.execute(sql, vals)
|
|
562
686
|
|
|
563
|
-
def duplicate_rows(self, columns=
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
687
|
+
def duplicate_rows(self, columns=None, where=None, orderby=None, **kwds):
|
|
688
|
+
"""
|
|
689
|
+
Returns rows that have duplicates in the specified `columns`.
|
|
690
|
+
TBD: Move code to generate sql to the sql module. it should not be
|
|
691
|
+
here so different sql engines can use this function.
|
|
692
|
+
"""
|
|
693
|
+
if not columns:
|
|
694
|
+
raise ValueError(
|
|
695
|
+
"You must specify at least one column to check for duplicates."
|
|
696
|
+
)
|
|
697
|
+
sql, vals = self.sql.select(
|
|
698
|
+
self.tx,
|
|
699
|
+
columns=columns,
|
|
700
|
+
table=self.name,
|
|
701
|
+
where=where,
|
|
702
|
+
groupby=columns,
|
|
703
|
+
having={">count(*)": 1},
|
|
704
|
+
)
|
|
705
|
+
if orderby:
|
|
706
|
+
orderby = [orderby] if isinstance(orderby, str) else orderby
|
|
707
|
+
else:
|
|
708
|
+
orderby = columns
|
|
709
|
+
subjoin = " AND ".join([f"t.{col} = dup.{col}" for col in columns])
|
|
710
|
+
ob = ", ".join(orderby)
|
|
711
|
+
final_sql = f"""
|
|
712
|
+
SELECT t.*
|
|
713
|
+
FROM {self.name} t
|
|
714
|
+
JOIN ({sql}) dup
|
|
715
|
+
ON {subjoin}
|
|
716
|
+
ORDER BY {ob}
|
|
717
|
+
"""
|
|
718
|
+
if kwds.get("sql_only", False):
|
|
719
|
+
return final_sql, vals
|
|
720
|
+
return self.tx.execute(final_sql, vals)
|
|
721
|
+
|
|
722
|
+
def has_duplicates(self, columns=None, where=None, **kwds):
|
|
723
|
+
"""
|
|
724
|
+
Returns True if there are duplicates in the specified columns, else False.
|
|
725
|
+
"""
|
|
726
|
+
if not columns:
|
|
727
|
+
raise ValueError(
|
|
728
|
+
"You must specify at least one column to check for duplicates."
|
|
729
|
+
)
|
|
730
|
+
sql, vals = self.sql.select(
|
|
731
|
+
self.tx,
|
|
732
|
+
columns=["1"],
|
|
733
|
+
table=self.name,
|
|
734
|
+
where=where,
|
|
735
|
+
groupby=columns,
|
|
736
|
+
having={">count(*)": 1},
|
|
737
|
+
qty=1,
|
|
738
|
+
)
|
|
739
|
+
if kwds.get("sql_only", False):
|
|
740
|
+
return sql, vals
|
|
741
|
+
return bool(self.tx.execute(sql, vals).scalar())
|
|
742
|
+
|
|
743
|
+
def create_view(self, name, query, temp=False, silent=True, **kwds):
|
|
744
|
+
"""
|
|
745
|
+
Creates (or replaces) a view.
|
|
746
|
+
"""
|
|
578
747
|
sql, vals = self.sql.create_view(
|
|
579
748
|
name=name, query=query, temp=temp, silent=silent
|
|
580
749
|
)
|
|
750
|
+
if kwds.get("sql_only", False):
|
|
751
|
+
return sql, vals
|
|
581
752
|
return self.tx.execute(sql, vals)
|
|
582
753
|
|
|
583
|
-
def drop_view(self, name, silent=True):
|
|
754
|
+
def drop_view(self, name, silent=True, **kwds):
|
|
755
|
+
"""
|
|
756
|
+
Drops a view.
|
|
757
|
+
"""
|
|
584
758
|
sql, vals = self.sql.drop_view(name=name, silent=silent)
|
|
759
|
+
if kwds.get("sql_only", False):
|
|
760
|
+
return sql, vals
|
|
585
761
|
return self.tx.execute(sql, vals)
|
|
586
762
|
|
|
587
|
-
def alter_trigger(self, name="USER", state="ENABLE"):
|
|
763
|
+
def alter_trigger(self, name="USER", state="ENABLE", **kwds):
|
|
764
|
+
"""
|
|
765
|
+
Alters a trigger's state on this table.
|
|
766
|
+
"""
|
|
588
767
|
sql, vals = self.sql.alter_trigger(table=self.name, state=state, name=name)
|
|
768
|
+
if kwds.get("sql_only", False):
|
|
769
|
+
return sql, vals
|
|
589
770
|
return self.tx.execute(sql, vals)
|
|
590
771
|
|
|
591
|
-
def rename_column(self, orig, new):
|
|
772
|
+
def rename_column(self, orig, new, **kwds):
|
|
773
|
+
"""
|
|
774
|
+
Renames a column in this table.
|
|
775
|
+
"""
|
|
592
776
|
sql, vals = self.sql.rename_column(table=self.name, orig=orig, new=new)
|
|
777
|
+
if kwds.get("sql_only", False):
|
|
778
|
+
return sql, vals
|
|
593
779
|
return self.tx.execute(sql, vals)
|
|
594
780
|
|
|
595
|
-
def set_sequence(self, next_value=1000):
|
|
781
|
+
def set_sequence(self, next_value=1000, **kwds):
|
|
782
|
+
"""
|
|
783
|
+
Sets the next value of the table's sys_id sequence.
|
|
784
|
+
"""
|
|
596
785
|
sql, vals = self.sql.set_sequence(table=self.name, next_value=next_value)
|
|
786
|
+
if kwds.get("sql_only", False):
|
|
787
|
+
return sql, vals
|
|
597
788
|
return self.tx.execute(sql, vals).scalar()
|
|
598
789
|
|
|
599
|
-
def get_sequence(self):
|
|
790
|
+
def get_sequence(self, **kwds):
|
|
791
|
+
"""
|
|
792
|
+
Returns the current value of the table's sys_id sequence.
|
|
793
|
+
"""
|
|
600
794
|
sql, vals = self.sql.current_id(table=self.name)
|
|
795
|
+
if kwds.get("sql_only", False):
|
|
796
|
+
return sql, vals
|
|
601
797
|
return self.tx.execute(sql, vals).scalar()
|
|
602
798
|
|
|
603
|
-
def missing(self,
|
|
799
|
+
def missing(self, list_, column="sys_id", where=None, **kwds):
|
|
800
|
+
"""
|
|
801
|
+
Given a list of IDs, returns which ones are missing from this table's `column` (defaults to sys_id).
|
|
802
|
+
"""
|
|
604
803
|
sql, vals = self.sql.missing(
|
|
605
|
-
table=self.name, list=
|
|
804
|
+
tx=self.tx, table=self.name, list=list_, column=column, where=where
|
|
606
805
|
)
|
|
806
|
+
if kwds.get("sql_only", False):
|
|
807
|
+
return sql, vals
|
|
607
808
|
return self.tx.execute(sql, vals).as_simple_list().all()
|
|
608
809
|
|
|
609
|
-
def lock(self, mode="ACCESS EXCLUSIVE", wait_for_lock=None):
|
|
610
|
-
|
|
810
|
+
def lock(self, mode="ACCESS EXCLUSIVE", wait_for_lock=None, **kwds):
|
|
811
|
+
"""
|
|
812
|
+
Issues a LOCK TABLE statement for this table.
|
|
813
|
+
TBD: MOve SQL To sql module so we can use this function with other engines.
|
|
814
|
+
"""
|
|
815
|
+
sql = f"LOCK TABLE {self.name} IN {mode} MODE"
|
|
611
816
|
if not wait_for_lock:
|
|
612
817
|
sql += " NOWAIT"
|
|
613
818
|
vals = None
|
|
819
|
+
if kwds.get("sql_only", False):
|
|
820
|
+
return sql, vals
|
|
614
821
|
return self.tx.execute(sql, vals)
|
|
615
822
|
|
|
616
823
|
@return_default(0)
|
|
617
|
-
def max(self, column, where=None):
|
|
824
|
+
def max(self, column, where=None, **kwds):
|
|
825
|
+
"""
|
|
826
|
+
Returns the MAX() of the specified column.
|
|
827
|
+
"""
|
|
618
828
|
sql, vals = self.sql.select(
|
|
619
|
-
columns="max({})"
|
|
829
|
+
self.tx, columns=f"max({column})", table=self.name, where=where
|
|
620
830
|
)
|
|
621
|
-
|
|
831
|
+
if kwds.get("sql_only", False):
|
|
832
|
+
return sql, vals
|
|
833
|
+
return self.tx.execute(sql, vals, cursor=self.cursor()).scalar()
|
|
622
834
|
|
|
623
835
|
@return_default(0)
|
|
624
|
-
def min(self, column, where=None):
|
|
836
|
+
def min(self, column, where=None, **kwds):
|
|
837
|
+
"""
|
|
838
|
+
Returns the MIN() of the specified column.
|
|
839
|
+
"""
|
|
625
840
|
sql, vals = self.sql.select(
|
|
626
|
-
columns="min({})"
|
|
841
|
+
self.tx, columns=f"min({column})", table=self.name, where=where
|
|
627
842
|
)
|
|
628
|
-
|
|
843
|
+
if kwds.get("sql_only", False):
|
|
844
|
+
return sql, vals
|
|
845
|
+
return self.tx.execute(sql, vals, cursor=self.cursor()).scalar()
|
|
846
|
+
|
|
847
|
+
@return_default(None)
|
|
848
|
+
def newest(self, where=None, **kwds):
|
|
849
|
+
"""
|
|
850
|
+
Returns the row with the highest sys_created value.
|
|
851
|
+
"""
|
|
852
|
+
return self.first(where=where, orderby="sys_modified DESC, sys_id DESC")
|
|
853
|
+
|
|
854
|
+
@return_default(None)
|
|
855
|
+
def oldest(self, where=None, **kwds):
|
|
856
|
+
"""
|
|
857
|
+
Returns the row with the lowest sys_created value.
|
|
858
|
+
"""
|
|
859
|
+
return self.first(where=where, orderby="sys_modified ASC, sys_id ASC")
|
|
860
|
+
|
|
861
|
+
def indexes(self, **kwds):
|
|
862
|
+
"""
|
|
863
|
+
Returns detailed information about all indexes on this table.
|
|
864
|
+
"""
|
|
865
|
+
sql, vals = self.sql.indexes(self.name)
|
|
866
|
+
if kwds.get("sql_only", False):
|
|
867
|
+
return sql, vals
|
|
868
|
+
return self.tx.execute(sql, vals, cursor=self.cursor())
|
|
869
|
+
|
|
870
|
+
def compare_rows(self, pk1, pk2):
|
|
871
|
+
"""
|
|
872
|
+
Compares two rows in the given table identified by their primary keys.
|
|
873
|
+
|
|
874
|
+
Parameters:
|
|
875
|
+
table (Table): An instance of the Table class.
|
|
876
|
+
pk1: Primary key of the first row.
|
|
877
|
+
pk2: Primary key of the second row.
|
|
878
|
+
|
|
879
|
+
Returns:
|
|
880
|
+
A string that lists differences between the two rows. If no differences,
|
|
881
|
+
returns "Rows are identical."
|
|
882
|
+
"""
|
|
883
|
+
# Retrieve rows based on primary keys.
|
|
884
|
+
data1 = self.row(pk1).to_dict()
|
|
885
|
+
data2 = self.row(pk2).to_dict()
|
|
886
|
+
|
|
887
|
+
differences = []
|
|
888
|
+
|
|
889
|
+
# Iterate through each column in the table (ignoring system columns).
|
|
890
|
+
for col in self.columns():
|
|
891
|
+
val1 = data1.get(col)
|
|
892
|
+
val2 = data2.get(col)
|
|
893
|
+
|
|
894
|
+
if val1 != val2:
|
|
895
|
+
differences.append(f"{col}: {val1} vs {val2}")
|
|
896
|
+
|
|
897
|
+
# Return a descriptive string of differences.
|
|
898
|
+
if differences:
|
|
899
|
+
differences.insert(0, f"Comparing {self.name}: {pk1} vs {pk2}")
|
|
900
|
+
differences.insert(0, f"--------------------------------------")
|
|
901
|
+
differences.append(f"--------------------------------------")
|
|
902
|
+
return "\n".join(differences)
|
|
903
|
+
else:
|
|
904
|
+
return f"{self.name} rows {pk1} and {pk2} are identical."
|
|
905
|
+
|
|
906
|
+
def interactive_merge_rows(self, left_pk, right_pk):
|
|
907
|
+
"""
|
|
908
|
+
Interactively merge two rows from this table.
|
|
909
|
+
|
|
910
|
+
For each non-primary key column where the two rows differ, you'll be prompted to choose
|
|
911
|
+
whether you want the left or right value. After all choices are made, the merged row
|
|
912
|
+
is displayed for confirmation. If confirmed, the left row is updated with the chosen values.
|
|
913
|
+
Optionally, you can also choose to delete the right row.
|
|
914
|
+
|
|
915
|
+
Parameters:
|
|
916
|
+
left_pk: Primary key of the left (destination) row.
|
|
917
|
+
right_pk: Primary key of the right (source) row.
|
|
918
|
+
"""
|
|
919
|
+
# Retrieve both rows and convert to dictionaries
|
|
920
|
+
left_row = self.row(left_pk)
|
|
921
|
+
right_row = self.row(right_pk)
|
|
922
|
+
|
|
923
|
+
left_data = left_row.to_dict()
|
|
924
|
+
right_data = right_row.to_dict()
|
|
925
|
+
|
|
926
|
+
# Get primary key columns to skip them in the merge.
|
|
927
|
+
pk_columns = self.primary_keys()
|
|
928
|
+
|
|
929
|
+
merged_values = {}
|
|
930
|
+
|
|
931
|
+
print("Comparing rows:\n")
|
|
932
|
+
# Iterate through all non-primary key columns.
|
|
933
|
+
for col in self.columns():
|
|
934
|
+
if col in pk_columns:
|
|
935
|
+
continue # Do not compare or merge primary key columns.
|
|
936
|
+
|
|
937
|
+
left_val = left_data.get(col)
|
|
938
|
+
right_val = right_data.get(col)
|
|
939
|
+
|
|
940
|
+
# If values are the same, simply use one of them.
|
|
941
|
+
if left_val == right_val:
|
|
942
|
+
merged_values[col] = left_val
|
|
943
|
+
else:
|
|
944
|
+
print(f"Column: '{col}'")
|
|
945
|
+
print(f" Left value: `{left_val}` Right value: `{right_val}`")
|
|
946
|
+
choice = None
|
|
947
|
+
# Prompt until a valid choice is provided.
|
|
948
|
+
while choice not in ("L", "R"):
|
|
949
|
+
choice = (
|
|
950
|
+
input("Choose which value to use (L for left, R for right): ")
|
|
951
|
+
.strip()
|
|
952
|
+
.upper()
|
|
953
|
+
)
|
|
954
|
+
merged_values[col] = left_val if choice == "L" else right_val
|
|
955
|
+
print("") # Blank line for readability.
|
|
956
|
+
|
|
957
|
+
# Display the merged row preview.
|
|
958
|
+
print("\nThe merged row will be:")
|
|
959
|
+
for col, value in merged_values.items():
|
|
960
|
+
print(f" {col}: {value}")
|
|
961
|
+
|
|
962
|
+
# Final confirmation before applying changes.
|
|
963
|
+
confirm = (
|
|
964
|
+
input("\nApply these changes and merge the rows? (y/n): ").strip().lower()
|
|
965
|
+
)
|
|
966
|
+
# Optionally, ask if the right row should be deleted.
|
|
967
|
+
delete_right = (
|
|
968
|
+
input("Do you want to delete the right row? (y/n): ").strip().lower()
|
|
969
|
+
)
|
|
970
|
+
|
|
971
|
+
if confirm != "y":
|
|
972
|
+
print("Merge cancelled. No changes made.")
|
|
973
|
+
return
|
|
974
|
+
|
|
975
|
+
if delete_right == "y":
|
|
976
|
+
self.delete(where={"sys_id": right_pk})
|
|
977
|
+
print("Right row deleted.")
|
|
978
|
+
|
|
979
|
+
# Update the left row with the merged values.
|
|
980
|
+
left_row.update(merged_values)
|
|
981
|
+
print("Merge applied: Left row updated with merged values.")
|
|
982
|
+
print("Merge completed.")
|
|
983
|
+
|
|
984
|
+
def find_duplicates(self, columns, sql_only=False):
|
|
985
|
+
"""
|
|
986
|
+
Returns duplicate groups from the table based on the specified columns in a case-insensitive way.
|
|
987
|
+
|
|
988
|
+
For each column, the subquery computes:
|
|
989
|
+
- lower(column) AS normalized_<column>
|
|
990
|
+
- array_agg(column) AS variations_<column>
|
|
991
|
+
- array_agg(sys_id) AS sys_ids
|
|
992
|
+
- COUNT(*) AS total_count
|
|
993
|
+
|
|
994
|
+
The subquery groups by lower(column) values and retains only groups with more than one row.
|
|
995
|
+
|
|
996
|
+
Example SQL for a single column "email_address":
|
|
997
|
+
|
|
998
|
+
SELECT
|
|
999
|
+
lower(email_address) AS normalized_email_address,
|
|
1000
|
+
array_agg(email_address) AS variations_email_address,
|
|
1001
|
+
array_agg(sys_id) AS sys_ids,
|
|
1002
|
+
COUNT(*) AS total_count
|
|
1003
|
+
FROM donor_users
|
|
1004
|
+
GROUP BY lower(email_address)
|
|
1005
|
+
HAVING COUNT(*) > 1
|
|
1006
|
+
|
|
1007
|
+
Parameters:
|
|
1008
|
+
columns (list or str): Column name or list of column names to check duplicates on.
|
|
1009
|
+
sql_only (bool): If True, returns the SQL string and an empty tuple; otherwise,
|
|
1010
|
+
executes the query using self.tx.execute.
|
|
1011
|
+
|
|
1012
|
+
Returns:
|
|
1013
|
+
A tuple of (SQL string, parameters) if sql_only is True, otherwise the result of executing the query.
|
|
1014
|
+
"""
|
|
1015
|
+
if not columns:
|
|
1016
|
+
raise ValueError(
|
|
1017
|
+
"You must specify at least one column to check for duplicates."
|
|
1018
|
+
)
|
|
1019
|
+
|
|
1020
|
+
if isinstance(columns, str):
|
|
1021
|
+
columns = [columns]
|
|
1022
|
+
|
|
1023
|
+
# Build subquery SELECT clause parts for normalized values and variations.
|
|
1024
|
+
normalized_cols = [f"lower({col}) AS normalized_{col}" for col in columns]
|
|
1025
|
+
variations_cols = [f"array_agg({col}) AS variations_{col}" for col in columns]
|
|
1026
|
+
|
|
1027
|
+
subquery_select = ",\n ".join(
|
|
1028
|
+
normalized_cols
|
|
1029
|
+
+ variations_cols
|
|
1030
|
+
+ ["array_agg(sys_id) AS sys_ids", "COUNT(*) AS total_count"]
|
|
1031
|
+
)
|
|
1032
|
+
|
|
1033
|
+
groupby_clause = ", ".join([f"lower({col})" for col in columns])
|
|
1034
|
+
|
|
1035
|
+
subquery = f"""
|
|
1036
|
+
SELECT
|
|
1037
|
+
{subquery_select}
|
|
1038
|
+
FROM {self.name}
|
|
1039
|
+
GROUP BY {groupby_clause}
|
|
1040
|
+
HAVING COUNT(*) > 1
|
|
1041
|
+
"""
|
|
1042
|
+
|
|
1043
|
+
if sql_only:
|
|
1044
|
+
return subquery, ()
|
|
1045
|
+
return self.tx.execute(subquery, ())
|