Skip to content

Commit 088e110

Browse files
authored
Regression of piping data into txt() in CLI to fix #2149 (#2159)
Co-authored-by: @ammeek
1 parent 8d50961 commit 088e110

File tree

6 files changed

+225
-26
lines changed

6 files changed

+225
-26
lines changed

bin/alasql-cli.js

Lines changed: 52 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
let alasql = require('../dist/alasql.fs.js');
1111
let path = require('path');
1212
let fs = require('fs');
13-
let stdin = process.openStdin();
1413
let yargs = require('yargs')
1514
.strict()
1615
.usage(
@@ -58,44 +57,73 @@ let yargs = require('yargs')
5857
.epilog('\nMore information about the library: www.alasql.org');
5958

6059
let argv = yargs.argv;
61-
let sql = '';
62-
let params = [];
63-
let pipedData = '';
64-
stdin.on('data', function (chunk) {
65-
pipedData += chunk;
66-
});
6760

68-
if (argv.v) {
69-
console.log(alasql.version);
70-
process.exit(0);
61+
// Regex patterns for detecting functions
62+
const re = {
63+
txt: /txt\s*\(\s*\)/i,
64+
};
65+
66+
// Helper function to check if SQL contains txt() and stdin is piped
67+
function isPipeData(sql) {
68+
return sql && !process.stdin.isTTY && re.txt.test(sql);
7169
}
7270

73-
if (argv.f) {
74-
if (!fs.existsSync(argv.f)) {
71+
// Helper function to setup stdin handling
72+
function pipeIsSQL() {
73+
const stdin = process.openStdin();
74+
let pipedData = '';
75+
stdin.on('data', function (chunk) {
76+
pipedData += chunk;
77+
});
78+
stdin.on('end', function () {
79+
execute(pipedData, argv._);
80+
});
81+
return stdin;
82+
}
83+
84+
// Helper function to load and validate SQL file
85+
function loadSqlFile(filePath) {
86+
if (!fs.existsSync(filePath)) {
7587
console.error('Error: file not found');
7688
process.exit(1);
7789
}
78-
79-
if (isDirectory(argv.f)) {
90+
if (isDirectory(filePath)) {
8091
console.error('Error: file expected but directory found');
8192
process.exit(1);
8293
}
94+
return fs.readFileSync(filePath, 'utf8').toString();
95+
}
96+
97+
// Main execution logic
98+
function main() {
99+
let sql = '';
100+
let pipeIsData = false;
101+
102+
if (argv.f) {
103+
sql = loadSqlFile(argv.f);
104+
pipeIsData = isPipeData(sql);
105+
} else {
106+
sql = argv._.shift() || '';
107+
pipeIsData = isPipeData(sql);
108+
}
83109

84-
sql = fs.readFileSync(argv.f, 'utf8').toString();
85-
execute(sql, argv._);
86-
} else {
87-
sql = argv._.shift() || '';
110+
// Setup stdin handling only if not using txt() pipe
111+
if (!pipeIsData) {
112+
pipeIsSQL();
113+
}
88114

89-
// if data is not piped
90-
if (Boolean(process.stdin.isTTY)) {
115+
// Execute immediately if we have SQL or are using txt() pipe
116+
if (sql || pipeIsData) {
91117
execute(sql, argv._);
92118
}
93119
}
94120

95-
// if data is piped
96-
stdin.on('end', function () {
97-
execute(pipedData, argv._);
98-
});
121+
if (argv.v) {
122+
console.log(alasql.version);
123+
process.exit(0);
124+
}
125+
126+
main();
99127

100128
/**
101129
* Execute SQL query

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"scripts": {
1616
"test": "sh build.sh && yarn test-only",
1717
"test-ci": "(yarn test-format-all || 1) && yarn test-only && yarn install-g && alasql 'select 1 as Succes'",
18-
"test-only": "node node_modules/mocha/bin/mocha.js ./test --reporter dot",
18+
"test-only": "node node_modules/mocha/bin/mocha.js ./test --reporter dot --bail",
1919
"#test-only": "(command -v bun && bun node_modules/.bin/mocha ./test --reporter dot) || npx bun node_modules/.bin/mocha ./test --reporter dot",
2020
"test-browser": "node test/browserTestRunner.js 7387",
2121
"test-cover": "# istanbul cover -x 'lib/zt/zt.js' --dir test/coverage _mocha",

src/15utility.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ let loadFile = (utils.loadFile = function (path, asy, success, error) {
310310
fs = require('fs');
311311

312312
// If path is empty, than read data from stdin (for Node)
313-
if (typeof path === 'undefined') {
313+
if ([null, undefined].includes(path)) {
314314
var buff = '';
315315
process.stdin.setEncoding('utf8');
316316
process.stdin.on('readable', function () {

test/test2149-with-txt.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
SELECT COUNT(*) > 0 as Success FROM txt()

test/test2149-without-txt.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
SELECT 1 as Success

test/test2149.js

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
if (typeof exports === 'object') {
2+
var assert = require('assert');
3+
var {execSync, spawn} = require('child_process');
4+
var fs = require('fs');
5+
var path = require('path');
6+
}
7+
8+
describe('Test CLI - Command Line Interface - Issue #2149 (Pipe handling with txt())', function () {
9+
console.log(__dirname);
10+
const cliPath = path.join(__dirname, '..', 'bin', 'alasql-cli.js');
11+
const testSqlFile = path.join(__dirname, 'temp-test.sql');
12+
const testWithTxtFile = path.join(__dirname, 'test2149-with-txt.sql');
13+
const testWithoutTxtFile = path.join(__dirname, 'test2149-without-txt.sql');
14+
15+
before(function () {
16+
// Create a temporary SQL file for testing
17+
fs.writeFileSync(testSqlFile, 'SELECT VALUE 42');
18+
});
19+
20+
after(function () {
21+
// Clean up temporary files
22+
if (fs.existsSync(testSqlFile)) {
23+
fs.unlinkSync(testSqlFile);
24+
}
25+
});
26+
27+
it('1. Should execute simple SQL statement', function () {
28+
const result = execSync(`node "${cliPath}" "SELECT VALUE 42"`).toString().trim();
29+
assert.strictEqual(result, '42');
30+
});
31+
32+
it('2. Should handle parameters', function () {
33+
const result = execSync(`node "${cliPath}" "SELECT VALUE ?" 100`).toString().trim();
34+
assert.strictEqual(result, '100');
35+
});
36+
37+
it('3. Should execute SQL from file', function () {
38+
const result = execSync(`node "${cliPath}" -f "${testSqlFile}"`).toString().trim();
39+
assert.strictEqual(result, '42');
40+
});
41+
42+
it('4. Should output minified JSON with -m flag', function () {
43+
const result = execSync(`node "${cliPath}" -m "SELECT {a:1,b:2} as obj"`).toString().trim();
44+
assert.strictEqual(result, '[{"obj":{"a":1,"b":2}}]');
45+
});
46+
47+
it('5. Should show version with -v flag', function () {
48+
const result = execSync(`node "${cliPath}" -v`).toString().trim();
49+
assert.match(result, /^\d+\.\d+\.\d+/);
50+
});
51+
52+
it('6. Should output AST with --ast flag', function () {
53+
const result = execSync(`node "${cliPath}" --ast "SELECT 1"`).toString().trim();
54+
const ast = JSON.parse(result);
55+
assert.strictEqual(ast.statements[0].columns[0].value, 1);
56+
});
57+
58+
it('7. Should handle file not found error', function () {
59+
try {
60+
execSync(`node "${cliPath}" -f "nonexistent.sql"`, {stdio: 'pipe'});
61+
assert.fail('Should have thrown an error');
62+
} catch (error) {
63+
assert.match(error.stderr.toString(), /Error: file not found/);
64+
}
65+
});
66+
67+
it('8. Should handle piped input data with txt() function - Issue #2149', function () {
68+
const result = execSync(
69+
`echo "hello" | node "${cliPath}" "SELECT COUNT(*) > 0 as Success FROM txt()"`
70+
).toString();
71+
assert.deepEqual(
72+
[
73+
{
74+
Success: true,
75+
},
76+
],
77+
JSON.parse(result)
78+
);
79+
});
80+
81+
it('9. Should handle piped input data without txt() function - backward compatibility', function () {
82+
const result = execSync(`echo "SELECT 1 as Success" | node "${cliPath}"`).toString();
83+
assert.deepEqual(
84+
[
85+
{
86+
Success: 1,
87+
},
88+
],
89+
JSON.parse(result)
90+
);
91+
});
92+
93+
it('10. Should handle redirected file input with txt() function', function () {
94+
const result = execSync(
95+
`node "${cliPath}" "SELECT COUNT(*) > 0 as Success FROM txt()" < ${testSqlFile}`
96+
).toString();
97+
assert.deepEqual(
98+
[
99+
{
100+
Success: true,
101+
},
102+
],
103+
JSON.parse(result)
104+
);
105+
});
106+
107+
it('11. Should handle file with txt() function and piped data - Issue #2149', function () {
108+
const result = execSync(
109+
`echo "hello world" | node "${cliPath}" -f "${testWithTxtFile}"`
110+
).toString();
111+
assert.deepEqual(
112+
[
113+
{
114+
Success: true,
115+
},
116+
],
117+
JSON.parse(result)
118+
);
119+
});
120+
121+
it('12. Should handle file without txt() function normally', function () {
122+
const result = execSync(`node "${cliPath}" -f "${testWithoutTxtFile}"`).toString();
123+
assert.deepEqual(
124+
[
125+
{
126+
Success: 1,
127+
},
128+
],
129+
JSON.parse(result)
130+
);
131+
});
132+
133+
it('13. Should handle piped input to file without txt() function - should be ignored', function () {
134+
const result = execSync(
135+
`echo "this should be ignored" | node "${cliPath}" -f "${testWithoutTxtFile}"`
136+
).toString();
137+
assert.deepEqual(
138+
[
139+
{
140+
Success: 1,
141+
},
142+
],
143+
JSON.parse(result)
144+
);
145+
});
146+
147+
it('14. Should handle complex SQL with txt() and piped data', function () {
148+
const result = execSync(
149+
`echo -e "line1\nline2\nline3" | node "${cliPath}" "SELECT COUNT(*) as LineCount FROM txt()"`
150+
).toString();
151+
assert.deepEqual(
152+
[
153+
{
154+
LineCount: 3,
155+
},
156+
],
157+
JSON.parse(result)
158+
);
159+
});
160+
161+
it('15. Should handle empty SQL error', function () {
162+
try {
163+
execSync(`node "${cliPath}" ""`, {stdio: 'pipe'});
164+
assert.fail('Should have thrown an error');
165+
} catch (error) {
166+
assert.match(error.stderr.toString(), /No SQL to process/);
167+
}
168+
});
169+
});

0 commit comments

Comments
 (0)