/* * New unified SQL implementation. * SQLJSON client and server API * Provides server and client API * Supports native sqlite3 or JS sqlite3 DB implementation */ var Database; // native sqlite3 and JS sqlite3 should have an identical API; needs bridge? Database = Require('sql/sqlite3'); try { Database = require('sqlite3') } catch (e) {} var fs = require('fs'); var path = require('path'); var time = function () { return Math.floor(Date.now()/1000) } /************************* SERVER API *************************/ // Support for multi-db mode function sqlServer (options) { if (!(this instanceof sqlServer)) return new sqlServer(options); if (!options) options={}; if (!options.path) options.path='/tmp/db.sql'; this.db={}; this.current='default'; this.sessions={}; if (options.path.split(',').length>1) { // multi-db mode var dbL = options.path.split(','); for(var i in dbL) { this.attach(Object.assign(options,{path:dbL[i]})); } } else { this.attach(options); } this.options=options; this.open=true; if (options.url) { urls[options.url]=this; this.url=options.url }; this.options.bidirectional=true; this.log('SQLJSON-RPC Ver. '+version); this.log('Current data base: '+this.current); } sqlServer.prototype.array2buffer = function (array,typ) { var b=Buffer(array.length*4); typ=typ||'uint32'; for(var i=0;i ('+options.path+')'); this.log('Set synchronous mode to '+(options.synchronous||0)); return true; } sqlServer.prototype.buffer2array = function (buffer,typ) { var dw=4,a=[]; typ=typ||'uint32'; if (buffer instanceof Array) return buffer; buffer=(buffer instanceof Uint8Array)?Buffer(buffer):buffer; if (typ.indexOf('64')>0) dw=8; else if (typ.indexOf('16')>0) dw=2; else if (typ.indexOf('8')>0) dw=1; for(var i=0;i0 && (!op.cond || boolean(op.cond))) { try { this.do(op.make,ctx); } catch (e) { // print(e) if (op.error) { try { return this.do(op.error,ctx) } catch (e) { return e && e.error?e:{ error:e } }; } else return { error:e } } safe--; } if (op.finalize) this.do(op.finalize,ctx); if (op.result) result=ctx[op.result]; if (result==undefined && ctx.result) result=ctx.result; return result; } if (op.result) { if (typeof op.result == 'string') ctx.result=ctx[op.result]; else ctx.result=op.result; return ctx.result; } if (op.assign) { with (ctx) { eval(op.assign) }; return {}; } if (op.incr) { try { ctx[op.incr]++; } catch (e) { return { error:e }}; return {}; } if (op.decr) { try { ctx[op.incr]--; } catch (e) { return { error:e }}; return {}; } if (op.decode) { this.decode(ctx,op.decode); } } // Main RPC interpreter entry point sqlServer.prototype.do = function (ops,ctx) { var self=this,result={},tid=ops.tid; if (ops.sessionID) this.session=this.openSession(ops.sessionID); else this.session=null; if (Array.isArray(ops)) { result = ops.map(function (op) { result=self.do(op,ctx); if (result && result.error) throw result; return result; }) return result; } if (ops.close || ops.create || ops.drop || ops.databases || ops.delete || ops.insert || ops.open || ops.select || ops.schema || ops.tables || ops.update) return this.operation(ops,ctx); else return this.command(ops,ctx); } sqlServer.prototype.log = function (msg) { print('[SQLJSON'+(this.port?' '+this.port:'')+'] '+msg); } /********************* CLIENT API *********************/ DB = { // pack generic number arrays into byte buffer (with support for array arrays) array2buffer : function (array,typ,space) { var size=array.length,dsize=4; typ=typ||'uint32'; if (!space && Utils.isArray(array[0])) { space=[size,array[0].length]; if (Utils.isArray(array[0][0])) space.push(array[0][0].length); } if (space) size=space.reduce(function (a,b) { return a*b }); if (!space) space=[size]; switch (typ) { case 'number': dsize=8; break; case 'uint16': dsize=2; break; case 'uint32': dsize=4; break; case 'int16': dsize=2; break; case 'int32': dsize=4; break; case 'float32': dsize=4; break; case 'float64': dsize=8; break; } var b=Buffer(size*dsize); function set(v,off) { switch (typ) { case 'uint16': b.writeUInt16LE(v,off); break; case 'uint32': b.writeUInt32LE(v,off); break; case 'int16': b.writeInt16LE(v,off); break; case 'int32': b.writeInt32LE(v,off); break; case 'float32': b.writeFloatLE(v,off); break; case 'float64': case 'number': default: b.writeDoubleLE(v,off); break; } } var v,off=0; for(var i=0;i0) dsize=8; else if (typ.indexOf('32')>0) dsize=4; else if (typ.indexOf('16')>0) dsize=2; else if (typ.indexOf('8')>0) dsize=1; typ=typ.toLowerCase(); if (space) size=space.reduce(function (a,b) { return a*b }); if (!space) space=[bsize/dsize]; if (size && (size*dsize)!=buffer.length) return new Error('EINVALID'); function get(off) { switch (typ) { case 'uint8': return buffer.readUInt8(off); break; case 'uint16': return buffer.readUInt16LE(off); break; case 'uint32': return buffer.readUInt32LE(off); break; case 'int8': return buffer.readInt8(off); break; case 'int16': return buffer.readInt16LE(off); break; case 'int32': return buffer.readInt32LE(off); break; case 'float32': return buffer.readFloatLE(off); break; case 'float64': case 'number': default: return buffer.readDoubleLE(off); break; } } var v,off=0; for(var i=0;i=0?false:new Error(result); if (!result) return new Error('EIO'); if (result instanceof Error) return result; if (typeof XMLHttpRequestException != 'undefined' && result instanceof XMLHttpRequestException) return new Error(result.message); if (typeof result == 'string' && result.indexOf('Error')!=-1) return new Error(result); if (result.error) return new Error(result.error); if (result.fs) result=result.fs; result=result[Object.keys(result)[0]]; if (!result) return false; if (result.error) return new Error(result.error); else return false; } catch (e) { console.log(e,result); return e; } }, fok : function (cb) { return function (result) { cb(DB.ok(result)) }; }, // Convert matrix to sql row [rows,columns,datatype,data] fromMatrix : function (mat,options) { if (Math.MatrixTA && mat instanceof Math.MatrixTA) { return { rows:mat.rows, columns:mat.columns, dataspace:mat.dataspace, datatype:mat.datatype, data:DB.toBuffer(mat) } } }, mimeType: function (data) { if (typeof data == 'string') return data.replace(/[^\x20-\x7E\n\r\t\s]+/g, '').length==data.length? 'text/plain':'application/octet-stream'; else { for(var i=0;i0x7e) && data[i] != 0x0a && data[i] != 0x0d && data[i] != 0x09) return 'application/octet-stream'; } return 'text/plain'; } }, ok : function (result) { if (!result) return new Error('ENOTFOUND'); if (result instanceof Error) return result; if (typeof result=='string') return new Error(result); if (result.error) return new Error(result.error); if (result.fs) result=result.fs; result=result[Object.keys(result)[0]]; if (!result) return new Error('EIO'); if (result.error) return new Error(result.error); else if (result.result) return result.result; else return result; }, // SQL operations API (generic) sql : function (url) { return { attach : function (name,dir,cb) { return DB.ok(DB.sqljson(this.url+'#'+this.sessionID,{ create: { database: { name : name, path : dir?dir+'/'+name:name, } }, }, cb?DB.fok(cb):null,cb!=undefined)) }, // copy an entire table from this DB to another (dst: sqljson API) // Hierarchical tables (e.g., sqlds) must be copied by the respective API (e.g, sqlds.copy) copy : function (name,dst,options,cb) { options=options||{}; if (!Utils.isObject(dst)) return new Error('EINVALID'); if (!cb) { var result,stat; var schema = this.schema(name); if (stat=DB.error(schema)) return stat; if (options.overwrite) { result = dst.drop(name); stat=DB.error(result); if (stat) return stat; } result = dst.create(name,schema); stat=DB.error(result); if (stat) return stat; var rows = this.count(name); if (DB.error(rows)) return DB.error(rows); rows=DB.ok(rows); for (var i=1;i<(rows+1);i++) { var data = this.select(name,'*','rowid="'+i+'"'); if (DB.error(data)) return DB.error(data); result=dst.insert(name,DB.ok(data)); stat=DB.error(result); if (options.progress) options.progress(i,rows,DB.ok(result)); if (stat) return stat; } } }, count : function (name,count,cb) { return DB.ok(DB.sqljson(this.url+'#'+this.sessionID,{ select: name, count:count||'*' }, cb?DB.fok(cb):null,cb!=undefined)) }, // create a new table create: function (name,columns,cb) { return DB.ok(DB.sqljson(this.url+'#'+this.sessionID,{ create: { table: name }, columns:columns }, cb?DB.fok(cb):null,cb!=undefined)) }, // create a new database or open if existing createDB: function (name,dir,url,cb) { if (typeof url == 'function') { cb=url; url=undefined }; return DB.ok(DB.sqljson(this.url+'#'+this.sessionID,{ create: { database: { name : name, path : dir?dir+'/'+name+'.sql':name+'.sql', url : url, } }, }, cb?DB.fok(cb):null,cb!=undefined)) }, databases: function (cb) { return DB.ok(DB.sqljson(this.url+'#'+this.sessionID,{ databases: {} }, cb?DB.fok(cb):null,cb!=undefined)) }, delete: function (name,where,cb) { return DB.ok(DB.sqljson(this.url+'#'+this.sessionID,{ delete: name, where:where }, cb?DB.fok(cb):null,cb!=undefined)) }, do: function (cmd,cb) { // TODO }, drop: function (name,ifnotexists,cb) { if (typeof ifnotexists=='function') { cb=ifnotexists; ifnotexists=undefined }; return DB.ok(DB.sqljson(this.url+'#'+this.sessionID,{ drop: name, forced : ifnotexists }, cb?DB.fok(cb):null,cb!=undefined)) }, // parse an sql query, return reply eval : function (query,cb) { var tokens = query.split(' '); // TODO:!!! switch (tokens[0].toLowerCase()) { case 'databases': return this.databases(cb); case 'tables': return this.tables(cb); } }, // returns { changes: number, lastInsertROWID: number, time: number } insert: function (name,values,cb) { return DB.ok(DB.sqljson(this.url+'#'+this.sessionID,{ insert: name, values:values }, cb?DB.fok(cb):null,cb!=undefined)) }, // open/select a database open: function (name,cb) { return DB.ok(DB.sqljson(this.url+'#'+this.sessionID,{ open: name }, cb?DB.fok(cb):null,cb!=undefined)) }, select: function (name,columns,where,cb) { return DB.ok(DB.sqljson(this.url+'#'+this.sessionID,{ select: name, columns:columns, where:where }, cb?DB.fok(cb):null,cb!=undefined)) }, schema: function (name, cb) { var matched; var result = DB.ok(DB.sqljson(this.url+'#'+this.sessionID,{ schema: name }, cb?DB.fok(function (result) { if (Utils.isError(result)) return cb(result); if (typeof result == 'string') cb((matched=result.match(/\((.+)\)$/)) && matched[1].split(',')) else if (Array.isArray(result)) result.forEach(function (part) { cb((matched=part.match(/\((.+)\)$/)) && matched[1].split(',')) }); else cb(result)}):null,cb!=undefined)); if (Utils.isError(result)) return result; if (typeof result == 'string') return (matched=result.match(/\((.+)\)$/)) && matched[1].split(',') else if (Array.isArray(result)) return result.map(function (part) { return (matched=part.match(/\((.+)\)$/)) && matched[1].split(',') }); }, tables: function (cb) { return DB.ok(DB.sqljson(this.url+'#'+this.sessionID,{ tables: {} }, cb?DB.fok(cb):null,cb!=undefined)) }, update: function (name,values,where,cb) { return DB.ok(DB.sqljson(this.url+'#'+this.sessionID,{ update: name, values:values, where:where, }, cb?DB.fok(cb):null,cb!=undefined)) }, sessionID: DB.unique(), url : url, }}, // complete async/promise version sqlA : function (url) { var self = { attach : async function (name,dir,cb) { if (!cb) return new Promise(function (resolve,reject) { DB.ok(DB.sqljson(self.url+'#'+self.sessionID,{ create: { database: { name : name, path : dir?dir+'/'+name:name, } }, }, DB.fok(resolve),true)) }); else return DB.ok(DB.sqljson(this.url+'#'+this.sessionID,{ create: { database: { name : name, path : dir?dir+'/'+name:name, } }, }, DB.fok(cb),true)) }, // copy an entire table from this DB to another (dst: sqljson API) // Hierarchical tables (e.g., sqlds) must be copied by the respective API (e.g, sqlds.copy) copy : async function (name,dst,options,cb) { options=options||{}; if (!Utils.isObject(dst)) return new Error('EINVALID'); if (!cb) { var result,stat; var schema = await this.schema(name); if (stat=DB.error(schema)) return stat; if (options.overwrite) { result = await dst.drop(name); stat=DB.error(result); if (stat) return stat; } result = await dst.create(name,schema); stat=DB.error(result); if (stat) return stat; var rows = this.count(name); if (DB.error(rows)) return DB.error(rows); rows=DB.ok(rows); for (var i=1;i<(rows+1);i++) { var data = await this.select(name,'*','rowid="'+i+'"'); if (DB.error(data)) return DB.error(data); result = await dst.insert(name,DB.ok(data)); stat=DB.error(result); if (options.progress) options.progress(i,rows,DB.ok(result)); if (stat) return stat; } } }, count : async function (name,count,cb) { if (!cb) return new Promise(function (resolve,reject) { DB.ok(DB.sqljson(self.url+'#'+self.sessionID,{ select : name, count : count||'*' }, DB.fok(resolve),true)) }); else return DB.ok(DB.sqljson(this.url+'#'+this.sessionID,{ select: name, count:count||'*' }, DB.fok(cb),true)) }, // create a new table create: async function (name,columns,cb) { if (!cb) return new Promise(function (resolve,reject) { DB.ok(DB.sqljson(self.url+'#'+self.sessionID,{ create: { table: name }, columns:columns }, DB.fok(resolve),true)) }); else return DB.ok(DB.sqljson(this.url+'#'+this.sessionID,{ create: { table: name }, columns:columns }, DB.fok(cb),true)) }, // create a new database or open if existing createDB: async function (name,dir,url,cb) { if (typeof url == 'function') { cb=url; url=undefined }; if (!cb) return new Promise(function (resolve,reject) { DB.ok(DB.sqljson(self.url+'#'+self.sessionID,{ create: { database: { name : name, path : dir?dir+'/'+name+'.sql':name+'.sql', url : url, } }, }, DB.fok(resolve),true)) }); else return DB.ok(DB.sqljson(this.url+'#'+this.sessionID,{ create: { database: { name : name, path : dir?dir+'/'+name+'.sql':name+'.sql', url : url, } }, }, DB.fok(cb),true)) }, databases: async function (cb) { if (!cb) return new Promise(function (resolve,reject) { DB.ok(DB.sqljson(self.url+'#'+self.sessionID,{ databases: {} }, DB.fok(resolve),true)) }); else return DB.ok(DB.sqljson(this.url+'#'+this.sessionID,{ databases: {} }, DB.fok(cb),true)) }, delete: async function (name,where,cb) { if (!cb) return new Promise(function (resolve,reject) { DB.ok(DB.sqljson(self.url+'#'+self.sessionID,{ delete: name, where:where }, DB.fok(resolve),true)) }); else return DB.ok(DB.sqljson(this.url+'#'+this.sessionID,{ delete: name, where:where }, DB.fok(cb),true)) }, do: function (cmd,cb) { // TODO }, drop: async function (name,ifnotexists,cb) { if (typeof ifnotexists=='function') { cb=ifnotexists; ifnotexists=undefined }; if (!cb) return new Promise(function (resolve,reject) { DB.ok(DB.sqljson(self.url+'#'+self.sessionID,{ drop: name, forced : ifnotexists }, DB.fok(resolve),true)) }); else return DB.ok(DB.sqljson(this.url+'#'+this.sessionID,{ drop: name, forced : ifnotexists }, DB.fok(cb),true)) }, // parse an sql query, return reply eval : function (query,cb) { var tokens = query.split(' '); // TODO:!!! switch (tokens[0].toLowerCase()) { case 'databases': return this.databases(cb); case 'tables': return this.tables(cb); } }, // returns { changes: number, lastInsertROWID: number, time: number } insert: async function (name,values,cb) { if (!cb) return new Promise(function (resolve,reject) { DB.ok(DB.sqljson(self.url+'#'+self.sessionID,{ insert: name, values:values }, DB.fok(resolve),true)) }); else return DB.ok(DB.sqljson(this.url+'#'+this.sessionID,{ insert: name, values:values }, DB.fok(cb),true)) }, // open/select a database open: async function (name,cb) { if (!cb) return new Promise(function (resolve,reject) { DB.ok(DB.sqljson(self.url+'#'+self.sessionID,{ open: name }, DB.fok(resolve),true)) }); else return DB.ok(DB.sqljson(this.url+'#'+this.sessionID,{ open: name }, DB.fok(cb),true)) }, select: async function (name,columns,where,cb) { if (!cb) return new Promise(function (resolve,reject) { DB.ok(DB.sqljson(self.url+'#'+self.sessionID,{ select: name, columns:columns, where:where }, DB.fok(resolve),true)) }); else return DB.ok(DB.sqljson(this.url+'#'+this.sessionID,{ select: name, columns:columns, where:where }, DB.fok(cb),true)) }, schema: async function (name, cb) { var matched; function exec(cb) { var result = DB.ok(DB.sqljson(self.url+'#'+self.sessionID,{ schema: name }, DB.fok(function (result) { if (Utils.isError(result)) return cb(result); if (typeof result == 'string') cb((matched=result.match(/\((.+)\)$/)) && matched[1].split(',')) else if (Array.isArray(result)) result.forEach(function (part) { cb((matched=part.match(/\((.+)\)$/)) && matched[1].split(',')) }); else cb(result)}),true)); } if (!cb) return new Promise(function (resolve,reject) { exec(resolve); }); else return exec(cb); }, tables: async function (cb) { if (!cb) return new Promise(function (resolve,reject) { DB.ok(DB.sqljson(self.url+'#'+self.sessionID,{ tables: {} }, DB.fok(resolve),true)) }); else return DB.ok(DB.sqljson(this.url+'#'+this.sessionID,{ tables: {} }, DB.fok(cb),true)) }, update: async function (name,values,where,cb) { if (!cb) return new Promise(function (resolve,reject) { DB.ok(DB.sqljson(self.url+'#'+self.sessionID,{ update: name, values:values, where:where, }, DB.fok(resolve),true)) }); else return DB.ok(DB.sqljson(this.url+'#'+this.sessionID,{ update: name, values:values, where:where, }, DB.fok(cb),true)) }, sessionID: DB.unique(), url : url, }; return self}, // SQLjson RPC client request (with optional access key) // format url = ["proto://"] ("host:port" | "host:port:K1:K2:K3:..") sqljson : function (url,request,callback,async) { if (!url) { // local SQL db access } var proto = url.match(/^([a-zA-Z]+):\/\//), tokens = url.split(':'), sessionID = url.match(/#([^$]+)$/); if (proto) proto=proto[1]; if (sessionID) { sessionID=sessionID[1]; tokens[tokens.length-1]=tokens[tokens.length-1].replace(/#[^$]+$/,''); request.sessionID=sessionID; url=url.replace(/#[^$]+$/,''); } if (tokens.length>(2+(proto?1:0))) { url = tokens.slice(0,2+(proto?1:0)).join(':'); request.key= tokens.slice(2+(proto?1:0)).join(':'); } // console.log('sqljson',url,request) if (!async && !callback) { return Utils.POST(url,request,null,true); } else if (callback) { return Utils.POST(url,request, function (res) { // console.log(res); callback(res); },!async); }; }, strict:false, time : function (format) { switch (format) { case 'milli': case 'ms': return Date.now(); case 'YYYYMMDD': var today = new Date(); return (today.getYear()+1900)+ (today.getMonth()<9?'0'+(today.getMonth()+1):today.getMonth()+1)+ (today.getDate()<10?'0'+today.getDate():today.getDate()) case 'YYYYMMDD@HHMM': var today = new Date(); return (today.getYear()+1900)+ (today.getMonth()<9?'0'+(today.getMonth()+1):today.getMonth()+1)+ (today.getDate()<10?'0'+(today.getDate()):today.getDate())+ '@'+ (today.getHours()<9?'0'+(today.getHours()+1):today.getHours()+1)+ (today.getMinutes()<10?'0'+(today.getMinutes()):today.getMinutes()) default: return Date().toString(); } }, timeCompare : function (t1,t2) { if (isNaN(Number(t1))) t1=Date.parse(t1); if (isNaN(Number(t2))) t2=Date.parse(t2); t1=Number(t1); t2=Number(t2); return t1t2?1:0); }, toArray: function (buff,ftyp,dims,layout) { var ta = DB.toTypedArray(buff,ftyp); if (!layout) layout=123; if (!ta) return; if (!dims) dims=[ta.length]; switch (dims.length) { case 1: return Array.prototype.slice.call(ta); case 2: var a=[]; for(var i=0;i