Coverage for src / mafw / db / db_model.py: 91%

216 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-30 16:10 +0000

1# Copyright 2025–2026 European Union 

2# Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu) 

3# SPDX-License-Identifier: EUPL-1.2 

4""" 

5The module provides functionality to MAFw to interface to a DB. 

6""" 

7 

8import warnings 

9from collections.abc import Mapping, Sequence 

10from itertools import tee 

11from typing import TYPE_CHECKING, Any, Iterable, cast 

12 

13# mypy is complaining that peewee does not have make_snake_case. 

14from peewee import ( # type: ignore[attr-defined] 

15 EXCLUDED, 

16 SQL, 

17 Database, 

18 DatabaseProxy, 

19 Field, 

20 ForeignKeyField, 

21 ModelBase, 

22 ModelInsert, 

23 MySQLDatabase, 

24 Select, 

25 Value, 

26 fn, 

27 make_snake_case, 

28) 

29from playhouse.shortcuts import ThreadSafeDatabaseMetadata, dict_to_model, model_to_dict, update_model_from_dict 

30 

31# noinspection PyUnresolvedReferences 

32from playhouse.signals import Model 

33 

34from mafw.db import trigger 

35from mafw.db.db_types import PeeweeModelWithMeta 

36from mafw.db.fields import FileNameField 

37from mafw.db.model_register import ModelRegister 

38from mafw.db.trigger import Trigger 

39from mafw.mafw_errors import MAFwException, UnsupportedDatabaseError 

40 

41database_proxy = DatabaseProxy() 

42"""This is a placeholder for the real database object that will be known only at run time""" 

43 

44 

45mafw_model_register = ModelRegister() 

46""" 

47This is the instance of the ModelRegister 

48 

49 .. seealso:: 

50  

51 :class:`.ModelRegister` for more information on how to retrieve models and :class:`.RegisteredMeta` and :class:`MAFwBaseModel` for the automatic registration of models` 

52 

53""" 

54 

55 

56class RegisteredMeta(ModelBase): 

57 """ 

58 Metaclass for registering models with the MAFw model registry. 

59 

60 This metaclass automatically registers model classes with the global model registry 

61 when they are defined, allowing for dynamic discovery and management of database models. 

62 It ensures that only concrete model classes (not the base classes themselves) are registered. 

63 

64 The registration process uses the table name from the model's metadata or generates 

65 a snake_case version of the class name if no explicit table name is set. 

66 """ 

67 

68 reserved_field_names = ['__logic__', '__conditional__'] 

69 

70 def __new__(cls, name: str, bases: tuple[type, ...], attrs: dict[str, Any], **kwargs: dict[str, Any]) -> type: 

71 """ 

72 Create a new model class and register it if applicable. 

73 

74 This method is called during class definition to create the actual class 

75 and register it with the MAFw model registry if it's a concrete model class. 

76 

77 :param name: The name of the class being created. 

78 :type name: str 

79 :param bases: The base classes of the class being created. 

80 :type bases: tuple 

81 :param attrs: The attributes of the class being created. 

82 :type attrs: dict 

83 :param kwargs: Other keyword attributes passed to the class. 

84 :type kwargs: dict[str, Any] 

85 :return: The newly created class. 

86 :rtype: type 

87 """ 

88 # store here any extra argument that is passed to the class defition. 

89 extra_args = kwargs 

90 

91 meta = attrs.get('Meta', None) 

92 if meta: 

93 custom_table_name = meta.__dict__.get('table_name', None) 

94 else: 

95 custom_table_name = None 

96 model = cast(RegisteredMeta, super().__new__(cls, name, bases, attrs)) # type: ignore[no-untyped-call] 

97 

98 if TYPE_CHECKING: 

99 assert hasattr(model, '_meta') 

100 

101 # for consistency, add the extra_args to the meta instance 

102 model._meta.extra_args = extra_args 

103 

104 # check for the use of reserved field names. 

105 field_names = model._meta.sorted_field_names 

