dataclassdb 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,372 @@
1
+ __all__ = [
2
+ "SQL",
3
+ "SQL_FUNC",
4
+ "SQL_TYPES",
5
+ "SQL_TYPES_EXTENDED",
6
+ "SQL_FUNC_PRAGMA",
7
+ "SQL_FUNC_PRAGMA_EXTENDED",
8
+ ]
9
+
10
+ from enum import StrEnum, auto
11
+ from dataclassdb.utils import UpperStrEnum
12
+
13
+
14
+ class SQL(UpperStrEnum):
15
+ ABORT = auto()
16
+ ACTION = auto()
17
+ ADD = auto()
18
+ AFTER = auto()
19
+ ALL = auto()
20
+ ALTER = auto()
21
+ ALWAYS = auto()
22
+ ANALYZE = auto()
23
+ AND = auto()
24
+ AS = auto()
25
+ ASC = auto()
26
+ ATTACH = auto()
27
+ AUTOINCREMENT = auto()
28
+ BEFORE = auto()
29
+ BEGIN = auto()
30
+ BETWEEN = auto()
31
+ BY = auto()
32
+ CASCADE = auto()
33
+ CASE = auto()
34
+ CAST = auto()
35
+ CHECK = auto()
36
+ COLLATE = auto()
37
+ COLUMN = auto()
38
+ COMMIT = auto()
39
+ CONFLICT = auto()
40
+ CONSTRAINT = auto()
41
+ CREATE = auto()
42
+ CROSS = auto()
43
+ CURRENT = auto()
44
+ CURRENT_DATE = auto()
45
+ CURRENT_TIME = auto()
46
+ CURRENT_TIMESTAMP = auto()
47
+ DATABASE = auto()
48
+ DEFAULT = auto()
49
+ DEFERRABLE = auto()
50
+ DEFERRED = auto()
51
+ DELETE = auto()
52
+ DESC = auto()
53
+ DETACH = auto()
54
+ DISTINCT = auto()
55
+ DO = auto()
56
+ DROP = auto()
57
+ EACH = auto()
58
+ ELSE = auto()
59
+ END = auto()
60
+ ESCAPE = auto()
61
+ EXCEPT = auto()
62
+ EXCLUDE = auto()
63
+ EXCLUSIVE = auto()
64
+ EXISTS = auto()
65
+ EXPLAIN = auto()
66
+ FAIL = auto()
67
+ FILTER = auto()
68
+ FIRST = auto()
69
+ FOLLOWING = auto()
70
+ FOR = auto()
71
+ FOREIGN = auto()
72
+ FROM = auto()
73
+ FULL = auto()
74
+ GENERATED = auto()
75
+ GLOB = auto()
76
+ GROUP = auto()
77
+ GROUPS = auto()
78
+ HAVING = auto()
79
+ IF = auto()
80
+ IGNORE = auto()
81
+ IMMEDIATE = auto()
82
+ IN = auto()
83
+ INDEX = auto()
84
+ INDEXED = auto()
85
+ INITIALLY = auto()
86
+ INNER = auto()
87
+ INSERT = auto()
88
+ INSTEAD = auto()
89
+ INTERSECT = auto()
90
+ INTO = auto()
91
+ IS = auto()
92
+ ISNULL = auto()
93
+ JOIN = auto()
94
+ KEY = auto()
95
+ LAST = auto()
96
+ LEFT = auto()
97
+ LIKE = auto()
98
+ LIMIT = auto()
99
+ MATCH = auto()
100
+ MATERIALIZED = auto()
101
+ NATURAL = auto()
102
+ NO = auto()
103
+ NOT = auto()
104
+ NOTHING = auto()
105
+ NOTNULL = auto()
106
+ NULL = auto()
107
+ NULLS = auto()
108
+ OF = auto()
109
+ OFFSET = auto()
110
+ ON = auto()
111
+ OR = auto()
112
+ ORDER = auto()
113
+ OTHERS = auto()
114
+ OUTER = auto()
115
+ OVER = auto()
116
+ PARTITION = auto()
117
+ PLAN = auto()
118
+ PRAGMA = auto()
119
+ PRECEDING = auto()
120
+ PRIMARY = auto()
121
+ PRIMARY_KEY = auto() # Manually adding PRIMARY KEY
122
+ QUERY = auto()
123
+ RAISE = auto()
124
+ RANGE = auto()
125
+ RECURSIVE = auto()
126
+ REFERENCES = auto()
127
+ REGEXP = auto()
128
+ REINDEX = auto()
129
+ RELEASE = auto()
130
+ RENAME = auto()
131
+ REPLACE = auto()
132
+ RESTRICT = auto()
133
+ RETURNING = auto()
134
+ RIGHT = auto()
135
+ ROLLBACK = auto()
136
+ ROW = auto()
137
+ ROWS = auto()
138
+ SAVEPOINT = auto()
139
+ SELECT = auto()
140
+ SET = auto()
141
+ STORED = auto() # https://sqlite.org/gencol.html
142
+ TABLE = auto()
143
+ TEMP = auto()
144
+ TEMPORARY = auto()
145
+ THEN = auto()
146
+ TIES = auto()
147
+ TO = auto()
148
+ TRANSACTION = auto()
149
+ TRIGGER = auto()
150
+ UNBOUNDED = auto()
151
+ UNION = auto()
152
+ UNIQUE = auto()
153
+ UPDATE = auto()
154
+ USING = auto()
155
+ VACUUM = auto()
156
+ VALUES = auto()
157
+ VIEW = auto()
158
+ VIRTUAL = auto()
159
+ WHEN = auto()
160
+ WHERE = auto()
161
+ WINDOW = auto()
162
+ WITH = auto()
163
+ WITHOUT = auto()
164
+
165
+ INTEGER = auto()
166
+ TEXT = auto()
167
+ BLOB = auto()
168
+ REAL = auto()
169
+ NUMERIC = auto()
170
+ ANY = auto() # FOR STRICT TABLE
171
+
172
+ OLD = auto() # Added to support CreateTrigger Queries https://www.sqlite.org/lang_createtrigger.html
173
+
174
+
175
+ class SQL_FUNC(StrEnum):
176
+ # https://sqlite.org/lang_corefunc.html
177
+ ABS = auto()
178
+ CHANGES = auto()
179
+ CHAR = auto()
180
+ COALESCE = auto()
181
+ CONCAT = auto()
182
+ CONCAT_WS = auto()
183
+ FORMAT = auto()
184
+ GLOB = auto()
185
+ HEX = auto()
186
+ IF = auto()
187
+ IFNULL = auto()
188
+ IIF = auto()
189
+ INSTR = auto()
190
+ LAST_INSERT_ROWID = auto()
191
+ LENGTH = auto()
192
+ LIKE = auto()
193
+ LIKELIHOOD = auto()
194
+ LIKELY = auto()
195
+ LOAD_EXTENSION = auto()
196
+ LOWER = auto()
197
+ LTRIM = auto()
198
+ MAX = auto()
199
+ MIN = auto()
200
+ NULLIF = auto()
201
+ OCTET_LENGTH = auto()
202
+ PRINTF = auto()
203
+ QUOTE = auto()
204
+ RANDOM = auto()
205
+ RANDOMBLOB = auto()
206
+ REPLACE = auto()
207
+ ROUND = auto()
208
+ RTRIM = auto()
209
+ SIGN = auto()
210
+ SOUNDEX = auto()
211
+ SQLITE_COMPILEOPTION_GET = auto()
212
+ SQLITE_COMPILEOPTION_USED = auto()
213
+ SQLITE_OFFSET = auto()
214
+ SQLITE_SOURCE_ID = auto()
215
+ SQLITE_VERSION = auto()
216
+ SUBSTR = auto()
217
+ SUBSTRING = auto()
218
+ TOTAL_CHANGES = auto()
219
+ TRIM = auto()
220
+ TYPEOF = auto()
221
+ UNHEX = auto()
222
+ UNICODE = auto()
223
+ UNISTR = auto()
224
+ UNISTR_QUOTE = auto()
225
+ UNLIKELY = auto()
226
+ UPPER = auto()
227
+ ZEROBLOB = auto()
228
+
229
+ # https://sqlite.org/lang_aggfunc.html
230
+ AVG = auto()
231
+ GROUP_CONCAT = auto()
232
+ MEDIAN = auto()
233
+ PERCENTILE = auto()
234
+ PERCENTILE_CONT = auto()
235
+ PERCENTILE_DISC = auto()
236
+ STRING_AGG = auto()
237
+ SUM = auto()
238
+ TOTAL = auto()
239
+ COUNT = auto()
240
+
241
+ # https://sqlite.org/lang_datefunc.html
242
+ DATE = auto()
243
+ TIME = auto()
244
+ DATETIME = auto()
245
+ JULIANDAY = auto()
246
+ UNIXEPOCH = auto()
247
+ STRFTIME = auto()
248
+ TIMEDIFF = auto()
249
+
250
+ INTEGER = auto()
251
+ TEXT = auto()
252
+ BLOB = auto()
253
+ REAL = auto()
254
+ NUMERIC = auto()
255
+ ANY = auto() # FOR STRICT TABLE
256
+
257
+
258
+ class SQL_TYPES(StrEnum):
259
+ # https://sqlite.org/datatype3.html
260
+ # https://sqlite.org/stricttables.html
261
+ INTEGER = auto()
262
+ TEXT = auto()
263
+ BLOB = auto()
264
+ REAL = auto()
265
+ NUMERIC = auto()
266
+ ANY = auto() # FOR STRICT TABLE
267
+
268
+
269
+ class SQL_TYPES_EXTENDED(UpperStrEnum):
270
+ INTEGER = auto()
271
+ TEXT = auto()
272
+ BLOB = auto()
273
+ REAL = auto()
274
+ NUMERIC = auto()
275
+ ANY = auto() # FOR STRICT TABLE
276
+ INT = auto()
277
+ TINYINT = auto()
278
+ SMALLINT = auto()
279
+ MEDIUMINT = auto()
280
+ BIGINT = auto()
281
+ UNSIGNED_BIG_INT = auto()
282
+ INT2 = auto()
283
+ INT8 = auto()
284
+ CHARACTER = auto()
285
+ VARCHAR = auto()
286
+ VARYING_CHARACTER = auto()
287
+ NCHAR = auto()
288
+ NATIVE_CHARACTER = auto()
289
+ NVARCHAR = auto()
290
+ CLOB = auto()
291
+ DOUBLE = auto()
292
+ DOUBLE_PRECISION = auto()
293
+ FLOAT = auto()
294
+ DECIMAL = auto()
295
+ BOOLEAN = auto()
296
+ DATE = auto()
297
+ DATETIME = auto()
298
+
299
+
300
+ class SQL_FUNC_PRAGMA(StrEnum):
301
+ # https://sqlite.org/pragma.html
302
+ ANALYSIS_LIMIT = auto()
303
+ APPLICATION_ID = auto()
304
+ AUTO_VACUUM = auto()
305
+ AUTOMATIC_INDEX = auto()
306
+ BUSY_TIMEOUT = auto()
307
+ CACHE_SIZE = auto()
308
+ CACHE_SPILL = auto()
309
+ CELL_SIZE_CHECK = auto()
310
+ CHECKPOINT_FULLFSYNC = auto()
311
+ COLLATION_LIST = auto()
312
+ COMPILE_OPTIONS = auto()
313
+ DATA_VERSION = auto()
314
+ DATABASE_LIST = auto()
315
+ DEFER_FOREIGN_KEYS = auto()
316
+ ENCODING = auto()
317
+ FOREIGN_KEY_CHECK = auto()
318
+ FOREIGN_KEY_LIST = auto()
319
+ FOREIGN_KEYS = auto()
320
+ FREELIST_COUNT = auto()
321
+ FULLFSYNC = auto()
322
+ FUNCTION_LIST = auto()
323
+ HARD_HEAP_LIMIT = auto()
324
+ IGNORE_CHECK_CONSTRAINTS = auto()
325
+ INCREMENTAL_VACUUM = auto()
326
+ INDEX_INFO = auto()
327
+ INDEX_LIST = auto()
328
+ INDEX_XINFO = auto()
329
+ INTEGRITY_CHECK = auto()
330
+ JOURNAL_MODE = auto()
331
+ JOURNAL_SIZE_LIMIT = auto()
332
+ LEGACY_ALTER_TABLE = auto()
333
+ LEGACY_FILE_FORMAT = auto()
334
+ LOCKING_MODE = auto()
335
+ MAX_PAGE_COUNT = auto()
336
+ MMAP_SIZE = auto()
337
+ MODULE_LIST = auto()
338
+ OPTIMIZE = auto()
339
+ PAGE_COUNT = auto()
340
+ PAGE_SIZE = auto()
341
+ PRAGMA_LIST = auto()
342
+ QUERY_ONLY = auto()
343
+ QUICK_CHECK = auto()
344
+ READ_UNCOMMITTED = auto()
345
+ RECURSIVE_TRIGGERS = auto()
346
+ REVERSE_UNORDERED_SELECTS = auto()
347
+ SECURE_DELETE = auto()
348
+ SHRINK_MEMORY = auto()
349
+ SOFT_HEAP_LIMIT = auto()
350
+ SYNCHRONOUS = auto()
351
+ TABLE_INFO = auto()
352
+ TABLE_LIST = auto()
353
+ TABLE_XINFO = auto()
354
+ TEMP_STORE = auto()
355
+ THREADS = auto()
356
+ TRUSTED_SCHEMA = auto()
357
+ USER_VERSION = auto()
358
+ WAL_AUTOCHECKPOINT = auto()
359
+ WAL_CHECKPOINT = auto()
360
+
361
+
362
+ class SQL_FUNC_PRAGMA_EXTENDED(StrEnum):
363
+ # https://sqlite.org/pragma.html
364
+ # Non-Standard and testing commands
365
+ PARSER_TRACE = auto()
366
+ SCHEMA_VERSION = auto()
367
+ STATS = auto()
368
+ VDBE_ADDOPTRACE = auto()
369
+ VDBE_DEBUG = auto()
370
+ VDBE_LISTING = auto()
371
+ VDBE_TRACE = auto()
372
+ WRITABLE_SCHEMA = auto()
@@ -0,0 +1,282 @@
1
+ import logging
2
+ import sqlite3
3
+ from dataclasses import is_dataclass
4
+ from typing import Any, TypeVar
5
+
6
+ from dataclassdb.builders.query_builder import QueryBuilder
7
+ from dataclassdb.dataclass_sqlite_table import get_class_codec
8
+ from dataclassdb.constants import SQL
9
+
10
+ # from dataclassdb.types import T
11
+ from dataclassdb.dataclass_types import IsDataclass
12
+ from dataclassdb.utils import table_exists
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ DataclassT = TypeVar("DataclassT", bound=IsDataclass)
17
+
18
+
19
+ class DataclassDb(QueryBuilder):
20
+ primary_key_map: dict[Any, list[str]] = {}
21
+ unique_map: dict[Any, list[str]] = {}
22
+
23
+ def __init__(
24
+ self,
25
+ data_class: DataclassT,
26
+ connection: str | sqlite3.Connection,
27
+ table_name="",
28
+ ):
29
+ super().__init__(connection=connection)
30
+ if not is_dataclass(data_class):
31
+ raise TypeError(f"Class {data_class.__name__} is not a dataclass")
32
+ if not connection:
33
+ raise ConnectionError("Connection must be set.")
34
+ self.data_class = data_class
35
+ self.table_name = table_name if table_name else data_class.__name__
36
+ self.insert_query = None
37
+ self.primary_keys = []
38
+ self.unique = []
39
+ self.codec = get_class_codec(data_class)
40
+ self.connection_name = ""
41
+ for name, _field in self.codec.class_fields.items():
42
+ if _field.primary_key:
43
+ self.primary_keys.append(name)
44
+ if _field.unique:
45
+ self.unique.append(name)
46
+ self.create_or_update_table()
47
+
48
+ def __contains__(self, key) -> bool:
49
+ try:
50
+ query = (
51
+ QueryBuilder()
52
+ .SELECT(1)
53
+ .FROM(self.table_name)
54
+ .WHERE(self.key_match_str())
55
+ .LIMIT(1)
56
+ )
57
+ result = self.execute_one(
58
+ *self.parse_key(key), sql_str=str(query), as_dict=False
59
+ )
60
+ return result is not None
61
+ finally:
62
+ self.clear
63
+
64
+ def __setitem__(self, key, value) -> DataclassT:
65
+ self.insert(value)
66
+
67
+ def key_match_str(self):
68
+ return " AND ".join([f"{key} = ?" for key in self.primary_keys])
69
+
70
+ def get_current_table_query(self) -> str:
71
+ row = (
72
+ QueryBuilder(self.connection)
73
+ .SELECT("sql")
74
+ .FROM("sqlite_master")
75
+ .WHERE("type")
76
+ .eq.quote("table")
77
+ .AND("name")
78
+ .eq.quote(self.table_name)
79
+ .end.execute_one(as_dict=False)
80
+ )
81
+ return row[0] if row else ""
82
+
83
+ def parse_key(self, key):
84
+ if not isinstance(key, tuple):
85
+ if isinstance(key, list):
86
+ key = tuple(key)
87
+ else:
88
+ key = (key,)
89
+ if len(self.primary_keys) != len(key):
90
+ raise KeyError(
91
+ "Input of (%s) does not match length of primary key (%s)",
92
+ key,
93
+ self.primary_keys,
94
+ )
95
+ return key
96
+
97
+ def insert(self, item: DataclassT):
98
+ params = self.codec.encode(item, as_tuple=False, ignore_none=True)
99
+
100
+ field_names = params.keys()
101
+ query = (
102
+ QueryBuilder()
103
+ .INSERT.INTO(self.table_name)
104
+ .par(*field_names)
105
+ .br.VALUES.placeholders(*field_names)
106
+ )
107
+ if self.primary_keys:
108
+ if len(self.primary_keys) < len(self.codec.class_fields):
109
+ (
110
+ query.br.ON.CONFLICT.par(*self.primary_keys).br.DO.UPDATE.SET(
111
+ *[
112
+ f"{col} = excluded.{col}"
113
+ for col in field_names
114
+ if col not in self.primary_keys
115
+ # and col not in self.codec.on_conflict_ignore
116
+ ]
117
+ )
118
+ )
119
+ else:
120
+ query.ON.CONFLICT.par(*self.primary_keys).DO.NOTHING
121
+ query.br.RETURNING(*self.primary_keys)
122
+ else:
123
+ query.br.RETURNING("rowid")
124
+ return self.execute_one(*params.values(), sql_str=query)
125
+
126
+ def get(
127
+ self, key, select_fields: list[str] = None, as_dict=False, as_tuple=False
128
+ ) -> DataclassT:
129
+ if self.primary_keys == []:
130
+ return self.peek(key, select_fields, as_dict, as_tuple)
131
+ if as_dict and as_tuple:
132
+ raise ValueError("as_dict and as _tuple cannot both be true")
133
+ if select_fields and not as_dict and not as_tuple:
134
+ raise ValueError(
135
+ "If select fields are provided either as_dict or as_tuple must be true."
136
+ )
137
+ if not select_fields:
138
+ field_str = "*"
139
+ else:
140
+ field_str = ", ".join(select_fields)
141
+ query = (
142
+ QueryBuilder()
143
+ .SELECT(field_str)
144
+ .FROM(self.table_name)
145
+ .WHERE(self.key_match_str())
146
+ )
147
+ row_dict: dict = self.execute_one(
148
+ *self.parse_key(key),
149
+ sql_str=str(query),
150
+ as_dict=True,
151
+ )
152
+ as_obj = not (as_tuple or as_dict)
153
+ decoded = self.codec.decode(row_dict, as_obj=as_obj)
154
+ if as_tuple:
155
+ return tuple(decoded.values())
156
+ else:
157
+ return decoded
158
+
159
+ def peek(
160
+ self, rowid=None, select_fields: list[str] = None, as_dict=False, as_tuple=False
161
+ ):
162
+ if as_dict and as_tuple:
163
+ raise ValueError("as_dict and as _tuple cannot both be true")
164
+ if select_fields and not as_dict and not as_tuple:
165
+ raise ValueError(
166
+ "If select fields are provided either as_dict or as_tuple must be true."
167
+ )
168
+ if not select_fields:
169
+ field_str = "*"
170
+ else:
171
+ field_str = ", ".join(select_fields)
172
+ query = QueryBuilder().SELECT(field_str).FROM(self.table_name)
173
+ if rowid is not None:
174
+ query.WHERE("rowid").eq("?")
175
+ query.ORDER.BY("rowid").DESC
176
+ if rowid:
177
+ row_dict: dict = self.execute_one(
178
+ rowid,
179
+ sql_str=str(query),
180
+ as_dict=True,
181
+ )
182
+ else:
183
+ row_dict: dict = self.execute_one(
184
+ sql_str=str(query),
185
+ as_dict=True,
186
+ )
187
+ as_obj = not (as_tuple or as_dict)
188
+ decoded = self.codec.decode(row_dict, as_obj=as_obj)
189
+ if as_tuple:
190
+ return tuple(decoded.values())
191
+ else:
192
+ return decoded
193
+
194
+ def delete(self, key) -> DataclassT:
195
+ query = QueryBuilder().DELETE.FROM(self.table_name).WHERE(self.key_match_str())
196
+ self.execute_one(*self.parse_key(key), sql_str=str(query))
197
+
198
+ def create_or_update_table(self):
199
+ logger.info("Creating or updating table %s", self.table_name)
200
+ if not table_exists(self.connection, self.table_name):
201
+ logger.info("Table %s does not exist. Creating table.", self.table_name)
202
+ query = QueryBuilder().add(self.sql_create_table())
203
+ self.execute_one(sql_str=str(query))
204
+ # query.execute_one()
205
+
206
+ elif self.get_curr_cols(self.data_class) != self.dataclass_sql_cols():
207
+ logger.info(
208
+ "Table %s does exist but does not match the dataclass. Attempting to update table..",
209
+ self.table_name,
210
+ )
211
+ fields_list = self.get_current_fields(self.table_name)
212
+
213
+ curr_fields = {f.name: f for f in fields_list}
214
+ dc_fields = self.codec.class_fields
215
+ overlapping_fields = []
216
+ for dc_field, dc_item in dc_fields.items():
217
+ if curr_fields.get(dc_field, None):
218
+ overlapping_fields.append(dc_field)
219
+ temp_table = f"temp_{self.table_name}"
220
+ logger.info(
221
+ "Attempting to update table on these overlapping fields",
222
+ self.table_name,
223
+ overlapping_fields,
224
+ )
225
+ if overlapping_fields:
226
+ query = (
227
+ QueryBuilder()
228
+ .BEGIN.IMMEDIATE.TRANSACTION.end.br(
229
+ self.sql_create_table(temp_table)
230
+ )
231
+ .br.INSERT.INTO(temp_table)
232
+ .par(*overlapping_fields)
233
+ .br.SELECT(*overlapping_fields)
234
+ .FROM(self.table_name)
235
+ .end.br.DROP.TABLE.IF.EXISTS(self.table_name)
236
+ .end.br.ALTER.TABLE(temp_table)
237
+ .RENAME.TO(self.table_name)
238
+ )
239
+ self.execute_script(sql_script=str(query))
240
+ else:
241
+ logger.warning(
242
+ "Table for dataclass %s has changed with no overlapping fields. Deleting table and creating a new one.",
243
+ self.data_class.__name__,
244
+ )
245
+ query = (
246
+ QueryBuilder()
247
+ .BEGIN.IMMEDIATE.TRANSACTION.end.br.DROP.TABLE.IF.EXISTS(
248
+ self.table_name
249
+ )
250
+ .end.br(self.sql_create_table())
251
+ .br
252
+ )
253
+ self.execute_script(sql_script=str(query))
254
+ logger.info(
255
+ "Table creation for %s successful.", self.table_name, overlapping_fields
256
+ )
257
+ else:
258
+ logger.info("Table has not changed.")
259
+
260
+ def sql_create_table(self, table_name=None) -> str:
261
+ table_name = table_name if table_name else self.table_name
262
+ cols_str = ",\n".join(self.dataclass_sql_cols())
263
+ return (
264
+ QueryBuilder()
265
+ .CREATE.TABLE.IF.NOT.EXISTS(table_name)
266
+ .par(f"\n{cols_str}\n")
267
+ .end
268
+ )
269
+
270
+ def dataclass_sql_cols(self):
271
+ """Returns a list of SQL columns used in CREATE TABLE"""
272
+ #
273
+ params = []
274
+ for name, f in self.codec.class_fields.items():
275
+ params.append(
276
+ f.sql_col_def(len(self.primary_keys) > 1, len(self.unique) > 1)
277
+ )
278
+ if len(self.primary_keys) > 1:
279
+ params.append(f"{SQL.PRIMARY_KEY}({', '.join(self.primary_keys)})")
280
+ if len(self.unique) > 1:
281
+ params.append(f"{SQL.UNIQUE}({', '.join(self.unique)})")
282
+ return params