From 6c9481296b2aa8c48ccdc0719cc7f26e9cad624a Mon Sep 17 00:00:00 2001
From: Pierre-Antoine Rouby <pierre-antoine.rouby@inrae.fr>
Date: Wed, 20 Sep 2023 15:21:46 +0200
Subject: [PATCH] doc: dev: Add SQL section with examples.

---
 doc/dev/documentation.org | 339 ++++++++++++++++++++++++++++----------
 1 file changed, 254 insertions(+), 85 deletions(-)

diff --git a/doc/dev/documentation.org b/doc/dev/documentation.org
index e245b959..56b45032 100644
--- a/doc/dev/documentation.org
+++ b/doc/dev/documentation.org
@@ -139,101 +139,270 @@ https://doc.qt.io/qt-5/model-view-programming.html
 
 ** TODO Model
 
-- Model de donnée Python
-- Correspond à une sauvegarde SQL
+The model is a set of Python classes. In Pamhyr2, this classes must
+respect some constraint. Each model class must inherits
+=Model.Tools.SQLSubModel= abstract class, except the =Model.Study=
+class who inherits =Model.Tools.SQLModel= (see [[SQL]]).
+
+The model entry point is the Study class. It contains infomation about
+the study: their name, description, time system, and so on. Their
+contains a River object too. This river object inherits the network
+graph and contains a list of =RiverNode= and a list of =RiverReach=
+(an edge who contains a source node, and destination node).
+=RiverReach= contrains geometry, so, the river network (node and edge)
+associated with the geometry forms the basis of the model, and the
+other components are linked to one of these basic components.
 
 #+name: graph-model
 #+header: :results drawer
 #+header: :exports results
-#+header: :post attr_wrap(width="16cm", data=*this*, name="graph-model", caption="Pamhyr2 model class dependencies", float="t")
+#+header: :post attr_wrap(width="16cm", data=*this*, name="graph-model", caption="Pamhyr2 model class dependencies (A -> B means A can contain references to B)", float="t")
 #+begin_src dot :file "images/graph-model.png" :cache no
   digraph {
-        bgcolor="transparent";
-        node[colorscheme=set19,shape=box,style="filled",fillcolor="2"];
-
-        study[label="Study"];
-        river[label="River"];
-
-        subgraph cluster00 {
-            label="Network"
-            rnode[label="RiverNode"];
-            redge[label="RiverReach"];
-        }
-
-        frictionlist[label="FrictionList"];
-
-        subgraph cluster01 {
-            label="Stricklers";
-            stricklers[label="Stricklers"];
-            stricklerslist[label="StricklersList"];
-        }
-        subgraph cluster02 {
-            label="BoundaryCondition";
-            boundaryconditionlist[label="BoundaryConditionList"];
-            boundarycondition[label="BoundaryCondition"];
-        }
-        subgraph cluster03 {
-            label="LateralContribution";
-            lateralcontributionlist[label="LateralContributionList"];
-            lateralcontribution[label="LateralContribution"];
-        }
-        subgraph cluster04 {
-            label="InitialConditions";
-            initialconditionsdict[label="InitialConditionsDict"];
-            initialconditions[label="InitialConditions"];
-        }
-
-        solverparameterslist[label="SolverParametersList"];
-
-        subgraph cluster05 {
-            label="Sediment";
-            sedimentlayerlist[label="SedimentLayerList"];
-            sedimentlayer[label="SedimentLayer"];
-            layer[label="Layer"];
-        }
-
-        subgraph cluster06 {
-            label="Greometry"
-            georeach[label="Reach"];
-            geocrosssection[label="Cross-section"];
-            geopoint[label="Point"];
-        }
+      bgcolor="transparent";
+      node[colorscheme=set19,shape=box,style="filled",fillcolor="2"];
+
+      subgraph cluster0 {
+          style=dashed;
+          study[label="Study"];
+          river[label="River"];
+
+          subgraph cluster00 {
+              style=solid;
+              label="Network"
+              rnode[label="RiverNode"];
+              redge[label="RiverReach"];
+          }
+
+          subgraph cluster06 {
+              style=solid;
+              label="Greometry"
+              georeach[label="Reach"];
+              geocrosssection[label="Cross-section"];
+              geopoint[label="Point"];
+          }
+      }
+
+      //subgraph cluster1 {
+      //    style=dashed;
+          frictionlist[label="FrictionList"];
+
+          subgraph cluster01 {
+              style=solid;
+              label="Stricklers";
+              stricklers[label="Stricklers"];
+              stricklerslist[label="StricklersList"];
+          }
+          subgraph cluster02 {
+              style=solid;
+              label="BoundaryCondition";
+              boundaryconditionlist[label="BoundaryConditionList"];
+              boundarycondition[label="BoundaryCondition"];
+          }
+          subgraph cluster03 {
+              style=solid;
+              label="LateralContribution";
+              lateralcontributionlist[label="LateralContributionList"];
+              lateralcontribution[label="LateralContribution"];
+          }
+          subgraph cluster04 {
+              style=solid;
+              label="InitialConditions";
+              initialconditionsdict[label="InitialConditionsDict"];
+              initialconditions[label="InitialConditions"];
+          }
+
+          solverparameterslist[label="SolverParametersList"];
+
+          subgraph cluster05 {
+              style=solid;
+              label="Sediment";
+              sedimentlayerlist[label="SedimentLayerList"];
+              sedimentlayer[label="SedimentLayer"];
+              layer[label="Layer"];
+          }
+      //}
+
+      subgraph cluster2 {
+          style=dashed;
+          label="Results"
+          results[label="Results"]
+          rriver[label="River"];
+          rreach[label="Reach"];
+          rcrosssection[label="Cross-section"];
+      }
+
+      study -> river;
+      river -> rnode;
+      river -> redge;
+      redge -> rnode;
+      river -> boundaryconditionlist -> boundarycondition -> rnode;
+      river -> lateralcontributionlist -> lateralcontribution -> redge;
+      river -> initialconditionsdict -> initialconditions;
+      initialconditions -> redge;
+      river -> stricklerslist -> stricklers;
+      river -> solverparameterslist;
+      river -> sedimentlayerlist -> sedimentlayer -> layer;
+      redge -> frictionlist -> stricklers;
+      redge -> georeach -> geocrosssection -> geopoint;
+      geocrosssection -> sedimentlayer;
+      geopoint -> sedimentlayer;
+
+      results -> study;
+      results -> rriver;
+      rriver -> river;
+      rriver -> rreach;
+      rreach -> georeach;
+      rreach -> rcrosssection;
+      rcrosssection -> geocrosssection;
+  }
+#+end_src
 