106 if any([reserved_name in field_names for reserved_name in RegisteredMeta.reserved_field_names]): 

107 warnings.warn( 

108 f'Model {model.__name__} is using a reserved field name. Filter configuration might not ' 

109 f'work properly. Reserved names are f{RegisteredMeta.reserved_field_names}.' 

110 ) 

111 

112 # this super call is actually creating the Model class according to peewee ModelBase (that is a metaclass) 

113 # the model class will have an attribute (_meta) created by the ModelBase using the inner class Meta as a template. 

114 # The additional attributes (like suffix, prefix, ...) that we have added in the MAFwBaseModel Meta class will be 

115 # available. There is one problem, that the _meta.table_name is generated with our make_prefixed_suffixed_table_name 

116 # but before that the additional attributes are actually transferred to the instance of the meta class. 

117 # for this reason we need to regenerate the table_name now, when the whole Meta class is available. 

118 # We do so only if the user didn't specify a custom table name 

119 if custom_table_name is None: 

120 if model._meta.table_function is None: 

121 table_name = make_snake_case(model.__name__) 

122 else: 

123 table_name = model._meta.table_function(model) 

124 model._meta.table_name = table_name 

125 

126 # Do we need to register the Model? 

127 # By default we want to register (so skip_registration = False) 

128 skip_registration = extra_args.get('do_not_register', False) 

129 if skip_registration: 

130 # no need to register, so just skip the registration. 

131 return model 

132 

133 # Register only concrete model classes that are not base classes 

134 # Check that we're not processing base classes themselves 

135 is_base_class = name in ['Model', 'MAFwBaseModel', 'StandardTable'] 

136 

137 # Check that we have valid bases and that at least one inherits from Model 

138 has_valid_bases = bases and any(issubclass(base, Model) for base in bases) 

139 

140 if not is_base_class and has_valid_bases: 

141 table_name = model._meta.table_name 

142 mafw_model_register.register_model(table_name, model) 

143 

144 if hasattr(model._meta, 'suffix'): 144 ↛ 147line 144 didn't jump to line 147 because the condition on line 144 was always true

145 mafw_model_register.register_suffix(model._meta.suffix) 

146 

147 if hasattr(model._meta, 'prefix'): 147 ↛ 150line 147 didn't jump to line 150 because the condition on line 147 was always true

148 mafw_model_register.register_prefix(model._meta.prefix) 

149 

150 return model 

151 

152 

153class MAFwBaseModelDoesNotExist(MAFwException): 

154 """Raised when the base model class is not existing.""" 

155 

156 

157def get_db(model_class: PeeweeModelWithMeta) -> Database: 

158 """ 

159 Extract the actual database instance from a model class. 

160 

161 This function retrieves the database object from a model class's metadata, 

162 handling the case where the database is a :class:`peewee.DatabaseProxy`. If the 

163 database is a proxy, it extracts the underlying database object. 

164 

165 :param model_class: The model class from which to extract the database. 

166 :type model_class: PeeweeModelWithMeta 

167 :return: The actual database instance. 

168 :rtype: Database 

169 """ 

170 db = model_class._meta.database 

171 if isinstance(db, DatabaseProxy): 171 ↛ 172line 171 didn't jump to line 172 because the condition on line 171 was never true

172 db = db.obj 

173 return cast(Database, db) 

174 

175 

176def get_primary_key_fields(model_class: PeeweeModelWithMeta) -> tuple[list[str], list[Field]]: 

177 """ 

178 Extract primary key field names and field objects from a model metadata class. 

179 

180 This function retrieves the primary key information from a Peewee model metadata 

181 class, handling both single-column primary keys and composite (multi-column) keys. 

182 It returns two parallel lists: the first containing the field names as strings, 

183 and the second containing the corresponding field objects. 

184 

185 :param model_class: The model class from which to extract primary key information. 

186 :type model_class: RegisteredMeta 

187 :return: A tuple containing two lists: the first with primary key field names (as strings) 

188 and the second with the corresponding field objects. 

189 :rtype: tuple[list[str], list[Field]] 

190 """ 

