From 663b1a186fa976b65a21e026b6095ad7704df4c0 Mon Sep 17 00:00:00 2001 From: sbosse Date: Mon, 21 Jul 2025 23:09:26 +0200 Subject: [PATCH] Mon 21 Jul 22:43:21 CEST 2025 --- js/db/db.sql.js | 1346 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1346 insertions(+) create mode 100644 js/db/db.sql.js diff --git a/js/db/db.sql.js b/js/db/db.sql.js new file mode 100644 index 0000000..a580969 --- /dev/null +++ b/js/db/db.sql.js @@ -0,0 +1,1346 @@ +/* + * 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