From e5b76b59cb9fdf027dccce268ce9347b693379e2 Mon Sep 17 00:00:00 2001 From: pvincent Date: Sun, 20 Sep 2020 18:23:59 +0400 Subject: [PATCH] first commit --- .gitignore | 4 + README.md | 18 + db.css | 167 ++++++ dbadmin.hxml | 4 + haxelib.json | 9 + sys/db/Admin.hx | 1161 ++++++++++++++++++++++++++++++++++++++++++ sys/db/AdminStyle.hx | 373 ++++++++++++++ sys/db/Custom.hx | 40 ++ sys/db/Id.hx | 65 +++ sys/db/Serialized.hx | 747 +++++++++++++++++++++++++++ sys/db/TableInfos.hx | 560 ++++++++++++++++++++ 11 files changed, 3148 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 db.css create mode 100644 dbadmin.hxml create mode 100644 haxelib.json create mode 100644 sys/db/Admin.hx create mode 100644 sys/db/AdminStyle.hx create mode 100644 sys/db/Custom.hx create mode 100644 sys/db/Id.hx create mode 100644 sys/db/Serialized.hx create mode 100644 sys/db/TableInfos.hx diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ddc5a91 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ + +/release +/release.zip +/dbadmin.n diff --git a/README.md b/README.md new file mode 100644 index 0000000..f040126 --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +Haxe DB Admin +============= + +Database Admin interface for Haxe Records + + * list all tables and enable to browse / search / edit / insert / delete + * view haxe and neko serialized data in clear and allow edit with -lib hscript + * allow to sync changes made in database schema : fields added/removed/changed type/renamed + * only tested with Neko/MySQL and PHP/MySQL + +How to setup : + + * map sys.db.Admin.handler() to "/db" URL on your website + * make sure it's only accessible by power users + * for best usage copy `db.css` from dbadmin directory to your website /css directory + * you can also use sys.db.Admin.initializeDatabase() which replaces sys.db.TableCreate + * enjoy ! + diff --git a/db.css b/db.css new file mode 100644 index 0000000..1d427a6 --- /dev/null +++ b/db.css @@ -0,0 +1,167 @@ +body, table { + margin : 0px; + font-family : "Trebuchet MS"; + font-size: 13px; +} + +.main { + padding : 10px; +} + +h1 { + color : rgb(244, 136, 33); + padding : 5px; + background-color : black; +} + +a { + color : #b32; +} + + + +input, +textarea, +select, +input.dfile { + border : 1px solid #bbb; + padding:4px; +} + +input.dint, +input.duint, +input.ddate, +input.dfloat { + width : 80px; +} + +img.dfile { + position : absolute; + left : 800px; +} + +input.button { + color : white; + font-weight : bold; + min-width : 100px; + background-color : #b32; + cursor : pointer; + border : 0px; + border-radius: 4px; + padding: 6px; +} + +input.ddatetime { + width : 120px; +} + +input.dstring { + width : 400px; +} + +input.dtinytext { + width : 700px; +} + +textarea.dtext { + width : 700px; + height : 200px; +} + +input:focus, +textarea:focus, +select:focus { + background-color : #f5f5f5; +} + +select { + width : 300px; +} + +input.button:hover { + background-color : rgb(244, 136, 33); +} + +input.button:focus { + background-color : rgb(244, 136, 33); +} + +.derror { + text-align : center; + background-color : #fee; +} + +table.results { + border-collapse : collapse; + border-spacing : 0px; + /*font-size : 90%;*/ + white-space : nowrap; + margin-bottom : 20px; + td { + padding : 1px; + border-left : 1px solid #bbb; + border-right : 1px solid #bbb; + } + tr.header { + background-color : #b32; + color : white; + a { + color : white; + text-decoration : underline; + } + } + tr { + background-color : white; + } + tr.odd { + background-color : #eee; + } + tr:hover td { + background-color : #fee; + } + tr.odd:hover td { + background-color : #ffd7d7; + } +} + +div.links { + position : absolute; + width : 100%; + height : 22px; + top : 40px; + left : 0px; + color : #e82; + background-color : #e82; +} + +div.links a:first-child { + margin-left : 2px; +} + +div.links a { + padding-left : 8px; + padding-right : 8px; + + + font-weight : bold; + line-height : 22px; + color : white; + text-decoration : none; +} + +table{ + border-collapse:collapse; + border-spacing:0; + +} +table td,table th{ + padding:8px; +} +table td{ + border-top: 1px solid #EEE; +} +table th{ + background-color:#EEE; + font-weight:bold; + color:#666; +} \ No newline at end of file diff --git a/dbadmin.hxml b/dbadmin.hxml new file mode 100644 index 0000000..4d0305f --- /dev/null +++ b/dbadmin.hxml @@ -0,0 +1,4 @@ +-neko dbadmin.n +-D haxe3 +-lib hscript +sys.db.Admin \ No newline at end of file diff --git a/haxelib.json b/haxelib.json new file mode 100644 index 0000000..929dc00 --- /dev/null +++ b/haxelib.json @@ -0,0 +1,9 @@ +{ + "name": "dbadmin", + "url": "http://github.com/ncannasse/dbadmin", + "license": "MIT", + "description": "Database Admin interface for Haxe DB Records", + "version": "1.1.0", + "releasenote": "Haxe 3 update", + "contributors": ["ncannasse"] +} \ No newline at end of file diff --git a/sys/db/Admin.hx b/sys/db/Admin.hx new file mode 100644 index 0000000..5b6840c --- /dev/null +++ b/sys/db/Admin.hx @@ -0,0 +1,1161 @@ +/* + * Copyright (c)2012 Nicolas Cannasse + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package sys.db; + + +import sys.db.Object; +import sys.db.Manager; +import sys.db.Types; + +import sys.db.Custom; +import sys.db.TableInfos.TableType; +import sys.db.TableInfos.ManagerAccess; +#if neko +import neko.Lib; +import neko.Web; +#elseif php +import php.Lib; +import php.Web; +#end + +@:access(sys.db.Manager) +class Admin { + + var style : AdminStyle; + var hasSyncAction : Bool; + var countCache : Map; + public var allowDrop : Bool; + public var default_rights : RightsInfos; + public var maxUploadSize : Int; + public var maxInstanceCount : Int; + + public function new() { + maxInstanceCount = 100; + maxUploadSize = 1000000; + allowDrop = false; + countCache = new Map(); + default_rights = { + can : { + insert : true, + delete : true, + modify : true, + truncate : false, + }, + invisible : [], + readOnly : [], + }; + } + + function execute(sql) { + return Manager.cnx.request(sql); + } + + function request( t : TableInfos, sql ) { + return Manager.cnx.request(sql); + } + + function boolResult(sql) { + try { + execute(sql); + return true; + } catch( e : Dynamic ) { + return false; + } + } + + function getTables():Array { + var tables = new Array(); + var classes = Lib.getClasses(); + crawl(tables, classes); + tables.sort(function(t1,t2) { return if( t1.name > t2.name ) 1 else if( t1.name < t2.name ) -1 else 0; }); + return tables; + } + + function has( a : { function iterator() : Iterator; }, v : T ) { + for( x in a.iterator() ) + if( x == v ) + return true; + return false; + } + + /** + * Filter classes who sublass sys.db.Object and get TableInfos + */ + function crawl(tables : Array,classes : Dynamic) { + for( cname in Reflect.fields(classes) ) { + + var v : Dynamic = Reflect.field(classes,cname); + var c = cname.charAt(0); + if( c >= "a" && c <= "z" ) { + //explore sub packages + crawl(tables,v); + continue; + } + #if !php + if( haxe.rtti.Meta.getType(v).rtti == null ) + continue; + #end + var s = Type.getSuperClass(v); + while( s != null ) { + if( s == sys.db.Object ) { + tables.push(new TableInfos(Type.getClassName(v))); + break; + } + s = Type.getSuperClass(s); + } + } + } + + public function index( ?errorMsg ) { + style.begin("Tables"); + style.beginForm("doSync"); + style.beginTable(); + var sync = false; + var allTables = new List(); + var rq = execute(TableInfos.allTablesRequest()); + for( r in rq ){ + if (rq.getResult(0) == null) continue; + allTables.add(rq.getResult(0)); + } + var windows = Sys.systemName() == "Windows"; + for( t in getTables() ) { + var rights = getRights(createInstance(t)); + style.beginLine(true); + style.text(t.name); + style.nextRow(); + if( !boolResult(t.existsRequest()) ) { + style.linkConfirm(t.className+"/doCreate","create"); + style.text("Table is Missing !"); + } else { + if( needSync(t) ) + sync = true; + if( rights.can.insert ) + style.link(t.className+"/insert","insert"); + style.nextRow(); + style.link(t.className+"/search","search"); + if( rights.can.truncate ) + style.linkConfirm(t.className+"/doCleanup","cleanup"); + if( allowDrop ) { + style.nextRow(); + style.linkConfirm(t.className+"/doDrop","drop"); + } + } + style.endLine(); + allTables.remove(t.name); + if( windows || TableInfos.OLD_COMPAT ) allTables.remove(t.name.toLowerCase()); + } + style.endTable(); + if( sync ) + style.addSubmit("Synchronize Database",true); + style.endForm(); + if( !allTables.isEmpty() ) { + style.beginList(); + for( t in allTables ) { + style.beginItem(); + style.text('Table "$t" does not have any matching object'); + style.endItem(); + } + style.endList(); + } + if( errorMsg != null ) + style.error(errorMsg); + style.end(); + } + + function isBinary(t) { + return switch( t ) { + case DBinary, DSmallBinary, DLongBinary, DBytes(_): true; + default: false; + }; + } + + function canDisplay( m : ManagerAccess ) { + var c = countCache.get(m.table_name); + if( c != null ) + return c; + c = execute(TableInfos.countRequest(m,maxInstanceCount)).length < maxInstanceCount; + countCache.set(m.table_name,c); + return c; + } + + function inputField( table : TableInfos, f, id : String, readonly, ?defval : Dynamic, ?rawValue : Bool ) { + var prim = has(table.primary,f.name); + var insert = (id == null); + for( r in table.relations ) + if( r.key == f.name ) { + var values = null; + if( canDisplay(r.manager) || r.className == "db.File" ) + values = r.manager.all(false).map(function(d) { + return { + id : Std.string(Reflect.field(d,r.manager.table_keys[0])), + str : d.toString() + }; + }); + var cname = r.className.substr(0,3) == "db." ? r.className.substr(3) : r.className; + style.choiceField( + r.prop, + values, + Std.string(defval), + cname+"/edit/", + !insert && (prim || readonly), + r.className == "db.File" + ); + return; + } + if( defval != null && !rawValue ) { + switch( f.type ) { + case DEncoded: + defval = Std.parseInt(Std.string(defval)); + defval = Id.decode(defval); + case DSerialized: + defval = new Serialized(defval).escape(); + + case DNekoSerialized: + #if neko + var v = try haxe.Serializer.run(Lib.localUnserialize(defval)) catch( e : Dynamic ) ("ERROR : " + Std.string(e)); + defval = new Serialized(v).escape(); + #else + throw "DNekoSerialized is only available on neko target"; + #end + case DData: + #if haxe3 + var str = try haxe.Serializer.run((untyped table.manager).doUnserialize(f.name, defval)) catch( e : Dynamic ) ("ERROR : " + Std.string(e)); + #else + var str = defval.toString(); + #end + defval = new Serialized(str).escape(); + default: + } + } + if( isBinary(f.type) ) + style.binField(f.name,table.nulls.exists(f.name),defval,if( insert ) null else function() { return table.name+"/doDownload/"+id+"/"+f.name; }); + else if( insert && readonly ) + return; + else if( !insert && (prim || readonly) ) + style.infoField(f.name,defval); + else + style.inputField(f.name,f.type,table.nulls.exists(f.name),defval); + } + + /** + * Prints an insert form + */ + function insert(table : TableInfos, ?params : Map, ?error : String, ?errorMsg : String ) { + var binary = false; + for( f in table.fields ) + if( isBinary(f.type) ) { + binary = true; + break; + } + style.begin("Insert new "+table.name); + style.beginForm(table.className+"/doInsert",binary,table.name); + var rights = getRights(table); + for( f in table.fields ) { + if( f.name == error ) { + style.errorField((errorMsg == null) ? "Invalid format" : errorMsg); + errorMsg = null; + } + if( has(rights.invisible,f.name) ) + continue; + var readonly = has(rights.readOnly,f.name); + inputField(table,f,null,readonly,if( params == null ) null else params.get(f.name), params != null ); + } + style.addSubmit("Insert"); + style.addSubmit("Insert New",null,false,"__new"); + style.endForm(); + if( errorMsg != null ) + style.error(errorMsg); + style.end(); + } + + function updateField( fname : String, v : String, ftype : TableType, table : TableInfos ) : Dynamic { + switch( ftype ) { + case DId, DUId, DBigId: + return null; + case DDate: + var d = if( v == "NOW" || v == "NOW()" ) Date.now() else try Date.fromString(v) catch( e : Dynamic ) null; + if( d == null ) + return null; + try d.toString() catch( e : Dynamic ) return null; + return d; + case DDateTime, DTimeStamp: + var d = if( v == "NOW" || v == "NOW()" ) Date.now() else try Date.fromString(v) catch( e : Dynamic ) null; + if( d == null ) + return null; + try d.toString() catch( e : Dynamic ) return null; + return d; + case DInt: + if( v == "" ) return 0; + return Std.parseInt(v); + case DUInt, DFlags(_): + if( v == "" ) return 0; + var i = Std.parseInt(v); + if( i < 0 ) + return null; + return i; + case DTinyInt: + if( v == "" ) return 0; + var i = Std.parseInt(v); + if( i < -128 || i > 127 ) + return null; + return i; + case DTinyUInt: + if( v == "" ) return 0; + var i = Std.parseInt(v); + if( i < 0 || i > 255 ) + return null; + return i; + case DSmallInt: + if( v == "" ) return 0; + var i = Std.parseInt(v); + if( i < -32768 || i > 32767 ) + return null; + return i; + case DSmallUInt: + if( v == "" ) return 0; + var i = Std.parseInt(v); + if( i < 0 || i > 65535 ) + return null; + return i; + case DMediumInt: + if( v == "" ) return 0; + var i = Std.parseInt(v); + if( i < -8388608 || i > 8388607 ) + return null; + return i; + case DMediumUInt: + if( v == "" ) return 0; + var i = Std.parseInt(v); + if( i < 0 || i > 16777215 ) + return null; + return i; + case DBigInt: + if( v == "" ) return 0; + var i = Std.parseFloat(v); + if( i == null || i%1 != 0 || i < -9223372036854775808.0 || i > 9223372036854775807.0 ) + return null; + return i; + case DFloat, DSingle: + if( v == "" ) return 0; + var fl = Std.parseFloat(v); + if( Math.isNaN(fl) ) + return null; + return fl; + case DString(n): + if( v.length > n ) + return null; + return v; + case DTinyText: + if( v.length > 255 ) + return null; + return v; + case DSmallText, DSmallBinary: + if( v.length > 0xFFFF ) + return null; + return v; + case DText, DBinary: + if( v.length > 0xFFFFFF ) + return null; + return v; + case DBytes(n): + if( v.length > n ) + return null; + return v; + case DLongBinary: + return v; + case DBool: + return (v == "true"); + case DEncoded: + if( v == "" ) return null; + return try Id.encode(v) catch( e : Dynamic ) null; + case DSerialized: + return new Serialized(v).encode(); + case DNekoSerialized: + #if neko + var str = new Serialized(v).encode(); + var val = neko.Lib.serialize(haxe.Unserializer.run(str)); + return val; + #else + throw "DNekoSerialized is only available on neko target"; + return null; + #end + case DData: + var s = new Serialized(v).encode(); + if( s.length > 0xFFFFFF ) + return null; + #if haxe3 + return (untyped table.manager).doSerialize(fname, haxe.Unserializer.run(s)) ; + #else + return haxe.io.Bytes.ofString(s); + #end + case DEnum(e): + if( v == "" ) return 0; + var i = Std.parseInt(v); + var ev = Type.resolveEnum(e); + if( i < 0 || (ev != null && i >= Type.getEnumConstructs(ev).length) ) + return null; + return i; + case DNull, DInterval: + throw "assert"; + } + } + + function createInstance( table : TableInfos ) : Object { + var c = Type.createEmptyInstance(table.cl); + #if !neko + untyped if( c._manager == null ) c._manager = c.__getManager(); + #end + return c; + } + + function getRights( ?t : Object, ?table : TableInfos ) : RightsInfos { + if( t == null ) + t = createInstance(table); + if( untyped t.dbRights == null ) + return default_rights; + var r : RightsInfos = untyped t.dbRights(); + if( r == null ) + return default_rights; + if( r.can == null ) + r.can = default_rights.can; + return r; + } + + function getSInfos( t : Object ) : SearchInfos { + if( untyped t.dbSearch == null ) + return null; + return untyped t.dbSearch(); + } + + /** + * insert a new object in a table + */ + function doInsert( table : TableInfos, params : Map ) { + var inst = createInstance(table); + updateParams(table,params); + for( f in table.fields ) { + var v = params.get(f.name); + #if (haxe_ver < 3.2) + var fieldName = f.name; + #else + var fieldName = Manager.getFieldName({name: f.name, t: f.type, isNull: table.nulls.exists(f.name)}); + #end + if( v == null ) { + if( table.nulls.exists(f.name) ) + Reflect.setField(inst,fieldName,null); + else + for( r in table.relations ) + if( f.name == r.key ) { + insert(table,params,f.name); + return; + } + continue; + } + var msg = null; + var v = try updateField(f.name, v, f.type, table) catch( err : String ) { msg = err; null; }; + if( v == null ) { + // loop in case of error Invalid_format + insert(table,params,f.name,msg); + return; + } + Reflect.setField(inst,fieldName,v); + } + if( table.primary.length == 1 && Reflect.field(inst,table.primary.first()) == 0 ) + Reflect.deleteField(inst,table.primary.first()); + try { + if( !getRights(inst).can.insert ) + throw "Can't insert"; + if(inst == null) + throw "instance is null"; + inst.insert(); + log("Inserted "+table.name+" "+table.identifier(inst)); + } catch( e : Dynamic ) { + insert(table,params,null,Std.string(e)); + return; + } + + //display a blank form to insert a new object + if( params.exists("__new") ) { + insert(table,params); + return; + } + style.goto(table.className+"/edit/"+table.identifier(inst)); + } + + function doCreate(table : TableInfos) { + try { + execute(table.createRequest(false)); + style.goto(""); + } catch( e : Dynamic ) { + index(Std.string(e)); + } + } + + function doDrop(table : TableInfos) { + if( !allowDrop ) + throw "Drop not allowed"; + execute(table.dropRequest()); + style.goto(""); + } + + function doCleanup( table : TableInfos ) { + if( !getRights(table).can.truncate ) + throw "Can't cleanup"; + execute(table.truncateRequest()); + style.goto(""); + } + + function edit( table : TableInfos, id : String, ?params : Map, ?error : String, ?errorMsg : String ) { + var obj = table.fromIdentifier(id); + var objStr = try Std.string(obj) catch( e : Dynamic ) "#"+id; + style.begin("Edit "+table.name+" "+objStr); + if( obj == null ) { + style.error("This object does not exists"); + style.end(); + return; + } + var binary = false; + for( f in table.fields ) + if( isBinary(f.type) ) { + binary = true; + break; + } + style.beginForm(table.className+"/doEdit/"+id,binary,table.name); + var rights = getRights(obj); + var hasBinary = false; + for( f in table.fields ) { + if( f.name == error ) { + style.errorField((errorMsg == null) ? "Invalid format" : errorMsg); + errorMsg = null; + } + if( has(rights.invisible,f.name) ) + continue; + var readonly = has(rights.readOnly,f.name); + inputField(table,f,id,readonly,if( params == null ) Reflect.field(obj,f.name) else params.get(f.name), params != null); + if( !readonly && isBinary(f.type) ) + hasBinary = true; + } + if( rights.can.modify ) { + style.addSubmit("Modify"); + if( hasBinary ) + style.addSubmit("Upload",null,null,"__upload"); + } + style.addSubmit("Cancel",table.className+"/edit/"+id); + if( rights.can.delete ) + style.addSubmit("Delete",table.className+"/doDelete/"+id,true); + style.endForm(); + if( errorMsg != null ) + style.error(errorMsg); + style.end(); + } + + function doEdit( table : TableInfos, id : String, params : Map ) { + var inst = table.fromIdentifier(id); + if( inst == null ) { + style.goto(table.className+"/edit/"+id); + return; + } + updateParams(table,params); + var rights = getRights(inst); + var binaries = new List(); + for( f in table.fields ) { + if( has(rights.readOnly,f.name) || has(rights.invisible,f.name) ) + continue; + #if (haxe_ver < 3.2) + var fieldName = f.name; + #else + var fieldName = Manager.getFieldName({name: f.name, t: f.type, isNull: table.nulls.exists(f.name)}); + #end + var v = params.get(f.name); + if( v == null ) { + if( table.nulls.exists(f.name) ) + Reflect.setField(inst,fieldName,null); + continue; + } + var msg = null; + var v = try updateField(f.name, v, f.type, table) catch( err : Dynamic ) { msg = err; null; }; + if( v == null ) { + // insert ID into params + if( table.primary != null ) { + for( p in table.primary ) + params.set(p,Reflect.field(inst,p)); + } + for( f in rights.readOnly ) + params.set(f, Reflect.field(inst, f)); + // error Invalid_format + edit(table,id,params,f.name, msg); + return; + } + var bin = isBinary(f.type); + if( Std.is(v,String) && v == "" && bin ) + continue; + Reflect.setField(inst,fieldName,v); + if( bin ) + binaries.add({ name : f.name, value : v }); + } + try { + if( !rights.can.modify ) + throw "Can't modify"; + if( params.exists("__upload") ) + request(table,table.updateFields(inst,binaries)); + else { + inst.update(); + log("Updated "+table.name+" "+table.identifier(inst)); + } + } catch( e : Dynamic ) { + edit(table,id,params,null,Std.string(e)); + return; + } + style.goto(table.className+"/edit/"+table.identifier(inst)); + } + + /** + * Sync some of the object fields from the web request + */ + function updateParams( table : TableInfos, params : Map ) { + var tmp = Web.getMultipart(maxUploadSize); + for( k in tmp.keys() ) + params.set(k,tmp.get(k)); + for( r in table.relations ) { + var p = params.get(r.prop); + params.remove(r.prop); + if( p == null || p == "" ) + continue; + params.set(r.key,p); + params.remove(r.prop+"__data"); + params.set(r.key+"__data","on"); + } + for( f in table.fields ) { + switch( f.type ) { + case DFlags(flags,_): + var vint = 0; + for( i in 0...flags.length ) + if( params.exists(f.name + "_" + flags[i]) ) + vint |= 1 << i; + if( table.nulls.exists(f.name) && !params.exists(f.name+"__data") && vint == 0 ) { + params.remove(f.name); + continue; + } + params.set(f.name, Std.string(vint)); + params.set(f.name + "__data", "true"); + default: + } + if( table.nulls.exists(f.name) && !params.exists(f.name+"__data") && (params.get(f.name) == "" || params.get(f.name) == null) ) { + params.remove(f.name); + continue; + } + if( f.type == DBool ) { + var v = params.exists(f.name); + params.set(f.name,if( v ) "true" else "false"); + } + } + } + + /** + * Deletes a record + */ + function doDelete( table : TableInfos, id : String ) { + var inst = table.fromIdentifier(id); + if( inst == null ) { + style.goto(table.className+"/edit/"+id); + return; + } + if( !getRights(inst).can.delete ) { + edit(table,id,null,null,"Can't Delete"); + return; + } + inst.delete(); + log("Deleted "+table.name+" "+id); + style.goto(""); + } + + function doDownload( table : TableInfos, id : String, field : String ) { + var inst = table.fromIdentifier(id); + if( inst == null ) { + style.goto(table.className+"/edit/"+id); + return; + } + var rights = getRights(inst); + var f = table.hfields.get(field); + var data : String = Reflect.field(inst,field); + if( has(rights.invisible,field) || data == null || !isBinary(f) ) { + edit(table,id,null,null,"Can't Download data"); + return; + } + Web.setHeader("Content-Type","text/binary"); + Web.setHeader("Content-Length",Std.string(data.length)); + Sys.print(data); + } + + function search( table : TableInfos, params : Map ) { + style.begin("Search "+table.name); + + var pagesize = 30; + var page = Std.parseInt(params.get("__p")); + var order = params.get("__o"); + if( page == null ) + page = 0; + params.remove("__p"); + params.remove("__o"); + + // save params for later usage + var paramsStr = ""; + for( p in params.keys() ) { + var v = params.get(p); + paramsStr += p+"="+StringTools.urlEncode(v)+";"; + } + + // set nullable for all types which can be searched with empty string or no values + for( f in table.fields ) + switch( f.type ) { + case DBool, DString(_), DTinyText, DText, DSmallText, DFlags(_): + table.nulls.set(f.name,true); + default: + } + + updateParams(table,params); + + // remove not null fields that have not been completed + for( f in table.fields ) + if( !table.nulls.exists(f.name) && params.get(f.name) == "" ) + params.remove(f.name); + + var rights = getRights(table); + for( f in rights.invisible ) + params.remove(f); + + var results = table.fromSearch(params,order,page*pagesize,pagesize+1); + var hasNext = false; + if( results.length > pagesize ) { + results.remove(results.last()); + hasNext = true; + } + + var sinfos : SearchInfos = getSInfos(createInstance(table)); + var fields; + if( sinfos != null && sinfos.fields != null ) + fields = sinfos.fields; + else { + fields = new Array(); + for( f in table.fields ) { + var bad = false; + for( r in table.relations ) + if( r.key == f.name && r.className == "db.File" ) { + bad = true; + break; + } + if( bad ) + continue; + fields.push(f.name); + } + } + + style.beginForm(table.className+"/search",table.name); + for( f in fields ) { + var t = table.hfields.get(f); + if( t == null ) + continue; + var t = switch( t ) { + case DId, DUId: DInt; + case DBigId: DFloat; + case DText,DSmallText: DTinyText; + default: t; + }; + inputField(table,{ name : f, type : t },null,false,if( params == null ) null else params.get(f)); + } + style.addSubmit("Search"); + style.endForm(); + + style.beginTable("results"); + style.beginLine(true,"header"); + style.text("actions"); + + if( sinfos != null && sinfos.names != null ) { + for( f in sinfos.names ) { + style.nextRow(true); + if( table.hfields.exists(f) ) { + var cur = (order == f); + var curNeg = (order == "-"+f); + style.link(table.className+"/search?"+paramsStr+"__o="+(cur ? "-"+f : f),(cur ? "+" : curNeg ? "-" : "") + f); + } else + style.text(f); + } + } else { + for( f in table.fields ) { + if( has(rights.invisible,f.name) ) + continue; + style.nextRow(true); + var cur = (order == f.name); + var curNeg = (order == "-"+f.name); + style.link(table.className+"/search?"+paramsStr+"__o="+(cur ? "-"+f.name : f.name),(cur ? "+" : curNeg ? "-" : "") + f.name); + } + } + style.endLine(); + + var odd = false; + for( r in results ) { + var k = table.fields.iterator(); + style.beginLine(if( odd ) "odd" else null); + style.link(table.className+"/edit/"+table.identifier(r),"Edit"); + odd = !odd; + if( sinfos != null && sinfos.names != null ) { + var rinfos = getSInfos(r); + for( v in rinfos.values ) { + style.nextRow(false); + style.text(Std.string(v)); + } + } else { + var rinst = getRights(r); + for( f in k ) { + if( has(rights.invisible,f.name) ) + continue; + var data = Reflect.field(r,f.name); + var str = try Std.string(data) catch( e : Dynamic ) { if(!Std.is(data,Date)) Lib.rethrow(e); "#INVALID"; }; + if( str.length >= 20 ) + str = str.substr(0,17) + "..."; + style.nextRow(false); + if( has(rinst.invisible,f.name) ) + style.text("???"); + else if( data == null ) + style.text(str) + else switch( f.type ) { + case DEncoded: + style.text(Id.decode(data),str); + case DDate: + style.text(str.substr(0,10)); // remove 00:00:00 time + case DFlags(flags,_): + var fl = []; + for( i in 0...flags.length ) + if( data & (1 << i) != 0 ) + fl.push(flags[i]); + str = fl.join(","); + if( str.length >= 20 ) + style.text(str.substr(0,17) + "...",fl.join(",")+" ("+data+")"); + else + style.text(str,"("+data+")"); + default: + style.text(str); + } + } + } + style.endLine(); + } + style.endTable(); + + if( order != null ) + paramsStr += "__o="+order+";"; + + if( page > 0 ) + style.link(table.className+"/search?"+paramsStr+"__p="+(page-1),"Previous"); + else + style.text("Previous"); + style.text(" | "); + if( hasNext ) + style.link(table.className+"/search?"+paramsStr+"__p="+(page+1),"Next"); + else + style.text("Next"); + style.end(); + } + + function syncAction( t : TableInfos, act : Array, text : String, ?def ) { + if( !hasSyncAction ) { + style.beginList(); + hasSyncAction = true; + } + style.beginItem(); + style.checkBox(t.className+"@"+act.join("@"),if( def == null ) true else def); + style.text(text); + style.endItem(); + } + + function doSync( params : Map ) { + var order = ["create","add","reldel","idxdel","update","remove","rename","idxadd","reladd"]; + var cmd = new Array(); + for( p in params.keys() ) { + if( !~/[A-Za-z0-9_@]*/.match(p) ) + throw "Invalid command "+p; + cmd.push(p.split("@")); + } + cmd.sort(function(c1,c2) { + var p1 = 0, p2 = 0; + for( i in 0...order.length ) + if( order[i] == c1[1] ) + p1 = i; + else if( order[i] == c2[1] ) + p2 = i; + return p1 - p2; + }); + for( data in cmd ) { + var tname = data.shift(); + #if php + // php replaces dots by _ in post keys + tname = tname.split("_").join("."); + #end + var table = new TableInfos(tname); + var act = data.shift(); + var field = data.shift(); + try { + switch( act ) { + case "create": + execute(table.createRequest(false)); + case "add": + execute(table.addFieldRequest(field)); + case "update": + execute(table.updateFieldRequest(field)); + case "remove": + execute(table.removeFieldRequest(field)); + case "rename": + execute(table.renameFieldRequest(field,data.shift())); + case "reladd": + execute(table.addRelationRequest(field,data.shift())); + case "reldel": + execute(table.deleteRelationRequest(field)); + case "idxadd": + execute(table.addIndexRequest(data,field == "true")); + case "idxdel": + execute(table.deleteIndexRequest(field)); + default: + throw "Unknown action "+act; + } + } catch( e : Dynamic ) { + index(Std.string(e)); + return; + } + } + style.goto(""); + } + + function indexId( i : { unique : Bool, keys : List } ) { + return i.unique+"@"+i.keys.join("@"); + } + + /** + * check for differences between classes and tables in DB + */ + function needSync( t : TableInfos ) { + var desc = execute(t.descriptionRequest()).getResult(1); + var inf = TableInfos.fromDescription(desc); + var renames = new Map(); + hasSyncAction = false; + + // ADD/CHANGE FIELDS + for( f in t.fields ) { + var t2 = inf.fields.get(f.name); + if( t2 == null ) { + var rename = false; + for( n in inf.fields.keys() ) + if( !t.hfields.exists(n) && Type.enumEq(inf.fields.get(n),f.type) && inf.nulls.get(n) == t.nulls.get(f.name) ) { + rename = true; + renames.set(n,true); + syncAction(t,["rename",n,f.name],'Rename field "$n" to "${f.name}"'); + break; + } + syncAction(t,["add",f.name],"Add field "+f.name,!rename); + } else { + inf.fields.remove(f.name); + var isnull = inf.nulls.get(f.name); + var changed = false; + var txt = "Change "+f.name+" : "; + if( !Type.enumEq(f.type,t2) && !TableInfos.sameDBStorage(t2,f.type) ) { + changed = true; + txt += " S"+Std.string(t2).substr(1)+" becomes S"+Std.string(f.type).substr(1); + } + if( isnull != t.nulls.get(f.name) ) { + if( changed ) + txt += " and"; + else + changed = true; + if( isnull ) + txt += " can't be NULL"; + else + txt += " can be NULL"; + } + if( changed ) + syncAction(t,["update",f.name],txt); + } + } + // REMOVE FIELDS + for( f in inf.fields.keys() ) + syncAction(t,["remove",f],"Remove field "+f,!renames.exists(f)); + // ADD RELATIONS + for( r in t.relations ) { + if( !t.isRelationActive(r) ) + continue; + var tname = TableInfos.unescape(r.manager.table_name); + var found = false; + var setnull = t.nulls.get(r.key); + if( setnull && untyped r.cascade == true ) + setnull = null; + for( r2 in inf.relations ) + if( (t.name + "_" + r.prop == r2.name || TableInfos.OLD_COMPAT) && + r.key == r2.key && + tname.toLowerCase() == r2.table.toLowerCase() && + r.manager.table_keys.length == 1 && r.manager.table_keys[0] == r2.id && + r2.setnull == setnull + ) { + found = true; + inf.relations.remove(r2); + break; + } + if( !found ) + syncAction(t,["reladd",r.key,r.prop],"Add Relation "+r.prop+"("+r.key+") on "+tname+"("+r.manager.table_keys[0]+")"+if( setnull ) " set-null" else ""); + } + // REMOVE RELATIONS + for( r in inf.relations ) + syncAction(t,["reldel",r.name],"Remove Relation "+r.name+"("+r.key+") on "+r.table+"("+r.id+")"+if( r.setnull ) " set-null" else ""); + // INDEXES + var hidx = new Map(); + for( i in t.indexes ) + hidx.set(indexId(i),i); + var used = new List(); + for( r in t.relations ) { + var found : { keys : List, unique : Bool } = null; + for( i in t.indexes ) + if( i.keys.first() == r.key && (found == null || found.keys.length < i.keys.length) ) + found = i; + if( found == null ) { + // in primary key ? + if( t.primary.first() == r.key ) + continue; + // default relation-index + found = { keys : Lambda.list([r.key]), unique : false }; + } + hidx.remove(indexId(found)); + for( i in inf.indexes ) + if( i.keys.join("#") == found.keys.join("#") && i.unique == found.unique ) { + used.add(i); + found = null; + break; + } + // we need it + if( found != null ) + hidx.set(indexId(found),found); + } + for( i in used ) + inf.indexes.remove(i.name); + for( iname in inf.indexes.keys() ) { + var i = inf.indexes.get(iname); + if( !hidx.remove(indexId(i)) ) + syncAction(t,["idxdel",iname],"Remove "+(if( i.unique ) "Unique " else "")+"Index "+iname+" ("+i.keys.join(",")+")"); + } + for( i in hidx ) + syncAction(t,["idxadd",indexId(i)],"Add "+(if( i.unique ) "Unique " else "")+"Index ("+i.keys.join(",")+")"); + // PRIMARY KEYS + if( (inf.primary == null) != (t.primary == null) || (inf.primary != null && inf.primary.join("-") != t.primary.join("-")) ) { + style.text("PRIMARY KEY CHANGED !"); + hasSyncAction = true; + } + if( hasSyncAction ) + style.endList(); + return hasSyncAction; + } + + public function process( ?url : Array ) { + if( url == null ) { + url = Web.getURI().split("/"); + url.shift(); // empty : url starts with / + url.shift(); // "admin" + if( url[0] == "index.n" ) + url.shift(); + } + if( url.length == 0 ) url.push(""); + var params = Web.getParams(); + switch( url[0] ) { + case "": + + style = new AdminStyle(null); + index(); + return; + + case "doSync": + style = new AdminStyle(null); + doSync(params); + return; + } + var table = new TableInfos(url.shift()); + style = new AdminStyle(table); + var act = url.shift(); + switch( act ) { + case "insert": + insert(table,params); + case "doInsert": + doInsert(table,params); + case "edit": + edit(table,url.join("/")); + case "doEdit": + doEdit(table,url.join("/"),params); + case "doCreate": + doCreate(table); + case "doDrop": + doDrop(table); + case "doCleanup": + doCleanup(table); + case "doDelete": + doDelete(table,url.join("/")); + case "doDownload": + doDownload(table,url.shift(),url.shift()); + case "search": + search(table,params); + default: + throw "Unknown action "+act; + } + } + + static function log(msg:String) { + #if neko + Web.logMessage("[DBADM] " + neko.Web.getHostName() + " " + Date.now().toString() + " " + neko.Web.getClientIP() + " - " + msg); + #end + } + + public static function handler() { + Manager.initialize(); // make sure it's been done + try { + new Admin().process(); + } catch( e : Dynamic ) { + // rollback in case of multiple delete/update - no effect on DB struct changes + // since they are done outside of transaction + Manager.cnx.rollback(); + + Sys.print("