191 if model_class._meta.composite_key: 

192 pk_fields = [model_class._meta.fields[name] for name in model_class._meta.primary_key.field_names] 

193 else: 

194 pk_fields = [model_class._meta.primary_key] 

195 

196 return [f.name for f in pk_fields], pk_fields 

197 

198 

199def make_prefixed_suffixed_name(model_class: RegisteredMeta) -> str: 

200 """ 

201 Generate a table name with optional prefix and suffix for a given model class. 

202 

203 This function constructs a table name by combining the prefix, the snake_case 

204 version of the model class name, and the suffix. If either prefix or suffix 

205 are not defined in the model's metadata, empty strings are used instead. 

206 

207 The prefix, table name, and suffix are joined using underscores. For example: 

208 

209 - If a model class is named "UserAccount" with prefix="app", suffix="data", 

210 the resulting table name will be "app_user_account_data" 

211 

212 - If a model class is named "Product" with prefix="ecommerce", suffix="_latest", 

213 the resulting table name will be "ecommerce_product_latest" 

214 

215 .. note:: 

216 

217 Underscores (_) will be automatically added to prefix and suffix if not already present. 

218 

219 :param model_class: The model class for which to generate the table name. 

220 :type model_class: RegisteredMeta 

221 :return: The constructed table name including prefix and suffix if applicable. 

222 :rtype: str 

223 """ 

224 if TYPE_CHECKING: 

225 assert hasattr(model_class, '_meta') 

226 

227 if hasattr(model_class._meta, 'suffix') and model_class._meta.suffix is not None: 

228 suffix = model_class._meta.suffix 

229 else: 

230 suffix = '' 

231 

232 if hasattr(model_class._meta, 'prefix') and model_class._meta.prefix is not None: 

233 prefix = model_class._meta.prefix 

234 else: 

235 prefix = '' 

236 

237 if not suffix.startswith('_') and suffix != '': 

238 suffix = '_' + suffix 

239 

240 if not prefix.endswith('_') and prefix != '': 

241 prefix = prefix + '_' 

242 

243 return f'{prefix}{make_snake_case(model_class.__name__)}{suffix}' 

244 

245 

246class MAFwBaseModel(Model, metaclass=RegisteredMeta): 

247 """The base model for the MAFw library. 

248 

249 Every model class (table) that the user wants to interface must inherit from this base. 

250 

251 This class extends peewee's Model with several additional features: 

252 

253 1. Automatic model registration: Models are automatically registered with the MAFw model registry 

254 during class definition, enabling dynamic discovery and management of database models. 

255 

256 2. Trigger support: The class supports defining database triggers through the :meth:`.triggers` method, 

257 which are automatically created when the table is created. File removal triggers can also be automatically 

258 generated using the `file_trigger_auto_create` boolean flag in the :ref:`meta class <auto_triggers>`. See also 

259 :meth:`.file_removal_triggers`. 

260 

261 3. Standard upsert operations: Provides :meth:`.std_upsert` and :meth:`.std_upsert_many` methods for 

262 performing upsert operations that work with SQLite and PostgreSQL. 

263 

264 4. Dictionary conversion utilities: Includes :meth:`.to_dict`, :meth:`.from_dict`, and :meth:`.update_from_dict` 

265 methods for easy serialization and deserialization of model instances. 

266 

267 5. Customizable table naming: Supports table name prefixes and suffixes through the Meta class 

268 with `prefix` and `suffix` attributes. See :func:`.make_prefixed_suffixed_name`. 

269 

270 6. Automatic table creation control: The `automatic_creation` Meta attribute controls whether 

271 tables are automatically created when the application starts. 

272 

273 .. note:: 

274 

275 The automatic model registration can be disabled for one single model class using the keyword argument 

276 `do_not_register` passed to the :class:`.RegisteredMeta` meta-class. For example: 

277 

278 .. code-block:: python 

279 

280 class AutoRegisterModel(MAFwBaseModel): 

281 pass 

282 

283 

284 class NoRegisterModel(MAFwBaseModel, do_not_register=True): 

285 pass 

286 

287 the first class will be automatically registered, while the second one will not. This is particularly useful if 

288 the user wants to define a base model class for the whole project without having it in the register where 

289 only concrete Model implementations are stored. 

290 

291 """ 

