Ver código fonte

added orderBy/limit options, string concat, fixed get/getWithKeys

Nicolas Cannasse 14 anos atrás
pai
commit
09dc737aae
2 arquivos alterados com 279 adições e 49 exclusões
  1. 48 13
      std/neko/db/MacroManager.hx
  2. 231 36
      std/neko/db/SpodData.hx

+ 48 - 13
std/neko/db/MacroManager.hx

@@ -98,30 +98,43 @@ class MacroManager<T : Object> {
 		return SpodData.macroGet(ethis,id,lock);
 	}
 
-	@:macro public function object(ethis, cond, ?lock:haxe.macro.Expr.ExprRequire<Bool>) : #if macro haxe.macro.Expr #else haxe.macro.Expr.ExprRequire<T> #end {
-		return SpodData.macroSearch(ethis, cond, lock, true);
+	@:macro public function select(ethis, cond, ?options, ?lock:haxe.macro.Expr.ExprRequire<Bool>) : #if macro haxe.macro.Expr #else haxe.macro.Expr.ExprRequire<T> #end {
+		return SpodData.macroSearch(ethis, cond, options, lock, true);
 	}
 
-	@:macro public function search(ethis, cond, ?lock:haxe.macro.Expr.ExprRequire<Bool>) : #if macro haxe.macro.Expr #else haxe.macro.Expr.ExprRequire<List<T>> #end {
-		return SpodData.macroSearch(ethis, cond, lock);
+	@:macro public function search(ethis, cond, ?options, ?lock:haxe.macro.Expr.ExprRequire<Bool>) : #if macro haxe.macro.Expr #else haxe.macro.Expr.ExprRequire<List<T>> #end {
+		return SpodData.macroSearch(ethis, cond, options, lock);
 	}
 
 	@:macro public function count(ethis, cond) : #if macro haxe.macro.Expr #else haxe.macro.Expr.ExprRequire<Int> #end {
 		return SpodData.macroCount(ethis, cond);
 	}
 
-	@:macro public function delete(ethis, cond) : #if macro haxe.macro.Expr #else haxe.macro.Expr.ExprRequire<Void> #end {
+	@:macro public function deleteCond(ethis, cond) : #if macro haxe.macro.Expr #else haxe.macro.Expr.ExprRequire<Void> #end {
 		return SpodData.macroDelete(ethis, cond);
 	}
 	
+	public function dynamicSearch( x : {}, ?lock : Bool ) : List<T> {
+		var s = new StringBuf();
+		s.add("SELECT * FROM ");
+		s.add(table_name);
+		s.add(" WHERE ");
+		addCondition(s,x);
+		return unsafeObjects(s.toString(),lock);
+	}
+	
 	// for backward compatibility
 	#if spod_compat
 	@:macro public function getWithKeys(ethis,cond,?lock:haxe.macro.Expr.ExprRequire<Bool>) : #if macro haxe.macro.Expr #else haxe.macro.Expr.ExprRequire<T> #end {
 		return SpodData.macroGet(ethis,cond,lock);
 	}
 
-	@:macro public function objects(ethis, cond, ?lock:haxe.macro.Expr.ExprRequire<Bool>) : #if macro haxe.macro.Expr #else haxe.macro.Expr.ExprRequire<List<T>> #end {
-		return SpodData.macroSearch(ethis, cond, lock);
+	@:macro public function object(ethis, cond, ?options, ?lock:haxe.macro.Expr.ExprRequire<Bool>) : #if macro haxe.macro.Expr #else haxe.macro.Expr.ExprRequire<T> #end {
+		return SpodData.macroSearch(ethis, cond, options, lock, true);
+	}
+	
+	@:macro public function objects(ethis, cond, ?options, ?lock:haxe.macro.Expr.ExprRequire<Bool>) : #if macro haxe.macro.Expr #else haxe.macro.Expr.ExprRequire<List<T>> #end {
+		return SpodData.macroSearch(ethis, cond, options, lock);
 	}
 	#end
 
