alasql 4.20.0 → 4.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,36 @@
1
+ // Sorts the failures printed in an alasql-sqllogictest result file by kind.
2
+ // Used for context/strategic-direction.md, "What sqllogictest already says". Reads only; runs no tests.
3
+ // Run: node context/sqllogictest-failure-kinds.js ../alasql-sqllogictest/results/output.md [kind-substring] [examples]
4
+ const fs=require('fs');
5
+ const f=process.argv[2];
6
+ const txt=fs.readFileSync(f,'utf8');
7
+ const blocks=[...txt.matchAll(/```sql\n([\s\S]*?)\n\n([\s\S]*?)\n```/g)].map(m=>({sql:m[1],msg:m[2]}));
8
+ console.log('printed errors:',blocks.length);
9
+ const cat={};
10
+ const ex={};
11
+ function add(k,b){cat[k]=(cat[k]||0)+1;(ex[k]=ex[k]||[]).push(b)}
12
+ let nullDiff=0,valDiff=0,lenDiff=0;
13
+ for(const b of blocks){
14
+ const m=b.msg;
15
+ let k;
16
+ if(/^Expected: /.test(m)){
17
+ const mm=m.match(/^Expected: (\[.*?\]) but got (\[.*\])/);
18
+ let e,g;try{e=JSON.parse(mm[1]);g=JSON.parse(mm[2]);}catch(x){k='expected/got (truncated)';add(k,b);continue}
19
+ if(e.length!==g.length){k='values: different count';}
20
+ else{
21
+ let n=0,o=0;
22
+ for(let i=0;i<e.length;i++){ if(e[i]!==g[i]){ if(e[i]==='NULL'||g[i]==='NULL') n++; else o++; } }
23
+ if(n&&!o)k='values: differ only where one side is NULL';
24
+ else if(n&&o)k='values: NULL and non-NULL differences';
25
+ else k='values: non-NULL differences only';
26
+ }
27
+ } else if(/Parse error/.test(m)) k='parse error';
28
+ else if(/expected to return results/.test(m)) k='no rows returned';
29
+ else if(/Expected to get an error/.test(m)) k='expected an error';
30
+ else if(/hash was different/.test(m)) k='hash differs (same count)';
31
+ else if(/results returned but expected/.test(m)) k='row count differs (hash)';
32
+ else k='runtime: '+m.replace(/'[^']*'/g,"'…'").replace(/\d+/g,'N').slice(0,60);
33
+ add(k,b);
34
+ }
35
+ Object.entries(cat).sort((a,b)=>b[1]-a[1]).forEach(([k,v])=>console.log(String(v).padStart(5),k));
36
+ if(process.argv[3]){for(const k of Object.keys(ex)){ if(k.includes(process.argv[3])){ex[k].slice(0,+process.argv[4]||8).forEach(b=>console.log('\n['+k+']\n'+b.sql.slice(0,400)+'\n => '+b.msg.slice(0,300)))}}}
@@ -0,0 +1,127 @@
1
+ // Prototype for context/strategic-direction.md, "Tagged templates". Not part of the build.
2
+ // Run: node context/tag-prototype.js
3
+ //
4
+ // Position-aware tagged template on the public 4.x API.
5
+ // No grammar change. A scanner walks the fixed text pieces and notes
6
+ // where each hole stands. The parser only ever sees ordinary SQL with `?`.
7
+ const alasql = require('../dist/alasql.js');
8
+
9
+ const CODE = 0, SQ = 1, DQ = 2, BR = 3, LINE = 4, BLOCK = 5;
10
+
11
+ function scan(raw) {
12
+ // returns {parts: [...], holes: [{kind:'value'|'string'|'name'}]}
13
+ let state = CODE;
14
+ let out = '';
15
+ let litStart = -1; // index in `out` where the current string literal opened
16
+ let litHasHole = false;
17
+ const holes = [];
18
+ const segments = []; // sql text split at each name hole
19
+ for (let s = 0; s < raw.length; s++) {
20
+ const t = raw[s];
21
+ for (let i = 0; i < t.length; i++) {
22
+ const c = t[i], n = t[i + 1];
23
+ if (state === CODE) {
24
+ if (c === "'") { state = SQ; litStart = out.length; litHasHole = false; }
25
+ else if (c === '"') { state = DQ; litStart = out.length; litHasHole = false; }
26
+ else if (c === '[') state = BR;
27
+ else if (c === '-' && n === '-') state = LINE;
28
+ else if (c === '/' && n === '*') state = BLOCK;
29
+ out += c;
30
+ } else if (state === SQ || state === DQ) {
31
+ const q = state === SQ ? "'" : '"';
32
+ out += c;
33
+ if (c === q) {
34
+ if (n === q) { out += n; i++; } // doubled quote stays inside
35
+ else {
36
+ if (litHasHole) { out = out.slice(0, litStart) + '(' + out.slice(litStart) + ')'; }
37
+ state = CODE;
38
+ }
39
+ }
40
+ } else if (state === BR) { out += c; if (c === ']') state = CODE; }
41
+ else if (state === LINE) { out += c; if (c === '\n') state = CODE; }
42
+ else if (state === BLOCK) { out += c; if (c === '*' && n === '/') { out += n; i++; state = CODE; } }
43
+ }
44
+ if (s === raw.length - 1) break;
45
+ // a hole
46
+ if (state === CODE) { out += '?'; holes.push('value'); }
47
+ else if (state === SQ || state === DQ) {
48
+ const q = state === SQ ? "'" : '"';
49
+ out += q + ' + ? + ' + q; litHasHole = true; holes.push('string');
50
+ } else if (state === BR) { segments.push(out); out = ''; litStart = -1; holes.push('name'); }
51
+ else throw new Error('A ${} cannot stand inside a comment');
52
+ }
53
+ if (state === SQ || state === DQ || state === BR) throw new Error('Unclosed quote or bracket in the query text');
54
+ segments.push(out);
55
+ return { segments, holes };
56
+ }
57
+
58
+ const sites = new WeakMap();
59
+
60
+ function tag(strings, ...values) {
61
+ let site = sites.get(strings);
62
+ if (!site) {
63
+ site = scan(strings.raw);
64
+ site.hasNames = site.holes.includes('name');
65
+ site.byNames = new Map();
66
+ sites.set(strings, site);
67
+ }
68
+ const params = [];
69
+ let key = '';
70
+ const names = [];
71
+ for (let i = 0; i < values.length; i++) {
72
+ const kind = site.holes[i], v = values[i];
73
+ if (kind === 'name') {
74
+ if (typeof v !== 'string' || v.includes(']')) throw new Error('A name in [${}] must be a string without "]"');
75
+ names.push(v); key += v + ']';
76
+ } else if (kind === 'string') params.push(v == null ? '' : String(v));
77
+ else params.push(v);
78
+ }
79
+ let st = site.byNames.get(key);
80
+ if (!st) {
81
+ let sql = site.segments[0];
82
+ for (let i = 0; i < names.length; i++) sql += names[i] + site.segments[i + 1];
83
+ st = alasql.compile(sql);
84
+ st.sqlText = sql;
85
+ site.byNames.set(key, st);
86
+ }
87
+ return st(params);
88
+ }
89
+ tag.show = (strings) => scan(strings.raw);
90
+ module.exports = tag;
91
+
92
+ if (require.main === module) {
93
+ const sql = tag;
94
+ const people = [
95
+ { name: 'Ann', age: 31, city: 'Oslo' },
96
+ { name: "O'Brien", age: 25, city: 'Cork' },
97
+ { name: 'Bo', age: 19, city: 'Oslo' },
98
+ ];
99
+ const j = (x) => JSON.stringify(x);
100
+ const run = (label, f) => { try { console.log(label.padEnd(46), '=>', j(f())); } catch (e) { console.log(label.padEnd(46), '!!', e.message.slice(0, 90)); } };
101
+
102
+ const city = 'Oslo', term = 'O', col = 'age', evil = "x' OR 1=1 --", min = 20;
103
+ run('bare value + table', () => sql`SELECT COLUMN name FROM ${people} WHERE city = ${city} AND age > ${min}`);
104
+ run("value inside quotes '${city}'", () => sql`SELECT COLUMN name FROM ${people} WHERE city = '${city}'`);
105
+ run("LIKE '${term}%'", () => sql`SELECT COLUMN name FROM ${people} WHERE name LIKE '${term}%'`);
106
+ run("LIKE '%${term}%' with a doubled quote before", () => sql`SELECT COLUMN name FROM ${people} WHERE 'it''s' = 'it''s' AND name LIKE '%${'ri'}%'`);
107
+ run('name inside brackets [${col}]', () => sql`SELECT COLUMN [${col}] FROM ${people} ORDER BY [${col}]`);
108
+ run('same call site, other name', () => { const f = (c) => sql`SELECT COLUMN [${c}] FROM ${people}`; return [f('name'), f('city')]; });
109
+ run('value with a quote in it, bare', () => sql`SELECT COLUMN name FROM ${people} WHERE name = ${"O'Brien"}`);
110
+ run('value with a quote in it, in quotes', () => sql`SELECT COLUMN name FROM ${people} WHERE name = '${"O'Brien"}'`);
111
+ run('injection attempt in quotes', () => sql`SELECT COLUMN name FROM ${people} WHERE name = '${evil}'`);
112
+ run('injection attempt bare', () => sql`SELECT COLUMN name FROM ${people} WHERE name = ${evil}`);
113
+ run('injection attempt as a name', () => sql`SELECT COLUMN [${'age] FROM x; DROP TABLE y; --'}] FROM ${people}`);
114
+ run('hole inside a comment', () => sql`SELECT 1 /* ${city} */`);
115
+ run('question mark inside a string is text', () => sql`SELECT VALUE 'why? ${city}'`);
116
+ console.log('\nWhat the parser is given for name LIKE \'%${term}%\' AND [${col}] > ${min}:');
117
+ const shown = tag.show`SELECT * FROM ${people} WHERE name LIKE '%${term}%' AND [${col}] > ${min}`;
118
+ console.log(' text pieces:', j(shown.segments), '\n holes:', j(shown.holes));
119
+
120
+ // timing
121
+ const data = []; for (let i = 0; i < 10; i++) data.push({ name: 'n' + i, age: 18 + i, city: i % 2 ? 'A' : 'B' });
122
+ const time = (label, f) => { for (let i = 0; i < 20000; i++) f(); const N = 200000; const s = process.hrtime.bigint(); for (let i = 0; i < N; i++) f(); const ns = Number(process.hrtime.bigint() - s) / N; console.log(label.padEnd(46), ns.toFixed(0), 'ns'); };
123
+ console.log();
124
+ time('alasql(sql, params)', () => alasql('SELECT name, age FROM ? WHERE age > ? AND city = ? ORDER BY age DESC', [data, 23, 'A']));
125
+ time('tag, bare holes', () => sql`SELECT name, age FROM ${data} WHERE age > ${23} AND city = ${'A'} ORDER BY age DESC`);
126
+ time("tag, quoted hole + name hole", () => sql`SELECT name, [${'age'}] FROM ${data} WHERE age > ${23} AND city = '${'A'}' ORDER BY age DESC`);
127
+ }
@@ -1,7 +1,7 @@
1
- //! AlaSQL v4.19.1 build: develop-cfae681d | © 2014-2025 Andrey Gershun & Mathias Wulff | License: MIT
1
+ //! AlaSQL v4.20.0 build: develop-a98910ff | © 2014-2025 Andrey Gershun & Mathias Wulff | License: MIT
2
2
  /*
3
3
  @module alasql
4
- @version 4.19.1
4
+ @version 4.20.0
5
5
 
6
6
  AlaSQL - JavaScript SQL database
7
7
  © 2014-2025 Andrey Gershun & Mathias Wulff
package/dist/alasql.d.ts CHANGED
@@ -39,6 +39,11 @@ declare module 'alasql' {
39
39
  <T = unknown>(params?: any, cb?: AlaSQLCallback<T>, scope?: unknown): T;
40
40
  }
41
41
 
42
+ // returned by alasql.compile(): a compiled statement with a promise helper
43
+ export interface AlaSQLCompiledStatement extends AlaSQLStatement {
44
+ promise<T = unknown>(params?: any): Promise<T>;
45
+ }
46
+
42
47
  export type AlaSQLDefaultColumns = {[columnName: string]: string};
43
48
 
44
49
  /**
@@ -52,7 +57,8 @@ declare module 'alasql' {
52
57
  export interface Op extends AlaSQLExpression {
53
58
  left: ExpressionNode;
54
59
  op: string;
55
- right?: ExpressionNode;
60
+ // `->` with a bare property name (`a->b`, `tx.a.b`) puts the name as a string
61
+ right?: ExpressionNode | string;
56
62
  right1?: ExpressionNode;
57
63
  right2?: ExpressionNode;
58
64
  }
@@ -84,8 +90,20 @@ declare module 'alasql' {
84
90
  right: ExpressionNode;
85
91
  }
86
92
 
93
+ export interface FuncValue extends AlaSQLExpression {
94
+ funcid: string;
95
+ args?: ExpressionNode[];
96
+ }
97
+
87
98
  export type ExpressionNode =
88
- Op | Column | ParamValue | NumValue | StringValue | LogicValue | UniOp;
99
+ | Op
100
+ | Column
101
+ | ParamValue
102
+ | NumValue
103
+ | StringValue
104
+ | LogicValue
105
+ | UniOp
106
+ | FuncValue;
89
107
 
90
108
  export interface ExpressionWrapper extends AlaSQLExpression {
91
109
  expression: ExpressionNode;
@@ -215,6 +233,7 @@ declare module 'alasql' {
215
233
  error: Error;
216
234
  <T = unknown>(sql: string, params?: any, cb?: AlaSQLCallback<T>, scope?: unknown): T;
217
235
  parse(sql: string): AlaSQLAST;
236
+ compile(sql: string, databaseid?: string): AlaSQLCompiledStatement;
218
237
  promise<T = unknown>(sql: string, params?: any): Promise<T>;
219
238
  fn: userDefinedFunctionLookUp;
220
239
  from: userFromFunctionLookUp;