Error :

"+Std.string(e)); + Sys.print("

Stack :

");
+			Sys.print(haxe.CallStack.toString(haxe.CallStack.exceptionStack()));
+			Sys.print("
"); + } + } + + public static function initializeDatabase( initIndexes = true, initRelations = true ) { + var a = new Admin(); + var tables = a.getTables(); + for( t in tables ) + a.execute(t.createRequest(false)); + for( t in tables ) { + if( initIndexes ) + for( i in t.indexes ) + a.execute(t.addIndexRequest(Lambda.array(i.keys), i.unique)); + if( initRelations ) + for( r in t.relations ) + a.execute(t.addRelationRequest(r.key, r.prop)); + } + } + +} diff --git a/sys/db/AdminStyle.hx b/sys/db/AdminStyle.hx new file mode 100644 index 0000000..6f37f25 --- /dev/null +++ b/sys/db/AdminStyle.hx @@ -0,0 +1,373 @@ +/* + * Copyright (c)2012 Nicolas Cannasse + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package sys.db; + +import haxe.macro.Context; +#if !macro +import sys.db.TableInfos.TableType; +#end + +#if neko +import neko.Lib; +import neko.Web; +#elseif php +import php.Lib; +import php.Web; +#end + +class MacroHelper { + + public macro static function importFile( file : String ) { + var data = try sys.io.File.getContent(Context.resolvePath(file)) catch( e : Dynamic ) null; + return Context.makeExpr(data,Context.currentPos()); + } + +} + +#if !macro + +class AdminStyle { + + public static var BASE_URL = "/db/"; + public static var CSS = { + var file = MacroHelper.importFile("db.css"); + if( file == null ) + null + else + ''; + } + public static var HTML_BOTTOM = ""; + + var isNull : Bool; + var value : String; + var isHeader : Bool; + var table : TableInfos; + + public function new(t) { + this.table = t; + } + + function out(str : String,?params : Dynamic) { + if( params != null ) { + for( x in Reflect.fields(params) ) + str = str.split("@"+x).join(Reflect.field(params,x)); + } + Sys.println(str); + } + + public function text(str,?title) { + str = StringTools.htmlEscape(str); + if( title != null ) str = ''+str+''; + out(str); + } + + public function begin( title ) { + out('@title',{ title: title }); + if( CSS != null ) + out(CSS); + out(''); + out(' + + ',{ base : BASE_URL }); + out(''); + out('

@title

',{ title : title }); + } + + public function end() { + out('
'); + out(HTML_BOTTOM); + out(''); + } + + public function beginList() { + out("
    "); + } + + public function endList() { + out("
"); + } + + public function beginItem() { + out("
  • "); + } + + public function endItem() { + out("
  • "); + } + + public function goto(url) { + Web.redirect(BASE_URL+url); + } + + public function link( url, name ) { + out('@name',{ url : BASE_URL+url, name : name }); + } + + public function linkConfirm( url, name ) { + out('@name',{ url : BASE_URL+url, name : name }); + } + + public function beginForm(url,?file,?id) { + out('
    ',{ id:id, url : BASE_URL+url, enc : if( file ) ' enctype="multipart/form-data"' else "" }); + beginTable(); + } + + public function endForm() { + endTable(); + out('
    '); + } + + public function beginTable( ?css ) { + if( css != null ) + out('',{ css : css }); + else + out('
    '); + } + + public function endTable() { + out('
    '); + } + + public function beginLine( ?isHeader, ?css ) { + var str = '' else ''; + out(str); + this.isHeader = isHeader; + } + + public function nextRow( ?isHeader ) { + out((if( this.isHeader ) '' else '')+(if( isHeader ) '' else '')); + this.isHeader = isHeader; + } + + public function endLine() { + out((if( this.isHeader ) '' else '')+''); + } + + public function addSubmit( name, ?url, ?confirm, ?iname ) { + beginLine(); + nextRow(); + out(''); + endLine(); + } + + public function checkBox(name,checked) { + out(''); + } + + function input(name,css,?options : Dynamic) { + if( options == null ) + options = {}; + beginLine(true); + out(name); + nextRow(); + if( isNull ) + checkBox(name+"__data",value != null); + out(''); + endLine(); + } + + function getFileURL( v : String ) { + return "/file/" + v + ".png"; + } + + function inputText(name, css, ?noWrap ) { + beginLine(true); + out(name); + nextRow(); + if( isNull ) + checkBox(name+"__data",value != null); + out('',{ noWrap : noWrap?' wrap="off"':'', name : name, css : css, value : if( value != null ) StringTools.htmlEscape(value) else "" }); + endLine(); + } + + public function inputField( name : String, type : TableType, isNull, value ) { + this.isNull = isNull; + this.value = value; + switch( type ) { + case DId, DUId, DBigId: + infoField(name,if( value == null ) "#ID" else value); + case DInt: + input(name,"dint",{ size : 10 }); + case DBigInt: + input(name,"dbigint",{ size : 20 }); + case DUInt: + input(name,"duint",{ size : 10 }); + case DTinyInt: + input(name, "dtint", { size : 4 } ); + case DTinyUInt, DSmallInt, DSmallUInt, DMediumInt, DMediumUInt: + input(name, "dint", { size : 10 } ); + case DFloat, DSingle: + input(name,"dfloat",{ size : 10 }); + case DBool: + input(name,"dbool",{ isCheck : true }); + case DString(n): + input(name,"dstring",{ size : n }); + case DTinyText: + input(name,"dtinytext"); + case DDate: + if( value != null ) + this.value = try value.toString().substr(0,10) catch( e : Dynamic ) "#INVALID"; + input(name,"ddate",{ size : 10 }); + case DDateTime, DTimeStamp: + if( value != null ) + this.value = try value.toString() catch( e : Dynamic ) "#INVALID"; + input(name, "ddatetime", { size : 19 } ); + case DText, DSmallText: + inputText(name, "dtext"); + case DSerialized, DNekoSerialized: + inputText(name, "dtext", true); + case DData: + inputText(name, "dtext", true); + case DEnum(_): + // todo : use a select box with possible constructors + input(name, "dtint", { size : 4 } ); + case DEncoded: + input(name,"denc",{ size : 6 }); + case DFlags(fl,_): + beginLine(true); + out(name); + nextRow(); + if( isNull ) + checkBox(name+"__data",value != null); + var vint = Std.parseInt(value); + if( vint == null ) vint = 0; + var pos = 0; + for( i in 0...fl.length ) { + out(''); + out(fl[i]); + } + endLine(); + case DBinary, DSmallBinary, DLongBinary, DBytes(_), DNull, DInterval: + throw "NotSupported"; + } + } + + public function binField( name : String, isNull, value : String, url : Void -> String ) { + beginLine(true); + out(name); + nextRow(); + if( isNull ) + checkBox(name+"__data",value != null); + if( value != null ) + text("["+value.length+" bytes]"); + else if( url != null ) + text("null"); + out('',{ name : name }); + if( value != null && url != null ) + link(url(),"download"); + endLine(); + } + + public function infoField( name : String, value ) { + beginLine(true); + out(name); + nextRow(); + out(value); + endLine(); + } + + public function choiceField( name : String, values : List<{ id : String, str : String }>, def : String, link, ?disabled : Bool, ?isImage: Bool ) { + beginLine(true); + out(name); + nextRow(); + var infos = { + func : if( isImage ) "updateImage" else "updateLink", + name : name, + link : link, + size : if( values != null && values.length > 15 ) 10 else 1, + dis : if( disabled ) 'disabled="yes"' else "", + def : if( def == "null" ) "" else def, + }; + if( values == null ) + out('',infos); + else { + out(''); + } + out('goto',{ name : name }); + if( isImage ) + out('',{ name : name, file : getFileURL(def) }); + out('',{ name : name }); + endLine(); + } + + public function errorField( message ) { + beginLine(true); + nextRow(); + error(message); + endLine(); + } + + public function error( message ) { + out('
    @msg
    ',{ msg : message }); + } + +} + +#end diff --git a/sys/db/Custom.hx b/sys/db/Custom.hx new file mode 100644 index 0000000..17f784f --- /dev/null +++ b/sys/db/Custom.hx @@ -0,0 +1,40 @@ +/* + * Copyright (c)2012 Nicolas Cannasse + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package sys.db; + +typedef SearchInfos = { + var fields : Array; + var names : Array; + var values : Array; +} + +typedef RightsInfos = { + var readOnly : Array; + var invisible : Array; + var can : { + var insert : Bool; + var modify : Bool; + var delete : Bool; + var truncate : Bool; + }; +} diff --git a/sys/db/Id.hx b/sys/db/Id.hx new file mode 100644 index 0000000..1aa3f0a --- /dev/null +++ b/sys/db/Id.hx @@ -0,0 +1,65 @@ +/* + * Copyright (c)2012 Nicolas Cannasse + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package sys.db; + +class Id { + + public static function encode( id : String ) : Int { + var l = id.length; + if( l > 6 ) + throw "Invalid identifier '"+id+"'"; + var k = 0; + var p = l; + while( p > 0 ) { + var c = id.charCodeAt(--p) - 96; + if( c < 1 || c > 26 ) { + c = c + 96 - 48; + if( c >= 1 && c <= 5 ) + c += 26; + else + throw "Invalid character "+id.charCodeAt(p)+" in "+id; + } + k <<= 5; + k += c; + } + return k; + } + + public static function decode( id : Int ) : String { + var s = new StringBuf(); + if( id < 1 ) { + if( id == 0 ) return ""; + throw "Invalid ID "+id; + } + while( id > 0 ) { + var k = id & 31; + if( k < 27 ) + s.addChar(k + 96); + else + s.addChar(k + 22); + id >>= 5; + } + return s.toString(); + } + +} \ No newline at end of file diff --git a/sys/db/Serialized.hx b/sys/db/Serialized.hx new file mode 100644 index 0000000..9f9d0a4 --- /dev/null +++ b/sys/db/Serialized.hx @@ -0,0 +1,747 @@ +/* + * Copyright (c)2012 Nicolas Cannasse + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package sys.db; + +#if hscript +import hscript.Expr; +#end + +private enum Errors { + Invalid; +} + +private typedef Current = { + var old : Current; + var lines : Array; + var totalSize : Int; + var maxSize : Int; + var prefix : String; + var sep : String; + var buf : StringBuf; +} + +class Serialized { + + var value : String; + var pos : Int; + var buf : StringBuf; + var shash : Map; + var scount : Int; + var scache : Array; + var useEnumIndex : Bool; + + var cur : Current; + var tabs : Int; + + static var IDENT = " "; + static var ident = ~/^[A-Za-z_][A-Za-z0-9_]*$/; + static var clname = ~/^[A-Za-z_][A-Z.a-z0-9_]*$/; + static var BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789%:"; + + public function new(v) { + this.value = v; + pos = 0; + tabs = 0; + } + + public function encode() : String { + #if !hscript + throw "You can't edit this without -lib hscript"; + return null; + #else + if( value == "" ) + return ""; + var p = new hscript.Parser(); + p.allowJSON = true; + var e = p.parse(new haxe.io.StringInput(value)); + buf = new StringBuf(); + shash = new Map(); + scount = 0; + encodeRec(e); + return buf.toString(); + #end + } + + #if hscript + + inline function expr( e ){ + #if hscriptPos + return e.e; + #else + return e; + #end + } + + function getString( e : Expr ) { + return (e == null) ? null : switch( expr(e) ) { + case EConst(v): + switch(v) { + case CString(s): s; + default: null; + } + default: null; + }; + } + + function getPath( e : Expr ) { + if( e == null ) + return null; + switch( expr(e) ) { + case EConst(v): + return switch(v) { + case CString(s): s; + default: null; + } + case EIdent(v): + return v; + case EField(e, f): + var path = "." + f; + while( true ) { + switch( expr(e) ) { + case EIdent(i): return i + path; + case EField(p, f): path = "." + f + path; e = p; + default: return null; + } + } + default: + } + return null; + } + + function encodeRec( e : Expr ) { + switch( expr(e) ) { + case EConst(v): + switch(v) { + case CString(s): + encodeString(s); + case CInt(v): + if( v == 0 ) { + buf.add("z"); + return; + } + buf.add("i"); + buf.add(v); + case CFloat(v): + if( Math.isNaN(v) ) + buf.add("k"); + else if( !Math.isFinite(v) ) + buf.add(if( v < 0 ) "m" else "p"); + else { + buf.add("d"); + buf.add(v); + } + #if !haxe3 + case CInt32(i): + buf.add("d"); + buf.add(v); + #end + } + case EUnop(op, _, es): + if( op == "-" ) + switch( expr(es) ) { + case EConst(v): + switch(v) { + case CInt(i): + #if hscriptPos + encodeRec({e: EConst(CInt(-i)), pmin: es.pmin, pmax: es.pmax}); + #else + encodeRec(EConst(CInt(-i))); + #end + return; + case CFloat(f): + #if hscriptPos + encodeRec({e: EConst(CFloat(-f)), pmin: es.pmin, pmax: es.pmax}); + #else + encodeRec(EConst(CFloat(-f))); + #end + return; + default: + } + default: + } + throw "Unsupported " + Type.enumConstructor(expr(e)); + case EIdent(v): + switch( v ) { + case "null": + buf.add("n"); + case "true": + buf.add("t"); + case "false": + buf.add("f"); + case "NaN": + buf.add("k"); + case "Inf": + buf.add("p"); + case "NegInf": + buf.add("m"); + default: + throw "Unknown identifier " + v; + } + case EArrayDecl(el): + var ucount = 0; + buf.add("a"); + for( e in el ) { + switch( expr(e) ) { + case EIdent(i): + if( i == "null" ) { + ucount++; + continue; + } + default: + } + if( ucount > 0 ) { + if( ucount == 1 ) + buf.add("n"); + else { + buf.add("u"); + buf.add(ucount); + } + ucount = 0; + } + encodeRec(e); + } + if( ucount > 0 ) { + if( ucount == 1 ) + buf.add("n"); + else { + buf.add("u"); + buf.add(ucount); + } + } + buf.add("h"); + case EObject(fields): + buf.add("o"); + for( f in fields ) { + encodeString(f.name); + encodeRec(f.e); + } + buf.add("g"); + case ECall(e, params): + switch( expr(e) ) { + case EIdent(call): + switch(call) { + case "empty": + if( params.length == 0 ) + return; + case "invalid": + var str = getString(params[0]); + if( params.length == 1 && str != null ) { + buf.add(str); + return; + } + case "list": + buf.add("l"); + for( e in params ) + encodeRec(e); + buf.add("h"); + return; + case "date": + var str = getString(params[0]); + // check format + if( params.length == 1 && str != null ) { + var d = Date.fromString(str); + buf.add("v"); + buf.add(d.toString()); + return; + } + case "now": + if( params.length == 0 ) { + buf.add("v"); + buf.add(Date.now()); + return; + } + case "error": + if( params.length == 1 ) { + buf.add("x"); + encodeRec(params[0]); + return; + } + case "hash": + if( params.length == 1 ) + switch( expr(params[0]) ) { + case EObject(fields): + buf.add("b"); + for( f in fields ) { + encodeString(f.name); + encodeRec(f.e); + } + buf.add("h"); + return; + default: + } + case "inthash": + if( params.length == 1 ) + switch( expr(params[0]) ) { + case EObject(fields): + buf.add("q"); + for( f in fields ) { + if( !~/^-?[0-9]+$/.match(f.name) ) + throw "Invalid IntHash key '"+f.name+"'"; + buf.add(":"); + buf.add(f.name); + encodeRec(f.e); + } + buf.add("h"); + return; + default: + } + case "bytes": + var str = getString(params[0]); + if( params.length == 1 && str != null ) { + for( i in 0...str.length ) + if( BASE64.indexOf(str.charAt(i)) == -1 ) + throw "Invalid Base64 char"; + buf.add("s"); + buf.add(str.length); + buf.add(":"); + buf.add(str); + return; + } + case "indexes": + if( params.length == 1 ) { + useEnumIndex = true; + encodeRec(params[0]); + return; + } + case "ref": + if( params.length == 1 ) { + switch( expr(params[0]) ) { + case EConst(v): + switch(v) { + case CInt(i): + buf.add("r"); + buf.add(i); + return; + default: + } + default: + } + } + default: + throw "Unsupported call '"+call+"'"; + } + case EField(e, f): + encodeEnum(e, f, params); + return; + case EArray(e, index): + encodeEnum(e, index, params); + return; + default: + } + throw "Unsupported call"; + case EField(e, f): + encodeEnum(e, f, []); + case EArray(e,index): + encodeEnum(e, index, []); + case ENew(c, params): + var fields = null, cname = null; + if( c == "class" ) { + if( params.length == 2 ) { + cname = getString(params[0]); + fields = switch( expr(params[1]) ) { case EObject(fields): fields; default : null; } + } + } else if( c == "custom" ) { + cname = getPath(params[0]); + if( cname != null ) { + buf.add("C"); + encodeString(cname); + for( i in 1...params.length ) + encodeRec(params[i]); + buf.add("g"); + return; + } + } else { + if( params.length == 1 ) { + cname = c; + fields = switch( expr(params[0]) ) { case EObject(fields): fields; default : null; } + } + } + if( cname == null || fields == null ) + throw "Invalid 'new'"; + buf.add("c"); + encodeString(cname); + for( f in fields ) { + encodeString(f.name); + encodeRec(f.e); + } + buf.add("g"); + default: + throw "Unsupported " + Type.enumConstructor(expr(e)); + } + } + + function encodeEnum( e : Expr, ?name : String, ?eindex : Expr, args : Array ) { + var ename = getPath(e); + if( ename == null ) + throw "Invalid enum path"; + var index : Null = null; + if( eindex != null ) { + switch( expr(eindex) ) { + case EConst(c): + switch( c ) { + case CInt(i): index = i; + case CString(s): name = s; + default: + } + default: + } + if( index == null && name == null ) throw "Invalid enum index"; + } + if( name != null ) { + var e = try Type.resolveEnum(ename) catch( e : Dynamic ) null; + if( e == null ) { + if( useEnumIndex ) + throw "Unknown enum '" + ename + "' : use index"; + } else { + index = Lambda.indexOf(Type.getEnumConstructs(e), name); + if( index < 0 ) throw name + " is not part of enum " + ename + "(" + Type.getEnumConstructs(e).join(",") + ")"; + if( useEnumIndex ) name = null else index = null; + } + } + buf.add((index != null)?"j":"w"); + encodeString(ename); + if( index != null ) { + buf.add(":"); + buf.add(index); + } else + encodeString(name); + buf.add(":"); + buf.add(args.length); + for( a in args ) + encodeRec(a); + } + + function encodeString( s : String ) { + var x = shash.get(s); + if( x != null ) { + buf.add("R"); + buf.add(x); + return; + } + shash.set(s,scount++); + buf.add("y"); + s = StringTools.urlEncode(s); + buf.add(s.length); + buf.add(":"); + buf.add(s); + } + #end + + function quote( s : String, ?r : EReg ) { + if( r != null && r.match(s) ) + return s; + return "'"+s.split("\\").join("\\\\").split("'").join("\\'").split("\n").join("\\n").split("\r").join("\\r").split("\t").join("\\t")+"'"; + } + + public function escape() { + if( value == "" ) + return "empty()"; + buf = new StringBuf(); + scache = new Array(); + try loop() catch( e : Errors ) pos = -1; + if( pos != value.length ) + return "invalid(" + quote(value) + ")"; + var str = buf.toString(); + if( useEnumIndex ) + str = "indexes(" + str + ")"; + return str; + } + + inline function get(pos) { + return value.charCodeAt(pos); + } + + function readDigits() { + var k = 0; + var s = false; + var fpos = pos; + while( true ) { + var c = get(pos); + if( c == null ) + break; + if( c == "-".code ) { + if( pos != fpos ) + break; + s = true; + pos++; + continue; + } + if( c < "0".code || c > "9".code ) + break; + k = k * 10 + (c - "0".code); + pos++; + } + if( s ) + k *= -1; + return k; + } + + function loop() { + switch( get(pos++) ) { + case "n".code: + buf.add(null); + case "i".code: + buf.add(readDigits()); + case "z".code: + buf.add(0); + case "t".code: + buf.add(true); + case "f".code: + buf.add(false); + case "k".code: + buf.add("NaN"); + case "p".code: + buf.add("Inf"); + case "m".code: + buf.add("NegInf"); + case "d".code: + var p1 = pos; + while( true ) { + var c = get(pos); + // + - . , 0-9 + if( (c >= 43 && c < 58) || c == "e".code || c == "E".code ) + pos++; + else + break; + } + buf.add(value.substr(p1, pos - p1)); + case "a".code: + open("[",", "); + while( true ) { + var c = get(pos); + if( c == "h".code ) { + pos++; + break; + } + if( c == "u".code ) { + pos++; + for( i in 0...readDigits() - 1 ) { + buf.add("null"); + next(); + } + buf.add("null"); + } else + loop(); + next(); + } + close("]"); + case "y".code, "R".code: + pos--; + buf.add(quote(readString())); + case "l".code: + open("list(",", "); + while( get(pos) != "h".code ) { + loop(); + next(); + } + close(")"); + pos++; + case "v".code: + buf.add("date("); + buf.add(quote(value.substr(pos, 19))); + buf.add(")"); + pos += 19; + case "x".code: + buf.add("error("); + loop(); + buf.add(")"); + case "o".code: + loopObj("g".code); + case "b".code: + buf.add("hash("); + loopObj("h".code); + buf.add(")"); + case "q".code: + buf.add("inthash("); + open("{",", "," "); + var c = get(pos++); + while( c == ":".code ) { + buf.add("'"+readDigits()+"'"); + buf.add(" : "); + loop(); + c = get(pos++); + next(); + } + if( c != "h".code ) + throw Invalid; + close("}", " "); + buf.add(")"); + case "s".code: + var len = readDigits(); + if( get(pos++) != ":".code || value.length - pos < len ) + throw Invalid; + buf.add("bytes("); + buf.add(quote(value.substr(pos, len))); + buf.add(")"); + pos += len; + case "w".code: + buf.add(quote(readString(), clname)); + var constr = readString(); + if( ident.match(constr) ) + buf.add("." + constr); + else + buf.add("["+quote(constr)+"]"); + if( get(pos++) != ":".code ) + throw Invalid; + var nargs = readDigits(); + if( nargs > 0 ) { + buf.add("("); + for( i in 0...nargs ) { + if( i > 0 ) buf.add(", "); + loop(); + } + buf.add(")"); + } + case "j".code: + var cl = readString(); + buf.add(quote(cl, clname)); + if( get(pos++) != ":".code ) + throw Invalid; + var index = readDigits(); + var e = Type.resolveEnum(cl); + if( e == null ) + buf.add("["+index+"]"); + else { + useEnumIndex = true; + buf.add("."+Type.getEnumConstructs(e)[index]); + } + if( get(pos++) != ":".code ) + throw Invalid; + var nargs = readDigits(); + if( nargs > 0 ) { + buf.add("("); + for( i in 0...nargs ) { + if( i > 0 ) buf.add(", "); + loop(); + } + buf.add(")"); + } + case "c".code: + buf.add("new "); + var cl = readString(); + if( clname.match(cl) ) + buf.add(cl + "("); + else { + buf.add("class("); + buf.add(quote(cl)); + buf.add(","); + } + loopObj("g".code); + buf.add(")"); + case "C".code: + open("new custom(",", "); + buf.add(quote(readString(), clname)); + next(); + while( get(pos) != "g".code ) { + loop(); + next(); + } + close(")"); + pos++; + case "r".code: + buf.add("ref("+readDigits()+")"); + default: + throw Invalid; + } + } + + function readString() : String { + switch( value.charCodeAt(pos++) ) { + case "y".code: + var len = readDigits(); + if( get(pos++) != ":".code || value.length - pos < len ) + throw Invalid; + var s = value.substr(pos,len); + pos += len; + s = StringTools.urlDecode(s); + scache.push(s); + return s; + case "R".code: + var n = readDigits(); + if( n < 0 || n >= scache.length ) + throw "Invalid string reference"; + return scache[n]; + default: + throw Invalid; + } + } + + function loopObj(eof) { + open("{",", "," "); + while( true ) { + if( pos >= value.length ) + throw Invalid; + if( get(pos) == eof ) + break; + buf.add(quote(readString(), ident)); + buf.add(" : "); + loop(); + next(); + } + close("}"," "); + pos++; + } + + function open(str, sep, ?prefix) { + buf.add(str); + tabs++; + cur = { old : cur, sep : sep, prefix : prefix, lines : [], buf : buf, totalSize : 0, maxSize : 0 }; + buf = new StringBuf(); + } + + function next() { + var line = buf.toString(); + if( line.length > cur.maxSize ) cur.maxSize = line.length; + cur.totalSize += line.length; + cur.lines.push(line); + buf = new StringBuf(); + } + + function close(end,?postfix) { + buf = cur.buf; + var t = "\n"; + for( i in 0...tabs-1 ) + t += IDENT; + if( t.length + cur.totalSize > 80 && cur.maxSize > 10 ) { + var first = true; + for( line in cur.lines ) { + if( first ) first = false else buf.add(cur.sep); + buf.add(t + IDENT + line); + } + buf.add(t); + buf.add(end); + } else { + if( cur.prefix != null && cur.lines.length > 0 ) buf.add(cur.prefix); + var first = true; + for( line in cur.lines ) { + if( first ) first = false else buf.add(cur.sep); + buf.add(line); + } + if( !first && postfix != null ) buf.add(postfix); + buf.add(end); + } + cur = cur.old; + tabs--; + } + +} \ No newline at end of file diff --git a/sys/db/TableInfos.hx b/sys/db/TableInfos.hx new file mode 100644 index 0000000..91924b6 --- /dev/null +++ b/sys/db/TableInfos.hx @@ -0,0 +1,560 @@ +/* + * Copyright (c)2012 Nicolas Cannasse + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package sys.db; + +import sys.db.Object; +import sys.db.Manager; + +typedef TableType = sys.db.RecordInfos.RecordType; + +typedef ManagerAccess = { + private var table_name : String; + private var table_keys : Array; + private function quote( v : Dynamic ) : String; + private function quoteField( f : String ) : String; + private function addKeys( s : StringBuf, x : {} ) : Void; + function all( ?lock : Bool ) : List; + function dbClass() : Class; +} + +private typedef TableRelation = { + var prop : String; + var key : String; + var lock : Bool; + var manager : ManagerAccess; + var className : String; + var cascade : Bool; +} + +class TableInfos { + + public static var ENGINE = "InnoDB"; + public static var OLD_COMPAT = false; // only set for old DBs ! + + public var primary(default,null) : List; + public var cl(default,null) : Class; + public var name(default,null) : String; + public var className(default,null) : String; + public var hfields(default,null) : Map; + public var fields(default,null) : List<{ name : String, type : TableType }>; + public var nulls(default,null) : Map; + public var relations(default,null) : Array; + public var indexes(default,null) : List<{ keys : List, unique : Bool }>; + public var manager : Manager; + + public function new( cname : String ) { + hfields = new Map(); + fields = new List(); + nulls = new Map(); + cl = cast Type.resolveClass("db."+cname); + if( cl == null ) + cl = cast Type.resolveClass(cname); + else + cname = "db."+cname; + if( cl == null ) + throw "Class not found : "+cname; + manager = untyped cl.manager; + if( manager == null ) + throw "No static manager for "+cname; + className = cname; + if( className.substr(0,3) == "db." ) className = className.substr(3); + var a = cname.split("."); + name = a.pop(); + processClass(); + } + + function processClass() { + var rtti = haxe.rtti.Meta.getType(cl).rtti; + if( rtti == null ) + throw "Class "+name+" does not have RTTI"; + var infos : sys.db.RecordInfos = haxe.Unserializer.run(rtti[0]); + name = infos.name; + primary = Lambda.list(infos.key); + for( f in infos.fields ) { + fields.add({ name : f.name, type : f.t }); + hfields.set(f.name, f.t); + if( f.isNull ) nulls.set(f.name, true); + } + relations = new Array(); + for( r in infos.relations ) { + var t = Type.resolveClass(r.type); + if( t == null ) throw "Missing type " + r.type + " for relation " + name + "." + r.prop; + var manager : ManagerAccess = Reflect.field(t, "manager"); + if( manager == null ) throw r.type + " does not have a static field manager"; + relations.push( { prop : r.prop, key : r.key, lock : r.lock, manager : manager, className : Type.getClassName(manager.dbClass()), cascade : r.cascade } ); + } + indexes = new List(); + for( i in infos.indexes ) + indexes.push( { keys : Lambda.list(i.keys), unique : i.unique } ); + } + + function escape( name : String ) { + var m : ManagerAccess = manager; + return m.quoteField(name); + } + + public static function unescape( field : String ) { + if( field.length > 1 && field.charAt(0) == '`' && field.charAt(field.length-1) == '`' ) + return field.substr(1,field.length-2); + return field; + } + + public function isRelationActive( r : Dynamic ) { + return true; + } + + public function createRequest( full : Bool ) { + var str = "CREATE TABLE "+escape(name)+" (\n"; + var keys = fields.iterator(); + for( f in keys ) { + str += escape(f.name)+" "+fieldInfos(f); + if( keys.hasNext() ) + str += ","; + str += "\n"; + } + if( primary != null ) + str += ", PRIMARY KEY ("+primary.map(escape).join(",")+")\n"; + if( full ) { + for( r in relations ) + if( isRelationActive(r) ) + str += ", "+relationInfos(r); + for( i in indexes ) + str += ", "+(if( i.unique ) "UNIQUE " else "")+"KEY "+escape(name+"_"+i.keys.join("_"))+"("+i.keys.map(escape).join(",")+")\n"; + } + str += ")"; + if( ENGINE != null ) + str += " ENGINE="+ENGINE; + return str; + } + + function relationInfos(r : TableRelation) { + if( r.manager.table_keys.length != 1 ) + throw "Relation on a multiple-keys table"; + var rq = "CONSTRAINT "+escape(name+"_"+r.prop)+" FOREIGN KEY ("+escape(r.key)+") REFERENCES "+escape(r.manager.table_name)+"("+escape(r.manager.table_keys[0])+") "; + rq += "ON DELETE "+(if( nulls.get(r.key) && r.cascade != true ) "SET NULL" else "CASCADE")+"\n"; + return rq; + } + + function fieldInfos(f) { + return (switch( f.type ) { + case DId: "INT AUTO_INCREMENT"; + case DUId: "INT UNSIGNED AUTO_INCREMENT"; + case DInt, DEncoded: "INT"; + case DFlags(fl, auto): auto ? (fl.length <= 8 ? "TINYINT UNSIGNED" : (fl.length <= 16 ? "SMALLINT UNSIGNED" : (fl.length <= 24 ? "MEDIUMINT UNSIGNED" : "INT"))) : "INT"; + case DTinyInt: "TINYINT"; + case DUInt: "INT UNSIGNED"; + case DSingle: "FLOAT"; + case DFloat: "DOUBLE"; + case DBool: "TINYINT(1)"; + case DString(n): "VARCHAR("+n+")"; + case DDate: "DATE"; + case DDateTime: "DATETIME"; + case DTimeStamp: "TIMESTAMP"+(nulls.exists(f.name) ? " NULL DEFAULT NULL" : " DEFAULT 0"); + case DTinyText: "TINYTEXT"; + case DSmallText: "TEXT"; + case DText, DSerialized: "MEDIUMTEXT"; + case DSmallBinary: "BLOB"; + case DBinary, DNekoSerialized: "MEDIUMBLOB"; + case DData: "MEDIUMBLOB"; + case DEnum(_): "TINYINT UNSIGNED"; + case DLongBinary: "LONGBLOB"; + case DBigInt: "BIGINT"; + case DBigId: "BIGINT AUTO_INCREMENT"; + case DBytes(n): "BINARY(" + n + ")"; + case DTinyUInt: "TINYINT UNSIGNED"; + case DSmallInt: "SMALLINT"; + case DSmallUInt: "SMALLINT UNSIGNED"; + case DMediumInt: "MEDIUMINT"; + case DMediumUInt: "MEDIUMINT UNSIGNED"; + case DNull, DInterval: throw "assert"; + }) + if( nulls.exists(f.name) ) "" else " NOT NULL"; + } + + public function dropRequest() { + return "DROP TABLE "+escape(name); + } + + public function truncateRequest() { + return "TRUNCATE TABLE "+escape(name); + } + + public function descriptionRequest() { + return "SHOW CREATE TABLE "+escape(name); + } + + public function existsRequest() { + return "SELECT * FROM "+escape(name)+" LIMIT 0"; + } + + public static function countRequest( m : ManagerAccess, max : Int ) { + return "SELECT " + m.quoteField(m.table_keys[0]) + " FROM " + m.quoteField(m.table_name) + " LIMIT " + max; + } + + public function addFieldRequest( fname : String ) { + var ftype = hfields.get(fname); + if( ftype == null ) + throw "No field "+fname; + var rq = "ALTER TABLE "+escape(name)+" ADD "; + return rq + escape(fname)+" "+fieldInfos({ name : fname, type : ftype }); + } + + public function removeFieldRequest( fname : String ) { + return "ALTER TABLE "+escape(name)+" DROP "+escape(fname); + } + + public function renameFieldRequest( old : String, newname : String ) { + var ftype = hfields.get(newname); + if( ftype == null ) + throw "No field "+newname; + var rq = "ALTER TABLE "+escape(name)+" CHANGE "+escape(old)+" "; + return rq + escape(newname) + " " + fieldInfos({ name : newname, type : ftype }); + } + + public function updateFieldRequest( fname : String ) { + var ftype = hfields.get(fname); + if( ftype == null ) + throw "No field "+fname; + var rq = "ALTER TABLE "+escape(name)+" MODIFY "; + return rq + escape(fname)+" "+fieldInfos({ name : fname, type : ftype }); + } + + public function addRelationRequest( key : String, prop : String ) { + for( r in relations ) + if( r.key == key && r.prop == prop ) + return "ALTER TABLE "+escape(name)+" ADD "+relationInfos(r); + return throw "No such relation : "+prop+"("+key+")"; + } + + public function deleteRelationRequest( rel : String ) { + return "ALTER TABLE "+escape(name)+" DROP FOREIGN KEY "+escape(rel); + } + + public function indexName( idx : Array ) { + return name+"_"+idx.join("_"); + } + + public function addIndexRequest( idx : Array, unique : Bool ) { + var eidx = new Array(); + for( i in idx ) { + var k = escape(i); + var f = hfields.get(i); + if( f != null ) + switch( f ) { + case DTinyText, DSmallText, DText, DSmallBinary, DLongBinary, DBinary: + k += "(4)"; // index size + default: + } + eidx.push(k); + } + return "ALTER TABLE "+escape(name)+" ADD "+(if( unique ) "UNIQUE " else "")+"INDEX "+escape(indexName(idx))+"("+eidx.join(",")+")"; + } + + public function deleteIndexRequest( idx : String ) { + return "ALTER TABLE "+escape(name)+" DROP INDEX "+escape(idx); + } + + public function updateFields( o : {}, fields : List<{ name : String, value : Dynamic }> ) { + var me = this; + var s = new StringBuf(); + s.add("UPDATE "); + s.add(escape(name)); + s.add(" SET "); + var first = true; + for( f in fields ) { + if( first ) + first = false; + else + s.add(", "); + s.add(escape(f.name)); + s.add(" = "); + Manager.cnx.addValue(s,f.value); + } + s.add(" WHERE "); + var m : ManagerAccess = manager; + m.addKeys(s,o); + return s.toString(); + } + + public function identifier( o : Object ) : String { + if( primary == null ) + throw "No primary key"; + return primary.map(function(p) { return Std.string(Reflect.field(o,p)).split(".").join("~"); }).join("@"); + } + + public function fromIdentifier( id : String ) : Object { + var ids = id.split("@"); + if( primary == null ) + throw "No primary key"; + if( ids.length != primary.length ) + throw "Invalid identifier"; + var keys = {}; + for( p in primary ) + Reflect.setField(keys, p, makeNativeValue(hfields.get(p), ids.shift().split("~").join("."))); + return manager.unsafeGetWithKeys(keys); + } + + function makeNativeValue( t : TableType, v : String ) : Dynamic { + return switch( t ) { + case DInt, DUInt, DId, DUId, DEncoded, DFlags(_), DTinyInt: cast Std.parseInt(v); + case DTinyUInt, DSmallInt, DSmallUInt, DMediumUInt, DMediumInt: cast Std.parseInt(v); + case DFloat, DSingle, DBigInt, DBigId: cast Std.parseFloat(v); + case DDate, DDateTime, DTimeStamp: cast Date.fromString(v); + case DBool: cast (v == "true"); + case DText, DString(_), DSmallText, DTinyText, DBinary, DSmallBinary, DLongBinary, DSerialized, DNekoSerialized, DBytes(_): cast v; + case DData: cast v; + case DEnum(_): cast v; + case DNull, DInterval: throw "assert"; + }; + } + + public function fromSearch( params : Map, order : String, pos : Int, count : Int ) : List { + var rop = ~/^([<>]=?)(.+)$/; + var cond = "TRUE"; + var m : ManagerAccess = manager; + for( p in params.keys() ) { + var f = hfields.get(p); + var v = params.get(p); + if( f == null ) + continue; + cond += " AND " + escape(p); + if( v == null || v == "NULL" ) + cond += " IS NULL"; + else switch( f ) { + case DEncoded: + cond += " = "+(try Id.encode(v) catch( e : Dynamic ) 0); + case DString(_),DTinyText,DSmallText,DText: + cond += " LIKE "+m.quote(v); + case DBool: + cond += " = "+((v == "true") ? 1 : 0); + case DId,DUId,DInt,DUInt,DSingle,DFloat,DDate,DDateTime,DBigInt,DBigId: + if( rop.match(v) ) + cond += " "+rop.matched(1)+" "+m.quote(rop.matched(2)); + else + cond += " = "+m.quote(v); + default: + cond += " = "+m.quote(v); + } + } + if( order != null ) { + if( order.charAt(0) == "-" ) + cond += " ORDER BY "+escape(order.substr(1))+" DESC"; + else + cond += " ORDER BY "+escape(order); + } + + var sql = "SELECT * FROM " + escape(name) + " WHERE " + cond + " LIMIT " + pos + "," + count; + return manager.unsafeObjects(sql, false); + } + + static function fromTypeDescription( desc : String ) { + var fdesc = desc.toUpperCase().split(" "); + var ftype = fdesc.shift(); + var tparam = ~/^([A-Za-z]+)\(([0-9]+)\)$/; + var param = null; + if( tparam.match(ftype) ) { + ftype = tparam.matched(1); + param = Std.parseInt(tparam.matched(2)); + } + var nullable = true; + var t = switch( ftype ) { + case "VARCHAR","CHAR": + if( param == null ) + null; + else + DString(param); + case "INT": + if( param == 11 && fdesc.remove("AUTO_INCREMENT") ) + DId + else if( param == 10 && fdesc.remove("UNSIGNED") ) { + if( fdesc.remove("AUTO_INCREMENT") ) + DUId + else + DUInt; + } else if( param == 11 ) + DInt; + else + null; + case "BIGINT": + if( fdesc.remove("AUTO_INCREMENT") ) DBigId else DBigInt; + case "DOUBLE": DFloat; + case "FLOAT": DSingle; + case "DATE": DDate; + case "DATETIME": DDateTime; + case "TIMESTAMP": DTimeStamp; + case "TINYTEXT": DTinyText; + case "TEXT": DSmallText; + case "MEDIUMTEXT": DText; + case "BLOB": DSmallBinary; + case "MEDIUMBLOB": DBinary; + case "LONGBLOB": DLongBinary; + case "TINYINT": + switch( param ) { + case 1: + fdesc.remove("UNSIGNED"); + DBool; + case 4: + DTinyInt; + case 3: + if( fdesc.remove("UNSIGNED") ) DTinyUInt else null; + default: + if( OLD_COMPAT ) + DInt; + else + null; + } + case "SMALLINT": + fdesc.remove("UNSIGNED") ? DSmallUInt : DSmallInt; + case "MEDIUMINT": + fdesc.remove("UNSIGNED") ? DMediumUInt : DMediumInt; + case "BINARY": + if( param == null ) + null; + else + DBytes(param); + default: + null; + } + if( t == null ) + return null; + while( fdesc.length > 0 ) { + var d = fdesc.shift(); + switch( d ) { + case "NOT": + if( fdesc.shift() != "NULL" ) + return null; + nullable = false; + case "DEFAULT": + var v = fdesc.shift(); + if( nullable ) { + if( v == "NULL" ) + continue; + return null; + } + var def = switch( t ) { + case DId, DUId, DInt, DUInt, DBool, DSingle, DFloat, DEncoded, DBigInt, DBigId, DFlags(_), DTinyInt: "'0'"; + case DTinyUInt, DSmallInt, DSmallUInt, DMediumUInt, DMediumInt: "'0'"; + case DTinyText, DText, DString(_), DSmallText, DSerialized: "''"; + case DDateTime,DTimeStamp: + if( v.length > 0 && v.charAt(v.length-1) != "'" ) + v += " "+fdesc.shift(); + "'0000-00-00 00:00:00'"; + case DDate: "'0000-00-00'"; + case DSmallBinary, DBinary, DLongBinary, DNekoSerialized, DBytes(_), DNull, DInterval: null; + case DData: null; + case DEnum(_): "'0'"; + } + if( v != def && !OLD_COMPAT ) + return null; + case "NULL": + if( !nullable ) return null; + nullable = true; + continue; + default: + return null; + } + } + return { t : t, nullable : nullable }; + } + + public static function fromDescription( desc : String ) { + var r = ~/^CREATE TABLE `([^`]*)` \((.*)\)( ENGINE=([^ ]+))?( AUTO_INCREMENT=[^ ]+)?( DEFAULT CHARSET=.*)?$/sm; + if( !r.match(desc) ) + throw "Invalid "+desc; + var tname = r.matched(1); + if( r.matched(4).toUpperCase() != "INNODB" ) + throw "Table "+tname+" should be INNODB"; + var matches = r.matched(2).split(",\n"); + var field_r = ~/^[ \r\n]*`(.*)` (.*)$/; + var primary_r = ~/^[ \r\n]*PRIMARY KEY +\((.*)\)[ \r\n]*$/; + var index_r = ~/^[ \r\n]*(UNIQUE )?KEY `(.*)` \((.*)\)[ \r\n]*$/; + var foreign_r = ~/^[ \r\n]*CONSTRAINT `(.*)` FOREIGN KEY \(`(.*)`\) REFERENCES `(.*)` \(`(.*)`\) ON DELETE (SET NULL|CASCADE)[ \r\n]*$/; + var index_key_r = ~/^`?(.*?)`?(\([0-9+]\))?$/; + var fields = new Map(); + var nulls = new Map(); + var indexes = new Map(); + var relations = new Array(); + var primary = null; + for( f in matches ) { + if( field_r.match(f) ) { + var fname = field_r.matched(1); + var ftype = fromTypeDescription(field_r.matched(2)); + if( ftype == null ) + throw "Unknown description '"+field_r.matched(2)+"'"; + fields.set(fname,ftype.t); + if( ftype.nullable ) + nulls.set(fname,true); + } else if( primary_r.match (f) ) { + if( primary != null ) + throw "Duplicate primary key"; + primary = primary_r.matched(1).split(","); + for( i in 0...primary.length ) { + var k = unescape(primary[i]); + primary[i] = k; + } + } else if( index_r.match(f) ) { + var unique = index_r.matched(1); + var idxname = index_r.matched(2); + var fs = Lambda.list(index_r.matched(3).split(",")); + indexes.set(idxname,{ keys : fs.map(function(r) { + if( !index_key_r.match(r) ) throw "Invalid index key "+r; + return index_key_r.matched(1); + }), unique : unique != "" && unique != null, name : idxname }); + } else if( foreign_r.match(f) ) { + var name = foreign_r.matched(1); + var key = foreign_r.matched(2); + var table = foreign_r.matched(3); + table = table.substr(0,1).toUpperCase() + table.substr(1); // hack for MySQL on windows + var id = foreign_r.matched(4); + var setnull = if( foreign_r.matched(5) == "SET NULL" ) true else null; + relations.push({ name : name, key : key, table : table, id : id, setnull : setnull }); + } else + throw "Invalid "+f+" in "+desc; + } + return { + table : tname, + fields : fields, + nulls : nulls, + indexes : indexes, + relations : relations, + primary : primary, + }; + } + + public static function sameDBStorage( dt : TableType, rt : TableType ) { + return switch( rt ) { + case DEncoded: dt == DInt; + case DFlags(fl, auto): auto ? (fl.length <= 8 ? dt == DTinyUInt : (fl.length <= 16 ? dt == DSmallUInt : (fl.length <= 24 ? dt == DMediumUInt : dt == DInt))) : (dt == DInt); + case DSerialized: (dt == DText); + case DNekoSerialized: (dt == DBinary); + case DData: dt == DBinary; + case DEnum(_): dt == DTinyUInt; + default: false; + }; + } + + public static function allTablesRequest() { + return "SHOW TABLES"; + } + +} +