Newer
Older
dom-persist / source / dom_persist.d
  1. module dom_persist;
  2.  
  3. //import std.algorithm; //https://dlang.org/phobos/std_algorithm_mutation.html
  4. //import std.exception : enforce;
  5.  
  6. import std.stdio;
  7. import std.file;
  8. import std.format;
  9. import std.array; //https://dlang.org/phobos/std_array.html
  10. import std.conv;
  11.  
  12.  
  13. import nodecode;
  14.  
  15. import d2sqlite3; // https://d2sqlite3.dpldocs.info/v1.0.0/d2sqlite3.database.Database.this.html
  16. // https://dlang-community.github.io/d2sqlite3/d2sqlite3.html
  17.  
  18.  
  19. bool db_exists( string sqlite_filename ){
  20. return sqlite_filename.exists;
  21. }
  22.  
  23. void db_drop( string sqlite_filename ){
  24. if( sqlite_filename.exists ){
  25. sqlite_filename.remove();
  26. }
  27. }
  28.  
  29. Database db_create( string sqlite_filename, int db_ver ){
  30. auto db = Database( sqlite_filename );
  31. db.run("CREATE TABLE \"Params\"(\"ID\" INTEGER, \"Name\" TEXT NOT NULL UNIQUE, \"Val\" TEXT, PRIMARY KEY(\"ID\" AUTOINCREMENT))");
  32. db.run("insert into params(Name, Val) values('DB_VERSION','"~to!string(db_ver)~"')");
  33. return db;
  34. }
  35.  
  36.  
  37. string get_openTag_commence( TreeNodeType nt, string e_data ){
  38. switch( nt ){
  39. case TreeNodeType.text:
  40. return e_data;
  41. case TreeNodeType.comment:
  42. return "<!--"~e_data;
  43. case TreeNodeType.docType:
  44. return "<DOCTYPE "~e_data;
  45.  
  46. default:
  47. return "<"~e_data;
  48. }
  49. }
  50.  
  51. string get_openTag_end( TreeNodeType nt, string e_data ){
  52.  
  53. switch( nt ){
  54. case TreeNodeType.comment:
  55. case TreeNodeType.text:
  56. return "";
  57. default:
  58. switch(e_data){
  59. case "input":
  60. case "br":
  61. return "/>";
  62. default:
  63. }
  64. return ">";
  65. }
  66. }
  67.  
  68. string get_closeTag( TreeNodeType nt, string e_data ){
  69.  
  70. switch( nt ){
  71. case TreeNodeType.docType:
  72. case TreeNodeType.text:
  73. return "";
  74. case TreeNodeType.comment:
  75. return "-->";
  76. default:
  77. switch(e_data){
  78. case "input":
  79. case "br":
  80. return "";
  81. default:
  82. }
  83.  
  84. return format("</%s>", e_data);
  85. }
  86. }
  87.  
  88. struct TreeNameID {
  89. long tree_id;
  90. string name;
  91. }
  92.  
  93.  
  94. /**
  95. * An instance of this class contains access to a single tree. Tree operations are cached in RAM and only written to
  96. * disk during a save operation. This can be done safely because of the single user access to Sqlite.
  97. *
  98. * Multiple database connections should work on the same thread provided each is using a different tree.
  99. *
  100. * Instantiation of the tree involves only one database select.
  101. */
  102. class Tree_Db {
  103.  
  104.  
  105. protected:
  106. long tree_id;
  107. Database* db;
  108. long nnid = 0;
  109. TreeNode[long] all_nodes; //https://dlang.org/spec/hash-map.html
  110.  
  111. long getNextNodeId(){
  112. nnid -= 1;
  113. return nnid;
  114. }
  115. // end protected
  116.  
  117. private:
  118.  
  119. this( Database* db, long tid, string tree_name = null ){
  120. this.db = db;
  121. if(tid==0){
  122. //new tree
  123. tree_id = getNextNodeId();
  124. TreeNode tn = new TreeNode(
  125. this,
  126. NodeData( tree_id, tree_name, 0, TreeNodeType.tree)
  127. );
  128. this.all_nodes[ tree_id ] = tn;
  129. return;
  130. }
  131. tree_id = tid;
  132. //load the tree in one hit using the tree_id
  133. //also order by the parent_id so that we know all siblings are grouped together
  134. //and then by child order
  135. auto results = db.execute( format("select ID, e_data, p_id, t_id from doctree where tree_id=%d or id=%d order by p_id,c_order", tid, tid) );
  136. foreach (row; results){
  137. long id = row.peek!long(0);
  138. long p_id = row.peek!long(2);
  139. TreeNode tn = new TreeNode( this, NodeData(
  140. id,
  141. row.peek!string(1),
  142. p_id,
  143. getTreeNodeType( row.peek!int(3) )
  144. ));
  145. all_nodes[id] = tn;
  146. if(p_id==0) continue;
  147. all_nodes[p_id].child_nodes ~= all_nodes[id];
  148. }
  149. }
  150. // end private
  151.  
  152. public:
  153. static void db_create_schema( ref Database db ){
  154. db.run("CREATE TABLE IF NOT EXISTS doctree (ID INTEGER, e_data TEXT,p_id INTEGER,t_id INTEGER NOT NULL,tree_id INTEGER NOT NULL, c_order INTEGER, PRIMARY KEY( ID AUTOINCREMENT))");
  155. }
  156.  
  157. /**
  158. * Return a list of all trees in the database with their ID's and names.
  159. */
  160. static TreeNameID[] getTreeList( ref Database db ){
  161.  
  162. TreeNameID[] tree_list;
  163. auto results = db.execute( format("select ID, e_data from doctree where p_id=0") );
  164. foreach (row; results){
  165. tree_list ~= TreeNameID (
  166. row.peek!long(0),
  167. row.peek!string(1)
  168. );
  169. }
  170. return tree_list;
  171. }
  172. /**
  173. * Create a new tree in RAM. No database activity takes place at this time.
  174. */
  175. static Tree_Db createTree( ref Database db, string tree_name ){
  176. return new Tree_Db( &db, 0, tree_name );
  177. }
  178.  
  179. /**
  180. * Load a tree into RAM from database given the tree ID.
  181. */
  182. static Tree_Db loadTree( ref Database db, long tree_id ){
  183. return new Tree_Db( &db, tree_id );
  184. }
  185. /**
  186. * Returns the root node of this tree. This is special node holds the tree name and ID and is not
  187. * usually part of the document but rather the parent container.
  188. */
  189. TreeNode getTreeNode(){
  190. return all_nodes[tree_id];
  191. }
  192.  
  193. /**
  194. * Return any node given its ID.
  195. *
  196. * Note about IDs:
  197. * positive IDs indicate that the record exists on the database and the ID is the database ID of that node.
  198. * negative IDs indicate that the record is a new one existing in RAM only until such times as 'flush' is called.
  199. *
  200. */
  201. TreeNode getTreeNodeById( long id ){
  202. TreeNode* ps = id in all_nodes;
  203. if(ps) return all_nodes[id];
  204. debug_out("getTreeNodeById: did not find key ", id );
  205. return null;
  206. }
  207.  
  208. /**
  209. * Return the tree as html text. This is the cached tree with all changes and not from the database.
  210. */
  211. string getTreeAsText( ){
  212. return getTreeAsText_r( all_nodes[tree_id] );
  213. }
  214.  
  215.  
  216. /**
  217. * Create and insert a new child into the parent node (p_node) at the position (pos) indicated.
  218. */
  219. TreeNode insertChild( TreeNode p_node, TreeNodeType n_type, string e_data, int pos ){
  220. /* Algorithm:
  221. add into the correct place in ram, assign a new id <=-1.
  222. An ID is required to add to the map. Using negative IDs indicates that it is not a DB ID.
  223. Mark the TreeNode parent as dirty indicating that children need adding and re-ordering
  224. flush: insert (into db) all nodes first then set c_order column for those with dirty parents(!)
  225. */
  226. long id = getNextNodeId();
  227. TreeNode tn = new TreeNode(
  228. this,
  229. NodeData( id, e_data, p_node.node_data.ID, n_type)
  230. );
  231. this.all_nodes[ id ] = tn;
  232. p_node.child_nodes.insertInPlace( pos, this.all_nodes[ id ]);
  233. p_node.dirty = true;
  234. return tn;
  235. }
  236.  
  237. /**
  238. * Move the given node (nodeToMove) to the new parent (nodeDestParent) inserting at position pos.
  239. * Currently only works for node relocation within the same tree.
  240. *
  241. */
  242. void moveNode( TreeNode nodeToMove, TreeNode nodeDestParent, int pos ){
  243. /* Algorithm:
  244. Move the node and all children into new parent (same parent also works)
  245. Mark old parent TreeNode as dirty indicating a re-order is necessary
  246. Mark new parent TreeNode as dirty indicating a re-order is necessary
  247. update parent id of moved child
  248. flush: locate the node using it's original id except if negative
  249. update the db with the new ID and set orig_id=0;
  250. */
  251. // 1st remove node
  252. auto old_p = nodeToMove.parentNode();
  253. if(nodeDestParent == old_p){
  254. // same parent, nothing to do except move in-place and re-order
  255. old_p.cutChild( nodeToMove );
  256. old_p.child_nodes.insertInPlace( pos, nodeToMove );
  257. old_p.dirty = true;
  258. return;
  259. }
  260. old_p.cutChild( nodeToMove );
  261. // add node to new parent
  262. nodeDestParent.child_nodes.insertInPlace( pos, nodeToMove );
  263. nodeDestParent.dirty = true;
  264. nodeToMove.node_data.pid = nodeDestParent.node_data.ID;
  265. nodeToMove.node_data.dirty = true; //dirty data will suffice for storing pid too
  266. }
  267.  
  268. /*
  269. Delete node (branch)
  270. If the id<=-1, then it was a new node, unsaved, can be removed entirely
  271. Otherwise, move the node and all children into a delete-map.
  272. Mark the TreeNode parent as dirty indicating that children need removing. Re-ordering is
  273. not required but may be advantageous
  274. flush: Delete entries using the delete-map and clear the map.
  275. */
  276.  
  277. /**
  278. * Save all edits to the tree to the database. After this call, the database values will be in sync
  279. * with the memory tree.
  280. */
  281. void flush(){
  282. // nodes must be written in document order so that a valid database ID is always available as a parent
  283. //ID for subsequent child writes. This is because the child update will write
  284.  
  285. debug_out("flush():");
  286.  
  287. long new_tree = tree_id<0;
  288.  
  289. DocOrderIterator it = new DocOrderIterator( all_nodes[tree_id] );
  290. TreeNode tnode;
  291. while( (tnode=it.nextNode) !is null ){
  292. NodeData* nd = &tnode.node_data;
  293. debug_out("next node:", *nd );
  294. //first we check the ID<0 which implies it is a new node
  295. if( nd.ID<0 ){
  296.  
  297. if(new_tree) tree_id=0;
  298. db.run( format("insert into doctree(e_data, p_id, t_id, tree_id ) values( '%s', %d, %d, %d )", nd.e_data, nd.pid, nd.type, tree_id ) );
  299.  
  300. //update the node id
  301. long oldid = tnode.node_data.ID;
  302. long nnid = db.lastInsertRowid;
  303. long newID = tnode.setNodeId( nnid );
  304. all_nodes[nnid] = tnode;
  305. all_nodes.remove( oldid );
  306. if(new_tree) tree_id = newID;
  307. }else if( nd.dirty ){
  308. //dirty data and possibly pid too
  309. db.run( format("update doctree set e_data='%s', p_id=%d where id=%d", nd.e_data, nd.pid, nd.ID ) );
  310. nd.dirty = false;
  311. }
  312. }
  313.  
  314. //Re-enumerate any children that have shifted positions (or are new) according to their array positions
  315. it.reset();
  316. while( (tnode=it.nextNode) !is null ){
  317. if( tnode.dirty ){
  318. foreach( i, cNode; tnode.child_nodes){
  319. db.run( format("update doctree set c_order=%d where id=%d", i, cNode.node_data.ID ) );
  320. }
  321. tnode.dirty = false;
  322. }
  323. }
  324. }
  325.  
  326. protected:
  327. string getTreeAsText_r( ref TreeNode tn ){
  328. string strRtn = "";
  329.  
  330. TreeNode[] children = tn.child_nodes;
  331. foreach( child; children){
  332. NodeData nd = child.node_data;
  333. strRtn ~= get_openTag_commence( nd.type, nd.e_data );
  334. // --> add attributes if required
  335. strRtn ~= get_openTag_end( nd.type, nd.e_data );
  336. strRtn ~= getTreeAsText_r( child );
  337. strRtn ~= get_closeTag( nd.type, nd.e_data );
  338. }
  339. return strRtn;
  340. }
  341. }
  342.  
  343.  
  344. /**
  345. * Iterator starting at any given TreeNode, traversing each of the descendent nodes, depth first and increasing child index.
  346. */
  347. class DocOrderIterator {
  348. TreeNode start_node;
  349. TreeNode next_node;
  350. this( TreeNode n ){
  351. start_node = n;
  352. next_node = n;
  353. }
  354. void reset(){
  355. next_node = start_node;
  356. }
  357. /**
  358. * The initial TreeNode is the first node to be returned.
  359. */
  360. TreeNode nextNode(){
  361. //the node we will return this time
  362. TreeNode rtnNode = next_node;
  363. if(rtnNode is null) return null;
  364. //now work out the node for the next call
  365. if( rtnNode.hasChildNodes() ){
  366. next_node = rtnNode.firstChild();
  367. return rtnNode;
  368. }
  369. TreeNode anc_node = rtnNode;
  370. while( anc_node !is null && anc_node.nextSibling() is null){
  371. anc_node = anc_node.parentNode();
  372. if( anc_node == start_node ){
  373. anc_node=null;
  374. break;
  375. }
  376. }
  377. if(anc_node is null) {
  378. next_node=null;
  379. return rtnNode;
  380. }
  381. next_node = anc_node.nextSibling();
  382. return rtnNode;
  383. }
  384. }
  385.  
  386. void removeAt(T)( ref T[] t, int pos ){
  387. // tests on a 10000 item array
  388.  
  389. //I really don't like these re-assignments but the ref makes it stay in-place
  390. // by far the fastest
  391. // 15290 cpu cycles
  392. t = t[0..pos] ~ t[pos+1..$];
  393. // my implementation cpu 116100 cycles
  394. /*for( int i=pos; i<t.length-1; i++){
  395. t[i] = t[i+1];
  396. }
  397. t.length -= 1;
  398. */
  399.  
  400. // by far the slowest 154000 cpu cycles
  401. //t = t.remove(pos);
  402.  
  403. }
  404.  
  405.  
  406. bool bDebug_out = false;
  407. void debug_out(){ writeln(); }
  408.  
  409. void debug_out(T, A...)(T t, A a){
  410. if(!bDebug_out) return;
  411. import std.stdio;
  412. write(t);
  413. debug_out(a);
  414. }
  415.