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.
@@ -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)