UNPKG

icql

Version:

YeSQL meets SQLite: A SQLite Adapter built with InterCourse and BetterSQLite

677 lines (645 loc) 25.9 kB
(function() { 'use strict'; var CND, FS, HOLLERITH, IC, LFT, assign, badge, debug, declare, echo, help, info, inspect, isa, jr, max_excerpt_length, rpr, size_of, type_of, urge, validate, warn, whisper, xrpr; //########################################################################################################### CND = require('cnd'); rpr = CND.rpr; badge = 'ICQL/MAIN'; debug = CND.get_logger('debug', badge); warn = CND.get_logger('warn', badge); info = CND.get_logger('info', badge); urge = CND.get_logger('urge', badge); help = CND.get_logger('help', badge); whisper = CND.get_logger('whisper', badge); echo = CND.echo.bind(CND); //........................................................................................................... // PATH = require 'path' // PD = require 'pipedreams' // { $ // $async // select } = PD ({assign, jr} = CND); // #........................................................................................................... // join_path = ( P... ) -> PATH.resolve PATH.join P... // boolean_as_int = ( x ) -> if x then 1 else 0 ({inspect} = require('util')); xrpr = function(x) { return inspect(x, { colors: true, breakLength: 2e308, maxArrayLength: 2e308, depth: 2e308 }); }; //........................................................................................................... FS = require('fs'); IC = require('intercourse'); this.HOLLERITH = HOLLERITH = require('hollerith-codec'); //........................................................................................................... this.types = require('./types'); ({isa, validate, declare, size_of, type_of} = this.types); max_excerpt_length = 10000; LFT = require('letsfreezethat'); //=========================================================================================================== // LOCAL METHODS //----------------------------------------------------------------------------------------------------------- this._local_methods = { //--------------------------------------------------------------------------------------------------------- _statements: {}, //--------------------------------------------------------------------------------------------------------- _echo: function(ref, sql) { if (!this.settings.echo) { return null; } echo((CND.reverse(CND.blue(`^icql@888-${ref}^`))) + (CND.reverse(CND.yellow(sql)))); return null; }, //--------------------------------------------------------------------------------------------------------- limit: function*(n, iterator) { var count, x; count = 0; for (x of iterator) { if (count >= n) { return; } count += +1; yield x; } }, //--------------------------------------------------------------------------------------------------------- single_row: function(iterator) { var R; if ((R = this.first_row(iterator)) === void 0) { throw new Error("µ33833 expected at least one row, got none"); } return R; }, //--------------------------------------------------------------------------------------------------------- all_first_values: function(iterator) { var R, key, row, value; R = []; for (row of iterator) { for (key in row) { value = row[key]; R.push(value); break; } } return R; }, //--------------------------------------------------------------------------------------------------------- first_values: function*(iterator) { var R, key, row, value; R = []; for (row of iterator) { for (key in row) { value = row[key]; yield value; } } return R; }, //--------------------------------------------------------------------------------------------------------- first_row: function(iterator) { var row; for (row of iterator) { return row; } }, /* TAINT must ensure order of keys in row is same as order of fields in query */ single_value: function(iterator) { var key, ref1, value; ref1 = this.single_row(iterator); for (key in ref1) { value = ref1[key]; return value; } }, first_value: function(iterator) { var key, ref1, value; ref1 = this.first_row(iterator); for (key in ref1) { value = ref1[key]; return value; } }, all_rows: function(iterator) { return [...iterator]; }, //--------------------------------------------------------------------------------------------------------- query: function(sql, ...P) { var base, statement; this._echo('1', sql); statement = ((base = this._statements)[sql] != null ? base[sql] : base[sql] = this.db.prepare(sql)); return statement.iterate(...P); }, //--------------------------------------------------------------------------------------------------------- run: function(sql, ...P) { var base, statement; this._echo('2', sql); statement = ((base = this._statements)[sql] != null ? base[sql] : base[sql] = this.db.prepare(sql)); return statement.run(...P); }, //--------------------------------------------------------------------------------------------------------- _run_or_query: function(entry_type, is_last, sql, Q) { var base, returns_data, statement; this._echo('3', sql); statement = ((base = this._statements)[sql] != null ? base[sql] : base[sql] = this.db.prepare(sql)); returns_data = statement.reader; //....................................................................................................... /* Always use `run()` method if statement does not return data: */ if (!returns_data) { if (Q != null) { return statement.run(Q); } else { return statement.run(); } } //....................................................................................................... /* If statement does return data, consume iterator unless this is the last statement: */ if ((entry_type === 'procedure') || (!is_last)) { if (Q != null) { return statement.all(Q); } else { return statement.all(); } } //....................................................................................................... /* Return iterator: */ if (Q != null) { return statement.iterate(Q); } else { return statement.iterate(); } }, //--------------------------------------------------------------------------------------------------------- execute: function(sql) { this._echo('4', sql); return this.db.exec(sql); }, //--------------------------------------------------------------------------------------------------------- prepare: function(...P) { return this.db.prepare(...P); }, aggregate: function(...P) { return this.db.aggregate(...P); }, backup: function(...P) { return this.db.backup(...P); }, checkpoint: function(...P) { return this.db.checkpoint(...P); }, close: function(...P) { return this.db.close(...P); }, read: function(path) { return this.db.exec(FS.readFileSync(path, { encoding: 'utf-8' })); }, function: function(...P) { return this.db.function(...P); }, load: function(...P) { return this.db.loadExtension(...P); }, pragma: function(...P) { return this.db.pragma(...P); }, transaction: function(...P) { return this.db.transaction(...P); }, //--------------------------------------------------------------------------------------------------------- catalog: function() { /* TAINT kludge: we sort by descending types so views, tables come before indexes (b/c you can't drop a primary key index in SQLite) */ // throw new Error "µ45222 deprecated until next major version" return this.query("select * from sqlite_master order by type desc, name;"); }, //--------------------------------------------------------------------------------------------------------- list_objects: function(schema = 'main') { validate.ic_schema(schema); return this.all_rows(this.query(`select type as type, name as name, sql as sql from ${this.as_identifier(schema)}.sqlite_master order by type desc, name;`)); }, //--------------------------------------------------------------------------------------------------------- list_schemas: function() { return this.pragma("database_list;"); }, list_schema_names: function() { var d; return new Set((function() { var i, len, ref1, results; ref1 = this.list_schemas(); results = []; for (i = 0, len = ref1.length; i < len; i++) { d = ref1[i]; results.push(d.name); } return results; }).call(this)); }, //--------------------------------------------------------------------------------------------------------- /* TAINT must escape path, schema */ attach: function(path, schema) { validate.ic_path(path); validate.ic_schema(schema); return this.execute(`attach ${this.as_sql(path)} as ${this.as_identifier(schema)};`); }, //----------------------------------------------------------------------------------------------------------- copy_schema: function(from_schema, to_schema) { var d, from_schema_x, i, inserts, j, len, len1, name_x, ref1, ref2, ref3, schemas, sql, to_schema_x; schemas = this.list_schema_names(); inserts = []; validate.ic_schema(from_schema); validate.ic_schema(to_schema); if (!schemas.has(from_schema)) { throw new Error(`µ57873 unknown schema ${rpr(from_schema)}`); } if (!schemas.has(to_schema)) { throw new Error(`µ57873 unknown schema ${rpr(to_schema)}`); } this.pragma(`${this.as_identifier(to_schema)}.foreign_keys = off;`); to_schema_x = this.as_identifier(to_schema); from_schema_x = this.as_identifier(from_schema); ref1 = this.list_objects(from_schema); //....................................................................................................... for (i = 0, len = ref1.length; i < len; i++) { d = ref1[i]; if (this.settings.verbose) { debug('^44463^', "DB object:", d); } if ((d.sql == null) || (d.sql === '')) { continue; } if ((ref2 = d.name) === 'sqlite_sequence') { continue; } //..................................................................................................... /* TAINT consider to use `validate.ic_db_object_type` */ if ((ref3 = d.type) !== 'table' && ref3 !== 'view' && ref3 !== 'index') { throw new Error(`µ49888 unknown type ${rpr(d.type)} for DB object ${rpr(d)}`); } //..................................................................................................... /* TAINT using not-so reliable string replacement as substitute for proper parsing */ name_x = this.as_identifier(d.name); sql = d.sql.replace(/\s*CREATE\s*(TABLE|INDEX|VIEW)\s*/i, `create ${d.type} ${to_schema_x}.`); //..................................................................................................... if (sql === d.sql) { throw new Error(`µ49889 unexpected SQL string ${rpr(d.sql)}`); } //..................................................................................................... this.execute(sql); if (d.type === 'table') { inserts.push(`insert into ${to_schema_x}.${name_x} select * from ${from_schema_x}.${name_x};`); } } //....................................................................................................... if (this.settings.verbose) { debug('^49864^', "starting with inserts"); debug('^49864^', `objects in ${rpr(from_schema)}: ${rpr(((function() { var j, len1, ref4, results; ref4 = this.list_objects(from_schema); results = []; for (j = 0, len1 = ref4.length; j < len1; j++) { d = ref4[j]; results.push(`(${d.type})${d.name}`); } return results; }).call(this)).join(', '))}`); debug('^49864^', `objects in ${rpr(to_schema)}: ${rpr(((function() { var j, len1, ref4, results; ref4 = this.list_objects(to_schema); results = []; for (j = 0, len1 = ref4.length; j < len1; j++) { d = ref4[j]; results.push(`(${d.type})${d.name}`); } return results; }).call(this)).join(', '))}`); } for (j = 0, len1 = inserts.length; j < len1; j++) { sql = inserts[j]; //....................................................................................................... this.execute(sql); } this.pragma(`${this.as_identifier(to_schema)}.foreign_keys = on;`); this.pragma(`${this.as_identifier(to_schema)}.foreign_key_check;`); return null; }, //--------------------------------------------------------------------------------------------------------- type_of: function(name, schema = 'main') { var ref1, row; ref1 = this.catalog(); for (row of ref1) { if (row.name === name) { return row.type; } } return null; }, //--------------------------------------------------------------------------------------------------------- column_types: function(table) { var R, ref1, row; R = {}; ref1 = this.query(this.interpolate("pragma table_info( $table );", {table})); /* TAINT we apparently have to call the pragma in this roundabout fashion since SQLite refuses to accept placeholders in that statement: */ for (row of ref1) { R[row.name] = row.type; } return R; }, //--------------------------------------------------------------------------------------------------------- _dependencies_of: function(table, schema = 'main') { return this.query(`pragma ${this.as_identifier(schema)}.foreign_key_list( ${this.as_identifier(table)} )`); }, //--------------------------------------------------------------------------------------------------------- dependencies_of: function(table, schema = 'main') { var row; validate.ic_schema(schema); return (function() { var ref1, results; ref1 = this._dependencies_of(table); results = []; for (row of ref1) { results.push(row.table); } return results; }).call(this); }, //--------------------------------------------------------------------------------------------------------- get_toposort: function(schema = 'main') { var LTSORT, R, dependencies, dependency, g, i, indexes, len, name, ref1, sqls, types, x; LTSORT = require('ltsort'); g = LTSORT.new_graph(); indexes = []; types = {}; sqls = {}; ref1 = this.list_objects(schema); for (x of ref1) { types[x.name] = x.type; sqls[x.name] = x.sql; if (x.type !== 'table') { indexes.push(x.name); continue; } dependencies = this.dependencies_of(x.name); if (dependencies.length === 0) { LTSORT.add(g, x.name); } else { for (i = 0, len = dependencies.length; i < len; i++) { dependency = dependencies[i]; LTSORT.add(g, x.name, dependency); } } } R = [...(LTSORT.linearize(g)), ...indexes]; return (function() { var j, len1, results; results = []; for (j = 0, len1 = R.length; j < len1; j++) { name = R[j]; results.push({ name, type: types[name], sql: sqls[name] }); } return results; })(); }, //--------------------------------------------------------------------------------------------------------- clear: function() { var count, i, len, name, ref1, statement, type; count = 0; ref1 = this.get_toposort(); for (i = 0, len = ref1.length; i < len; i++) { ({type, name} = ref1[i]); statement = `drop ${type} if exists ${this.as_identifier(name)};`; this.execute(statement); count += +1; } return count; }, //--------------------------------------------------------------------------------------------------------- as_identifier: function(text) { return '"' + (text.replace(/"/g, '""')) + '"'; }, // as_identifier: ( text ) -> '[' + ( text.replace /\]/g, ']]' ) + ']' //--------------------------------------------------------------------------------------------------------- escape_text: function(x) { validate.text(x); return x.replace(/'/g, "''"); }, //--------------------------------------------------------------------------------------------------------- list_as_json: function(x) { validate.list(x); return jr(x); }, //--------------------------------------------------------------------------------------------------------- as_sql: function(x) { var type; switch (type = type_of(x)) { case 'text': return `'${this.escape_text(x)}'`; case 'list': return `'${this.list_as_json(x)}'`; case 'float': return x.toString(); case 'boolean': return (x ? '1' : '0'); case 'null': return 'null'; case 'undefined': throw new Error("µ12341 unable to express 'undefined' as SQL literal"); } throw new Error(`µ12342 unable to express a ${type} as SQL literal, got ${rpr(x)}`); }, //--------------------------------------------------------------------------------------------------------- interpolate: function(sql, Q) { return sql.replace(this._interpolation_pattern, ($0, $1) => { var error; try { return this.as_sql(Q[$1]); } catch (error1) { error = error1; throw new Error(`µ55563 when trying to express placeholder ${rpr($1)} as SQL literal, an error occurred: ${rpr(error.message)}`); } }); }, _interpolation_pattern: /\$(?:(.+?)\b|\{([^}]+)\})/g, //--------------------------------------------------------------------------------------------------------- as_hollerith: function(x) { return HOLLERITH.encode(x); }, from_hollerith: function(x) { return HOLLERITH.decode(x); } }; //=========================================================================================================== //----------------------------------------------------------------------------------------------------------- this.bind = function(settings) { var connector, me, ref1; validate.icql_settings(settings); me = { $: {settings} }; connector = (ref1 = settings.connector) != null ? ref1 : require('better-sqlite3'); me.icql_path = settings.icql_path; this.connect(me, connector, settings.db_path, settings.db_settings); this.definitions_from_path_sync(me, settings.icql_path); this.bind_definitions(me); this.bind_udfs(me); return me; }; //----------------------------------------------------------------------------------------------------------- /* TAINT should check connector API compatibility */ /* TAINT consider to use `new`-less call convention (should be possible acc. to bsql3 docs) */ this.connect = function(me, connector, db_path, db_settings = {}) { if (me.$ == null) { me.$ = {}; } me.$.db = new connector(db_path, db_settings); // me.$.dbr = me.$.db // me.$.dbw = new connector db_path, db_settings return me; }; //----------------------------------------------------------------------------------------------------------- this.definitions_from_path_sync = function(me, icql_path) { (me.$ != null ? me.$ : me.$ = {}).sql = IC.definitions_from_path_sync(icql_path); return me; }; //----------------------------------------------------------------------------------------------------------- this.bind_definitions = function(me) { var check_unique, ic_entry, local_method, name, ref1, ref2; check_unique = function(name) { if (me[name] != null) { throw new Error(`µ11292 name collision: ${rpr(name)} already defined`); } }; if (me.$ == null) { me.$ = {}; } ref1 = LFT._deep_copy(this._local_methods); //......................................................................................................... /* TAINT use `new` */ for (name in ref1) { local_method = ref1[name]; (function(name, local_method) { var method; check_unique(name); if (isa.function(local_method)) { local_method = local_method.bind(me.$); method = function(...P) { var error, excerpt, ref2, x; try { return local_method(...P); } catch (error1) { error = error1; excerpt = rpr(P); if (excerpt.length > max_excerpt_length) { x = max_excerpt_length / 2; excerpt = excerpt.slice(0, +x + 1 || 9e9) + ' ... ' + excerpt.slice(excerpt.length - x); } warn(`^icql#15543^ when trying to call method ${name} with ${excerpt}`); warn(`^icql#15544^ an error occurred: ${(ref2 = error.name) != null ? ref2 : error.code}: ${error.message}`); throw error; } }; return me.$[name] = method.bind(me.$); } else { return me.$[name] = local_method; } })(name, local_method); } ref2 = me.$.sql; //......................................................................................................... for (name in ref2) { ic_entry = ref2[name]; /* TAINT fix in intercourse */ ic_entry.name = name; check_unique(name); me[name] = this._method_from_ic_entry(me, ic_entry); } //......................................................................................................... return me; }; //----------------------------------------------------------------------------------------------------------- this.bind_udfs = function(me) { me.$.function('as_hollerith', { deterministic: true, varargs: false }, (x) => { return HOLLERITH.encode(x); }); me.$.function('from_hollerith', { deterministic: true, varargs: false }, (x) => { return HOLLERITH.decode(x); }); return me; }; //----------------------------------------------------------------------------------------------------------- this._method_from_ic_entry = function(me, ic_entry) { validate.ic_entry_type(ic_entry.type); //......................................................................................................... if (ic_entry.type === 'fragment') { return (Q) => { var descriptor, sql; descriptor = this._descriptor_from_arguments(me, ic_entry, Q); sql = descriptor.parts.join('\n'); return me.$.interpolate(sql, Q); }; } //......................................................................................................... return (Q) => { var R, descriptor, error, i, idx, is_last, kenning, last_idx, len, line_nr, location, name, part, ref1, type; descriptor = this._descriptor_from_arguments(me, ic_entry, Q); last_idx = descriptor.parts.length - 1; try { ref1 = descriptor.parts; for (idx = i = 0, len = ref1.length; i < len; idx = ++i) { part = ref1[idx]; is_last = idx === last_idx; R = me.$._run_or_query(ic_entry.type, is_last, part, Q); } } catch (error1) { error = error1; name = ic_entry.name; type = ic_entry.type; kenning = descriptor.kenning; line_nr = descriptor.location.line_nr; location = `line ${line_nr}, ${type} ${name}${kenning}`; throw new Error(`µ11123 At *.icql ${location}: ${error.message}`); } return R; }; }; //----------------------------------------------------------------------------------------------------------- this._descriptor_from_arguments = function(me, ic_entry, Q) { var R, is_void_signature, kenning, ref1, signature; [signature, kenning] = IC.get_signature_and_kenning(Q); is_void_signature = kenning === '()' || kenning === 'null'; if (is_void_signature) { R = (ref1 = ic_entry['()']) != null ? ref1 : ic_entry['null']; } else { R = ic_entry[kenning]; } if (R == null) { R = ic_entry['null']; } //......................................................................................................... if (R == null) { throw new Error(`µ93832 calling method ${rpr(ic_entry.name)} with signature ${kenning} not implemented`); } return R; }; }).call(this); //# sourceMappingURL=main.js.map