-        subgraph cluster07 {
-            label="Results"
-            results[label="Results"]
-            rriver[label="River"];
-            rreach[label="Reach"];
-            rcrosssection[label="Cross-section"];
-        }
+*** TODO SQL
+
+The model must be export to a database file to create a study save
+file. This file use SQLite3[fn:sqlite] format and the extention
+=.pamhyr=. So, each model componante must be register into this study
+file. To create, update, set and get information into SQLite database
+we use SQL command. The database use version number and some
+modification could be perform to update database. For each model
+componante, correspond one or more SQL table to store information. To
+normalize the interaction with database we made two classes, SQLModel
+and SQLSubModel. The Study class use SQLModel because is the top of
+the model hierachy. The rest of model class inherits to SQLSubModel.
+
+A class who inherits SQLSubModel, must implement some methods:
+- =_sql_create=: Class method to create the database scheme
+- =_sql_update=: Class method to update the database scheme if necessary
+- =_sql_load=: Class method to load data from DB
+- =_sql_save=: Method to save current object into DB
+
+Class method take in arguments: The class (=cls=), a function to
+execute SQL command into the database (=execute=). In addition, the
+update method take the previous version of database, load method take
+an optional arguments =data= if additional infomation ar needed, and
+who can contains whatever you want. The method save take in arguments
+the current object (=self=), a function to execute SQL command into
+the database (=execute=), and optional data (=data=).
+
+The class who inherits SQLSubModel can also define an class attribute
+=_sub_classes= to set a formal class dependencies into database. This
+attribute is use at database creation to create all table, and at
+update to update all the database table. Let see examples of
+SQLSubModel usage for two classes Foo and Bar with Foo contains list
+of Bar (Listing [[sql-bar]] and [[sql-foo]]).
+
+#+NAME: sql-bar
+#+CAPTION: Exemple of class Bar inherits SQLSubModel.
+#+begin_src python :python python3 :results output :noweb yes
+  from Model.Tools.PamhyrDB import SQLSubModel
+
+  class Bar(SQLSubModel):
+      _id_cnt = 0
+      def __init__(self, id = -1, x = 0, y = 0):
+          self._x = x
+          self._y = y
+          if id == -1:
+              self.id = Bar._id_cnt + 1
+          else:
+              self.id = id
+          Bar._id_cnt = max(id, Bar._id_cnt+1)
+
+      @classmethod
+      def _sql_create(cls, execute):
+          execute("""
+            CREATE TABLE bar (
+              id INTEGER NOT NULL PRIMARY KEY,
+              x INTEGER NOT NULL,
+              foo_id INTEGER NOT NULL,
+              FOREIGN KEY(foo_id) REFERENCES foo(id),
+            )""")
+          return True
+
+      @classmethod
+      def _sql_update(cls, execute, version):
+          # If version is lesser than 0.0.2, add column to bar table
+          major, minor, release = version.strip().split(".")
+          if major == minor == "0":
+              if int(release) < 2:
+                  execute("ALTER TABLE bar ADD COLUMN y INTEGER")
+          return True
+
+      @classmethod
+      def _sql_load(cls, execute, data = None):
+          new = []
+          table = execute(
+              f"SELECT id, x, y FROM bar WHERE foo_id = {data['id']}"
+          )
+          for row in table:
+              bar = cls(
+                  id = row[0], x = row[1], y = row[2],
+              )
+              new.append(bar)
+          return new
+
+      def _sql_save(self, execute, data = None):
+          execute("INSERT INTO bar (id,x,y,foo_id) VALUES " +
+                  f"({self.id}, {self._x}, {self._y}, {data['id']})")
+#+end_src
 