292 

293 @classmethod 

294 def get_fields_by_type(cls, field_type: type[Field]) -> dict[str, Field]: 

295 """ 

296 Return a dict {field_name: field_object} for all fields of the given type. 

297 

298 .. versionadded:: v2.0.0 

299 

300 :param field_type: Field type 

301 :type field_type: peewee.Field 

302 :return: A dict {field_name: field_object} for all fields of the given type. 

303 :rtype: dict[str, peewee.Field] 

304 """ 

305 if TYPE_CHECKING: 

306 assert hasattr(cls, '_meta') 

307 

308 return {name: field for name, field in cls._meta.fields.items() if isinstance(field, field_type)} 

309 

310 @classmethod 

311 def file_removal_triggers(cls) -> list[Trigger]: 

312 """ 

313 Generate a list of triggers for automatic file removal when records are deleted. 

314 

315 This method creates database triggers that automatically handle file cleanup when 

316 records containing :class:`~mafw.db.fields.FileNameField` fields are removed from 

317 the database table. The triggers insert the filenames and checksums into the 

318 :class:`~mafw.db.std_tables.OrphanFile` table for later processing. 

319 

320 The triggers are only created if the model has at least one field of type 

321 :class:`~mafw.db.fields.FileNameField`. If no such fields exist, an empty list 

322 is returned. 

323 

324 :class:`.FileNameListField` is a subclass of :class:`.FileNameField` and is treated in the same 

325 way. 

326 

327 .. versionadded:: v2.0.0 

328 

329 .. note:: 

330 

331 This functionality requires the ``file_trigger_auto_create`` attribute in the 

332 model's Meta class to be set to ``True`` for automatic trigger creation. 

333 

334 :return: A list containing the trigger object for file removal, or an empty list 

335 if no :class:`~mafw.db.fields.FileNameField` fields are found. 

336 :rtype: list[:class:`~mafw.db.trigger.Trigger`] 

337 """ 

338 from mafw.db.std_tables import OrphanFile, TriggerStatus 

339 

340 # it includes also FileNameListField that is a subclass of FileNameField 

341 file_fields = cls.get_fields_by_type(FileNameField) 

342 

343 if len(file_fields) == 0: 

344 return [] 

345 

346 if TYPE_CHECKING: 

347 assert hasattr(cls, '_meta') 

348 

349 new_trigger = Trigger( 

350 trigger_name=cls._meta.table_name + '_delete_files', 

351 trigger_type=(trigger.TriggerWhen.Before, trigger.TriggerAction.Delete), 

352 source_table=cls, 

353 safe=True, 

354 for_each_row=True, 

355 ) 

356 sub_query = TriggerStatus.select(TriggerStatus.status).where(TriggerStatus.trigger_type == 'DELETE_FILES') 

357 trigger_condition = Value(1) == sub_query 

358 new_trigger.add_when(trigger_condition) 

359 data = [] 

360 for f in file_fields: 

361 c = cast(FileNameField, file_fields[f]).checksum_field or f 

362 # the checksum is not really used in the OrphanFile. 

363 # in versions before 2.0.0, the checksum field in OrphanFile was not nullable, 

364 # so it should contain something. In order to be backward compatible, in case of a missing checksum field 

365 # we will just use the filename 

366 # note that the automatic filename to checksum conversion will not work in this case because the trigger 

367 # lives in the database and not in the application. 

368 data.append({'filenames': SQL(f'OLD.{f}'), 'checksum': SQL(f'OLD.{c}')}) 

369 insert_query = OrphanFile.insert_many(data) 

370 new_trigger.add_sql(insert_query) 

371 

372 return [new_trigger] 

373 

374 @classmethod 

375 def triggers(cls) -> list[Trigger]: 

