database-postgresql-local 0.1.0__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.
- database_postgresql_local/__init__.py +1 -0
- database_postgresql_local/connector.py +156 -0
- database_postgresql_local/constants_src.py +36 -0
- database_postgresql_local/cursor.py +142 -0
- database_postgresql_local/generic_crud_postgresql.py +967 -0
- database_postgresql_local/table_columns.py +4 -0
- database_postgresql_local/table_definition.py +4 -0
- database_postgresql_local/utils.py +263 -0
- database_postgresql_local-0.1.0.dist-info/METADATA +24 -0
- database_postgresql_local-0.1.0.dist-info/RECORD +12 -0
- database_postgresql_local-0.1.0.dist-info/WHEEL +5 -0
- database_postgresql_local-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,967 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
from functools import lru_cache
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
import psycopg2
|
|
6
|
+
from database_infrastructure_local.number_generator import NumberGenerator
|
|
7
|
+
from database_infrastructure_local.generic_crud_abstract import GenericCrudAbstract
|
|
8
|
+
from database_infrastructure_local.constants import DEFAULT_SQL_SELECT_LIMIT
|
|
9
|
+
from logger_local.MetaLogger import ABCMetaLogger
|
|
10
|
+
from user_context_remote.user_context import UserContext
|
|
11
|
+
from python_sdk_remote.our_object import OurObject
|
|
12
|
+
|
|
13
|
+
from .connector import Connector
|
|
14
|
+
from .constants_src import CRUD_POSTGRESQL_CODE_LOGGER_OBJECT
|
|
15
|
+
from .cursor import Cursor
|
|
16
|
+
from .table_definition import table_definition
|
|
17
|
+
from .utils import (detect_if_is_test_data, generate_id_column_name,
|
|
18
|
+
generate_table_name, generate_view_name,
|
|
19
|
+
get_entity_type_by_table_name, get_where_params,
|
|
20
|
+
process_insert_data_dict, process_update_data_dict,
|
|
21
|
+
process_upsert_data_dict,
|
|
22
|
+
replace_view_with_table, validate_none_select_table_name,
|
|
23
|
+
validate_select_table_name, validate_single_clause_value,
|
|
24
|
+
where_skip_null_values, insert_is_undelete,
|
|
25
|
+
is_column_in_table,
|
|
26
|
+
is_end_timestamp_in_table, get_table_columns,
|
|
27
|
+
group_list_by_columns,
|
|
28
|
+
process_select_data_dict,
|
|
29
|
+
generate_where_clause_for_ignore_duplicate,
|
|
30
|
+
fix_select_clause_value, fix_where_clause_value)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class GenericCrudPostgresql(GenericCrudAbstract, metaclass=ABCMetaLogger,
|
|
34
|
+
object=CRUD_POSTGRESQL_CODE_LOGGER_OBJECT):
|
|
35
|
+
"""A class that provides generic CRUD functionality for PostgreSQL databases.
|
|
36
|
+
There are 4 main functions to create, read, update, and delete data from the database.
|
|
37
|
+
The rest of the functions are helper functions or wrappers around the main functions."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, *, default_schema_name: str,
|
|
40
|
+
default_table_name: str = None,
|
|
41
|
+
default_view_table_name: str = None,
|
|
42
|
+
default_view_with_deleted_and_test_data: str = None,
|
|
43
|
+
default_column_name: str = None,
|
|
44
|
+
default_select_clause_value: str = "*",
|
|
45
|
+
default_where: str = None,
|
|
46
|
+
is_test_data: bool = False) -> None:
|
|
47
|
+
"""Initializes the GenericCrudPostgresql class. If a connection is not provided, a new connection will be created."""
|
|
48
|
+
self.default_schema_name = default_schema_name
|
|
49
|
+
# We do not need a connection per schema, and it makes terrible performance.
|
|
50
|
+
# In special cases, you can always use set_schema.
|
|
51
|
+
self.connection = Connector.connect(schema_name=default_schema_name)
|
|
52
|
+
self._cursor = self.connection.cursor()
|
|
53
|
+
self.default_table_name = default_table_name or generate_table_name(default_schema_name)
|
|
54
|
+
self.default_view_table_name = default_view_table_name or generate_view_name(
|
|
55
|
+
self.default_table_name)
|
|
56
|
+
self.default_column_name = default_column_name or generate_id_column_name(self.default_table_name)
|
|
57
|
+
self.default_view_with_deleted_and_test_data = default_view_with_deleted_and_test_data
|
|
58
|
+
self.default_select_clause_value = default_select_clause_value
|
|
59
|
+
self.default_where = default_where
|
|
60
|
+
self.is_test_data = is_test_data or detect_if_is_test_data()
|
|
61
|
+
self.is_ignore_duplicate = False
|
|
62
|
+
self.user_context = UserContext()
|
|
63
|
+
|
|
64
|
+
def insert(self, *, schema_name: str = None, table_name: str = None, data_dict: dict = None,
|
|
65
|
+
ignore_duplicate: bool = False, commit_changes: bool = True) -> int:
|
|
66
|
+
"""Inserts a new row into the table and returns the id of the new row or -1 if an error occurred.
|
|
67
|
+
ignore_duplicate should be False as default, because for example if a user register with existing name,
|
|
68
|
+
he should get an error and not existing id
|
|
69
|
+
"""
|
|
70
|
+
table_name = table_name or self.default_table_name
|
|
71
|
+
schema_name = schema_name or self.default_schema_name
|
|
72
|
+
self._validate_args(args=locals())
|
|
73
|
+
if ignore_duplicate:
|
|
74
|
+
self.logger.info(f"GenericCrudPostgresql.insert({schema_name}.{table_name}) using ignore_duplicate, is it needed? - Not recommended",
|
|
75
|
+
object={"data_dict": data_dict})
|
|
76
|
+
|
|
77
|
+
data_dict = self.__add_create_updated_user_profile_ids(
|
|
78
|
+
data_dict=data_dict, add_created_user_id=True, schema_name=schema_name, table_name=table_name)
|
|
79
|
+
|
|
80
|
+
columns, values, params = process_insert_data_dict(data_dict=data_dict)
|
|
81
|
+
|
|
82
|
+
# PostgreSQL uses RETURNING for getting the inserted ID
|
|
83
|
+
id_column = generate_id_column_name(table_name)
|
|
84
|
+
insert_query = f'INSERT INTO "{schema_name}"."{table_name}" ({columns}) VALUES ({values}) RETURNING {id_column};'
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
self.logger.info("insert_query=" + insert_query)
|
|
88
|
+
self.cursor.execute(insert_query, params)
|
|
89
|
+
result = self.cursor.fetchone()
|
|
90
|
+
inserted_id = result[0] if result else None
|
|
91
|
+
|
|
92
|
+
if commit_changes:
|
|
93
|
+
self.connection.commit()
|
|
94
|
+
|
|
95
|
+
except psycopg2.errors.UniqueViolation as exception:
|
|
96
|
+
if ignore_duplicate:
|
|
97
|
+
self.logger.warning(f"GenericCrudPostgresql.insert({schema_name}.{table_name}) using ignore_duplicate - Trying to insert a duplicate value",
|
|
98
|
+
object={"data_dict": data_dict})
|
|
99
|
+
self.is_ignore_duplicate = True
|
|
100
|
+
self.connection.rollback() # Need to rollback the failed transaction
|
|
101
|
+
inserted_id = self._get_existing_duplicate_id(schema_name, table_name, exception, data_dict=data_dict)
|
|
102
|
+
else:
|
|
103
|
+
self.connection.rollback()
|
|
104
|
+
raise exception
|
|
105
|
+
finally:
|
|
106
|
+
self.is_ignore_duplicate = False
|
|
107
|
+
self.logger.debug(object=locals())
|
|
108
|
+
return inserted_id
|
|
109
|
+
|
|
110
|
+
def _get_existing_duplicate_id(self, schema_name: str, table_name: str, error: Exception,
|
|
111
|
+
data_dict: dict) -> int | None:
|
|
112
|
+
if is_end_timestamp_in_table(table_name=table_name) and insert_is_undelete(table_name=table_name):
|
|
113
|
+
existing_duplicate_id = self.__get_existing_duplicate_id_with_undelete(
|
|
114
|
+
schema_name=schema_name, table_name=table_name, data_dict=data_dict)
|
|
115
|
+
else:
|
|
116
|
+
existing_duplicate_id = self.__get_existing_duplicate_id_without_undelete(
|
|
117
|
+
schema_name=schema_name, table_name=table_name, data_dict=data_dict)
|
|
118
|
+
if existing_duplicate_id is None:
|
|
119
|
+
self.logger.error(
|
|
120
|
+
f"GenericCrudPostgresql._get_existing_duplicate_id: no existing row found for {schema_name}.{table_name}.{data_dict}",
|
|
121
|
+
object={"data_dict": data_dict, "error": error}
|
|
122
|
+
)
|
|
123
|
+
raise error
|
|
124
|
+
self.logger.debug(object=locals())
|
|
125
|
+
return existing_duplicate_id
|
|
126
|
+
|
|
127
|
+
def __get_existing_duplicate_id_with_undelete(
|
|
128
|
+
self, schema_name: str, table_name: str, data_dict: dict) -> int | None:
|
|
129
|
+
column_name = generate_id_column_name(table_name)
|
|
130
|
+
where, params = self.get_constraint_where_clause(schema_name=schema_name, table_name=table_name,
|
|
131
|
+
data_dict=data_dict)
|
|
132
|
+
row = self.select_one_tuple_by_where(
|
|
133
|
+
schema_name=schema_name, view_table_name=table_name,
|
|
134
|
+
select_clause_value=f"{column_name}, end_timestamp",
|
|
135
|
+
where=where, params=params
|
|
136
|
+
)
|
|
137
|
+
if not row:
|
|
138
|
+
existing_duplicate_id = None
|
|
139
|
+
return existing_duplicate_id
|
|
140
|
+
else:
|
|
141
|
+
existing_duplicate_id, end_timestamp = row
|
|
142
|
+
if end_timestamp and datetime.now(timezone.utc) > end_timestamp.replace(tzinfo=timezone.utc):
|
|
143
|
+
self.undelete_by_column_and_value(
|
|
144
|
+
schema_name=schema_name, table_name=table_name,
|
|
145
|
+
column_name=column_name, column_value=existing_duplicate_id)
|
|
146
|
+
return existing_duplicate_id
|
|
147
|
+
|
|
148
|
+
def __get_existing_duplicate_id_without_undelete(
|
|
149
|
+
self, *, schema_name: str, table_name: str, data_dict: dict) -> int | None:
|
|
150
|
+
column_name = generate_id_column_name(table_name)
|
|
151
|
+
where, params = self.get_constraint_where_clause(schema_name=schema_name, table_name=table_name,
|
|
152
|
+
data_dict=data_dict)
|
|
153
|
+
if is_end_timestamp_in_table(table_name=table_name):
|
|
154
|
+
row = self.select_one_tuple_by_where(
|
|
155
|
+
schema_name=schema_name, view_table_name=table_name,
|
|
156
|
+
select_clause_value=f"{column_name}, end_timestamp",
|
|
157
|
+
where=where, params=params)
|
|
158
|
+
if not row:
|
|
159
|
+
existing_duplicate_id = None
|
|
160
|
+
return existing_duplicate_id
|
|
161
|
+
else:
|
|
162
|
+
existing_duplicate_id, end_timestamp = row
|
|
163
|
+
if end_timestamp is not None:
|
|
164
|
+
self.logger.error(
|
|
165
|
+
f"GenericCrudPostgresql.__get_existing_duplicate_id_without_undelete: existing row found for "
|
|
166
|
+
f"{schema_name}.{table_name}.{data_dict} but it is deleted",
|
|
167
|
+
object={"data_dict": data_dict, "existing_duplicate_id": existing_duplicate_id,
|
|
168
|
+
"end_timestamp": end_timestamp}
|
|
169
|
+
)
|
|
170
|
+
existing_duplicate_id = None
|
|
171
|
+
else:
|
|
172
|
+
existing_duplicate_id = self.select_one_value_by_where(
|
|
173
|
+
schema_name=schema_name, view_table_name=table_name, select_clause_value=column_name,
|
|
174
|
+
where=where, params=params)
|
|
175
|
+
return existing_duplicate_id
|
|
176
|
+
def update_by_column_and_value(
|
|
177
|
+
self, *, schema_name: str = None, table_name: str = None,
|
|
178
|
+
column_name: str = None, column_value: Any,
|
|
179
|
+
data_dict: dict = None,
|
|
180
|
+
limit: int = DEFAULT_SQL_SELECT_LIMIT, order_by: str = None,
|
|
181
|
+
commit_changes: bool = True) -> int:
|
|
182
|
+
"""Updates data in the table by ID."""
|
|
183
|
+
|
|
184
|
+
table_name = table_name or self.default_table_name
|
|
185
|
+
column_name = column_name or self.default_column_name
|
|
186
|
+
|
|
187
|
+
if column_name:
|
|
188
|
+
where, params = get_where_params(column_name, column_value)
|
|
189
|
+
updated_rows = self.update_by_where(
|
|
190
|
+
schema_name=schema_name, table_name=table_name, where=where, data_dict=data_dict,
|
|
191
|
+
params=params, limit=limit, order_by=order_by, commit_changes=commit_changes)
|
|
192
|
+
return updated_rows
|
|
193
|
+
|
|
194
|
+
else:
|
|
195
|
+
raise Exception("Update by id requires an column_name")
|
|
196
|
+
|
|
197
|
+
def update_by_where(self, *, schema_name: str = None,
|
|
198
|
+
table_name: str = None, where: str = None,
|
|
199
|
+
params: tuple = None, data_dict: dict = None,
|
|
200
|
+
limit: int = DEFAULT_SQL_SELECT_LIMIT,
|
|
201
|
+
order_by: str = None,
|
|
202
|
+
commit_changes: bool = True) -> int:
|
|
203
|
+
"""Updates data in the table by WHERE.
|
|
204
|
+
Example:
|
|
205
|
+
"UPDATE table_name SET A=A_val, B=B_val WHERE C=C_val AND D=D_val"
|
|
206
|
+
translates into:
|
|
207
|
+
update_by_where(table_name="table_name",
|
|
208
|
+
data_dict={"A": A_val, "B": B_val},
|
|
209
|
+
where="C=%s AND D=%s",
|
|
210
|
+
params=(C_val, D_val)"
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
table_name = table_name or self.default_table_name
|
|
214
|
+
schema_name = schema_name or self.default_schema_name
|
|
215
|
+
self._validate_args(args=locals())
|
|
216
|
+
|
|
217
|
+
data_dict = self.__add_create_updated_user_profile_ids(data_dict=data_dict, add_created_user_id=False,
|
|
218
|
+
schema_name=schema_name, table_name=table_name)
|
|
219
|
+
|
|
220
|
+
set_values, data_dict_params = process_update_data_dict(data_dict)
|
|
221
|
+
if not where:
|
|
222
|
+
raise Exception("update_by_where requires a 'where'")
|
|
223
|
+
|
|
224
|
+
# PostgreSQL doesn't support LIMIT in UPDATE without a subquery
|
|
225
|
+
# We'll handle this differently based on whether limit and order_by are provided
|
|
226
|
+
if limit != DEFAULT_SQL_SELECT_LIMIT or order_by:
|
|
227
|
+
# For PostgreSQL, we need to use a subquery with LIMIT and ORDER BY
|
|
228
|
+
id_column = generate_id_column_name(table_name)
|
|
229
|
+
|
|
230
|
+
# First, get the IDs of rows to update
|
|
231
|
+
select_query = f'SELECT "{id_column}" FROM "{schema_name}"."{table_name}" WHERE {where}'
|
|
232
|
+
if order_by:
|
|
233
|
+
select_query += f' ORDER BY {order_by}'
|
|
234
|
+
if limit != DEFAULT_SQL_SELECT_LIMIT:
|
|
235
|
+
select_query += f' LIMIT {limit}'
|
|
236
|
+
|
|
237
|
+
# Then update only those rows
|
|
238
|
+
if 'mapping' in self.default_table_name:
|
|
239
|
+
update_query = f'UPDATE "{schema_name}"."{table_name}" SET {set_values} ' \
|
|
240
|
+
f'WHERE "{id_column}" IN ({select_query});'
|
|
241
|
+
else:
|
|
242
|
+
update_query = f'UPDATE "{schema_name}"."{table_name}" SET {set_values}, ' \
|
|
243
|
+
f'updated_timestamp=CURRENT_TIMESTAMP ' \
|
|
244
|
+
f'WHERE "{id_column}" IN ({select_query});'
|
|
245
|
+
else:
|
|
246
|
+
# Simple update without LIMIT or ORDER BY
|
|
247
|
+
if 'mapping' in self.default_table_name:
|
|
248
|
+
update_query = f'UPDATE "{schema_name}"."{table_name}" SET {set_values} WHERE {where};'
|
|
249
|
+
else:
|
|
250
|
+
update_query = f'UPDATE "{schema_name}"."{table_name}" SET {set_values}, ' \
|
|
251
|
+
f'updated_timestamp=CURRENT_TIMESTAMP WHERE {where};'
|
|
252
|
+
|
|
253
|
+
where_params = params or tuple()
|
|
254
|
+
|
|
255
|
+
self.cursor.execute(update_query, data_dict_params + where_params)
|
|
256
|
+
if commit_changes:
|
|
257
|
+
self.connection.commit()
|
|
258
|
+
updated_rows = self.cursor.get_affected_row_count()
|
|
259
|
+
return updated_rows
|
|
260
|
+
def update_by_column_and_value(
|
|
261
|
+
self, *, schema_name: str = None, table_name: str = None,
|
|
262
|
+
column_name: str = None, column_value: Any,
|
|
263
|
+
data_dict: dict = None,
|
|
264
|
+
limit: int = DEFAULT_SQL_SELECT_LIMIT, order_by: str = None,
|
|
265
|
+
commit_changes: bool = True) -> int:
|
|
266
|
+
"""Updates data in the table by ID."""
|
|
267
|
+
|
|
268
|
+
table_name = table_name or self.default_table_name
|
|
269
|
+
column_name = column_name or self.default_column_name
|
|
270
|
+
|
|
271
|
+
if column_name:
|
|
272
|
+
where, params = get_where_params(column_name, column_value)
|
|
273
|
+
updated_rows = self.update_by_where(
|
|
274
|
+
schema_name=schema_name, table_name=table_name, where=where, data_dict=data_dict,
|
|
275
|
+
params=params, limit=limit, order_by=order_by, commit_changes=commit_changes)
|
|
276
|
+
return updated_rows
|
|
277
|
+
|
|
278
|
+
else:
|
|
279
|
+
raise Exception("Update by id requires an column_name")
|
|
280
|
+
|
|
281
|
+
def update_by_where(self, *, schema_name: str = None,
|
|
282
|
+
table_name: str = None, where: str = None,
|
|
283
|
+
params: tuple = None, data_dict: dict = None,
|
|
284
|
+
limit: int = DEFAULT_SQL_SELECT_LIMIT,
|
|
285
|
+
order_by: str = None,
|
|
286
|
+
commit_changes: bool = True) -> int:
|
|
287
|
+
"""Updates data in the table by WHERE.
|
|
288
|
+
Example:
|
|
289
|
+
"UPDATE table_name SET A=A_val, B=B_val WHERE C=C_val AND D=D_val"
|
|
290
|
+
translates into:
|
|
291
|
+
update_by_where(table_name="table_name",
|
|
292
|
+
data_dict={"A": A_val, "B": B_val},
|
|
293
|
+
where="C=%s AND D=%s",
|
|
294
|
+
params=(C_val, D_val)"
|
|
295
|
+
"""
|
|
296
|
+
|
|
297
|
+
table_name = table_name or self.default_table_name
|
|
298
|
+
schema_name = schema_name or self.default_schema_name
|
|
299
|
+
self._validate_args(args=locals())
|
|
300
|
+
|
|
301
|
+
data_dict = self.__add_create_updated_user_profile_ids(data_dict=data_dict, add_created_user_id=False,
|
|
302
|
+
schema_name=schema_name, table_name=table_name)
|
|
303
|
+
|
|
304
|
+
set_values, data_dict_params = process_update_data_dict(data_dict)
|
|
305
|
+
if not where:
|
|
306
|
+
raise Exception("update_by_where requires a 'where'")
|
|
307
|
+
|
|
308
|
+
# PostgreSQL doesn't support LIMIT in UPDATE without a subquery
|
|
309
|
+
# We'll handle this differently based on whether limit and order_by are provided
|
|
310
|
+
if limit != DEFAULT_SQL_SELECT_LIMIT or order_by:
|
|
311
|
+
# For PostgreSQL, we need to use a subquery with LIMIT and ORDER BY
|
|
312
|
+
id_column = generate_id_column_name(table_name)
|
|
313
|
+
|
|
314
|
+
# First, get the IDs of rows to update
|
|
315
|
+
select_query = f'SELECT "{id_column}" FROM "{schema_name}"."{table_name}" WHERE {where}'
|
|
316
|
+
if order_by:
|
|
317
|
+
select_query += f' ORDER BY {order_by}'
|
|
318
|
+
if limit != DEFAULT_SQL_SELECT_LIMIT:
|
|
319
|
+
select_query += f' LIMIT {limit}'
|
|
320
|
+
|
|
321
|
+
# Then update only those rows
|
|
322
|
+
if 'mapping' in self.default_table_name:
|
|
323
|
+
update_query = f'UPDATE "{schema_name}"."{table_name}" SET {set_values} ' \
|
|
324
|
+
f'WHERE "{id_column}" IN ({select_query});'
|
|
325
|
+
else:
|
|
326
|
+
update_query = f'UPDATE "{schema_name}"."{table_name}" SET {set_values}, ' \
|
|
327
|
+
f'updated_timestamp=CURRENT_TIMESTAMP ' \
|
|
328
|
+
f'WHERE "{id_column}" IN ({select_query});'
|
|
329
|
+
else:
|
|
330
|
+
# Simple update without LIMIT or ORDER BY
|
|
331
|
+
if 'mapping' in self.default_table_name:
|
|
332
|
+
update_query = f'UPDATE "{schema_name}"."{table_name}" SET {set_values} WHERE {where};'
|
|
333
|
+
else:
|
|
334
|
+
update_query = f'UPDATE "{schema_name}"."{table_name}" SET {set_values}, ' \
|
|
335
|
+
f'updated_timestamp=CURRENT_TIMESTAMP WHERE {where};'
|
|
336
|
+
|
|
337
|
+
where_params = params or tuple()
|
|
338
|
+
|
|
339
|
+
self.cursor.execute(update_query, data_dict_params + where_params)
|
|
340
|
+
if commit_changes:
|
|
341
|
+
self.connection.commit()
|
|
342
|
+
updated_rows = self.cursor.get_affected_row_count()
|
|
343
|
+
return updated_rows
|
|
344
|
+
|
|
345
|
+
def delete_by_column_and_value(self, *, schema_name: str = None, table_name: str = None,
|
|
346
|
+
column_name: str = None, column_value: Any) -> int:
|
|
347
|
+
"""Deletes data from the table by id.
|
|
348
|
+
Returns the number of deleted rows."""
|
|
349
|
+
# checks are done inside delete_by_where
|
|
350
|
+
column_name = column_name or self.default_column_name
|
|
351
|
+
|
|
352
|
+
if column_name: # column_value can be empty
|
|
353
|
+
where, params = get_where_params(column_name, column_value)
|
|
354
|
+
deleted_rows = self.delete_by_where(schema_name=schema_name, table_name=table_name, where=where,
|
|
355
|
+
params=params)
|
|
356
|
+
return deleted_rows
|
|
357
|
+
else:
|
|
358
|
+
raise Exception("Delete by id requires an column_name and column_value.")
|
|
359
|
+
|
|
360
|
+
def delete_by_where(self, *, schema_name: str = None, table_name: str = None, where: str = None,
|
|
361
|
+
params: tuple = None) -> int:
|
|
362
|
+
"""Deletes data from the table by WHERE.
|
|
363
|
+
Returns the number of deleted rows."""
|
|
364
|
+
|
|
365
|
+
table_name = table_name or self.default_table_name
|
|
366
|
+
schema_name = schema_name or self.default_schema_name
|
|
367
|
+
self._validate_args(args=locals())
|
|
368
|
+
if not where:
|
|
369
|
+
raise Exception("delete_by_where requires a 'where'")
|
|
370
|
+
if "end_timestamp" not in where and is_end_timestamp_in_table(table_name):
|
|
371
|
+
where += " AND end_timestamp IS NULL "
|
|
372
|
+
|
|
373
|
+
# In PostgreSQL, we use the same approach as MySQL for soft deletes
|
|
374
|
+
update_query = f'UPDATE "{schema_name}"."{table_name}" ' \
|
|
375
|
+
f'SET end_timestamp=CURRENT_TIMESTAMP ' \
|
|
376
|
+
f'WHERE {where};'
|
|
377
|
+
|
|
378
|
+
self.cursor.execute(update_query, params)
|
|
379
|
+
self.connection.commit()
|
|
380
|
+
deleted_rows = self.cursor.get_affected_row_count()
|
|
381
|
+
return deleted_rows
|
|
382
|
+
|
|
383
|
+
def undelete_by_column_and_value(self, *, schema_name: str = None, table_name: str = None,
|
|
384
|
+
column_name: str = None, column_value: Any) -> None:
|
|
385
|
+
"""Undeletes a row by setting the end_timestamp to NULL."""
|
|
386
|
+
schema_name = schema_name or self.default_schema_name
|
|
387
|
+
table_name = table_name or self.default_table_name
|
|
388
|
+
column_name = column_name or self.default_column_name
|
|
389
|
+
self._validate_args(args=locals())
|
|
390
|
+
self.update_by_column_and_value(
|
|
391
|
+
schema_name=schema_name, table_name=table_name,
|
|
392
|
+
column_name=column_name, column_value=column_value,
|
|
393
|
+
data_dict={"end_timestamp": None})
|
|
394
|
+
# Main select function
|
|
395
|
+
def select_multi_tuple_by_where(
|
|
396
|
+
self, *, schema_name: str = None, view_table_name: str = None, select_clause_value: str = None,
|
|
397
|
+
where: str = None, params: tuple = None, distinct: bool = False, limit: int = DEFAULT_SQL_SELECT_LIMIT,
|
|
398
|
+
order_by: str = None) -> list:
|
|
399
|
+
"""Selects multiple rows from the table based on a WHERE clause and returns them as a list of tuples."""
|
|
400
|
+
|
|
401
|
+
schema_name = schema_name or self.default_schema_name
|
|
402
|
+
view_table_name = view_table_name or self.default_view_table_name
|
|
403
|
+
select_clause_value = select_clause_value or self.default_select_clause_value
|
|
404
|
+
select_clause_value = fix_select_clause_value(select_clause_value=select_clause_value)
|
|
405
|
+
where = where or self.default_where
|
|
406
|
+
where = fix_where_clause_value(where_clause_value=where)
|
|
407
|
+
where = self.__where_security(where=where, view_name=view_table_name)
|
|
408
|
+
self._validate_args(args=locals())
|
|
409
|
+
|
|
410
|
+
if self.is_test_data and not self.is_ignore_duplicate:
|
|
411
|
+
if self.default_view_with_deleted_and_test_data:
|
|
412
|
+
view_table_name = self.default_view_with_deleted_and_test_data
|
|
413
|
+
else:
|
|
414
|
+
view_table_name = replace_view_with_table(view_table_name=view_table_name,
|
|
415
|
+
select_clause_value=select_clause_value)
|
|
416
|
+
elif is_column_in_table(table_name=view_table_name, column_name="is_test_data"):
|
|
417
|
+
if not where:
|
|
418
|
+
where = "(is_test_data <> 1 OR is_test_data IS NULL)"
|
|
419
|
+
elif not self.is_test_data and "is_test_data" not in where:
|
|
420
|
+
where += " AND (is_test_data <> 1 OR is_test_data IS NULL)" # hide test data from real users.
|
|
421
|
+
|
|
422
|
+
if where and "end_timestamp" not in where and is_end_timestamp_in_table(
|
|
423
|
+
view_table_name) and not self.is_ignore_duplicate:
|
|
424
|
+
# The () around the where is important, because we might have a where with AND and OR
|
|
425
|
+
where = f"({where}) AND end_timestamp IS NULL " # not deleted
|
|
426
|
+
|
|
427
|
+
select_query = f'SELECT {("DISTINCT " if distinct else "")} {select_clause_value} ' \
|
|
428
|
+
f'FROM "{schema_name}"."{view_table_name}" ' + \
|
|
429
|
+
(f'WHERE {where} ' if where else '') + \
|
|
430
|
+
(f'ORDER BY {order_by} ' if order_by else '') + \
|
|
431
|
+
f'LIMIT {limit};'
|
|
432
|
+
|
|
433
|
+
self.connection.commit() # Ensure we're working with the latest data
|
|
434
|
+
if isinstance(params, int):
|
|
435
|
+
params = [params]
|
|
436
|
+
self.cursor.execute(select_query, params)
|
|
437
|
+
result = self.cursor.fetchall()
|
|
438
|
+
|
|
439
|
+
self.logger.debug(object=locals())
|
|
440
|
+
return result
|
|
441
|
+
|
|
442
|
+
def select_multi_dict_by_where(
|
|
443
|
+
self, *, schema_name: str = None, view_table_name: str = None, select_clause_value: str = None,
|
|
444
|
+
where: str = None, params: tuple = None, distinct: bool = False, group_by: str = None,
|
|
445
|
+
limit: int = DEFAULT_SQL_SELECT_LIMIT, order_by: str = None) -> list | dict[tuple | str, list]:
|
|
446
|
+
"""Selects multiple rows from the table based on a WHERE clause and returns them as a list of dictionaries."""
|
|
447
|
+
result = self.select_multi_tuple_by_where(
|
|
448
|
+
schema_name=schema_name, view_table_name=view_table_name, select_clause_value=select_clause_value,
|
|
449
|
+
where=where, params=params, distinct=distinct, limit=limit, order_by=order_by)
|
|
450
|
+
result_as_dicts = self.convert_multi_to_dict(result, select_clause_value)
|
|
451
|
+
if group_by:
|
|
452
|
+
result_as_dicts = group_list_by_columns(list_of_dicts=result_as_dicts, group_by=group_by)
|
|
453
|
+
return result_as_dicts
|
|
454
|
+
|
|
455
|
+
def select_one_tuple_by_where(
|
|
456
|
+
self, *, schema_name: str = None, view_table_name: str = None, select_clause_value: str = None,
|
|
457
|
+
where: str = None, params: tuple = None, distinct: bool = False, order_by: str = None) -> tuple:
|
|
458
|
+
"""Selects one row from the table based on a WHERE clause and returns it as a tuple."""
|
|
459
|
+
result = self.select_multi_tuple_by_where(
|
|
460
|
+
schema_name=schema_name, view_table_name=view_table_name, select_clause_value=select_clause_value,
|
|
461
|
+
where=where, params=params, distinct=distinct, limit=1, order_by=order_by)
|
|
462
|
+
if result:
|
|
463
|
+
tuple_result = result[0]
|
|
464
|
+
else:
|
|
465
|
+
tuple_result = tuple()
|
|
466
|
+
return tuple_result
|
|
467
|
+
|
|
468
|
+
def select_one_dict_by_where(
|
|
469
|
+
self, *, schema_name: str = None, view_table_name: str = None, select_clause_value: str = None,
|
|
470
|
+
where: str = None, params: tuple = None, distinct: bool = False, order_by: str = None) -> dict:
|
|
471
|
+
"""Selects one row from the table based on a WHERE clause and returns it as a dictionary."""
|
|
472
|
+
result = self.select_one_tuple_by_where(
|
|
473
|
+
schema_name=schema_name, view_table_name=view_table_name, select_clause_value=select_clause_value,
|
|
474
|
+
where=where, params=params, distinct=distinct, order_by=order_by)
|
|
475
|
+
result = self.convert_to_dict(row=result, select_clause_value=select_clause_value)
|
|
476
|
+
return result
|
|
477
|
+
|
|
478
|
+
def select_one_value_by_where(
|
|
479
|
+
self, *, select_clause_value: str, schema_name: str = None, view_table_name: str = None,
|
|
480
|
+
where: str = None, params: tuple = None, distinct: bool = False, order_by: str = None,
|
|
481
|
+
skip_null_values: bool = True) -> Any:
|
|
482
|
+
"""Selects one value from the table based on a WHERE clause and returns it."""
|
|
483
|
+
select_clause_value = select_clause_value or self.default_select_clause_value
|
|
484
|
+
validate_single_clause_value(select_clause_value)
|
|
485
|
+
where = where_skip_null_values(where, select_clause_value, skip_null_values)
|
|
486
|
+
result = self.select_one_tuple_by_where(
|
|
487
|
+
schema_name=schema_name, view_table_name=view_table_name, select_clause_value=select_clause_value,
|
|
488
|
+
where=where, params=params, distinct=distinct, order_by=order_by)
|
|
489
|
+
if result:
|
|
490
|
+
value = result[0]
|
|
491
|
+
else:
|
|
492
|
+
value = None
|
|
493
|
+
return value
|
|
494
|
+
def select_one_tuple_by_column_and_value(
|
|
495
|
+
self, *, schema_name: str = None, view_table_name: str = None, select_clause_value: str = None,
|
|
496
|
+
column_name: str = None, column_value: Any,
|
|
497
|
+
distinct: bool = False, order_by: str = None) -> tuple:
|
|
498
|
+
"""Selects one row from the table by ID and returns it as a tuple."""
|
|
499
|
+
result = self.select_multi_tuple_by_column_and_value(
|
|
500
|
+
schema_name=schema_name, view_table_name=view_table_name, select_clause_value=select_clause_value,
|
|
501
|
+
column_name=column_name, column_value=column_value, distinct=distinct, limit=1, order_by=order_by)
|
|
502
|
+
if result:
|
|
503
|
+
one_tuple_result = result[0]
|
|
504
|
+
else:
|
|
505
|
+
one_tuple_result = tuple() # or None?
|
|
506
|
+
return one_tuple_result
|
|
507
|
+
|
|
508
|
+
def select_one_dict_by_column_and_value(
|
|
509
|
+
self, *, schema_name: str = None, view_table_name: str = None, select_clause_value: str = None,
|
|
510
|
+
column_name: str = None, column_value: Any,
|
|
511
|
+
distinct: bool = False, order_by: str = None) -> dict:
|
|
512
|
+
"""Selects one row from the table by ID and returns it as a dictionary (column_name: value)"""
|
|
513
|
+
result = self.select_one_tuple_by_column_and_value(
|
|
514
|
+
schema_name=schema_name, view_table_name=view_table_name, select_clause_value=select_clause_value,
|
|
515
|
+
column_name=column_name, column_value=column_value, distinct=distinct, order_by=order_by)
|
|
516
|
+
result = self.convert_to_dict(row=result, select_clause_value=select_clause_value)
|
|
517
|
+
return result
|
|
518
|
+
|
|
519
|
+
def select_one_value_by_column_and_value(
|
|
520
|
+
self, *, select_clause_value: str = None, schema_name: str = None, view_table_name: str = None,
|
|
521
|
+
column_name: str = None, column_value: Any,
|
|
522
|
+
distinct: bool = False, order_by: str = None, skip_null_values: bool = True) -> Any:
|
|
523
|
+
"""Selects one value from the table by ID and returns it."""
|
|
524
|
+
|
|
525
|
+
column_name = column_name or self.default_column_name
|
|
526
|
+
select_clause_value = select_clause_value or self.default_select_clause_value
|
|
527
|
+
validate_single_clause_value(select_clause_value)
|
|
528
|
+
where, params = get_where_params(column_name, column_value)
|
|
529
|
+
where = where_skip_null_values(where, select_clause_value, skip_null_values)
|
|
530
|
+
result = self.select_one_tuple_by_where(
|
|
531
|
+
schema_name=schema_name, view_table_name=view_table_name, select_clause_value=select_clause_value,
|
|
532
|
+
where=where, params=params, distinct=distinct, order_by=order_by)
|
|
533
|
+
if result: # TODO: the caller can't tell if not found, or found null
|
|
534
|
+
value = result[0]
|
|
535
|
+
else:
|
|
536
|
+
value = None
|
|
537
|
+
return value
|
|
538
|
+
|
|
539
|
+
def select_multi_tuple_by_column_and_value(
|
|
540
|
+
self, *, schema_name: str = None, view_table_name: str = None, select_clause_value: str = None,
|
|
541
|
+
column_name: str = None, column_value: Any,
|
|
542
|
+
distinct: bool = False, limit: int = DEFAULT_SQL_SELECT_LIMIT, order_by: str = None) -> list:
|
|
543
|
+
"""Selects multiple rows from the table by ID and returns them as a list of tuples.
|
|
544
|
+
If column_value is list / tuple, it will be used as multiple values for the column_name (SQL IN)."""
|
|
545
|
+
|
|
546
|
+
column_name = column_name or self.default_column_name
|
|
547
|
+
|
|
548
|
+
where, params = get_where_params(column_name, column_value)
|
|
549
|
+
result = self.select_multi_tuple_by_where(
|
|
550
|
+
schema_name=schema_name, view_table_name=view_table_name, select_clause_value=select_clause_value,
|
|
551
|
+
where=where, params=params, distinct=distinct, limit=limit, order_by=order_by)
|
|
552
|
+
return result
|
|
553
|
+
|
|
554
|
+
def select_multi_dict_by_column_and_value(
|
|
555
|
+
self, *, schema_name: str = None, view_table_name: str = None, select_clause_value: str = None,
|
|
556
|
+
column_name: str = None, column_value: Any,
|
|
557
|
+
distinct: bool = False, limit: int = DEFAULT_SQL_SELECT_LIMIT, order_by: str = None,
|
|
558
|
+
group_by: str = None) -> list | dict[tuple | str, list]:
|
|
559
|
+
"""Selects multiple rows from the table by ID and returns them as a list of dictionaries."""
|
|
560
|
+
result = self.select_multi_tuple_by_column_and_value(
|
|
561
|
+
schema_name=schema_name, view_table_name=view_table_name, select_clause_value=select_clause_value,
|
|
562
|
+
column_name=column_name, column_value=column_value, distinct=distinct, limit=limit, order_by=order_by)
|
|
563
|
+
result_as_dicts = self.convert_multi_to_dict(result, select_clause_value)
|
|
564
|
+
if group_by:
|
|
565
|
+
result_as_dicts = group_list_by_columns(list_of_dicts=result_as_dicts, group_by=group_by)
|
|
566
|
+
return result_as_dicts
|
|
567
|
+
def insert_if_not_exists(self, *, schema_name: str = None, table_name: str = None, data_dict: dict = None,
|
|
568
|
+
data_dict_compare: dict = None, view_table_name: str = None,
|
|
569
|
+
commit_changes: bool = True, compare_with_or: bool = False) -> int:
|
|
570
|
+
"""Inserts a new row into the table if a row with the same values does not exist,
|
|
571
|
+
and returns the id of the new row | None if an error occurred."""
|
|
572
|
+
schema_name = schema_name or self.default_schema_name
|
|
573
|
+
table_name = table_name or self.default_table_name
|
|
574
|
+
view_table_name = view_table_name or self.default_view_table_name
|
|
575
|
+
data_dict_compare = data_dict_compare or data_dict
|
|
576
|
+
self._validate_args(args=locals())
|
|
577
|
+
|
|
578
|
+
# Try to select
|
|
579
|
+
where_clause, params = process_upsert_data_dict(data_dict=data_dict_compare, compare_with_or=compare_with_or)
|
|
580
|
+
row_tuple = self.select_one_tuple_by_where(
|
|
581
|
+
schema_name=schema_name, view_table_name=view_table_name, select_clause_value="*",
|
|
582
|
+
where=where_clause, params=params)
|
|
583
|
+
if row_tuple:
|
|
584
|
+
entity_id = row_tuple[0]
|
|
585
|
+
self.logger.info(f"GenericCrudPostgresql.insert_if_not_exists: row already exists, returning id {entity_id}",
|
|
586
|
+
object={"id": entity_id})
|
|
587
|
+
else:
|
|
588
|
+
entity_id = self.insert(schema_name=schema_name, table_name=table_name, data_dict=data_dict,
|
|
589
|
+
commit_changes=commit_changes, ignore_duplicate=True)
|
|
590
|
+
self.logger.info(f"GenericCrudPostgresql.insert_if_not_exists: row inserted with id {entity_id}",
|
|
591
|
+
object={"id": entity_id})
|
|
592
|
+
return entity_id
|
|
593
|
+
|
|
594
|
+
def insert_many_dicts(self, *, schema_name: str = None, table_name: str = None, data_dicts: list[dict],
|
|
595
|
+
commit_changes: bool = True) -> int:
|
|
596
|
+
"""Inserts multiple rows into the table.
|
|
597
|
+
data_dicts should be in the following format: [{col1: val1, col2: val2}, {col1: val3, col2: val4}, ...]
|
|
598
|
+
Returns the number of inserted rows.
|
|
599
|
+
"""
|
|
600
|
+
if not data_dicts:
|
|
601
|
+
self.logger.warning("GenericCrudPostgresql.insert_many_dicts: data_dicts is empty")
|
|
602
|
+
inserted_rows = 0
|
|
603
|
+
else:
|
|
604
|
+
converted_data_dicts = {col: [row[col] for row in data_dicts] for col in data_dicts[0]}
|
|
605
|
+
inserted_rows = self.insert_many(schema_name=schema_name, table_name=table_name,
|
|
606
|
+
data_dict=converted_data_dicts, commit_changes=commit_changes)
|
|
607
|
+
|
|
608
|
+
return inserted_rows
|
|
609
|
+
|
|
610
|
+
def insert_many(self, *, schema_name: str = None, table_name: str = None, data_dict: dict[str, list | tuple],
|
|
611
|
+
commit_changes: bool = True) -> int:
|
|
612
|
+
"""Inserts multiple rows into the table.
|
|
613
|
+
data_dict should be in the following format: {col1: [val1, val2], col2: [val3, val4], ...}
|
|
614
|
+
Returns the number of inserted rows.
|
|
615
|
+
"""
|
|
616
|
+
if not data_dict:
|
|
617
|
+
self.logger.warning("GenericCrudPostgresql.insert_many: data_dict is empty")
|
|
618
|
+
inserted_rows = 0
|
|
619
|
+
return inserted_rows
|
|
620
|
+
schema_name = schema_name or self.default_schema_name
|
|
621
|
+
table_name = table_name or self.default_table_name
|
|
622
|
+
|
|
623
|
+
self._validate_args(args=locals())
|
|
624
|
+
|
|
625
|
+
len_rows = len(next(v for v in data_dict.values()))
|
|
626
|
+
data_dict = self.__add_create_updated_user_profile_ids(
|
|
627
|
+
data_dict=data_dict, add_created_user_id=True, schema_name=schema_name, table_name=table_name)
|
|
628
|
+
# Fix values from __add_create_updated_user_profile_ids
|
|
629
|
+
for k, v in data_dict.items():
|
|
630
|
+
if not isinstance(v, list) and not isinstance(v, tuple):
|
|
631
|
+
data_dict[k] = [v] * len_rows
|
|
632
|
+
|
|
633
|
+
columns = ", ".join(f'"{key}"' for key in data_dict)
|
|
634
|
+
values = ", ".join(["%s"] * len(data_dict))
|
|
635
|
+
sql_statement = f'INSERT INTO "{schema_name}"."{table_name}" ({columns}) VALUES ({values});'
|
|
636
|
+
sql_parameters = list(zip(*data_dict.values()))
|
|
637
|
+
|
|
638
|
+
self.cursor.executemany(sql_statement=sql_statement, sql_parameters=sql_parameters)
|
|
639
|
+
if commit_changes:
|
|
640
|
+
self.connection.commit()
|
|
641
|
+
inserted_rows = self.cursor.get_affected_row_count()
|
|
642
|
+
return inserted_rows
|
|
643
|
+
def upsert_with_select_clause(self, *, schema_name: str = None, table_name: str = None, view_table_name: str = None,
|
|
644
|
+
data_dict: dict = None, where_compare: str = None, params_compare: tuple = None,
|
|
645
|
+
data_dict_compare: dict = None, order_by: str = None, compare_with_or: bool = False,
|
|
646
|
+
select_clause_value: str = "*") -> dict:
|
|
647
|
+
"""
|
|
648
|
+
Inserts a new row into the table if a row with the same values does not exist,
|
|
649
|
+
the logic:
|
|
650
|
+
1. If data_dict_compare is empty, insert the row and return the id.
|
|
651
|
+
2. If data_dict_compare is not empty, select the row with the same values as data_dict_compare.
|
|
652
|
+
|
|
653
|
+
a. If the row exists, update it with data_dict and return the id.
|
|
654
|
+
|
|
655
|
+
b. If the row does not exist, insert it with data_dict and return the id.
|
|
656
|
+
"""
|
|
657
|
+
schema_name = schema_name or self.default_schema_name
|
|
658
|
+
table_name = table_name or self.default_table_name
|
|
659
|
+
view_table_name = view_table_name or self.default_view_table_name
|
|
660
|
+
column_name = generate_id_column_name(table_name)
|
|
661
|
+
self._validate_args(args=locals())
|
|
662
|
+
result_dict = {}
|
|
663
|
+
if not data_dict:
|
|
664
|
+
self.logger.warning(log_message="GenericCrudPostgresql.upsert_with_select_clause: data_dict is empty")
|
|
665
|
+
return result_dict
|
|
666
|
+
if not data_dict_compare:
|
|
667
|
+
inserted_id = self.insert(schema_name=schema_name,
|
|
668
|
+
table_name=table_name, data_dict=data_dict)
|
|
669
|
+
data_dict[column_name] = inserted_id
|
|
670
|
+
result_dict = data_dict
|
|
671
|
+
return result_dict
|
|
672
|
+
|
|
673
|
+
where_clause, params = process_upsert_data_dict(data_dict=data_dict_compare, compare_with_or=compare_with_or,
|
|
674
|
+
where_compare=where_compare, params_compare=params_compare)
|
|
675
|
+
# Add table_id if it's not in select_clause_value
|
|
676
|
+
if select_clause_value != "*" and column_name not in select_clause_value:
|
|
677
|
+
select_clause_value += f", {column_name}"
|
|
678
|
+
row_dict = self.select_one_dict_by_where(
|
|
679
|
+
schema_name=schema_name, view_table_name=view_table_name, select_clause_value=select_clause_value,
|
|
680
|
+
where=where_clause, params=params, order_by=order_by)
|
|
681
|
+
if row_dict:
|
|
682
|
+
table_id = row_dict[column_name]
|
|
683
|
+
try:
|
|
684
|
+
self.update_by_column_and_value(
|
|
685
|
+
schema_name=schema_name, table_name=table_name, column_name=column_name, column_value=table_id,
|
|
686
|
+
data_dict=data_dict)
|
|
687
|
+
except Exception as exception:
|
|
688
|
+
self.logger.error(
|
|
689
|
+
"GenericCrudPostgresql.upsert_with_select_clause: error updating row",
|
|
690
|
+
object={"data_dict": data_dict, "row_dict": row_dict, "exception": exception}
|
|
691
|
+
)
|
|
692
|
+
raise exception
|
|
693
|
+
else:
|
|
694
|
+
try:
|
|
695
|
+
table_id = self.insert(schema_name=schema_name, table_name=table_name, data_dict=data_dict)
|
|
696
|
+
except Exception as exception:
|
|
697
|
+
self.logger.error(
|
|
698
|
+
"GenericCrudPostgresql.upsert_with_select_clause: error inserting row",
|
|
699
|
+
object={"data_dict": data_dict, "exception": exception}
|
|
700
|
+
)
|
|
701
|
+
raise exception
|
|
702
|
+
result_dict = data_dict
|
|
703
|
+
result_dict.update({k: v for k, v in row_dict.items() if k not in result_dict}) if row_dict else None
|
|
704
|
+
result_dict[column_name] = table_id
|
|
705
|
+
self.logger.debug(object=locals())
|
|
706
|
+
return result_dict
|
|
707
|
+
|
|
708
|
+
# We want upsert to call upsert_with_select_clause, and return only the id
|
|
709
|
+
def upsert(self, *, schema_name: str = None, table_name: str = None, view_table_name: str = None,
|
|
710
|
+
data_dict: dict = None, where_compare: str = None,
|
|
711
|
+
params_compare: tuple = None, data_dict_compare: dict = None,
|
|
712
|
+
order_by: str = None, compare_with_or: bool = False) -> Optional[int]:
|
|
713
|
+
"""
|
|
714
|
+
Inserts a new row into the table if a row with the same values does not exist,
|
|
715
|
+
the "same values" are extracted from data_dict_compare.
|
|
716
|
+
data_dict_compare is a dictionary with the same keys as data_dict, but with the values to compare.
|
|
717
|
+
|
|
718
|
+
**you don't need to have the full fields in data_dict_compare, only the fields you want to compare.**
|
|
719
|
+
"""
|
|
720
|
+
table_name = table_name or self.default_table_name
|
|
721
|
+
column_name = generate_id_column_name(table_name)
|
|
722
|
+
inserted_id_per_column_dict = self.upsert_with_select_clause(
|
|
723
|
+
schema_name=schema_name, table_name=table_name,
|
|
724
|
+
view_table_name=view_table_name, data_dict=data_dict,
|
|
725
|
+
where_compare=where_compare, params_compare=params_compare,
|
|
726
|
+
data_dict_compare=data_dict_compare, order_by=order_by,
|
|
727
|
+
compare_with_or=compare_with_or, select_clause_value=column_name)
|
|
728
|
+
inserted_id = inserted_id_per_column_dict[column_name]
|
|
729
|
+
return inserted_id
|
|
730
|
+
# Helper methods
|
|
731
|
+
def convert_to_dict(self, row: tuple, select_clause_value: str = None) -> dict:
|
|
732
|
+
"""Returns a dictionary of the column names and their values."""
|
|
733
|
+
select_clause_value = select_clause_value or self.default_select_clause_value
|
|
734
|
+
if select_clause_value == "*":
|
|
735
|
+
column_names = self.cursor.column_names()
|
|
736
|
+
else:
|
|
737
|
+
column_names = [x.strip() for x in select_clause_value.split(",")]
|
|
738
|
+
dict_result = dict(zip(column_names, row or tuple()))
|
|
739
|
+
self.logger.debug(object=locals())
|
|
740
|
+
return dict_result
|
|
741
|
+
|
|
742
|
+
def convert_multi_to_dict(self, rows: list[tuple], select_clause_value: str = None) -> list[dict]:
|
|
743
|
+
"""Converts multiple rows to dictionaries."""
|
|
744
|
+
multiple_dict_result = [self.convert_to_dict(row=row, select_clause_value=select_clause_value)
|
|
745
|
+
for row in rows]
|
|
746
|
+
return multiple_dict_result
|
|
747
|
+
|
|
748
|
+
def _validate_args(self, args: dict) -> None:
|
|
749
|
+
# args = locals() of the calling function
|
|
750
|
+
required_args = ("table_name", "view_table_name", "schema_name",
|
|
751
|
+
"select_clause_value", "data_dict")
|
|
752
|
+
for arg_name, arg_value in args.items():
|
|
753
|
+
message = ""
|
|
754
|
+
if arg_name in ("self", "__class__"):
|
|
755
|
+
continue
|
|
756
|
+
elif arg_name in required_args and not arg_value:
|
|
757
|
+
message = f"Invalid value for {arg_name}: {arg_value}"
|
|
758
|
+
elif arg_name == "table_name":
|
|
759
|
+
validate_none_select_table_name(arg_value)
|
|
760
|
+
elif arg_name == "view_table_name":
|
|
761
|
+
validate_select_table_name(view_table_name=arg_value, is_ignore_duplicate=self.is_ignore_duplicate)
|
|
762
|
+
|
|
763
|
+
# data_dict values are allowed to contain ';', as we use them with %s
|
|
764
|
+
if ((arg_name.startswith("data_") and arg_value and any(
|
|
765
|
+
";" in str(x) for x in arg_value.keys())) or # check columns
|
|
766
|
+
(not arg_name == "data_dict" and arg_name != "params" and ";" in str(arg_value))):
|
|
767
|
+
message = f"Invalid value for {arg_name}: {arg_value} (contains ';')"
|
|
768
|
+
|
|
769
|
+
if message:
|
|
770
|
+
raise Exception(message)
|
|
771
|
+
|
|
772
|
+
def __where_security(self, where: str, view_name: str) -> str:
|
|
773
|
+
"""Adds security to the where clause."""
|
|
774
|
+
if view_name in table_definition:
|
|
775
|
+
if table_definition[view_name].get("is_visibility"):
|
|
776
|
+
effective_profile_id = self.user_context.get_effective_profile_id()
|
|
777
|
+
where_security = f'(visibility_id > 1 OR created_effective_profile_id = {effective_profile_id})'
|
|
778
|
+
if where:
|
|
779
|
+
where_security += f" AND ({where})"
|
|
780
|
+
return where_security
|
|
781
|
+
return where
|
|
782
|
+
|
|
783
|
+
def __add_create_updated_user_profile_ids(self, data_dict: dict, add_created_user_id: bool = False,
|
|
784
|
+
schema_name: str = None, table_name: str = None) -> dict:
|
|
785
|
+
"""Adds created_user_id and updated_user_id to data_dict."""
|
|
786
|
+
data_dict = data_dict or {}
|
|
787
|
+
schema_name = schema_name or self.default_schema_name
|
|
788
|
+
table_name = table_name or self.default_table_name
|
|
789
|
+
table_columns = get_table_columns(table_name=table_name)
|
|
790
|
+
if len(table_columns) == 0:
|
|
791
|
+
self.logger.warning(f"Table {schema_name}.{table_name} was not generated by the generate_table_columns.py script.")
|
|
792
|
+
return data_dict
|
|
793
|
+
if add_created_user_id:
|
|
794
|
+
if "created_user_id" in table_columns:
|
|
795
|
+
data_dict["created_user_id"] = self.user_context.get_effective_user_id()
|
|
796
|
+
else:
|
|
797
|
+
self.__log_warning("created_user_id", schema_name, table_name)
|
|
798
|
+
if "created_real_user_id" in table_columns:
|
|
799
|
+
data_dict["created_real_user_id"] = self.user_context.get_real_user_id()
|
|
800
|
+
else:
|
|
801
|
+
self.__log_warning("created_real_user_id", schema_name, table_name)
|
|
802
|
+
if "created_effective_user_id" in table_columns:
|
|
803
|
+
data_dict["created_effective_user_id"] = self.user_context.get_effective_user_id()
|
|
804
|
+
else:
|
|
805
|
+
self.__log_warning("created_effective_user_id", schema_name, table_name)
|
|
806
|
+
if "created_effective_profile_id" in table_columns:
|
|
807
|
+
data_dict["created_effective_profile_id"] = self.user_context.get_effective_profile_id()
|
|
808
|
+
else:
|
|
809
|
+
self.__log_warning("created_effective_profile_id", schema_name, table_name)
|
|
810
|
+
if "is_test_data" in table_columns:
|
|
811
|
+
data_dict["is_test_data"] = self.is_test_data
|
|
812
|
+
else:
|
|
813
|
+
self.__log_warning("is_test_data", schema_name, table_name)
|
|
814
|
+
|
|
815
|
+
if "number" in table_columns:
|
|
816
|
+
view_name = table_name
|
|
817
|
+
number = NumberGenerator.get_random_number(
|
|
818
|
+
schema_name=schema_name, view_name=view_name)
|
|
819
|
+
data_dict["number"] = number
|
|
820
|
+
else:
|
|
821
|
+
self.__log_warning("number", schema_name, table_name)
|
|
822
|
+
|
|
823
|
+
if "identifier" in table_columns:
|
|
824
|
+
self.__add_identifier(data_dict=data_dict, table_name=table_name)
|
|
825
|
+
else:
|
|
826
|
+
self.__log_warning("identifier", schema_name, table_name)
|
|
827
|
+
if "updated_user_id" in table_columns:
|
|
828
|
+
data_dict["updated_user_id"] = self.user_context.get_effective_user_id()
|
|
829
|
+
else:
|
|
830
|
+
self.__log_warning("updated_user_id", schema_name, table_name)
|
|
831
|
+
if "updated_real_user_id" in table_columns:
|
|
832
|
+
data_dict["updated_real_user_id"] = self.user_context.get_real_user_id()
|
|
833
|
+
else:
|
|
834
|
+
self.__log_warning("updated_real_user_id", schema_name, table_name)
|
|
835
|
+
if "updated_effective_user_id" in table_columns:
|
|
836
|
+
data_dict["updated_effective_user_id"] = self.user_context.get_effective_user_id()
|
|
837
|
+
else:
|
|
838
|
+
self.__log_warning("updated_effective_user_id", schema_name, table_name)
|
|
839
|
+
if "updated_effective_profile_id" in table_columns:
|
|
840
|
+
data_dict["updated_effective_profile_id"] = self.user_context.get_effective_profile_id()
|
|
841
|
+
else:
|
|
842
|
+
self.__log_warning("updated_effective_profile_id", schema_name, table_name)
|
|
843
|
+
|
|
844
|
+
self.logger.debug(object=locals())
|
|
845
|
+
return data_dict
|
|
846
|
+
|
|
847
|
+
@lru_cache(maxsize=64) # Don't show the same warning twice
|
|
848
|
+
def __log_warning(self, column_name: str, schema_name: str, table_name: str):
|
|
849
|
+
"""Generates a warning log message and logs it."""
|
|
850
|
+
self.logger.warning(f"{schema_name}.{table_name}.{column_name} not found in table_columns.")
|
|
851
|
+
|
|
852
|
+
def __add_identifier(self, data_dict: dict, table_name: str) -> None:
|
|
853
|
+
# If there's an "identifier" column in the table, we want to insert a random identifier
|
|
854
|
+
identifier_entity_type_id = get_entity_type_by_table_name(table_name)
|
|
855
|
+
if not identifier_entity_type_id:
|
|
856
|
+
return
|
|
857
|
+
identifier = NumberGenerator.get_random_identifier(
|
|
858
|
+
schema_name="identifier", view_name="identifier_view", identifier_column_name="identifier")
|
|
859
|
+
data_dict["identifier"] = identifier
|
|
860
|
+
# Insert into identifier table
|
|
861
|
+
insert_query = 'INSERT INTO "identifier"."identifier_table" (identifier, entity_type_id) VALUES (%s, %s);'
|
|
862
|
+
self.cursor.execute(insert_query, (identifier, identifier_entity_type_id))
|
|
863
|
+
self.connection.commit()
|
|
864
|
+
|
|
865
|
+
@lru_cache
|
|
866
|
+
def get_primary_key(self, schema_name: str = None, table_name: str = None) -> str | None:
|
|
867
|
+
schema_name = schema_name or self.default_schema_name
|
|
868
|
+
table_name = table_name or self.default_table_name
|
|
869
|
+
query = """
|
|
870
|
+
SELECT a.attname
|
|
871
|
+
FROM pg_index i
|
|
872
|
+
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
|
|
873
|
+
WHERE i.indrelid = %s::regclass
|
|
874
|
+
AND i.indisprimary;
|
|
875
|
+
"""
|
|
876
|
+
|
|
877
|
+
self.connection.commit()
|
|
878
|
+
try:
|
|
879
|
+
self.cursor.execute(query, (f'"{schema_name}"."{table_name}"',))
|
|
880
|
+
column_name = (self.cursor.fetchone() or [None])[0]
|
|
881
|
+
return column_name
|
|
882
|
+
except Exception as e:
|
|
883
|
+
self.logger.error(f"Error getting primary key: {e}")
|
|
884
|
+
return None
|
|
885
|
+
|
|
886
|
+
@lru_cache
|
|
887
|
+
def get_constraint_columns(self, schema_name: str, table_name: str) -> list[list[str]]:
|
|
888
|
+
schema_name = schema_name or self.default_schema_name
|
|
889
|
+
table_name = table_name or self.default_table_name
|
|
890
|
+
query = """
|
|
891
|
+
SELECT con.conname as constraint_name,
|
|
892
|
+
array_agg(att.attname) as column_names
|
|
893
|
+
FROM pg_constraint con
|
|
894
|
+
JOIN pg_class rel ON rel.oid = con.conrelid
|
|
895
|
+
JOIN pg_namespace nsp ON nsp.oid = rel.relnamespace
|
|
896
|
+
JOIN pg_attribute att ON att.attrelid = rel.oid AND att.attnum = ANY(con.conkey)
|
|
897
|
+
WHERE nsp.nspname = %s
|
|
898
|
+
AND rel.relname = %s
|
|
899
|
+
AND con.contype IN ('p', 'u')
|
|
900
|
+
GROUP BY con.conname;
|
|
901
|
+
"""
|
|
902
|
+
self.connection.commit()
|
|
903
|
+
try:
|
|
904
|
+
self.cursor.execute(query, (schema_name, table_name))
|
|
905
|
+
results = self.cursor.fetchall()
|
|
906
|
+
constraints = []
|
|
907
|
+
for _, column_names in results:
|
|
908
|
+
constraints.append(list(column_names))
|
|
909
|
+
return constraints
|
|
910
|
+
except Exception as e:
|
|
911
|
+
self.logger.error(f"Error getting constraint columns: {e}")
|
|
912
|
+
return []
|
|
913
|
+
|
|
914
|
+
def get_constraint_where_clause(self, schema_name: str, table_name: str, data_dict: dict):
|
|
915
|
+
constraint_columns = self.get_constraint_columns(schema_name, table_name)
|
|
916
|
+
if constraint_columns:
|
|
917
|
+
where, params = generate_where_clause_for_ignore_duplicate(
|
|
918
|
+
data_dict=data_dict, constraint_columns=constraint_columns)
|
|
919
|
+
else:
|
|
920
|
+
where, params = None, None
|
|
921
|
+
return where, params
|
|
922
|
+
|
|
923
|
+
@property
|
|
924
|
+
def cursor(self) -> Cursor:
|
|
925
|
+
"""Get a new cursor"""
|
|
926
|
+
if self._cursor.is_closed():
|
|
927
|
+
self._cursor = self.connection.cursor()
|
|
928
|
+
cursor = self._cursor
|
|
929
|
+
return cursor
|
|
930
|
+
|
|
931
|
+
@cursor.setter
|
|
932
|
+
def cursor(self, value: Cursor) -> None:
|
|
933
|
+
"""Set the cursor"""
|
|
934
|
+
self._cursor = value
|
|
935
|
+
|
|
936
|
+
def set_schema(self, schema_name: Optional[str]):
|
|
937
|
+
"""Sets the given schema to be the default schema."""
|
|
938
|
+
if schema_name and self.default_schema_name != schema_name:
|
|
939
|
+
self.connection.set_schema(schema_name)
|
|
940
|
+
self.default_schema_name = schema_name
|
|
941
|
+
|
|
942
|
+
def close(self) -> None:
|
|
943
|
+
"""Closes the connection to the database (we usually do not have to call this)"""
|
|
944
|
+
try:
|
|
945
|
+
self.connection.close()
|
|
946
|
+
except Exception as e:
|
|
947
|
+
self.logger.error(f"Error while closing the connection: {e}")
|
|
948
|
+
|
|
949
|
+
def create_view(self, schema_name=None, table_name=None, view_name=None):
|
|
950
|
+
if table_name is not None and '_view' in table_name:
|
|
951
|
+
return
|
|
952
|
+
if schema_name is None:
|
|
953
|
+
schema_name = self.default_schema_name
|
|
954
|
+
|
|
955
|
+
if table_name is None:
|
|
956
|
+
table_name = self.default_table_name
|
|
957
|
+
|
|
958
|
+
if view_name is None:
|
|
959
|
+
view_name = self.default_view_table_name
|
|
960
|
+
|
|
961
|
+
print(f"Creating view {view_name} from {table_name}")
|
|
962
|
+
create_view_query = f"""
|
|
963
|
+
CREATE OR REPLACE VIEW "{schema_name}"."{view_name}" AS
|
|
964
|
+
SELECT * FROM "{schema_name}"."{table_name}";
|
|
965
|
+
"""
|
|
966
|
+
|
|
967
|
+
self.cursor.execute(create_view_query)
|