|
| 1 | +# Copyright 2025 Zeppelin Bend Pty Ltd |
| 2 | +# This Source Code Form is subject to the terms of the Mozilla Public |
| 3 | +# License, v. 2.0. If a copy of the MPL was not distributed with this |
| 4 | +# file, You can obtain one at https://mozilla.org/MPL/2.0/. |
| 5 | +from __future__ import annotations |
| 6 | + |
| 7 | +__all__ = ["SqlTable"] |
| 8 | + |
| 9 | +from abc import abstractmethod, ABCMeta |
| 10 | +from operator import attrgetter |
| 11 | +from typing import List, Optional, Type, Any, Generator |
| 12 | + |
| 13 | +from zepben.ewb.database.sql.column import Column, Nullable |
| 14 | + |
| 15 | + |
| 16 | +class SqlTable(metaclass=ABCMeta): |
| 17 | + """ |
| 18 | + Represents a table in an SQL Database. |
| 19 | +
|
| 20 | + By default, this class doesn't support creating schema creation statements, allowing support for database with external schema management. |
| 21 | + """ |
| 22 | + |
| 23 | + column_index: int = 0 |
| 24 | + """Used to specify index of the column in the table during initialisation. Always increment BEFORE creating a Column. Indices start from 1.""" |
| 25 | + |
| 26 | + _column_set: Optional[List[Column]] = None |
| 27 | + _create_table_sql: Optional[str] = None |
| 28 | + _prepared_insert_sql: Optional[str] = None |
| 29 | + _prepared_update_sql: Optional[str] = None |
| 30 | + _create_indexes_sql: Optional[List[str]] = None |
| 31 | + _select_sql: Optional[str] = None |
| 32 | + |
| 33 | + @property |
| 34 | + @abstractmethod |
| 35 | + def name(self) -> str: |
| 36 | + """ |
| 37 | + The name of the table in the actual database. |
| 38 | + """ |
| 39 | + pass |
| 40 | + |
| 41 | + # |
| 42 | + # NOTE: This function is called `description` in teh JVM SDK, but in the python SDK this |
| 43 | + # conflicts with the `description` column of `TableIdentifiedObjects`. |
| 44 | + # |
| 45 | + def describe(self) -> str: |
| 46 | + """ |
| 47 | + Readable description of the contents of the table for adding to logs. |
| 48 | + """ |
| 49 | + return self.name.replace("_", " ") |
| 50 | + |
| 51 | + @property |
| 52 | + @abstractmethod |
| 53 | + def create_table_sql(self): |
| 54 | + """ |
| 55 | + The SQL statement that should be executed to create the table in the database. |
| 56 | + """ |
| 57 | + raise NotImplemented |
| 58 | + |
| 59 | + @property |
| 60 | + def prepared_insert_sql(self): |
| 61 | + """ |
| 62 | + The SQL statement that should be used with a `PreparedStatement` to insert entries into the table. |
| 63 | + """ |
| 64 | + if self._prepared_insert_sql is None: |
| 65 | + self._prepared_insert_sql = (f"INSERT INTO {self.name} ({', '.join([c.name for c in self.column_set])}) " |
| 66 | + f"VALUES ({', '.join(['?' for _ in self.column_set])})") |
| 67 | + return self._prepared_insert_sql |
| 68 | + |
| 69 | + @property |
| 70 | + @abstractmethod |
| 71 | + def create_indexes_sql(self): |
| 72 | + """ |
| 73 | + The SQL statement that should be executed to create the indexes for the table in the database. Should be executed after all |
| 74 | + entries are inserted into the table. |
| 75 | + """ |
| 76 | + raise NotImplemented |
| 77 | + |
| 78 | + @property |
| 79 | + def select_sql(self): |
| 80 | + """ |
| 81 | + The SQL statement that should be used to read the entries from the table in the database. |
| 82 | + """ |
| 83 | + if self._select_sql is None: |
| 84 | + self._select_sql = f"SELECT {', '.join([c.name for c in self.column_set])} FROM {self.name}" |
| 85 | + return self._select_sql |
| 86 | + |
| 87 | + @property |
| 88 | + def prepared_update_sql(self): |
| 89 | + """ |
| 90 | + The SQL statement that should be used with a `PreparedStatement` to update entries into the table. |
| 91 | + """ |
| 92 | + if self._prepared_update_sql is None: |
| 93 | + self._prepared_update_sql = f"UPDATE {self.name} SET {', '.join([f'{c.name} = ?' for c in self.column_set])}" |
| 94 | + return self._prepared_update_sql |
| 95 | + |
| 96 | + @property |
| 97 | + def unique_index_columns(self) -> Generator[List[Column], None, None]: |
| 98 | + """ |
| 99 | + A list of column groups that require a unique index in the database |
| 100 | + """ |
| 101 | + yield from [] |
| 102 | + |
| 103 | + @property |
| 104 | + def non_unique_index_columns(self) -> Generator[List[Column], None, None]: |
| 105 | + """ |
| 106 | + A list of column groups that require a non-unique index in the database |
| 107 | + """ |
| 108 | + yield from [] |
| 109 | + |
| 110 | + @property |
| 111 | + def column_set(self) -> List[Column]: |
| 112 | + if self._column_set is None: |
| 113 | + self._column_set = list(self._build_column_set(self.__class__, self)) |
| 114 | + return self._column_set |
| 115 | + |
| 116 | + @staticmethod |
| 117 | + def _build_column_set(clazz: Type[Any], instance: SqlTable) -> Generator[Column, None, None]: |
| 118 | + """ |
| 119 | + Builds the list of columns for use in DDL statements for this table. |
| 120 | +
|
| 121 | + :param clazz: The class of this table. |
| 122 | + :param instance: |
| 123 | + """ |
| 124 | + cols = list() |
| 125 | + for field, x in instance.__dict__.items(): |
| 126 | + if isinstance(x, Column): |
| 127 | + if x.query_index != len(cols) + 1: |
| 128 | + raise ValueError( |
| 129 | + f"Field {field} in SQL Table class {clazz.__name__} is using an invalid column index. " |
| 130 | + f"Did you forget to increment column_index, or did you skip one?" |
| 131 | + ) |
| 132 | + cols.append(x) |
| 133 | + |
| 134 | + if len(set([c.name for c in cols])) != len(cols): |
| 135 | + raise ValueError("You have duplicate column names, go fix that.") |
| 136 | + |
| 137 | + yield from sorted(cols, key=attrgetter('query_index')) |
| 138 | + |
| 139 | + def _create_column(self, name: str, type_: str, nullable: Nullable = Nullable.NONE) -> Column: |
| 140 | + self.column_index += 1 |
| 141 | + # noinspection PyArgumentList |
| 142 | + return Column(self.column_index, name, type_, nullable) |
0 commit comments