From: Benjamin Auder Date: Fri, 2 Feb 2018 17:10:09 +0000 (+0100) Subject: First draft of drawMld() + fillSql() X-Git-Url: https://git.auder.net/doc/html/pieces/img/vendor/%7B%7B%20pkg.url%20%7D%7D?a=commitdiff_plain;h=19addd10d9f37cc081779b9ec17f2f98ee161889;p=erdiag.git First draft of drawMld() + fillSql() --- diff --git a/README.md b/README.md index acf706e..b39f8a7 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,9 @@ Inspired by [this repository](https://code.google.com/archive/p/merisier/). This parser reads ER diagrams definition files, and produces two types of diagrams + SQL code. [Graphviz](https://www.graphviz.org/) is used on server side to translate parsed graph descriptions into SVG objects. -*Note:* at the moment only the conceptual graph is implemented, and no comments are allowed in textual descriptions. -At least the former is planned, and also probably a way to indicate relative identifiers, and maybe links between relationships. +*TODO list:* functional integrity constraints (CIF), inter-relations constraints (or, and, xor...), inheritance with the right symbol (triangle). -*Note bis:* temporary dependency to [underscore](http://underscorejs.org/); good library but used so far only for its shuffle() method. +*Note:* temporary dependency to [underscore](http://underscorejs.org/); good library but used so far only for its shuffle() method. ----- @@ -37,6 +36,8 @@ Cardinality dictionary: * 1 = 1..1 * ? = 0..1 +Special cardinalities are also available to indicate relative identification: `?R` and `1R`. + To mark a weak entity, just surround its name by extra-brackets [[WeakEntity]] diff --git a/parser.js b/parser.js index 0d52ee0..f718f31 100644 --- a/parser.js +++ b/parser.js @@ -6,19 +6,15 @@ class ErDiags this.entities = { }; this.inheritances = [ ]; this.associations = [ ]; - this.txt2json(description); + this.tables = { }; + this.mcdParsing(description); + this.mldParsing(); // Cache SVG graphs returned by server (in addition to server cache = good perfs) this.mcdGraph = ""; this.mldGraph = ""; this.sqlText = ""; } - static get TYPES() - { - // SQLite storage classes without null - return ["integer","real","text","blob"]; - } - static get CARDINAL() { return { @@ -31,12 +27,12 @@ class ErDiags }; } - ////////////////// - // PARSING STAGE 1 - ////////////////// + /////////////////////////////// + // PARSING STAGE 1: text to MCD + /////////////////////////////// // Parse a textual description into a json object - txt2json(text) + mcdParsing(text) { let lines = text.split("\n"); lines.push(""); //easier parsing: always empty line at the end @@ -108,14 +104,8 @@ class ErDiags if (parenthesis !== null) { let sqlClues = parenthesis[1]; - let qualifiers = sqlClues; - let firstWord = sqlClues.match(/[^\s]+/)[0]; - if (ErDiags.TYPES.includes(firstWord)) - { - field.type = firstWord; - qualifiers = sqlClues.substring(firstWord.length).trim(); - } - field.qualifiers = qualifiers; + field.type = sqlClues.match(/[^\s]+/)[0]; //type is always the first indication (mandatory) + field.qualifiers = sqlClues.substring(field.type.length).trim(); } attributes.push(field); } @@ -165,9 +155,95 @@ class ErDiags return assoce; } - ////////////////// - // PARSING STAGE 2 - ////////////////// + ////////////////////////////// + // PARSING STAGE 2: MCD to MLD + ////////////////////////////// + + // From entities + relationships to tables + mldParsing() + { + // Pass 1: initialize tables + Object.keys(this.entities).forEach( name => { + let newTable = [ ]; //array of fields + this.entities[name].attributes.forEach( attr => { + newTable.push({ + name: attr.name, + type: attr.type, + isKey: attr.isKey, + qualifiers: attr.qualifiers, + }); + }); + this.tables[name] = newTable; + }); + // Pass 2: parse associations, add foreign keys when cardinality is 0,1 or 1,1 + this.associations.forEach( a => { + let newTableAttrs = [ ]; + a.entities.forEach( e => { + if (['?','1'].includes(e.card[0])) + { + // Foreign key apparition (for each entity in association minus current one, for each identifying attribute) + a.entities.forEach( e2 => { + if (e2.name == e.name) + return; + e2.attributes.forEach( attr => { + if (attr.isKey) + { + this.tables[e.name].push({ + isKey: e.card.length >= 2 && e.card[1] == 'R', //"weak tables" foreign keys become part of the key + name: "#" + e2.name + "_" + attr.name, + type: attr.type, + qualifiers: "foreign key references " + e2.name + " " + (e.card[0]=='1' : "not null" : ""), + ref: e2.name, //easier drawMld function (fewer regexps) + }); + } + }); + }); + } + else + { + // Add all keys in current entity + let fields = e.attributes.filter( attr => { return attr.isKey; }); + newTableAttrs.push({ + fields: fields, + entity: e.name, + }); + } + } + if (newTableAttrs.length > 1) + { + // Ok, really create a new table + let newTable = { + name: a.name || newTableAttrs.map( item => { return item.entity; }).join("_"); + fields: [ ], + }; + newTableAttrs.forEach( item => { + item.fields.forEach( f => { + newTable.fields.push({ + name: item.entity + "_" + f.name, + isKey: true, + type: f.type,, + qualifiers: (f.qualifiers+" " || "") + "foreign key references " + item.entity + " not null", + ref: item.entity, + }); + }); + }); + // Add relationship potential own attributes + a.attributes.forEach( attr => { + newTable.fields.push({ + name: attr.name, + isKey: false, + type: attr.type, + qualifiers: attr.qualifiers, + }); + }); + this.tables[newTable.name] = newTable.fields; + } + }); + } + + ///////////////////////////////// + // DRAWING + GET SQL FROM PARSING + ///////////////////////////////// static AjaxGet(dotInput, callback) { @@ -260,9 +336,7 @@ class ErDiags mcdDot += 'node [shape=rectangle, style=rounded];\n'; let assoceCounter = 0; _.shuffle(this.associations).forEach( a => { - let name = !!a.name && a.name.length > 0 - ? a.name - : '_assoce' + assoceCounter++; + let name = a.name || "_assoce" + assoceCounter++; if (mcdStyle == "bubble") { mcdDot += '"' + name + '" [shape="diamond", style="filled", color="lightgrey", label="' + name + '"'; @@ -313,7 +387,7 @@ class ErDiags }); } - // "Modèle logique des données" + // "Modèle logique des données", from MCD without anomalies // TODO: this one should draw links from foreign keys to keys (port=... in ) drawMld(id) { @@ -325,26 +399,27 @@ class ErDiags } // Build dot graph input (assuming foreign keys not already present...) let mldDot = 'graph {\n'; - // Pass 1: initialize tables - let tables = [ ]; - Object.keys(this.entities).forEach( name => { - tables.push({ name: this.entities[name] }); //TODO: should be a (deep) copy - }); - // Pass 2: parse associations, add foreign keys + new tables - this.associations.forEach( a => { - a.entities.forEach( e => { // e.card e.name ... - switch (e.card) + mldDot += 'node [shape=plaintext];\n'; + let links = ""; + _.shuffle(Object.keys(this.tables)).forEach( name => { + mldDot += '"' + name + '" [label=<\n'; + mldDot += '\n'; + this.tables[name].fields.forEach( f => { + let label = (f.isKey ? '' : '') + (!!f.qualifiers && f.qualifiers.indexOf("foreign")>=0 ? '#' : '') + f.name + (f.isKey ? '' : ''); + mldDot += '\n'; + if (!!f.ref) { - case '?': - case '?R': //"weak tables" foreign keys become part of the key - // TODO - // multi-arite : sub-loop si 0,1 ou 1,1 : aspiré comme attribut de l'association (phase 1) - // ensuite, que du 0,n ou 1,n : si == 1, OK une table - // si 2 ou + : n tables + 1 pour l'assoce, avec attrs clés étrangères - // clé étrangère NOT NULL si 1,1 + if (Math.random() < 0.5) + links += '"' + f.ref + '":__key -- "' + '"'+name+'":"'+f.name+'"\n'; + else + links += '"'+name+'":"'+f.name+'" -- "' + f.ref + '":__key\n'; + } }); + mldDot += '
' + name + '
' + label + '
>];\n'; }); - // this.graphMld = ... + mldDot += links + '\n'; + mldDot += '}\n'; //console.log(mldDot); ErDiags.AjaxGet(mldDot, graphSvg => { this.mldGraph = graphSvg; @@ -360,6 +435,20 @@ class ErDiags element.innerHTML = this.sqlText; return; } - //UNIMPLEMENTED (should be straightforward from MLD) + let sqlText = ""; + Object.keys(this.tables).forEach( name => { + sqlText += "CREATE TABLE " + name + " (\n"; + let key = ""; + this.tables[name].forEach( f => { + sqlText += f.name + " " + (f.type || "TEXT") + (" "+f.qualifiers || "") + ",\n"; + if (f.isKey) + key += (key.length>0 ? "," : "") + f.name; + }); + sqlText += "PRIMARY KEY (" + key + ")\n"; + sqlText += ");\n"; + }); + //console.log(sqlText); + this.sqlText = sqlText; + element.innerHTML = "
" + sqlText + "
"; } }