376 """ 

377 Returns an iterable of :class:`~mafw.db.trigger.Trigger` objects to create upon table creation. 

378 

379 The user must overload this returning all the triggers that must be created along with this class. 

380 """ 

381 return [] 

382 

383 # noinspection PyUnresolvedReferences 

384 @classmethod 

385 def create_table(cls, safe: bool = True, **options: Any) -> None: 

386 """ 

387 Create the table in the underlying DB and all the related trigger as well. 

388 

389 If the creation of a trigger fails, then the whole table dropped, and the original exception is re-raised. 

390 

391 .. warning:: 

392 

393 Trigger creation has been extensively tested with :link:`SQLite`, but not with the other database implementation. 

394 Please report any malfunction. 

395 

396 :param safe: Flag to add an IF NOT EXISTS to the creation statement. Defaults to True. 

397 :type safe: bool, Optional 

398 :param options: Additional options passed to the super method. 

399 """ 

400 super().create_table(safe, **options) 

401 

402 # this is just use to make mypy happy. 

403 meta_cls = cast(PeeweeModelWithMeta, cls) 

404 

405 # Get the database instance, it is used for trigger creation 

406 db = meta_cls._meta.database 

407 

408 triggers_list = cls.triggers() 

409 

410 if meta_cls._meta.file_trigger_auto_create: 

411 triggers_list.extend(cls.file_removal_triggers()) 

412 

413 if len(triggers_list): 

414 # Create tables with appropriate error handling 

415 try: 

416 for trigger in triggers_list: 

417 trigger.set_database(db) 

418 try: 

419 db.execute_sql(trigger.create()) 

420 except UnsupportedDatabaseError as e: 

421 warnings.warn(f'Skipping unsupported trigger {trigger.trigger_name}: {str(e)}') 

422 except Exception: 

423 raise 

424 except: 

425 # If an error occurs, drop the table and any created triggers 

426 meta_cls._meta.database.drop_tables([cls], safe=safe) 

427 for trigger in triggers_list: 

428 try: 

429 db.execute_sql(trigger.drop(True)) 

430 except Exception: 

431 pass # Ignore errors when dropping triggers during cleanup 

432 raise 

433 

434 @classmethod 

435 def std_upsert(cls, __data: dict[str, Any] | None = None, **mapping: Any) -> ModelInsert: 

436 """ 

437 Perform a so-called standard upsert. 

438 

439 An upsert statement is not part of the standard SQL and different databases have different ways to implement it. 

440 This method will work for the three DB backends generating a valid query for the three dialects. 

441 

442 An upsert is a statement in which we try to insert some data in a table where there are some constraints. 

443 If one constraint is failing, then instead of inserting a new row, we will try to update the existing row 

444 causing the constraint violation. 

445 

446 A standard upsert, in the naming convention of MAFw, is setting the conflict cause to the primary key with all 

447 other fields being updated. In other words, the database will try to insert the data provided in the table, but 

448 if the primary key already exists, then all other fields will be updated. 

449 

450 This method is equivalent to the following: 

451 

452 .. code-block:: python 

453 

454 class Sample(MAFwBaseModel): 

455 sample_id = AutoField( 

456 primary_key=True, 

457 help_text='The sample id primary key', 

458 ) 

459 sample_name = TextField(help_text='The sample name') 

460 

461 

462 ( 

463 Sample.insert(sample_id=1, sample_name='my_sample') 

464 .on_conflict( 

465 preserve=[Sample.sample_name] 

466 ) # use the value we would have inserted 

467 .execute() 

468 ) 

469 

470 :param __data: A dictionary containing the key/value pair for the insert. The key is the column name. 

471 Defaults to None 

472 :type __data: dict, Optional 

473 :param mapping: Keyword arguments representing the value to be inserted. 

474 

475 """ 

476 

477 meta_cls = cast('PeeweeModelWithMeta', cls) 

478 db = get_db(meta_cls) 

479 

480 # Normalize input 

481 if __data is not None: 

482 data = dict(__data) 

483 data.update(mapping) 

