You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

560 lines
18 KiB

4 years ago
  1. /*
  2. * Copyright (c)2012 Nicolas Cannasse
  3. *
  4. * Permission is hereby granted, free of charge, to any person obtaining
  5. * a copy of this software and associated documentation files (the
  6. * "Software"), to deal in the Software without restriction, including
  7. * without limitation the rights to use, copy, modify, merge, publish,
  8. * distribute, sublicense, and/or sell copies of the Software, and to
  9. * permit persons to whom the Software is furnished to do so, subject to
  10. * the following conditions:
  11. *
  12. * The above copyright notice and this permission notice shall be
  13. * included in all copies or substantial portions of the Software.
  14. *
  15. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  16. * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  17. * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  18. * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  19. * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  20. * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  21. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  22. */
  23. package sys.db;
  24. import sys.db.Object;
  25. import sys.db.Manager;
  26. typedef TableType = sys.db.RecordInfos.RecordType;
  27. typedef ManagerAccess = {
  28. private var table_name : String;
  29. private var table_keys : Array<String>;
  30. private function quote( v : Dynamic ) : String;
  31. private function quoteField( f : String ) : String;
  32. private function addKeys( s : StringBuf, x : {} ) : Void;
  33. function all( ?lock : Bool ) : List<Object>;
  34. function dbClass() : Class<Dynamic>;
  35. }
  36. private typedef TableRelation = {
  37. var prop : String;
  38. var key : String;
  39. var lock : Bool;
  40. var manager : ManagerAccess;
  41. var className : String;
  42. var cascade : Bool;
  43. }
  44. class TableInfos {
  45. public static var ENGINE = "InnoDB";
  46. public static var OLD_COMPAT = false; // only set for old DBs !
  47. public var primary(default,null) : List<String>;
  48. public var cl(default,null) : Class<Object>;
  49. public var name(default,null) : String;
  50. public var className(default,null) : String;
  51. public var hfields(default,null) : Map<String,TableType>;
  52. public var fields(default,null) : List<{ name : String, type : TableType }>;
  53. public var nulls(default,null) : Map<String,Bool>;
  54. public var relations(default,null) : Array<TableRelation>;
  55. public var indexes(default,null) : List<{ keys : List<String>, unique : Bool }>;
  56. public var manager : Manager<Object>;
  57. public function new( cname : String ) {
  58. hfields = new Map();
  59. fields = new List();
  60. nulls = new Map();
  61. cl = cast Type.resolveClass("db."+cname);
  62. if( cl == null )
  63. cl = cast Type.resolveClass(cname);
  64. else
  65. cname = "db."+cname;
  66. if( cl == null )
  67. throw "Class not found : "+cname;
  68. manager = untyped cl.manager;
  69. if( manager == null )
  70. throw "No static manager for "+cname;
  71. className = cname;
  72. if( className.substr(0,3) == "db." ) className = className.substr(3);
  73. var a = cname.split(".");
  74. name = a.pop();
  75. processClass();
  76. }
  77. function processClass() {
  78. var rtti = haxe.rtti.Meta.getType(cl).rtti;
  79. if( rtti == null )
  80. throw "Class "+name+" does not have RTTI";
  81. var infos : sys.db.RecordInfos = haxe.Unserializer.run(rtti[0]);
  82. name = infos.name;
  83. primary = Lambda.list(infos.key);
  84. for( f in infos.fields ) {
  85. fields.add({ name : f.name, type : f.t });
  86. hfields.set(f.name, f.t);
  87. if( f.isNull ) nulls.set(f.name, true);
  88. }
  89. relations = new Array();
  90. for( r in infos.relations ) {
  91. var t = Type.resolveClass(r.type);
  92. if( t == null ) throw "Missing type " + r.type + " for relation " + name + "." + r.prop;
  93. var manager : ManagerAccess = Reflect.field(t, "manager");
  94. if( manager == null ) throw r.type + " does not have a static field manager";
  95. relations.push( { prop : r.prop, key : r.key, lock : r.lock, manager : manager, className : Type.getClassName(manager.dbClass()), cascade : r.cascade } );
  96. }
  97. indexes = new List();
  98. for( i in infos.indexes )
  99. indexes.push( { keys : Lambda.list(i.keys), unique : i.unique } );
  100. }
  101. function escape( name : String ) {
  102. var m : ManagerAccess = manager;
  103. return m.quoteField(name);
  104. }
  105. public static function unescape( field : String ) {
  106. if( field.length > 1 && field.charAt(0) == '`' && field.charAt(field.length-1) == '`' )
  107. return field.substr(1,field.length-2);
  108. return field;
  109. }
  110. public function isRelationActive( r : Dynamic ) {
  111. return true;
  112. }
  113. public function createRequest( full : Bool ) {
  114. var str = "CREATE TABLE "+escape(name)+" (\n";
  115. var keys = fields.iterator();
  116. for( f in keys ) {
  117. str += escape(f.name)+" "+fieldInfos(f);
  118. if( keys.hasNext() )
  119. str += ",";
  120. str += "\n";
  121. }
  122. if( primary != null )
  123. str += ", PRIMARY KEY ("+primary.map(escape).join(",")+")\n";
  124. if( full ) {
  125. for( r in relations )
  126. if( isRelationActive(r) )
  127. str += ", "+relationInfos(r);
  128. for( i in indexes )
  129. str += ", "+(if( i.unique ) "UNIQUE " else "")+"KEY "+escape(name+"_"+i.keys.join("_"))+"("+i.keys.map(escape).join(",")+")\n";
  130. }
  131. str += ")";
  132. if( ENGINE != null )
  133. str += " ENGINE="+ENGINE;
  134. return str;
  135. }
  136. function relationInfos(r : TableRelation) {
  137. if( r.manager.table_keys.length != 1 )
  138. throw "Relation on a multiple-keys table";
  139. var rq = "CONSTRAINT "+escape(name+"_"+r.prop)+" FOREIGN KEY ("+escape(r.key)+") REFERENCES "+escape(r.manager.table_name)+"("+escape(r.manager.table_keys[0])+") ";
  140. rq += "ON DELETE "+(if( nulls.get(r.key) && r.cascade != true ) "SET NULL" else "CASCADE")+"\n";
  141. return rq;
  142. }
  143. function fieldInfos(f) {
  144. return (switch( f.type ) {
  145. case DId: "INT AUTO_INCREMENT";
  146. case DUId: "INT UNSIGNED AUTO_INCREMENT";
  147. case DInt, DEncoded: "INT";
  148. case DFlags(fl, auto): auto ? (fl.length <= 8 ? "TINYINT UNSIGNED" : (fl.length <= 16 ? "SMALLINT UNSIGNED" : (fl.length <= 24 ? "MEDIUMINT UNSIGNED" : "INT"))) : "INT";
  149. case DTinyInt: "TINYINT";
  150. case DUInt: "INT UNSIGNED";
  151. case DSingle: "FLOAT";
  152. case DFloat: "DOUBLE";
  153. case DBool: "TINYINT(1)";
  154. case DString(n): "VARCHAR("+n+")";
  155. case DDate: "DATE";
  156. case DDateTime: "DATETIME";
  157. case DTimeStamp: "TIMESTAMP"+(nulls.exists(f.name) ? " NULL DEFAULT NULL" : " DEFAULT 0");
  158. case DTinyText: "TINYTEXT";
  159. case DSmallText: "TEXT";
  160. case DText, DSerialized: "MEDIUMTEXT";
  161. case DSmallBinary: "BLOB";
  162. case DBinary, DNekoSerialized: "MEDIUMBLOB";
  163. case DData: "MEDIUMBLOB";
  164. case DEnum(_): "TINYINT UNSIGNED";
  165. case DLongBinary: "LONGBLOB";
  166. case DBigInt: "BIGINT";
  167. case DBigId: "BIGINT AUTO_INCREMENT";
  168. case DBytes(n): "BINARY(" + n + ")";
  169. case DTinyUInt: "TINYINT UNSIGNED";
  170. case DSmallInt: "SMALLINT";
  171. case DSmallUInt: "SMALLINT UNSIGNED";
  172. case DMediumInt: "MEDIUMINT";
  173. case DMediumUInt: "MEDIUMINT UNSIGNED";
  174. case DNull, DInterval: throw "assert";
  175. }) + if( nulls.exists(f.name) ) "" else " NOT NULL";
  176. }
  177. public function dropRequest() {
  178. return "DROP TABLE "+escape(name);
  179. }
  180. public function truncateRequest() {
  181. return "TRUNCATE TABLE "+escape(name);
  182. }
  183. public function descriptionRequest() {
  184. return "SHOW CREATE TABLE "+escape(name);
  185. }
  186. public function existsRequest() {
  187. return "SELECT * FROM "+escape(name)+" LIMIT 0";
  188. }
  189. public static function countRequest( m : ManagerAccess, max : Int ) {
  190. return "SELECT " + m.quoteField(m.table_keys[0]) + " FROM " + m.quoteField(m.table_name) + " LIMIT " + max;
  191. }
  192. public function addFieldRequest( fname : String ) {
  193. var ftype = hfields.get(fname);
  194. if( ftype == null )
  195. throw "No field "+fname;
  196. var rq = "ALTER TABLE "+escape(name)+" ADD ";
  197. return rq + escape(fname)+" "+fieldInfos({ name : fname, type : ftype });
  198. }
  199. public function removeFieldRequest( fname : String ) {
  200. return "ALTER TABLE "+escape(name)+" DROP "+escape(fname);
  201. }
  202. public function renameFieldRequest( old : String, newname : String ) {
  203. var ftype = hfields.get(newname);
  204. if( ftype == null )
  205. throw "No field "+newname;
  206. var rq = "ALTER TABLE "+escape(name)+" CHANGE "+escape(old)+" ";
  207. return rq + escape(newname) + " " + fieldInfos({ name : newname, type : ftype });
  208. }
  209. public function updateFieldRequest( fname : String ) {
  210. var ftype = hfields.get(fname);
  211. if( ftype == null )
  212. throw "No field "+fname;
  213. var rq = "ALTER TABLE "+escape(name)+" MODIFY ";
  214. return rq + escape(fname)+" "+fieldInfos({ name : fname, type : ftype });
  215. }
  216. public function addRelationRequest( key : String, prop : String ) {
  217. for( r in relations )
  218. if( r.key == key && r.prop == prop )
  219. return "ALTER TABLE "+escape(name)+" ADD "+relationInfos(r);
  220. return throw "No such relation : "+prop+"("+key+")";
  221. }
  222. public function deleteRelationRequest( rel : String ) {
  223. return "ALTER TABLE "+escape(name)+" DROP FOREIGN KEY "+escape(rel);
  224. }
  225. public function indexName( idx : Array<String> ) {
  226. return name+"_"+idx.join("_");
  227. }
  228. public function addIndexRequest( idx : Array<String>, unique : Bool ) {
  229. var eidx = new Array();
  230. for( i in idx ) {
  231. var k = escape(i);
  232. var f = hfields.get(i);
  233. if( f != null )
  234. switch( f ) {
  235. case DTinyText, DSmallText, DText, DSmallBinary, DLongBinary, DBinary:
  236. k += "(4)"; // index size
  237. default:
  238. }
  239. eidx.push(k);
  240. }
  241. return "ALTER TABLE "+escape(name)+" ADD "+(if( unique ) "UNIQUE " else "")+"INDEX "+escape(indexName(idx))+"("+eidx.join(",")+")";
  242. }
  243. public function deleteIndexRequest( idx : String ) {
  244. return "ALTER TABLE "+escape(name)+" DROP INDEX "+escape(idx);
  245. }
  246. public function updateFields( o : {}, fields : List<{ name : String, value : Dynamic }> ) {
  247. var me = this;
  248. var s = new StringBuf();
  249. s.add("UPDATE ");
  250. s.add(escape(name));
  251. s.add(" SET ");
  252. var first = true;
  253. for( f in fields ) {
  254. if( first )
  255. first = false;
  256. else
  257. s.add(", ");
  258. s.add(escape(f.name));
  259. s.add(" = ");
  260. Manager.cnx.addValue(s,f.value);
  261. }
  262. s.add(" WHERE ");
  263. var m : ManagerAccess = manager;
  264. m.addKeys(s,o);
  265. return s.toString();
  266. }
  267. public function identifier( o : Object ) : String {
  268. if( primary == null )
  269. throw "No primary key";
  270. return primary.map(function(p) { return Std.string(Reflect.field(o,p)).split(".").join("~"); }).join("@");
  271. }
  272. public function fromIdentifier( id : String ) : Object {
  273. var ids = id.split("@");
  274. if( primary == null )
  275. throw "No primary key";
  276. if( ids.length != primary.length )
  277. throw "Invalid identifier";
  278. var keys = {};
  279. for( p in primary )
  280. Reflect.setField(keys, p, makeNativeValue(hfields.get(p), ids.shift().split("~").join(".")));
  281. return manager.unsafeGetWithKeys(keys);
  282. }
  283. function makeNativeValue( t : TableType, v : String ) : Dynamic {
  284. return switch( t ) {
  285. case DInt, DUInt, DId, DUId, DEncoded, DFlags(_), DTinyInt: cast Std.parseInt(v);
  286. case DTinyUInt, DSmallInt, DSmallUInt, DMediumUInt, DMediumInt: cast Std.parseInt(v);
  287. case DFloat, DSingle, DBigInt, DBigId: cast Std.parseFloat(v);
  288. case DDate, DDateTime, DTimeStamp: cast Date.fromString(v);
  289. case DBool: cast (v == "true");
  290. case DText, DString(_), DSmallText, DTinyText, DBinary, DSmallBinary, DLongBinary, DSerialized, DNekoSerialized, DBytes(_): cast v;
  291. case DData: cast v;
  292. case DEnum(_): cast v;
  293. case DNull, DInterval: throw "assert";
  294. };
  295. }
  296. public function fromSearch( params : Map<String,String>, order : String, pos : Int, count : Int ) : List<Object> {
  297. var rop = ~/^([<>]=?)(.+)$/;
  298. var cond = "TRUE";
  299. var m : ManagerAccess = manager;
  300. for( p in params.keys() ) {
  301. var f = hfields.get(p);
  302. var v = params.get(p);
  303. if( f == null )
  304. continue;
  305. cond += " AND " + escape(p);
  306. if( v == null || v == "NULL" )
  307. cond += " IS NULL";
  308. else switch( f ) {
  309. case DEncoded:
  310. cond += " = "+(try Id.encode(v) catch( e : Dynamic ) 0);
  311. case DString(_),DTinyText,DSmallText,DText:
  312. cond += " LIKE "+m.quote(v);
  313. case DBool:
  314. cond += " = "+((v == "true") ? 1 : 0);
  315. case DId,DUId,DInt,DUInt,DSingle,DFloat,DDate,DDateTime,DBigInt,DBigId:
  316. if( rop.match(v) )
  317. cond += " "+rop.matched(1)+" "+m.quote(rop.matched(2));
  318. else
  319. cond += " = "+m.quote(v);
  320. default:
  321. cond += " = "+m.quote(v);
  322. }
  323. }
  324. if( order != null ) {
  325. if( order.charAt(0) == "-" )
  326. cond += " ORDER BY "+escape(order.substr(1))+" DESC";
  327. else
  328. cond += " ORDER BY "+escape(order);
  329. }
  330. var sql = "SELECT * FROM " + escape(name) + " WHERE " + cond + " LIMIT " + pos + "," + count;
  331. return manager.unsafeObjects(sql, false);
  332. }
  333. static function fromTypeDescription( desc : String ) {
  334. var fdesc = desc.toUpperCase().split(" ");
  335. var ftype = fdesc.shift();
  336. var tparam = ~/^([A-Za-z]+)\(([0-9]+)\)$/;
  337. var param = null;
  338. if( tparam.match(ftype) ) {
  339. ftype = tparam.matched(1);
  340. param = Std.parseInt(tparam.matched(2));
  341. }
  342. var nullable = true;
  343. var t = switch( ftype ) {
  344. case "VARCHAR","CHAR":
  345. if( param == null )
  346. null;
  347. else
  348. DString(param);
  349. case "INT":
  350. if( param == 11 && fdesc.remove("AUTO_INCREMENT") )
  351. DId
  352. else if( param == 10 && fdesc.remove("UNSIGNED") ) {
  353. if( fdesc.remove("AUTO_INCREMENT") )
  354. DUId
  355. else
  356. DUInt;
  357. } else if( param == 11 )
  358. DInt;
  359. else
  360. null;
  361. case "BIGINT":
  362. if( fdesc.remove("AUTO_INCREMENT") ) DBigId else DBigInt;
  363. case "DOUBLE": DFloat;
  364. case "FLOAT": DSingle;
  365. case "DATE": DDate;
  366. case "DATETIME": DDateTime;
  367. case "TIMESTAMP": DTimeStamp;
  368. case "TINYTEXT": DTinyText;
  369. case "TEXT": DSmallText;
  370. case "MEDIUMTEXT": DText;
  371. case "BLOB": DSmallBinary;
  372. case "MEDIUMBLOB": DBinary;
  373. case "LONGBLOB": DLongBinary;
  374. case "TINYINT":
  375. switch( param ) {
  376. case 1:
  377. fdesc.remove("UNSIGNED");
  378. DBool;
  379. case 4:
  380. DTinyInt;
  381. case 3:
  382. if( fdesc.remove("UNSIGNED") ) DTinyUInt else null;
  383. default:
  384. if( OLD_COMPAT )
  385. DInt;
  386. else
  387. null;
  388. }
  389. case "SMALLINT":
  390. fdesc.remove("UNSIGNED") ? DSmallUInt : DSmallInt;
  391. case "MEDIUMINT":
  392. fdesc.remove("UNSIGNED") ? DMediumUInt : DMediumInt;
  393. case "BINARY":
  394. if( param == null )
  395. null;
  396. else
  397. DBytes(param);
  398. default:
  399. null;
  400. }
  401. if( t == null )
  402. return null;
  403. while( fdesc.length > 0 ) {
  404. var d = fdesc.shift();
  405. switch( d ) {
  406. case "NOT":
  407. if( fdesc.shift() != "NULL" )
  408. return null;
  409. nullable = false;
  410. case "DEFAULT":
  411. var v = fdesc.shift();
  412. if( nullable ) {
  413. if( v == "NULL" )
  414. continue;
  415. return null;
  416. }
  417. var def = switch( t ) {
  418. case DId, DUId, DInt, DUInt, DBool, DSingle, DFloat, DEncoded, DBigInt, DBigId, DFlags(_), DTinyInt: "'0'";
  419. case DTinyUInt, DSmallInt, DSmallUInt, DMediumUInt, DMediumInt: "'0'";
  420. case DTinyText, DText, DString(_), DSmallText, DSerialized: "''";
  421. case DDateTime,DTimeStamp:
  422. if( v.length > 0 && v.charAt(v.length-1) != "'" )
  423. v += " "+fdesc.shift();
  424. "'0000-00-00 00:00:00'";
  425. case DDate: "'0000-00-00'";
  426. case DSmallBinary, DBinary, DLongBinary, DNekoSerialized, DBytes(_), DNull, DInterval: null;
  427. case DData: null;
  428. case DEnum(_): "'0'";
  429. }
  430. if( v != def && !OLD_COMPAT )
  431. return null;
  432. case "NULL":
  433. if( !nullable ) return null;
  434. nullable = true;
  435. continue;
  436. default:
  437. return null;
  438. }
  439. }
  440. return { t : t, nullable : nullable };
  441. }
  442. public static function fromDescription( desc : String ) {
  443. var r = ~/^CREATE TABLE `([^`]*)` \((.*)\)( ENGINE=([^ ]+))?( AUTO_INCREMENT=[^ ]+)?( DEFAULT CHARSET=.*)?$/sm;
  444. if( !r.match(desc) )
  445. throw "Invalid "+desc;
  446. var tname = r.matched(1);
  447. if( r.matched(4).toUpperCase() != "INNODB" )
  448. throw "Table "+tname+" should be INNODB";
  449. var matches = r.matched(2).split(",\n");
  450. var field_r = ~/^[ \r\n]*`(.*)` (.*)$/;
  451. var primary_r = ~/^[ \r\n]*PRIMARY KEY +\((.*)\)[ \r\n]*$/;
  452. var index_r = ~/^[ \r\n]*(UNIQUE )?KEY `(.*)` \((.*)\)[ \r\n]*$/;
  453. var foreign_r = ~/^[ \r\n]*CONSTRAINT `(.*)` FOREIGN KEY \(`(.*)`\) REFERENCES `(.*)` \(`(.*)`\) ON DELETE (SET NULL|CASCADE)[ \r\n]*$/;
  454. var index_key_r = ~/^`?(.*?)`?(\([0-9+]\))?$/;
  455. var fields = new Map();
  456. var nulls = new Map();
  457. var indexes = new Map();
  458. var relations = new Array();
  459. var primary = null;
  460. for( f in matches ) {
  461. if( field_r.match(f) ) {
  462. var fname = field_r.matched(1);
  463. var ftype = fromTypeDescription(field_r.matched(2));
  464. if( ftype == null )
  465. throw "Unknown description '"+field_r.matched(2)+"'";
  466. fields.set(fname,ftype.t);
  467. if( ftype.nullable )
  468. nulls.set(fname,true);
  469. } else if( primary_r.match (f) ) {
  470. if( primary != null )
  471. throw "Duplicate primary key";
  472. primary = primary_r.matched(1).split(",");
  473. for( i in 0...primary.length ) {
  474. var k = unescape(primary[i]);
  475. primary[i] = k;
  476. }
  477. } else if( index_r.match(f) ) {
  478. var unique = index_r.matched(1);
  479. var idxname = index_r.matched(2);
  480. var fs = Lambda.list(index_r.matched(3).split(","));
  481. indexes.set(idxname,{ keys : fs.map(function(r) {
  482. if( !index_key_r.match(r) ) throw "Invalid index key "+r;
  483. return index_key_r.matched(1);
  484. }), unique : unique != "" && unique != null, name : idxname });
  485. } else if( foreign_r.match(f) ) {
  486. var name = foreign_r.matched(1);
  487. var key = foreign_r.matched(2);
  488. var table = foreign_r.matched(3);
  489. table = table.substr(0,1).toUpperCase() + table.substr(1); // hack for MySQL on windows
  490. var id = foreign_r.matched(4);
  491. var setnull = if( foreign_r.matched(5) == "SET NULL" ) true else null;
  492. relations.push({ name : name, key : key, table : table, id : id, setnull : setnull });
  493. } else
  494. throw "Invalid "+f+" in "+desc;
  495. }
  496. return {
  497. table : tname,
  498. fields : fields,
  499. nulls : nulls,
  500. indexes : indexes,
  501. relations : relations,
  502. primary : primary,
  503. };
  504. }
  505. public static function sameDBStorage( dt : TableType, rt : TableType ) {
  506. return switch( rt ) {
  507. case DEncoded: dt == DInt;
  508. case DFlags(fl, auto): auto ? (fl.length <= 8 ? dt == DTinyUInt : (fl.length <= 16 ? dt == DSmallUInt : (fl.length <= 24 ? dt == DMediumUInt : dt == DInt))) : (dt == DInt);
  509. case DSerialized: (dt == DText);
  510. case DNekoSerialized: (dt == DBinary);
  511. case DData: dt == DBinary;
  512. case DEnum(_): dt == DTinyUInt;
  513. default: false;
  514. };
  515. }
  516. public static function allTablesRequest() {
  517. return "SHOW TABLES";
  518. }
  519. }