@@ -332,13 +345,36 @@ class MacroManager<T : Object> {
 		unsafeExecute(sql);
 	}
 	
-	public function dynamicSearch( x : {}, ?lock : Bool ) : List<T> {
+	public function unsafeGet( id : Dynamic, ?lock : Bool ) : T {
+		if( lock == null ) lock = true;
+		if( table_keys.length != 1 )
+			throw "Invalid number of keys";
+		if( id == null )
+			return null;
+		var x : Dynamic = getFromCacheKey(Std.string(id) + table_name);
+		if( x != null && (!lock || x.update != no_update) )
+			return x;
 		var s = new StringBuf();
 		s.add("SELECT * FROM ");
 		s.add(table_name);
 		s.add(" WHERE ");
-		addCondition(s,x);
-		return unsafeObjects(s.toString(),lock);
+		s.add(quoteField(table_keys[0]));
+		s.add(" = ");
+		cnx.addValue(s,id);
+		return unsafeObject(s.toString(), lock);
+	}
+	
+	public function unsafeGetWithKeys( keys : { }, ?lock : Bool ) : T {
+		if( lock == null ) lock = true;
+		var x : Dynamic = getFromCacheKey(makeCacheKey(cast keys));
+		if( x != null && (!lock || x.update != no_update) )
+			return x;
+		var s = new StringBuf();
+		s.add("SELECT * FROM ");
+		s.add(table_name);
+		s.add(" WHERE ");
+		addKeys(s,keys);
+		return unsafeObject(s.toString(),lock);
 	}
 
 	function addCondition(s : StringBuf,x) {
@@ -400,7 +436,6 @@ class MacroManager<T : Object> {
 		var lock = r.lock;
 		if( manager == null || manager.table_keys == null ) throw ("Invalid manager for relation "+table_name+":"+r.prop);
 		if( manager.table_keys.length != 1 ) throw ("Relation " + r.prop + "(" + r.key + ") on a multiple key table");
-		var sql = "SELECT * FROM " + manager.table_name + " WHERE " + quoteField(manager.table_keys[0]) + " = ";
 		Reflect.setField(class_proto.prototype,"get_"+r.prop,function() {
 			var othis = untyped this;
 			var f = Reflect.field(othis,hprop);
@@ -409,12 +444,12 @@ class MacroManager<T : Object> {
 			var id = Reflect.field(othis, hkey);
 			if( id == null )
 				return null;
-			f = manager.unsafeObject(sql+id,lock);
+			f = manager.unsafeGet(id,lock);
 			// it's highly possible that in that case the object has been inserted
 			// after we started our transaction : in that case, let's lock it, since
 			// it's still better than returning 'null' while it exists
 			if( f == null && id != null && !lock )
-				f = manager.unsafeObject(sql+id,true);
+				f = manager.unsafeGet(id,true);
 			Reflect.setField(othis,hprop,f);
 			return f;
 		});

+ 231 - 36
std/neko/db/SpodData.hx

@@ -20,6 +20,7 @@ private enum BuildError {
 class SpodData {
 
 	static var inst : SpodData = null;
+	static var simpleString = ~/^[A-Za-z0-9 ]*$/;
 
 	var cache : Hash<SpodInfos>;
 	var types : Hash<SpodType>;
@@ -40,7 +41,7 @@ class SpodData {
 		functions = new Hash();
 		for( f in [
 			{ name : "now", params : [], ret : DDateTime, sql : "NOW($)" },
-			{ name : "curdate", params : [], ret : DDate, sql : "CURDATE($)" },
+			{ name : "curDate", params : [], ret : DDate, sql : "CURDATE($)" },
 			{ name : "seconds", params : [DFloat], ret : DInterval, sql : "INTERVAL $ SECOND" },
 			{ name : "minutes", params : [DFloat], ret : DInterval, sql : "INTERVAL $ MINUTE" },
 			{ name : "hours", params : [DFloat], ret : DInterval, sql : "INTERVAL $ HOUR" },
@@ -103,7 +104,7 @@ class SpodData {
 			}
 		case TEnum(e, p):
 			var name = e.toString();
-			switch( name ) {
+			return switch( name ) {
 			case "Bool": DBool;
 			default: throw "Unsupported " + name;
 			}
@@ -288,6 +289,22 @@ class SpodData {
 	}
 	
 	function sqlQuoteValue( v : Expr, t : SpodType ) {
+		switch( v.expr ) {
+		case EConst(c):
+			switch( c ) {
+			case CInt(_), CFloat(_): return v;
+			case CString(s):
+				if( simpleString.match(s) ) return { expr : EConst(CString("'"+s+"'")), pos : v.pos };
+			case CIdent(n):
+				switch( n ) {
+				case "null": return { expr : EConst(CString("NULL")), pos : v.pos };
+				case "true": return { expr : EConst(CInt("1")), pos : v.pos };
+				case "false": return { expr : EConst(CInt("0")), pos : v.pos };
+				}
+			default:
+			}
+		default:
+		}
 		var meth = switch( t ) {
 		case DId, DInt, DUId, DUInt, DEncoded, DFlags(_): "quoteInt";
 		case DBigId, DBigInt, DSingle, DFloat: "quoteFloat";
@@ -335,6 +352,23 @@ class SpodData {
 		};
 	}
 	
+	function convertType( t : SpodType ) {
+		return TPath( {
+			name : switch( unifyClass(t) ) {
+			case 0: "Int";
+			case 1: "Float";
+			case 2: "Bool";
+			case 3: "String";
+			case 4: "Date";
+			case 5: "String";
+			default: throw "assert";
+			},
+			pack : [],
+			params : [],
+			sub : null,
+		});
+	}
+	
 	function unify( t : SpodType, rt : SpodType, pos : Position ) {
 		if( !tryUnify(t, rt) )
 			error(typeStr(t) + " should be " + typeStr(rt), pos);
@@ -382,16 +416,15 @@ class SpodData {
 			unify(r2.t, r1.t, e2.pos);
 			unify(r1.t, r2.t, e1.pos);
 		}
-		// use some different operators if there is a possibility for NULL matching
-		var isNull = r1.n || r2.n;
 		var sql;
-		if( isNull ) {
+		// use some different operators if there is a possibility for comparing two NULLs
+		if( r1.n && r2.n ) {
 			sql = makeOp(" <=> ", r1.sql, r2.sql, pos);
 			if( !eq )
 				sql = sqlAdd(makeString("NOT(", pos), sqlAddString(sql, ")"), pos);
 		} else
 			sql = makeOp(eq?" = ":" != ", r1.sql, r2.sql, pos);
-		return { sql : sql, t : DBool, n : isNull };
+		return { sql : sql, t : DBool, n : r1.n || r2.n };
 	}
 	
 	function buildDefault( cond : Expr ) {
@@ -407,6 +440,7 @@ class SpodData {
 		case EObjectDecl(fl):
 			var first = true;
 			var sql = makeString("(", p);
+			var fields = new Hash();
 			for( f in fl ) {
 				var fi = current.hfields.get(f.field);
 				if( fi == null ) error("No database field " + f.field, p);
@@ -416,12 +450,16 @@ class SpodData {
 					sql = sqlAddString(sql, " AND ");
 				sql = sqlAddString(sql, quoteField(fi.name) + " = ");
 				sql = sqlAddValue(sql, f.expr, fi.t);
+				if( fields.exists(fi.name) )
+					error("Duplicate field " + fi.name, p);
+				else
+					fields.set(fi.name, true);
 			}
 			if( first ) sqlAddString(sql, "TRUE");
 			sql = sqlAddString(sql, ")");
 			return { sql : sql, t : DBool, n : false };
 		case EParenthesis(e):
-			var r = buildCond(cond);
+			var r = buildCond(e);
 			r.sql = sqlAdd(makeString("(", p), r.sql, p);
 			r.sql = sqlAddString(r.sql, ")");
 			return r;
@@ -498,17 +536,23 @@ class SpodData {
 			switch( c ) {
 			case CInt(s): return { sql : makeString(s, p), t : DInt, n : false };
 			case CFloat(s): return { sql : makeString(s, p), t : DFloat, n : false };
-			case CString(s): return { sql : if( ~/^[A-Za-z0-9 ]$/.match(s) ) makeString("'" + s + "'", p) else sqlQuoteValue(cond, DText), t : DString(s.length), n : false };
+			case CString(s): return { sql : sqlQuoteValue(cond, DText), t : DString(s.length), n : false };
 			case CRegexp(_): error("Unsupported", p);
 			case CIdent(n), CType(n):
 				var f = current.hfields.get(n);
 				if( f != null ) {
 					if( (try Context.typeof(cond) catch( e : Dynamic ) null) != null )
 						error("Possible conflict between variable and database field", p);
-					return { sql : makeString(n, p), t : f.t, n : f.isNull };
+					return { sql : makeString(f.name, p), t : f.t, n : f.isNull };
 				}
-				if( n == "null" )
+				switch( n ) {
+				case "null":
 					return { sql : makeString("NULL", p), t : DNull, n : true };
+				case "true":
+					return { sql : makeString("1", p), t : DBool, n : false };
+				case "false":
+					return { sql : makeString("0", p), t : DBool, n : false };
+				}
 				return buildDefault(cond);
 			}
 		case ECall(c, pl):
@@ -551,37 +595,171 @@ class SpodData {
 				}
 			default:
 			}
+			return buildDefault(cond);
 		case EField(_, _), EType(_, _):
 			return buildDefault(cond);
+		case EIf(e, e1, e2), ETernary(e, e1, e2):
+			if( e2 == null ) error("If must have an else statement", p);
+			var r1 = buildCond(e1);
+			var r2 = buildCond(e2);
+			unify(r2.t, r1.t, e2.pos);
+			unify(r1.t, r2.t, e1.pos);
+			return { sql : { expr : EIf(e, r1.sql, r2.sql), pos : p }, t : r1.t, n : r1.n || r2.n };
 		default:
 		}
 		error("Unsupported expression", p);
 		return null;
 	}
 	
-	function buildSingleCond( inf : SpodInfos, sql : Expr, cond : Expr ) {
-		switch( cond.expr ) {
+	function ensureType( e : Expr, rt : SpodType ) {
+		var t = try Context.typeof(e) catch( _ : Dynamic ) throw BuildError.EExpr(e);
+		switch( t ) {
+		case TMono:
+			// pseudo-cast
+			return { expr : EBlock([
+				{ expr : EVars([ { name : "__tmp", type : convertType(rt), expr : e } ]), pos : e.pos },
+				{ expr : EConst(CIdent("__tmp")), pos : e.pos },
+			]), pos : e.pos };
+		default:
+			var d = try makeType(t) catch( e : Dynamic ) try makeType(Context.follow(t)) catch( e : Dynamic ) throw BuildError.EExpr(sqlQuoteValue(e,rt)); // will produce an error
+			unify(d, rt, e.pos);
+			return e;
+		}
+	}
+	
+	function checkKeys( econd : Expr ) {
+		var p = econd.pos;
+		switch( econd.expr ) {
 		case EObjectDecl(fl):
-			var first = true;
-			var key = inf.key.copy();
+			var key = current.key.copy();
 			for( f in fl ) {
-				var fi = inf.hfields.get(f.field);
-				if( fi == null ) error("No database field " + f.field, cond.pos);
-				if( first )
-					first = false;
+				var fi = current.hfields.get(f.field);
+				if( fi == null ) error("No database field " + f.field, p);
+				if( !key.remove(f.field) ) {
+					if( Lambda.has(current.key, f.field) )
+						error("Duplicate field " + f.field, p);
+					else
+						error("Field " + f.field + " is not part of table key (" + current.key.join(",") + ")", p);
+				}
+				f.expr = ensureType(f.expr, fi.t);
+			}
+		default:
+			if( current.key.length > 1 )
+				error("You can't use a single value on a table with multiple keys (" + current.key.join(",") + ")", p);
+			var fi = current.hfields.get(current.key[0]);
+			var t = try Context.typeof(econd) catch( _ : Dynamic ) throw BuildError.EExpr(econd);
+			switch( t ) {
+			case TMono:
+				
+			default:
+				var d = try makeType(t) catch( e : Dynamic ) try makeType(Context.follow(t)) catch( e : Dynamic ) throw BuildError.EExpr(sqlQuoteValue(econd, fi.t));
+				unify(d, fi.t, p);
+			}
+		}
+	}
+	
+	function orderField(e) {
+		switch( e.expr ) {
+		case EConst(c):
+			switch( c ) {
+			case CIdent(t), CType(t):
+				if( !current.hfields.exists(t) )
+					error("Unknown database field", e.pos);
+				return quoteField(t);
+			default:
+			}
+		case EUnop(op, _, e):
+			if( op == OpNeg )
+				return orderField(e) + " DESC";
+		default:
+		}
+		error("Invalid order field", e.pos);
+		return null;
+	}
+	
+	function concatStrings( e : Expr ) {
+		var inf = { e : null, str : null };
+		browseStrings(inf, e);
+		if( inf.str != null ) {
+			var es = { expr : EConst(CString(inf.str)), pos : e.pos };
+			if( inf.e == null )
+				inf.e = es;
+			else
+				inf.e = { expr : EBinop(OpAdd, inf.e, es), pos : e.pos };
+		}
+		return inf.e;
+	}
+	
+	function browseStrings( inf : { e : Expr, str : String }, e : Expr ) {
+		switch( e.expr ) {
+		case EConst(c):
+			switch( c ) {
+			case CString(s):
+				if( inf.str == null )
+					inf.str = s;
 				else
-					sql = sqlAddString(sql, " AND ");
-				sql = sqlAddString(sql, quoteField(fi.name) + " = ");
-				sql = sqlAddValue(sql, f.expr, fi.t);
-				key.remove(fi.name);
+					inf.str += s;
+				return;
+			case CInt(s), CFloat(s):
+				if( inf.str != null ) {
+					inf.str += s;
+					return;
+				}
+			default:
+			}
+		case EBinop(op, e1, e2):
+			if( op == OpAdd ) {
+				browseStrings(inf,e1);
+				browseStrings(inf,e2);
+				return;
 			}
-			if( key.length > 0 )
-				error("Missing key '" + key[0] + "'", cond.pos);
+		case EIf(cond, e1, e2):
+			e = { expr : EIf(cond, concatStrings(e1), concatStrings(e2)), pos : e.pos };
 		default:
-			if( inf.key.length > 1 )
-				error("You can't use a single value on a table with multiple keys (" + inf.key.join(",") + ")", cond.pos);
-			sql = sqlAddString(sql, quoteField(inf.key[0]) + " = ");
-			sql = sqlAddValue(sql, cond, inf.hfields.get(inf.key[0]).t);
+		}
+		if( inf.str != null ) {
+			e = { expr : EBinop(OpAdd, { expr : EConst(CString(inf.str)), pos : e.pos }, e), pos : e.pos };
+			inf.str = null;
+		}
+		if( inf.e == null )
+			inf.e = e;
+		else
+			inf.e = { expr : EBinop(OpAdd, inf.e, e), pos : e.pos };
+	}
+
+	function buildOptions( eopt : Expr ) {
+		var opts = new Hash();
+		var p = eopt.pos;
+		var sql = makeString("",p);
+		switch( eopt.expr ) {
+		case EObjectDecl(fields):
+			for( o in fields ) {
+				if( opts.exists(o.field) ) error("Duplicate option " + o.field, p);
+				switch( o.field ) {
+				case "orderBy":
+					var fields = switch( o.expr.expr ) {
+					case EArrayDecl(vl): Lambda.array(Lambda.map(vl, orderField));
+					default: [orderField(o.expr)];
+					};
+					sql = sqlAddString(sql, " ORDER BY " + fields.join(","));
+				case "limit":
+					var limits = switch( o.expr.expr ) {
+					case EArrayDecl(vl): Lambda.array(Lambda.map(vl, buildDefault));
+					default: [buildDefault(o.expr)];
+					}
+					if( limits.length == 0 || limits.length > 2 ) error("Invalid limits", o.expr.pos);
+					var l0 = limits[0], l1 = limits[1];
+					unify(l0.t, DInt, l0.sql.pos);
+					if( l1 != null ) unify(l1.t, DInt, l1.sql.pos);
+					sql = sqlAdd(sqlAddString(sql, " LIMIT "), l0.sql, p);
+					if( l1 != null )
+						sql = sqlAdd(sqlAddString(sql, ","), l1.sql, p);
+				default:
+					error("Unknown option '" + o.field + "'", p);
+				}
+			}
+		default:
+			error("Options should be { orderBy : field, limit : [a,b] }", p);
 		}
 		return sql;
 	}
@@ -658,27 +836,44 @@ class SpodData {
 		return inf;
 	}
 	
-	static function buildSQL( em : Expr, econd : Expr, prefix : String, ?single : Bool ) {
+	static function buildSQL( em : Expr, econd : Expr, prefix : String, ?eopt : Expr ) {
 		var pos = Context.currentPos();
 		var inf = getManagerInfos(Context.typeof(em));
 		var sql = { expr : EConst(CString(prefix + " " + inst.quoteField(inf.name) + " WHERE ")), pos : econd.pos };
 		inst.current = inf;
 		inst.initManager(pos);
-		if( single )
-			return inst.buildSingleCond(inf, sql, econd);
 		var r = try inst.buildCond(econd) catch( e : BuildError ) switch( e ) { case EExpr(e): return e; };
 		if( r.t != DBool ) Context.error("Expression should be a condition", econd.pos);
-		return inst.sqlAdd(sql, r.sql, sql.pos);
+		if( eopt != null && !Type.enumEq(eopt.expr, EConst(CIdent("null"))) )
+			r.sql = inst.sqlAdd(r.sql, inst.buildOptions(eopt), eopt.pos);
+		var sql = inst.sqlAdd(sql, r.sql, sql.pos);
+		#if !display
+		sql = inst.concatStrings(sql);
+		#end
+		return sql;
 	}
 	
 	public static function macroGet( em : Expr, econd : Expr, elock : Expr ) {
-		var sql = buildSQL(em, econd, "SELECT * FROM", true);
 		var pos = Context.currentPos();
-		return { expr : ECall({ expr : EField(em,"unsafeObject"), pos : pos },[sql,elock]), pos : pos };
+		var inf = getManagerInfos(Context.typeof(em));
+		inst.current = inf;
+		inst.initManager(pos);
+		try inst.checkKeys(econd) catch( e : BuildError ) switch( e ) { case EExpr(e): return e; };
+		switch( econd.expr ) {
+		case EObjectDecl(_):
+			return { expr : ECall({ expr : EField(em,"unsafeGetWithKeys"), pos : pos },[econd,elock]), pos : pos };
+		default:
+			return { expr : ECall({ expr : EField(em,"unsafeGet"), pos : pos },[econd,elock]), pos : pos };
+		}
 	}
 	
-	public static function macroSearch( em : Expr, econd : Expr, elock : Expr, ?single ) {
-		var sql = buildSQL(em, econd, "SELECT * FROM");
+	public static function macroSearch( em : Expr, econd : Expr, eopt : Expr, elock : Expr, ?single ) {
+		if( elock == null || Type.enumEq(elock.expr, EConst(CIdent("null"))) ) {
+			var tmp = eopt;
+			eopt = elock;
+			elock = tmp;
+		}
+		var sql = buildSQL(em, econd, "SELECT * FROM", eopt);
 		var pos = Context.currentPos();
 		var e = { expr : ECall( { expr : EField(em, "unsafeObjects"), pos : pos }, [sql,elock]), pos : pos };
 		if( single )