-        study -> river;
-        river -> rnode;
-        river -> redge;
-        redge -> rnode;
-        river -> boundaryconditionlist -> boundarycondition -> rnode;
-        river -> lateralcontributionlist -> lateralcontribution -> redge;
-        river -> initialconditionsdict -> initialconditions;
-        initialconditions -> redge;
-        river -> stricklerslist -> stricklers;
-        river -> solverparameterslist;
-        river -> sedimentlayerlist -> sedimentlayer -> layer;
-        redge -> frictionlist -> stricklers;
-        redge -> georeach -> geocrosssection -> geopoint;
-        geocrosssection -> sedimentlayer;
-        geopoint -> sedimentlayer;
-
-        results -> study[style="dashed"];
-        results -> rriver;
-        rriver -> river[style="dashed"];
-        rriver -> rreach;
-        rreach -> georeach[style="dashed"];
-        rreach -> rcrosssection;
-        rcrosssection -> geocrosssection[style="dashed"];
-  }
+#+NAME: sql-foo
+#+CAPTION: Exemple of class Foo inherits SQLSubModel.
+#+begin_src python :python python3 :results output :noweb yes
+  class Foo(SQLSubModel):
+      _id_cnt = 0
+      _sub_classes = [Bar]
+
+      def __init__(self, id = -1, name = ""):
+          self._name = name
+          self._bar = []
+          # ...
+
+      @classmethod
+      def _sql_create(cls, execute):
+          execute("""
+            CREATE TABLE foo (
+              id INTEGER NOT NULL PRIMARY KEY,
+              name TEXT NOT NULL,
+            )
+          """)
+          return cls._create_submodel(execute)
+
+      @classmethod
+      def _sql_update(cls, excute, version):
+          return cls._update_submodel(execute, version)
+
+      @classmethod
+      def _sql_load(cls, execute, data = None):
+          new = []
+          table = execute(
+              "SELECT id, name FROM foo"
+          )
+          for row in table:
+              foo = cls(
+                  id = row[0],
+                  name = row[1],
+              )
+              data = {
+                  "id": row[0],     # Current Foo ID
+              }
+              foo._bar = Bar._sql_load(execute, data=data)
+              new.append(foo)
+          return new
+
+      def _sql_save(self, execute, data = None):
+          execute(f"DELETE FROM foo WHERE id = {self.id}")
+          execute(f"DELETE FROM bar WHERE foo_id = {self.id}")
+          # Save new data
+          execute(f"INSERT INTO bar (id,name) VALUES ({self.id}, {self._name})")
+          data = {"id": self.id}
+          for bar in self._bar:
+              bar._sql_save(execute, data=data)
 #+end_src
 
-*** TODO SQL study file
+[fn:sqlite] The SQLite web site: https://www.sqlite.org/index.html
+(last access 2023-09-20)
+
 *** TODO List class
 *** TODO Dict class
 ** TODO View
-- 
GitLab