484 else: 

485 data = dict(mapping) 

486 

487 # --- PK detection --- 

488 pk_names, pk_fields = get_primary_key_fields(meta_cls) 

489 

490 # --- Build insert --- 

491 insert_query = cls.insert(data) 

492 

493 # --- Backend-specific update mapping --- 

494 if isinstance(db, MySQLDatabase): 494 ↛ 496line 494 didn't jump to line 496 because the condition on line 494 was never true

495 # MySQL: VALUES(col) 

496 update_mapping = { 

497 meta_cls._meta.fields[name]: fn.VALUES(meta_cls._meta.fields[name]) 

498 for name in data 

499 if name not in pk_names 

500 } 

501 q = insert_query.on_conflict(update=update_mapping) 

502 return q 

503 

504 else: 

505 # PostgreSQL / SQLite: EXCLUDED.col 

506 update_mapping = { 

507 meta_cls._meta.fields[name]: getattr(EXCLUDED, name) for name in data if name not in pk_names 

508 } 

509 

510 q = insert_query.on_conflict(conflict_target=pk_fields, update=update_mapping) 

511 return q 

512 

513 # noinspection PyProtectedMember 

514 @classmethod 

515 def std_upsert_many(cls, rows: Iterable[Any], fields: list[str] | None = None) -> ModelInsert: 

516 """ 

517 Perform a standard upsert with many rows. 

518 

519 .. seealso:: 

520 

521 Read the :meth:`std_upsert` documentation for an explanation of this method. 

522 

523 :param rows: A list with the rows to be inserted. Each item can be a dictionary or a tuple of values. If a 

524 tuple is provided, then the `fields` must be provided. 

525 :type rows: Iterable 

526 :param fields: A list of field names. Defaults to None. 

527 :type fields: list[str], Optional 

528 """ 

529 meta_cls = cast(PeeweeModelWithMeta, cls) 

530 db = get_db(meta_cls) 

531 

532 # Initialise the output iterator and field names list. 

533 # rows_to_insert will hold the actual data to insert (possibly split from rows for inspection) 

534 # insert_field_names will hold the column names (either provided or inferred from first row) 

535 rows_to_insert: Iterable[Any] = rows 

536 insert_field_names: list[str] = list(fields) if fields is not None else [] 

537 

538 # If fields are not explicitly provided, we need to infer them from the input data. 

539 # This branch handles the case where rows is a Sequence (list, tuple) that supports indexing. 

540 if fields is None: 

541 if isinstance(rows, Sequence) and not isinstance(rows, (str, bytes)): 541 ↛ 552line 541 didn't jump to line 552 because the condition on line 541 was always true

542 # Sequence types (list, tuple) allow direct indexing to inspect the first row. 

543 if len(rows) > 0: 543 ↛ 560line 543 didn't jump to line 560 because the condition on line 543 was always true

544 first_row = rows[0] 

545 # If the first row is a dictionary (Mapping), extract its keys as field names. 

546 if isinstance(first_row, Mapping): 546 ↛ 560line 546 didn't jump to line 560 because the condition on line 546 was always true

547 insert_field_names = list(first_row.keys()) 

548 else: 

549 # For non-Sequence iterables (generators, etc.), we need to use tee() to inspect 

550 # the first element without consuming the iterator. This creates two iterators: 

551 # rows_inspect for looking at the first row, rows_to_insert for actual insertion. 

552 rows_inspect, rows_to_insert = tee(rows) 

553 first_row = next(rows_inspect, None) 

554 if isinstance(first_row, Mapping): 

555 insert_field_names = list(first_row.keys()) 

556 

557 # Retrieve primary key field information. pk_names is used to exclude PK fields from 

558 # the update operation (PK should not be updated during upsert), while pk_fields is 

559 # used as the conflict target for the database. 

560 pk_names, pk_fields = get_primary_key_fields(meta_cls) 

561 

562 # Build the list of fields to update during the upsert. 

563 # We need to: 

564 # 1. Remove duplicates (seen_names set) 

