# PamhyrDB.py -- Pamhyr abstract model database classes # Copyright (C) 2023-2025 INRAE # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -*- coding: utf-8 -*- import os import sqlite3 import logging from pathlib import Path from functools import reduce from SQL import SQL from Model.Except import NotImplementedMethodeError from tools import trace from Model.Tools.PamhyrID import PamhyrID logger = logging.getLogger() # Top level model class class SQLModel(SQL): _sub_classes = [] def _init_db_file(self, db, is_new=True): if self._db is not None: self._db.close() exists = Path(db).exists() if exists and is_new: os.remove(db) self._db = sqlite3.connect(db, check_same_thread=False) self._cur = self._db.cursor() if is_new: self._create() # Create db # self._save() # Save else: self._update() # Update db scheme if necessary # self._load() # Load data def __init__(self, filename=None): self._db = None def _create_submodel(self): def fn(sql): return self.execute( sql, fetch_one=False, commit=False ) for cls in self._sub_classes: requests = cls._db_create(fn) self.commit() return True def _create(self): raise NotImplementedMethodeError(self, self._create) def _update_submodel(self, version, data=None): def fn(sql): return self.execute( sql, fetch_one=False, commit=False ) ok = True for cls in self._sub_classes: ok &= cls._db_update(fn, version, data=data) self.commit() return ok def _update(self): raise NotImplementedMethodeError(self, self._update) def _save_submodel(self, objs, data=None): progress = data if data is not None else lambda: None def fn(sql, *args, **kargs): if "fetch_one" not in kargs: kargs["fetch_one"] = False if "commit" not in kargs: kargs["commit"] = False res = self.execute( sql, *args, **kargs ) progress() return res ok = True for obj in objs: ok &= obj._db_save(fn, data={}) self.commit() return ok def _save(self, progress=None): raise NotImplementedMethodeError(self, self._save) def _count(self): raise NotImplementedMethodeError(self, self._count) def _save_count(self, objs, *args, data={}, **kargs): counter = { "insert": 0, "update": 0, "delete": 0, "other": 0, } def fn(sql, *args, **kargs): if "insert" in sql.lower(): counter["insert"] = counter["insert"] + 1 elif "update" in sql.lower(): counter["update"] = counter["update"] + 1 elif "delete" in sql.lower(): counter["delete"] = counter["delete"] + 1 else: counter["other"] = counter["other"] + 1 return [] ok = True for obj in objs: ok &= obj._db_save(fn, data=data) logger.debug(counter) return reduce( lambda acc, k: acc + counter[k], counter, 0 ) @classmethod def _load(cls, filename=None): raise NotImplementedMethodeError(cls, cls._load) # Sub model class class SQLSubModel(PamhyrID): _sub_classes = [] def __init__(self, id: int = -1, status=None, owner_scenario=-1, **kwargs): super(SQLSubModel, self).__init__(id=id, **kwargs) self._status = status # Deletion status of the model object. This status MUST be set # to True if an object that exists in a parent scenario is # deleted in current scenario. self._deleted = False # The 'owner_scenario' is the id of the scenario to which the # object belongs. This id CAN be different to current # scenario, but in case of object modification, this id MUST # be set to current scenario id. (This action is made in # 'modified' method.) self._owner_scenario = 0 if owner_scenario == -1: if status is not None: self._owner_scenario = self._status.scenario_id else: self._owner_scenario = owner_scenario def must_be_saved(self): """Return True if this object MUST be save in the save file. Returns: True if this object MUST be save, otherelse False """ return self._owner_scenario == self._status.scenario_id def modified(self): """Set study status to modified and update the object owner_scenario to current scenario Returns: Nothing """ if self._status is None: return if self._status.is_loading(): return self._owner_scenario = self._status.scenario_id self._status.modified() def set_owner_scenario(self): """Set study status the object owner_scenario to current scenario if predicate(scenario_id) is true Args: predicate: Function predicate on owner_scenario id Returns: Nothing """ if self._status is None: return if self._status.is_loading(): return self._owner_scenario = self._status.scenario_id self._status.modified() def is_deleted(self): """This object is deleted? Returns: True if this object is deleted, otherelse False """ return self._deleted def set_as_deleted(self): """Set object deleted flag to True. Returns: Nothing """ self._deleted = True self.modified() def set_as_not_deleted(self): """Set object deleted flag to False. Returns: Nothing """ self._deleted = False self.modified() def _db_format(self, value): # Replace ''' by ''' to preserve SQL injection if type(value) is str: value = value.replace("'", "'") elif type(value) is bool: value = 'TRUE' if value else 'FALSE' elif value is None: value = "NULL" return value @classmethod def _create_submodel(cls, execute): for sc in cls._sub_classes: sc._db_create(execute) @classmethod def _db_create(cls, execute): """Create data base scheme Args: execute: Function to exec SQL resquest Returns: Return true, otherelse false if an issue appear """ raise NotImplementedMethodeError(cls, cls._db_create) @classmethod def _update_submodel(cls, execute, version, data=None): ok = True for sc in cls._sub_classes: ok &= sc._db_update(execute, version, data) return ok @classmethod def _db_update(cls, execute, version, data=None): """Update data base scheme Args: execute: Function to exec SQL resquest version: Current database version Returns: Return true, otherelse false if an issue appear """ raise NotImplementedMethodeError(cls, cls._db_update) @classmethod def is_table_exists(cls, execute, table): return any( execute( "SELECT name FROM sqlite_master " + f"WHERE type='table' AND name='{table}'" ) ) @classmethod def _db_update_to_0_2_0_set_node_pid(cls, execute, table, nodes): els = execute( f"SELECT pamhyr_id, node FROM {table}" ) for row in els: it = iter(row) pid = next(it) node_id = next(it) if node_id == -1: continue if node_id not in nodes: # ⚠️ cas important : probablement déjà migré logger.warning( f"node_id {node_id} not in nodes " + "→ probably already migrated" ) continue execute( f"UPDATE {table} " + f"SET node = {nodes[node_id]} " + f"WHERE pamhyr_id = {pid}" ) @classmethod def _db_update_to_0_2_0_set_reach_pid(cls, execute, table, reachs): els = execute( f"SELECT pamhyr_id, reach FROM {table}" ) if els is None: return True for row in els: it = iter(row) pid = next(it) reach_id = next(it) if reach_id == -1: continue execute( f"UPDATE {table} " + f"SET reach = {reachs[reach_id]} " + f"WHERE pamhyr_id = {pid}" ) return True @classmethod def _db_update_to_0_1_1_assoc_section_from_rk( cls, execute, table, reach_column="reach", rk_column="rk", section_column="section", origin_version="0.1.0"): kid = "pamhyr_id" if origin_version == "0.0.*": kid = "id" els = execute( "SELECT " + f"{kid}, {reach_column}, {rk_column} " + f"FROM {table}" ) for row in els: it = iter(row) pid = next(it) reach_id = next(it) rk = next(it) if reach_id == -1 or reach_id is None: continue section_id = -1 section = execute( f"SELECT pamhyr_id FROM geometry_profileXYZ " + f"WHERE reach == {reach_id} AND rk == {rk}" ) if len(section) != 0: section_id = section[0][0] execute( f"UPDATE {table} " + f"SET {section_column} = {section_id} " + f"WHERE {kid} = {pid}" ) @classmethod def _db_load(cls, execute, data=None): """Load instance of this class from SQL data base Args: execute: Function to exec SQL request data: Optional data for the class constructor Returns: Return new instance of class """ raise NotImplementedMethodeError(cls, cls._db_load) def _save_submodel(self, execute, objs, data=None): ok = True for o in objs: ok &= o._db_save(execute, data=data) return ok def _db_save(self, execute, data=None): """Save class data to data base Args: execute: Function to exec SQL resquest data: Optional additional information for save Returns: Return true, otherelse false if an issue appear during save """ raise NotImplementedMethodeError(self, self._db_save) def _data_traversal(self, predicate=lambda obj, data: True, modifier=lambda obj, data: None, data={}): """Traversal data and execute modifier fonction if predicate true Args: predicate: Function predicate, take current obj and data as input modifier: Function modifier, take current obj and data as input Returns: Nothing """ if predicate(self, data): modifier(self, data)