alasql 4.20.1 → 4.22.0

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "alasql",
3
3
  "description": "Use SQL to select and filter javascript data - including relational joins and search in nested objects (JSON). Export to and import from Excel and CSV",
4
- "version": "4.20.1",
4
+ "version": "4.22.0",
5
5
  "author": "Andrey Gershun <agershun@gmail.com>",
6
6
  "contributors": [
7
7
  "Mathias Wulff <m@rawu.dk>"
@@ -29,6 +29,8 @@
29
29
  "test-cover": "# istanbul cover -x 'lib/zt/zt.js' --dir test/coverage _mocha",
30
30
  "build": "yarn format && yarn build-only",
31
31
  "build-only": "sh build.sh",
32
+ "test-perf": "bun test/performance/vs-npm.js",
33
+ "test-perf-mini": "bun test/performance/vs-npm.js --scale=0.5 --runs=2 --runtime=node --fail-above=105",
32
34
  "install-g": "yarn build && npm uninstall alasql -g && npm install -g .",
33
35
  "release": "yarn version",
34
36
  "jison": "jison ./src/alasqlparser.jison -o ./src/alasqlparser.js",
@@ -51,6 +53,7 @@
51
53
  "pre-pr": "cmdmix 'yarn fresh '%1' && yarn repush'",
52
54
  "prepare": "husky",
53
55
  "preversion": "yarn && yarn test && npm login",
56
+ "prepublishOnly": "sh build.sh </dev/null",
54
57
  "postversion": "npm publish && git push && git push --tags && echo \"Successfully released version $npm_package_version\""
55
58
  },
56
59
  "dependencies": {
@@ -71,7 +74,7 @@
71
74
  "open": "11.0.4",
72
75
  "prettier": "3.9.10",
73
76
  "react-native-fetch-blob": "^0.10.8",
74
- "rexreplace": "7.1.14",
77
+ "rexreplace": "7.1.15",
75
78
  "strftime": "0.10.3",
76
79
  "tabletop": "^1.6.2",
77
80
  "uglify-js": "3.19.3"
package/test1107.json ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "alasql": "{\"databases\":{}}"
3
+ }
package/types/alasql.d.ts CHANGED
@@ -8,6 +8,11 @@ declare module 'alasql' {
8
8
  (err: Error | null, data?: T): void;
9
9
  }
10
10
 
11
+ // Callback of a compiled statement: data first, then the error (data is null when err is set)
12
+ export interface AlaSQLStatementCallback<T = unknown> {
13
+ (data: T | null, err?: Error): void;
14
+ }
15
+
11
16
  export interface AlaSQLOptions {
12
17
  errorlog: boolean;
13
18
  valueof: boolean;
@@ -39,8 +44,10 @@ declare module 'alasql' {
39
44
  <T = unknown>(params?: any, cb?: AlaSQLCallback<T>, scope?: unknown): T;
40
45
  }
41
46
 
42
- // returned by alasql.compile(): a compiled statement with a promise helper
43
- export interface AlaSQLCompiledStatement extends AlaSQLStatement {
47
+ // returned by alasql.compile(): a compiled statement with a promise helper. Only this one takes
48
+ // the data-first callback, so the older Statement.compile() types stay as they were in 4.x
49
+ export interface AlaSQLCompiledStatement {
50
+ <T = unknown>(params?: any, cb?: AlaSQLStatementCallback<T>, scope?: unknown): T;
44
51
  promise<T = unknown>(params?: any): Promise<T>;
45
52
  }
46
53
 
@@ -57,12 +64,19 @@ declare module 'alasql' {
57
64
  export interface Op extends AlaSQLExpression {
58
65
  left: ExpressionNode;
59
66
  op: string;
60
- // `->` with a bare property name (`a->b`, `tx.a.b`) puts the name as a string
61
- right?: ExpressionNode | string;
67
+ right?: ExpressionNode;
62
68
  right1?: ExpressionNode;
63
69
  right2?: ExpressionNode;
64
70
  }
65
71
 
72
+ // Not in the ExpressionNode union, so code that walks the tree compiles as it did before 4.21.0
73
+ export interface ArrowOp extends AlaSQLExpression {
74
+ left: ExpressionNode;
75
+ op: '->';
76
+ // a bare property name (`a->b`, `tx.a.b`) puts the name as a string
77
+ right: ExpressionNode | string;
78
+ }
79
+
66
80
  export interface Column extends AlaSQLExpression {
67
81
  columnid: string;
68
82
  tableid?: string;
@@ -96,14 +110,7 @@ declare module 'alasql' {
96
110
  }
97
111
 
98
112
  export type ExpressionNode =
99
- | Op
100
- | Column
101
- | ParamValue
102
- | NumValue
103
- | StringValue
104
- | LogicValue
105
- | UniOp
106
- | FuncValue;
113
+ Op | Column | ParamValue | NumValue | StringValue | LogicValue | UniOp | FuncValue;
107
114
 
108
115
  export interface ExpressionWrapper extends AlaSQLExpression {
109
116
  expression: ExpressionNode;
@@ -1,36 +0,0 @@
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)))}}}
@@ -1,127 +0,0 @@
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
- }