565 # 2. Exclude primary key fields (they're used for conflict detection, not update) 

566 # 3. Verify the field actually exists in the model (field_obj may be None) 

567 seen_names: set[str] = set() 

568 update_fields: list[tuple[Field, str]] = [] 

569 for field_name in insert_field_names: 

570 # Skip duplicate field names 

571 if field_name in seen_names: 571 ↛ 572line 571 didn't jump to line 572 because the condition on line 571 was never true

572 continue 

573 seen_names.add(field_name) 

574 # Skip primary key fields - they identify the row, not data to update 

575 if field_name in pk_names: 

576 continue 

577 # Get the actual field object from the model metadata 

578 field_obj = meta_cls._meta.fields.get(field_name) 

579 if field_obj is None: 579 ↛ 580line 579 didn't jump to line 580 because the condition on line 579 was never true

580 continue 

581 update_fields.append((field_obj, field_name)) 

582 

583 # Create the bulk insert query using insert_many 

584 insert_query = cls.insert_many(rows_to_insert, fields) 

585 

586 # Define a helper for MySQL's VALUES() syntax. 

587 # NOTE: VALUES() is deprecated in MySQL 8.0.20+, but still required by Peewee 

588 def _mysql_value_expression(field: Field) -> Any: 

589 return fn.VALUES(field) 

590 

591 # Handle database-specific on_conflict syntax: 

592 # - MySQL uses VALUES(field_name) to reference the values being inserted 

593 # - PostgreSQL and SQLite use EXCLUDED.field_name to reference rejected values 

594 if isinstance(db, MySQLDatabase): 594 ↛ 595line 594 didn't jump to line 595 because the condition on line 594 was never true

595 update_mapping = {field: _mysql_value_expression(field) for field, _ in update_fields} 

596 return insert_query.on_conflict(update=update_mapping) 

597 

598 # For PostgreSQL/SQLite, use EXCLUDED table to reference the rejected insert values 

599 update_mapping = {field: getattr(EXCLUDED, field_name) for field, field_name in update_fields} 

600 return insert_query.on_conflict( 

601 conflict_target=pk_fields, 

602 update=update_mapping, 

603 ) 

604 

605 def to_dict( 

606 self, 

607 recurse: bool = True, 

608 backrefs: bool = False, 

609 only: list[str] | None = None, 

610 exclude: list[str] | None = None, 

611 **kwargs: Any, 

612 ) -> dict[str, Any]: 

613 """ 

614 Convert model instance to dictionary with optional parameters 

615 

616 See full documentation directly on the `peewee documentation 

617 <https://docs.peewee-orm.com/en/latest/peewee/playhouse.html#dict_to_model>`__. 

618 

619 :param recurse: If to recurse through foreign keys. Default to True. 

620 :type recurse: bool, Optional 

621 :param backrefs: If to include backrefs. Default to False. 

622 :type backrefs: bool, Optional 

623 :param only: A list of fields to be included. Defaults to None. 

624 :type only: list[str], Optional 

625 :param exclude: A list of fields to be excluded. Defaults to None. 

626 :type exclude: list[str], Optional 

627 :param kwargs: Other keyword arguments to be passed to peewee `playhouse shortcut <https://docs.peewee-orm.com/en/latest/peewee/playhouse.html#dict_to_model>`__. 

628 :return: A dictionary containing the key/value of the model. 

629 :rtype: dict[str, Any] 

630 """ 

631 # the playhouse module of peewee is not typed. 

632 return model_to_dict( # type: ignore[no-any-return] 

633 self, 

634 recurse=recurse, 

635 backrefs=backrefs, # type: ignore[no-untyped-call] 

636 only=only, 

637 exclude=exclude, 

638 **kwargs, 

639 ) 

640 

641 @classmethod 

642 def from_dict(cls, data: dict[str, Any], ignore_unknown: bool = False) -> 'MAFwBaseModel': 

643 """ 

644 Create a new model instance from dictionary 

645 

646 :param data: The dictionary containing the key/value pairs of the model. 

647 :type data: dict[str, Any] 

648 :param ignore_unknown: If unknown dictionary keys should be ignored. 

649 :type ignore_unknown: bool 

650 :return: A new model instance. 

651 :rtype: MAFwBaseModel 

652 """ 

653 # the playhouse module of peewee is not typed. 

654 return dict_to_model(cls, data, ignore_unknown=ignore_unknown) # type: ignore[no-untyped-call,no-any-return] 

655 

656 def update_from_dict(self, data: dict[str, Any], ignore_unknown: bool = False) -> 'MAFwBaseModel': 

657 """ 

658 Update current model instance from dictionary 

659 

660 The model instance is returned for daisy-chaining. 

661 

662 :param data: The dictionary containing the key/value pairs of the model. 

663 :type data: dict[str, Any] 

664 :param ignore_unknown: If unknown dictionary keys should be ignored. 

665 :type ignore_unknown: bool 

666 """ 

667 update_model_from_dict(self, data, ignore_unknown=ignore_unknown) # type: ignore[no-untyped-call] 

668 return self 

669 

670 @classmethod 

671 def join_dependent_models(cls, base_query: Select | None = None) -> Select: 

672 """Return a query that joins every model referenced through foreign keys. 

673 

674 Each foreign-key relation defined on ``cls`` is joined and attached to the 

675 resulting rows via an attribute named ``_<field name>`` so that callers have 

676 the same convenient access that manual ``.join_from`` calls would have 

677 provided. 

678 

679 :param base_query: Optional query to extend. If not provided a new ``Select`` 

680 is created that includes ``cls`` and the dependent models. 

681 :type base_query: :class:`peewee.Select`, optional 

682 :return: The query joining every dependent model. 

683 :rtype: :class:`peewee.Select` 

684 """ 

685 if TYPE_CHECKING: 

686 assert hasattr(cls, '_meta') 

687 

688 foreign_keys = [field for field in cls._meta.fields.values() if isinstance(field, ForeignKeyField)] 

689 

690 if len(foreign_keys) == 0: 

691 return base_query or cls.select(cls) 

692 

693 dependent_models: list[type[ModelBase]] = [] 

694 for fk in foreign_keys: 

695 rel_model = fk.rel_model 

696 if rel_model not in dependent_models: 696 ↛ 694line 696 didn't jump to line 694 because the condition on line 696 was always true

697 dependent_models.append(rel_model) 

698 

699 query: Select = base_query or cls.select(cls, *dependent_models) 

700 

701 for idx, fk in enumerate(foreign_keys): 

702 query = query.join(fk.rel_model, attr=f'_{fk.name}') # type: ignore[call-arg] 

703 if idx != len(foreign_keys) - 1: 

704 query = query.switch(cls) # type: ignore[attr-defined] 

705 return query 

706 

707 class Meta: 

708 """The metadata container for the Model class""" 

709 

710 database = database_proxy 

711 """The reference database. A proxy is used as a placeholder that will be automatically replaced by the real  

712 instance of the database at runtime.""" 

713 

714 model_metadata_class = ThreadSafeDatabaseMetadata 

715 """Thread-safe metadata storage to allow runtime database binding across threads.""" 

716 

717 legacy_table_names = False 

718 """ 

719 Set the default table name as the snake case of the Model camel case name. 

720  

721 So for example, a model named ThisIsMyTable will corresponds to a database table named this_is_my_table. 

722 """ 

723 

724 suffix = '' 

725 """ 

726 Set the value to append to the table name.  

727 """ 

728 

729 prefix = '' 

730 """ 

731 Set the value to prepend to the table name.  

732 """ 

733 

734 table_function = make_prefixed_suffixed_name 

735 """ 

736 Set the table naming function. 

737 """ 

738 

739 automatic_creation = True 

740 """ 

741 Whether the table linked to the model should be created automatically 

742 """ 

743 

744 file_trigger_auto_create = False 

745 """ 

746 Whether to automatically create triggers to delete files once a row with a FilenameField is removed 

747 """