Deleting all unused files, deleting unused functions from loot-core (#1158)

Last one before I add the actual linter rules!
This commit is contained in:
Shazib Hussain
2023-06-23 19:38:57 +01:00
committed by GitHub
parent d6ed860bc3
commit 0c0f9e6ccf
30 changed files with 6 additions and 3300 deletions

View File

@@ -1,65 +0,0 @@
export function RepairSyncNotification() {}
// TODO: sync button shouldn't show error status if it's a local file
// and needs uploading.. should just be grayed out
//
// TODO: improve styling of these modals
// export function NeedsUploadNotification({ actions }) {
// let [loading, setLoading] = useState(false);
// return (
// <Stack align="center" direction="row">
// <Text>
// This file is not a cloud file. You need to register it to take advantage
// of syncing which allows you to use it across devices and never worry
// about losing your data.
// </Text>
// <ButtonWithLoading
// bare
// loading={loading}
// onClick={async () => {
// setLoading(true);
// await actions.uploadBudget();
// actions.removeNotification('file-needs-upload');
// setLoading(false);
// actions.sync();
// actions.loadPrefs();
// }}
// style={{
// backgroundColor: 'rgba(100, 100, 100, .12)',
// color: colors.n1,
// fontSize: 14,
// flexShrink: 0,
// '&:hover, &:active': { backgroundColor: 'rgba(100, 100, 100, .25)' }
// }}
// >
// Register
// </ButtonWithLoading>
// </Stack>
// );
// }
// export function SyncResetNotification({ cloudFileId, actions }) {
// return (
// <Stack align="center" direction="row">
// <Text>
// </Text>
// <Button
// bare
// onClick={async () => {
// actions.removeNotification('out-of-date-key');
// }}
// style={{
// backgroundColor: colors.r10,
// flexShrink: 0,
// '&:hover': { backgroundColor: colors.r10 },
// '&:active': { backgroundColor: colors.r8 }
// }}
// >
// Revert
// </Button>
// </Stack>
// );
// }

View File

@@ -1,143 +0,0 @@
import React, { Component } from 'react';
import styled from 'styled-components';
import { send } from 'loot-core/src/platform/client/fetch';
const Container = styled.div`
width: 100%;
overflow: auto;
`;
const Code = styled.textarea`
width: 100%;
height: 10em;
font-size: 1em;
`;
const Output = styled.pre`
width: 100%;
background-color: #333333;
color: white;
padding: 0.5em;
`;
class Debug extends Component {
constructor() {
super();
this.state = {
value: localStorage.debugValue,
outputType: 'ast',
ast: null,
code: null,
sql: null,
sqlgenValue: localStorage.sqlgenValue,
sqlgenRow: localStorage.sqlgenRow,
};
}
componentDidMount() {
this.fetchResults(this.state.value);
this.fetchSqlGenResult(this.state.value);
}
fetchResults(value) {
localStorage.debugValue = value;
send('debug-ast', { code: value }).then(ast => {
this.setState({ ast });
});
send('debug-code', { code: value }).then(code => {
this.setState({ code });
});
send('debug-query', { code: value }).then(sql => {
this.setState({ sql });
});
}
async fetchSqlGenResult() {
let row = {};
try {
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-eval
row = (0, eval)('(' + this.state.sqlgenRow + ')');
} catch (e) {}
const res = await send('debug-sqlgen', {
expr: this.state.sqlgenValue,
});
this.setState({ sqlgenResult: res });
}
processInput(e) {
this.setState({ value: e.target.value });
this.fetchResults(e.target.value);
}
processSqlGen(value, field) {
localStorage[field] = value;
this.setState({ [field]: value }, () => {
this.fetchSqlGenResult();
});
}
onInputType(e) {
this.setState({ outputType: e.target.value });
}
render() {
const {
// value,
// outputType,
// ast,
// code,
// sql,
sqlgenValue,
sqlgenRow,
sqlgenResult,
} = this.state;
return (
<Container>
{/*<h2>Debug</h2>
<p>Input:</p>
<Code value={value} onChange={this.processInput.bind(this)} />
<select
value={this.state.outputType}
onChange={this.onInputType.bind(this)}
>
<option value="ast">AST</option>
<option value="code">code</option>
<option value="sql">SQL</option>
</select>
<div style={{ display: outputType === 'ast' ? 'block' : 'none' }}>
<p>AST:</p>
<Output>{ast ? JSON.stringify(ast, null, 2) : ''}</Output>
</div>
<div style={{ display: outputType === 'code' ? 'block' : 'none' }}>
<p>Code:</p>
<Output>{code || ''}</Output>
</div>
<div style={{ display: outputType === 'sql' ? 'block' : 'none' }}>
<p>SQL:</p>
<Output>{sql || ''}</Output>
</div>*/}
<h3>sqlgen</h3>
<Code
value={sqlgenValue}
onChange={e => this.processSqlGen(e.target.value, 'sqlgenValue')}
/>
<Code
value={sqlgenRow}
onChange={e => this.processSqlGen(e.target.value, 'sqlgenRow')}
/>
<Output>{JSON.stringify(sqlgenResult)}</Output>
</Container>
);
}
}
export default Debug;

View File

@@ -1,4 +0,0 @@
export function getModalRoute(name: string): [string, string] {
let parts = name.split('/');
return [parts[0], parts.slice(1).join('/')];
}

View File

@@ -1,82 +1,5 @@
import * as models from './models';
export const transactionModel = {
...models.transactionModel,
toExternal(transactions, idx, payees) {
return transactions;
// function convert(t, payee) {
// return {
// id: t.id,
// account_id: t.acct,
// amount: t.amount,
// payee_id: payee ? payee.id : null,
// payee: payee ? payee.name : null,
// imported_payee: t.imported_description,
// category_id: t.category,
// date: t.date,
// notes: t.notes,
// imported_id: t.financial_id,
// transfer_id: t.transferred_id,
// cleared: t.cleared
// };
// }
// let splits = getAllSplitTransactions(transactions, idx);
// if (splits) {
// let payee =
// splits.parent.description && payees[splits.parent.description];
// return {
// ...convert(splits.parent, payee),
// subtransactions: splits.children.map(child => convert(child, payee))
// };
// }
// let transaction = transactions[idx];
// let payee = transaction.description && payees[transaction.description];
// return convert(transaction, payee);
},
fromExternal(transaction) {
let result: Record<string, unknown> = {};
if ('id' in transaction) {
result.id = transaction.id;
}
if ('account_id' in transaction) {
result.acct = transaction.account_id;
}
if ('amount' in transaction) {
result.amount = transaction.amount;
}
if ('payee_id' in transaction) {
result.description = transaction.payee_id;
}
if ('imported_payee' in transaction) {
result.imported_description = transaction.imported_payee;
}
if ('category_id' in transaction) {
result.category = transaction.category_id;
}
if ('date' in transaction) {
result.date = transaction.date;
}
if ('notes' in transaction) {
result.notes = transaction.notes;
}
if ('imported_id' in transaction) {
result.financial_id = transaction.imported_id;
}
if ('transfer_id' in transaction) {
result.transferred_id = transaction.transfer_id;
}
if ('cleared' in transaction) {
result.cleared = transaction.cleared;
}
return result;
},
};
export const accountModel = {
...models.accountModel,

View File

@@ -1118,7 +1118,3 @@ export function generateSQLWithState(
let { sqlPieces, state } = compileQuery(queryState, schema, schemaConfig);
return { sql: defaultConstructQuery(queryState, state, sqlPieces), state };
}
export function generateSQL(queryState) {
return generateSQLWithState(queryState).sql;
}

View File

@@ -56,7 +56,3 @@ export function FileDownloadError(reason, meta?) {
export function FileUploadError(reason, meta?) {
return { type: 'FileUploadError', reason, meta };
}
export function isCodeError(err) {
return err instanceof ReferenceError || err instanceof SyntaxError;
}

View File

@@ -1,59 +0,0 @@
let enabled = false;
let entries = {};
let counters = {};
export function reset() {
entries = {};
counters = {};
}
export function record(name) {
const start = Date.now();
return () => unrecord(name, start);
}
function unrecord(name, start) {
const end = Date.now();
if (enabled) {
if (entries[name] == null) {
entries[name] = [];
}
entries[name].push(end - start);
}
}
export function increment(name) {
if (enabled) {
if (counters[name] == null) {
counters[name] = 0;
}
counters[name]++;
}
}
export function start() {
enabled = true;
}
export function stop() {
enabled = false;
console.log('~~ PERFORMANCE REPORT ~~');
for (let name in entries) {
const records = entries[name];
const total = records.reduce((total, n) => total + n / 1000, 0);
const avg = total / records.length;
console.log(
`[${name}] count: ${records.length} total: ${total}s avg: ${avg}`,
);
}
for (let name in counters) {
console.log(`[${name}] ${counters[name]}`);
}
console.log('~~ END REPORT ~~');
reset();
}

View File

@@ -1,12 +1,3 @@
export function first(arr) {
return arr[0];
}
export function firstValue(arr) {
const keys = Object.keys(arr[0]);
return arr[0][keys[0]];
}
export function number(v) {
if (typeof v === 'number') {
return v;
@@ -20,11 +11,3 @@ export function number(v) {
return 0;
}
export function min(x, y) {
return Math.min(x, y);
}
export function max(x, y) {
return Math.max(x, y);
}

View File

@@ -1,33 +0,0 @@
* Function calls (native hooks)
* Operators: + - / * > < >= <= =
* Queries: from t in transactions select { amount }
* Types
** Boolean (true / false)
** Integer
** Float
** String
* Variables (only global lookup)
Need a stack to hold temporary values since function calls can be
nested. Instructions:
MOV
CALL
QUERY
BOP
UOP
Registers:
PC
SP
REG1
Query language:
=from transactions
where
date >= 20170101 and
date <= 20170131 and
category.is_income = 1
calculate sum(amount)

View File

@@ -1,734 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Compiler basic 1`] = `
Array [
Array [
Symbol(query),
"
SELECT sum(amount) FROM transactions
LEFT JOIN category_mapping __cm ON __cm.id = transactions.category
LEFT JOIN accounts t1 ON t1.id = transactions.acct
LEFT JOIN banks t2 ON t2.id = t1.bank
WHERE ((((date >= 20170101) and (date <= 20170131)) and (t2.name = 1))) AND transactions.isParent = 0 AND transactions.tombstone = 0
",
true,
],
Array [
Symbol(mov),
Object {
"index": 1,
"type": "__reg",
},
Object {
"index": 0,
"type": "__stack",
},
],
Array [
Symbol(call),
Object {
"name": "generated!number",
"type": "__var",
},
Array [
Object {
"index": 0,
"type": "__stack",
},
],
],
Array [
Symbol(mov),
Object {
"index": 1,
"type": "__reg",
},
Object {
"index": 0,
"type": "__stack",
},
],
Array [
Symbol(call),
Object {
"name": "generated!first",
"type": "__var",
},
Array [
Object {
"index": 0,
"type": "__stack",
},
],
],
Array [
Symbol(mov),
Object {
"index": 1,
"type": "__reg",
},
Object {
"name": "generated!result",
"type": "__var",
},
],
]
`;
exports[`Compiler basic 2`] = `
Array [
Array [
Symbol(mov),
"",
Object {
"name": "generated!result",
"type": "__var",
},
],
]
`;
exports[`Compiler compiler binary ops 1`] = `
Array [
Array [
Symbol(mov),
Object {
"name": "generated!bar",
"type": "__var",
},
Object {
"index": 1,
"type": "__reg",
},
],
Array [
Symbol(bop),
"+",
Object {
"name": "generated!foo",
"type": "__var",
},
Object {
"index": 1,
"type": "__reg",
},
],
Array [
Symbol(mov),
Object {
"index": 1,
"type": "__reg",
},
Object {
"index": 0,
"type": "__stack",
},
],
Array [
Symbol(mov),
Object {
"name": "generated!baz",
"type": "__var",
},
Object {
"index": 1,
"type": "__reg",
},
],
Array [
Symbol(bop),
"+",
Object {
"index": 0,
"type": "__stack",
},
Object {
"index": 1,
"type": "__reg",
},
],
Array [
Symbol(mov),
Object {
"index": 1,
"type": "__reg",
},
Object {
"index": 0,
"type": "__stack",
},
],
Array [
Symbol(mov),
Object {
"name": "generated!boo",
"type": "__var",
},
Object {
"index": 1,
"type": "__reg",
},
],
Array [
Symbol(bop),
"+",
Object {
"index": 0,
"type": "__stack",
},
Object {
"index": 1,
"type": "__reg",
},
],
Array [
Symbol(mov),
Object {
"index": 1,
"type": "__reg",
},
Object {
"name": "generated!result",
"type": "__var",
},
],
]
`;
exports[`Compiler compiler nested funcs 1`] = `
Array [
Array [
Symbol(mov),
0,
Object {
"index": 1,
"type": "__reg",
},
],
Array [
Symbol(mov),
Object {
"index": 1,
"type": "__reg",
},
Object {
"index": 0,
"type": "__stack",
},
],
Array [
Symbol(mov),
-20000,
Object {
"index": 1,
"type": "__reg",
},
],
Array [
Symbol(mov),
Object {
"index": 1,
"type": "__reg",
},
Object {
"index": 1,
"type": "__stack",
},
],
Array [
Symbol(call),
Object {
"name": "generated!number",
"type": "__var",
},
Array [
Object {
"index": 1,
"type": "__stack",
},
],
],
Array [
Symbol(mov),
Object {
"index": 1,
"type": "__reg",
},
Object {
"index": 1,
"type": "__stack",
},
],
Array [
Symbol(call),
Object {
"name": "generated!min",
"type": "__var",
},
Array [
Object {
"index": 0,
"type": "__stack",
},
Object {
"index": 1,
"type": "__stack",
},
],
],
Array [
Symbol(mov),
Object {
"index": 1,
"type": "__reg",
},
Object {
"name": "generated!result",
"type": "__var",
},
],
]
`;
exports[`Compiler compiles boolean types 1`] = `
Array [
Array [
Symbol(mov),
true,
Object {
"index": 1,
"type": "__reg",
},
],
Array [
Symbol(mov),
Object {
"index": 1,
"type": "__reg",
},
Object {
"index": 0,
"type": "__stack",
},
],
Array [
Symbol(mov),
1,
Object {
"index": 1,
"type": "__reg",
},
],
Array [
Symbol(bop),
"and",
Object {
"index": 0,
"type": "__stack",
},
Object {
"index": 1,
"type": "__reg",
},
],
Array [
Symbol(mov),
Object {
"index": 1,
"type": "__reg",
},
Object {
"index": 0,
"type": "__stack",
},
],
Array [
Symbol(jumpf),
Object {
"index": 0,
"type": "__stack",
},
Object {
"get": [Function],
"resolve": [Function],
},
],
Array [
Symbol(mov),
0,
Object {
"index": 1,
"type": "__reg",
},
],
Array [
Symbol(jumpt),
Object {
"index": 0,
"type": "__stack",
},
Object {
"get": [Function],
"resolve": [Function],
},
],
Array [
Symbol(mov),
1,
Object {
"index": 1,
"type": "__reg",
},
],
Array [
Symbol(mov),
Object {
"index": 1,
"type": "__reg",
},
Object {
"name": "generated!result",
"type": "__var",
},
],
]
`;
exports[`Compiler complex query expressions 1`] = `
Array [
Array [
Symbol(query),
"
SELECT substr(date,0,7), sum(amount) FROM transactions
LEFT JOIN category_mapping __cm ON __cm.id = transactions.category
WHERE transactions.isParent = 0 AND transactions.tombstone = 0
GROUP BY substr(date,0,7)",
false,
],
Array [
Symbol(mov),
Object {
"index": 1,
"type": "__reg",
},
Object {
"name": "generated!result",
"type": "__var",
},
],
]
`;
exports[`Compiler field dependencies 1`] = `
Array [
"acct",
"category",
"description",
"isParent",
"tombstone",
"date",
]
`;
exports[`Compiler parens 1`] = `
Array [
Array [
Symbol(mov),
1,
Object {
"index": 1,
"type": "__reg",
},
],
Array [
Symbol(mov),
Object {
"index": 1,
"type": "__reg",
},
Object {
"index": 0,
"type": "__stack",
},
],
Array [
Symbol(mov),
2,
Object {
"index": 1,
"type": "__reg",
},
],
Array [
Symbol(bop),
"+",
Object {
"index": 0,
"type": "__stack",
},
Object {
"index": 1,
"type": "__reg",
},
],
Array [
Symbol(mov),
Object {
"index": 1,
"type": "__reg",
},
Object {
"name": "generated!result",
"type": "__var",
},
],
]
`;
exports[`Compiler parens 2`] = `
Array [
Array [
Symbol(mov),
1232,
Object {
"index": 1,
"type": "__reg",
},
],
Array [
Symbol(mov),
Object {
"index": 1,
"type": "__reg",
},
Object {
"index": 0,
"type": "__stack",
},
],
Array [
Symbol(mov),
2,
Object {
"index": 1,
"type": "__reg",
},
],
Array [
Symbol(bop),
"+",
Object {
"index": 0,
"type": "__stack",
},
Object {
"index": 1,
"type": "__reg",
},
],
Array [
Symbol(mov),
Object {
"index": 1,
"type": "__reg",
},
Object {
"index": 0,
"type": "__stack",
},
],
Array [
Symbol(mov),
3,
Object {
"index": 1,
"type": "__reg",
},
],
Array [
Symbol(mov),
Object {
"index": 1,
"type": "__reg",
},
Object {
"index": 2,
"type": "__stack",
},
],
Array [
Symbol(mov),
4,
Object {
"index": 1,
"type": "__reg",
},
],
Array [
Symbol(bop),
"+",
Object {
"index": 2,
"type": "__stack",
},
Object {
"index": 1,
"type": "__reg",
},
],
Array [
Symbol(bop),
"-",
Object {
"index": 0,
"type": "__stack",
},
Object {
"index": 1,
"type": "__reg",
},
],
Array [
Symbol(mov),
Object {
"index": 1,
"type": "__reg",
},
Object {
"name": "generated!result",
"type": "__var",
},
],
]
`;
exports[`Compiler query expressions 1`] = `
Array [
Array [
Symbol(query),
"
SELECT sum(amount) as a FROM transactions
LEFT JOIN category_mapping __cm ON __cm.id = transactions.category
WHERE ((amount > 0)) AND transactions.isParent = 0 AND transactions.tombstone = 0
",
false,
],
Array [
Symbol(mov),
Object {
"index": 1,
"type": "__reg",
},
Object {
"name": "generated!result",
"type": "__var",
},
],
]
`;
exports[`Compiler query expressions with field remapping 1`] = `
Array [
Array [
Symbol(query),
"
SELECT id FROM transactions
LEFT JOIN category_mapping __cm ON __cm.id = transactions.category
WHERE ((__cm.transferId = \\"50\\")) AND transactions.isParent = 0 AND transactions.tombstone = 0
",
false,
],
Array [
Symbol(mov),
Object {
"index": 1,
"type": "__reg",
},
Object {
"name": "generated!result",
"type": "__var",
},
],
]
`;
exports[`Compiler query expressions with field remapping 2`] = `
Array [
Array [
Symbol(query),
"
SELECT id FROM transactions
LEFT JOIN category_mapping __cm ON __cm.id = transactions.category
LEFT JOIN categories t1 ON __cm.transferId = t1.id
WHERE ((t1.name = \\"foo\\")) AND transactions.isParent = 0 AND transactions.tombstone = 0
",
false,
],
Array [
Symbol(mov),
Object {
"index": 1,
"type": "__reg",
},
Object {
"name": "generated!result",
"type": "__var",
},
],
]
`;
exports[`Compiler query expressions with field remapping 3`] = `
Array [
Array [
Symbol(query),
"
SELECT id, t1.name FROM transactions
LEFT JOIN category_mapping __cm ON __cm.id = transactions.category
LEFT JOIN categories t1 ON __cm.transferId = t1.id
WHERE ((__cm.transferId = \\"50\\")) AND transactions.isParent = 0 AND transactions.tombstone = 0
",
false,
],
Array [
Symbol(mov),
Object {
"index": 1,
"type": "__reg",
},
Object {
"name": "generated!result",
"type": "__var",
},
],
]
`;
exports[`Compiler query expressions with null 1`] = `
Array [
Array [
Symbol(query),
"
SELECT count(amount) FROM transactions
LEFT JOIN category_mapping __cm ON __cm.id = transactions.category
LEFT JOIN accounts t1 ON t1.id = transactions.acct
WHERE (((t1.offbudget = 0) and (__cm.transferId IS NULL))) AND transactions.isParent = 0 AND transactions.tombstone = 0
",
true,
],
Array [
Symbol(mov),
Object {
"index": 1,
"type": "__reg",
},
Object {
"name": "generated!result",
"type": "__var",
},
],
]
`;

View File

@@ -1,50 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`lexer basic 1`] = `
Array [
Object {
"colno": 0,
"lineno": 0,
"type": "whitespace",
"value": "
",
},
Object {
"colno": 5,
"lineno": 1,
"type": "symbol",
"value": "x",
},
Object {
"colno": 6,
"lineno": 1,
"type": "whitespace",
"value": " ",
},
Object {
"colno": 7,
"lineno": 1,
"type": "operator",
"value": "!=~",
},
Object {
"colno": 10,
"lineno": 1,
"type": "whitespace",
"value": " ",
},
Object {
"colno": 11,
"lineno": 1,
"type": "int",
"value": "4",
},
Object {
"colno": 12,
"lineno": 1,
"type": "whitespace",
"value": "
",
},
]
`;

View File

@@ -1,15 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`vm basic 1`] = `
Object {
"firstValue": [Function],
"generated!result": -6,
"number": [Function],
}
`;
exports[`vm boolean types 1`] = `
Object {
"generated!result": 0,
}
`;

View File

@@ -1,95 +0,0 @@
import { compile } from './compiler';
describe('Compiler', () => {
test('get-query', () => {
compile(
'=from transactions where acct.offbudget = 0 and category = null and (description.transfer_acct.offbudget = 1 or description.transfer_acct = null) calculate { count(date) }',
);
});
test('basic', () => {
let ops = compile(`
=first(number(from transactions
where
date >= 20170101 and
date <= 20170131 and
acct.bank.name = 1
calculate { sum(amount) }))
`).ops;
expect(ops).toMatchSnapshot();
ops = compile('').ops;
expect(ops).toMatchSnapshot();
});
test('parens', () => {
let ops = compile('=(1 + 2)').ops;
expect(ops).toMatchSnapshot();
ops = compile('=(1232 + 2) - (3 + 4)').ops;
expect(ops).toMatchSnapshot();
});
test('compiler binary ops', () => {
let ops = compile('=foo + bar + baz + boo').ops;
expect(ops).toMatchSnapshot();
});
test('compiler nested funcs', () => {
let ops = compile('=min(0, number(-20000))').ops;
expect(ops).toMatchSnapshot();
});
test('compiles boolean types', () => {
let ops = compile('=if(true and 1) { 0 } else { 1 } ').ops;
expect(ops).toMatchSnapshot();
});
test('query expressions', () => {
let ops = compile(`
=from transactions
where amount > 0
select { sum(amount) as a }
`).ops;
expect(ops).toMatchSnapshot();
});
test('query expressions with null', () => {
let ops = compile(`
=from transactions where acct.offbudget = 0 and category = null calculate { count(amount) }
`).ops;
expect(ops).toMatchSnapshot();
});
test('complex query expressions', () => {
let ops = compile(`
=from transactions groupby substr(date, 0, 7) select { substr(date, 0, 7), sum(amount) }
`).ops;
expect(ops).toMatchSnapshot();
});
test('query expressions with field remapping', () => {
let ops = compile(`
=from transactions where category = "50" select { id }
`).ops;
expect(ops).toMatchSnapshot();
ops = compile(`
=from transactions where category.name = "foo" select { id }
`).ops;
expect(ops).toMatchSnapshot();
ops = compile(`
=from transactions where category = "50" select { id, category.name }
`).ops;
expect(ops).toMatchSnapshot();
});
test('field dependencies', () => {
let sqlDependencies = compile(
'=from transactions where acct.offbudget = 0 and category = null and (description.transfer_acct.offbudget = 1 or description.transfer_acct = null) calculate { count(date) }',
).sqlDependencies;
expect(sqlDependencies[0].fields).toMatchSnapshot();
});
});

View File

@@ -1,193 +0,0 @@
import getSqlFields from './get-sql-fields';
import * as nodes from './nodes';
import {
MOV,
CALL,
QUERY,
UOP,
BOP,
REG1,
SP,
VAR,
JUMPF,
JUMPT,
LABEL,
} from './ops';
import parse from './parser';
import generateSql from './sqlgen';
class Compiler {
src;
scopeName;
binding;
ops;
dependencies;
sqlDependencies;
constructor() {
this.ops = [];
this.dependencies = [];
this.sqlDependencies = [];
}
fail(msg, lineno, colno) {
const lines = this.src.split('\n');
let space = '';
for (let i = 0; i < colno; i++) {
space += ' ';
}
throw new Error(
`[${lineno + 1}, ${colno + 1}] ${msg}:\n${lines[lineno]}\n${space}^`,
);
}
resolveVariable(name) {
if (name.indexOf('!') === -1) {
return this.scopeName + '!' + name;
}
return name;
}
maybePushStack(node, si) {
if (node instanceof nodes.Symbol) {
// There's no need to push anything to the stack since it's a
// direct variable reference. Just store the referenced variable
// and pop the symbol operation off the stack.
const op = this.ops.pop();
return [si, op[1]];
}
this.ops.push([MOV, REG1, SP(si)]);
return [si + 1, SP(si)];
}
compileLiteral(node, si) {
this.ops.push([MOV, node.value, REG1]);
}
compileSymbol(node, si) {
const resolved = this.resolveVariable(node.value);
this.dependencies.push(resolved);
this.ops.push([MOV, VAR(resolved), REG1]);
}
compileBinOp(node, si) {
this.compile(node.left, si);
// TODO: Get rid of all this and add a second pass which optimizes
// the opcodes.
let left;
[si, left] = this.maybePushStack(node.left, si);
this.compile(node.right, si + 1);
this.ops.push([BOP, node.op, left, REG1]);
}
compileUnaryOp(node, si) {
this.compile(node.target, si);
this.ops.push([UOP, node.op, REG1]);
}
compileFunCall(node, si) {
this.compile(node.callee, si);
let callee;
[si, callee] = this.maybePushStack(node.callee, si);
const args = node.args.children.map((arg, i) => {
this.compile(arg, si + i);
this.ops.push([MOV, REG1, SP(si + i)]);
return SP(si + i);
});
this.ops.push([CALL, callee, args]);
}
compileQuery(node, si) {
let fields = getSqlFields(node.table, node.where)
.concat(getSqlFields(node.table, node.groupby))
.concat(...node.select.map(s => getSqlFields(node.table, s.expr)));
const { sql, where } = generateSql(
node.table,
node.where,
node.groupby,
node.select,
);
// TODO: This is a hack, but I'm pretty sure we can get rid of all
// of this. Just need to think through it.
fields = fields.map(f => (f === '__cm.transferId' ? 'category' : f));
// Uniquify them
fields = [...new Set(fields)];
this.sqlDependencies.push({ table: node.table, where, fields });
this.ops.push([QUERY, sql, node.calculated]);
}
compileIf(node, si) {
const L0 = LABEL();
const L1 = LABEL();
this.compile(node.cond, si);
this.ops.push([MOV, REG1, SP(si)]);
this.ops.push([JUMPF, SP(si), L0]);
this.compile(node.body, si + 1);
this.ops.push([JUMPT, SP(si), L1]);
L0.resolve(this.ops.length - 1);
this.compile(node.else_, si + 1);
L1.resolve(this.ops.length - 1);
}
compileRoot(node, si) {
node.children.forEach(node => {
this.compile(node, si);
});
}
compile(node, si) {
const method = this['compile' + node.getTypeName()];
if (!method) {
this.fail(
'Unknown node type: ' + node.getTypeName(),
node.lineno,
node.colno,
);
}
return method.call(this, node, si);
}
compileSource(binding, scopeName, src) {
this.src = src;
this.scopeName = scopeName;
this.binding = binding;
this.compile(parse(src), 0);
const resolvedBinding = this.resolveVariable(binding);
if (this.ops.length !== 0) {
this.ops.push([MOV, REG1, VAR(resolvedBinding)]);
} else {
this.ops.push([MOV, '', VAR(resolvedBinding)]);
}
return {
ops: this.ops,
dependencies: this.dependencies,
sqlDependencies: this.sqlDependencies,
};
}
}
export function compile(src) {
return compileBinding('result', 'generated', src);
}
export function compileBinding(binding, scopeName, src) {
const compiler = new Compiler();
return compiler.compileSource(binding, scopeName, src);
}

View File

@@ -1,48 +0,0 @@
function traverse(expr, fields) {
switch (expr.getTypeName()) {
case 'FunCall':
expr.args.children.map(arg => traverse(arg, fields));
break;
case 'Member':
// Right now we only track dependencies on the top-level table,
// and not any of the joined data. This tracks that field itself
// that is joined on, but not the joined data yet.
traverse(expr.object, fields);
break;
case 'Literal':
break;
case 'Symbol':
if (fields.indexOf(expr.value) === -1 && expr.value !== 'null') {
fields.push(expr.value);
}
break;
case 'BinOp':
traverse(expr.left, fields);
traverse(expr.right, fields);
break;
default:
throw new Error('Unhandled node type: ' + expr.getTypeName());
}
}
export default function getSqlFields(table, ast) {
let fields: string[] = [];
if (!ast) {
return fields;
}
traverse(ast, fields);
// These are implicit fields added by the sql generator. Going to
// revisit how to track all of this.
if (table === 'transactions') {
fields.push('isParent');
fields.push('tombstone');
}
return fields;
}

View File

@@ -1,17 +0,0 @@
import lex from './lexer';
function getTokens(tokens) {
const toks = [];
while (!tokens.is_finished()) {
toks.push(tokens.nextToken());
}
return toks;
}
test('lexer basic', () => {
const tokens = lex(`
=x !=~ 4
`);
expect(getTokens(tokens)).toMatchSnapshot();
});

View File

@@ -1,310 +0,0 @@
const whitespaceChars = new Set(' \n\t\r\u00A0');
const delimChars = new Set('()[]{}%*-+~/#,:|.<>=!');
const whitespaceAndDelimChars = new Set([...whitespaceChars, ...delimChars]);
const intChars = new Set('0123456789');
const complexOps = new Set(['==', '!=', '<=', '>=', '=~', '!=~']);
export const TOKEN_STRING = 'string';
export const TOKEN_WHITESPACE = 'whitespace';
export const TOKEN_LEFT_PAREN = 'left-paren';
export const TOKEN_RIGHT_PAREN = 'right-paren';
export const TOKEN_LEFT_BRACKET = 'left-bracket';
export const TOKEN_RIGHT_BRACKET = 'right-bracket';
export const TOKEN_LEFT_CURLY = 'left-curly';
export const TOKEN_RIGHT_CURLY = 'right-curly';
export const TOKEN_COMMA = 'comma';
export const TOKEN_INT = 'int';
export const TOKEN_FLOAT = 'float';
export const TOKEN_BOOLEAN = 'boolean';
export const TOKEN_SYMBOL = 'symbol';
export const TOKEN_DOT = 'dot';
export const TOKEN_EXCLAIM = 'exclaim';
export const TOKEN_OPERATOR = 'operator';
function token(type, value, lineno, colno) {
return {
type: type,
value: value,
lineno: lineno,
colno: colno,
};
}
class Tokenizer {
colno;
hasCheckedMode;
index;
len;
lineno;
str;
constructor(str, opts = {}) {
this.str = str;
this.index = 0;
this.len = str.length;
this.lineno = 0;
this.colno = 0;
this.hasCheckedMode = false;
}
nextToken() {
let lineno = this.lineno;
let colno = this.colno;
let tok;
let cur = this.current();
if (this.is_finished()) {
return null;
} else if ((tok = this._extract(whitespaceChars))) {
// We hit some whitespace
return token(TOKEN_WHITESPACE, tok, lineno, colno);
} else if (!this.hasCheckedMode) {
this.hasCheckedMode = true;
if (cur === '=') {
this.forward();
cur = this.current();
return this.nextToken();
} else {
this.index = this.str.length;
return token(TOKEN_STRING, this.str, lineno, colno);
}
// eslint-disable-next-line rulesdir/typography
} else if (cur === '"' || cur === "'") {
// We've hit a string
return token(TOKEN_STRING, this.parseString(cur), lineno, colno);
} else if (delimChars.has(cur)) {
// We've hit a delimiter (a special char like a bracket)
let type;
if (complexOps.has(cur + this.next() + this.next(2))) {
cur = cur + this.next() + this.next(2);
this.forward();
this.forward();
} else if (complexOps.has(cur + this.next())) {
cur = cur + this.next();
this.forward();
}
this.forward();
switch (cur) {
case '(':
type = TOKEN_LEFT_PAREN;
break;
case ')':
type = TOKEN_RIGHT_PAREN;
break;
case '[':
type = TOKEN_LEFT_BRACKET;
break;
case ']':
type = TOKEN_RIGHT_BRACKET;
break;
case '{':
type = TOKEN_LEFT_CURLY;
break;
case '}':
type = TOKEN_RIGHT_CURLY;
break;
case ',':
type = TOKEN_COMMA;
break;
case '.':
type = TOKEN_DOT;
break;
case '!':
type = TOKEN_EXCLAIM;
break;
default:
type = TOKEN_OPERATOR;
}
return token(type, cur, lineno, colno);
} else {
// We are not at whitespace or a delimiter, so extract the
// text and parse it
tok = this._extractUntil(whitespaceAndDelimChars);
if (tok.match(/^[-+]?[0-9]+$/)) {
if (this.current() === '.') {
this.forward();
let dec = this._extract(intChars);
return token(TOKEN_FLOAT, tok + '.' + dec, lineno, colno);
} else {
return token(TOKEN_INT, tok, lineno, colno);
}
} else if (tok.match(/^(true|false)$/)) {
return token(TOKEN_BOOLEAN, tok, lineno, colno);
} else if (tok.match(/^(or|and|not)$/)) {
return token(TOKEN_OPERATOR, tok, lineno, colno);
} else if (tok) {
return token(TOKEN_SYMBOL, tok, lineno, colno);
} else {
throw new Error('Unexpected value while parsing: ' + tok);
}
}
}
parseString(delimiter) {
this.forward();
let str = '';
while (!this.is_finished() && this.current() !== delimiter) {
let cur = this.current();
if (cur === '\\') {
this.forward();
switch (this.current()) {
case 'n':
str += '\n';
break;
case 't':
str += '\t';
break;
case 'r':
str += '\r';
break;
default:
str += this.current();
}
this.forward();
} else {
str += cur;
this.forward();
}
}
this.forward();
return str;
}
_matches(str) {
if (this.index + str.length > this.len) {
return null;
}
let m = this.str.slice(this.index, this.index + str.length);
return m === str;
}
_extractString(str) {
if (this._matches(str)) {
this.index += str.length;
return str;
}
return null;
}
_extractUntil(chars) {
// Extract all non-matching chars, with the default matching set
// to everything
return this._extractMatching(true, chars || new Set());
}
_extract(chars) {
// Extract all matching chars (no default, so charString must be
// explicit)
return this._extractMatching(false, chars);
}
_extractMatching(breakOnMatch, chars) {
// Pull out characters until a breaking char is hit.
// If breakOnMatch is false, a non-matching char stops it.
// If breakOnMatch is true, a matching char stops it.
if (this.is_finished()) {
return null;
}
let matches = chars.has(this.current());
// Only proceed if the first character meets our condition
if ((breakOnMatch && !matches) || (!breakOnMatch && matches)) {
let t = this.current();
this.forward();
// And pull out all the chars one at a time until we hit a
// breaking char
let isMatch = chars.has(this.current());
while (
((breakOnMatch && !isMatch) || (!breakOnMatch && isMatch)) &&
!this.is_finished()
) {
t += this.current();
this.forward();
isMatch = chars.has(this.current());
}
return t;
}
return '';
}
is_finished() {
return this.index >= this.len;
}
forward() {
this.index++;
if (this.previous() === '\n') {
this.lineno++;
this.colno = 0;
} else {
this.colno++;
}
}
back() {
this.index--;
if (this.current() === '\n') {
this.lineno--;
let idx = this.str.lastIndexOf('\n', this.index - 1);
if (idx === -1) {
this.colno = this.index;
} else {
this.colno = this.index - idx;
}
} else {
this.colno--;
}
}
// current returns current character
current() {
if (!this.is_finished()) {
return this.str.charAt(this.index);
}
return '';
}
next(idx = 1) {
if (this.index + idx < this.str.length) {
return this.str.charAt(this.index + idx);
}
return '';
}
// currentStr returns what's left of the unparsed string
currentStr() {
if (!this.is_finished()) {
return this.str.substr(this.index);
}
return '';
}
previous() {
return this.str.charAt(this.index - 1);
}
}
export default function lex(src, opts?: Record<string, unknown>) {
return new Tokenizer(src, opts);
}

View File

@@ -1,201 +0,0 @@
class Node {
colno;
fieldNames;
lineno;
constructor(lineno, colno, fieldNames) {
this.lineno = lineno;
this.colno = colno;
this.fieldNames = fieldNames;
}
getTypeName() {
return 'Node';
}
traverseFields(onEnter, onExit) {
const fieldNames = this.fieldNames;
for (let i = 0; i < fieldNames.length; i++) {
const val = this[fieldNames[i]];
if (val instanceof Node) {
const ret = val.traverse(onEnter, onExit);
if (ret) {
this[fieldNames[i]] = ret;
}
}
}
}
traverse(onEnter, onExit) {
if (onEnter) {
const val = onEnter(this);
if (val === true) {
return;
} else if (val != null) {
return val;
}
}
this.traverseFields(onEnter, onExit);
onExit && onExit(this);
}
copy() {
const inst = Object.assign(
Object.create(Object.getPrototypeOf(this)),
this,
);
for (let i = 0; i < inst.fieldNames.length; i++) {
const field = inst.fieldNames[i];
if (inst[field] instanceof Node) {
inst[field] = inst[field].copy();
}
}
return inst;
}
}
export class NodeList extends Node {
children;
constructor(lineno, colno, nodes: unknown[] = []) {
super(lineno, colno, ['children']);
this.children = nodes;
}
getTypeName() {
return 'NodeList';
}
addChild(node) {
this.children.push(node);
}
traverseFields(onEnter, onExit) {
for (let i = 0; i < this.children.length; i++) {
this.children[i].traverse(onEnter, onExit);
}
}
}
export class Root extends NodeList {
getTypeName() {
return 'Root';
}
}
export class Value extends Node {
value;
constructor(lineno, colno, value) {
super(lineno, colno, ['value']);
this.value = value ?? null;
}
getTypeName() {
return 'Value';
}
}
export class UnaryOp extends Node {
op;
target;
constructor(lineno, colno, op, target) {
super(lineno, colno, ['op', 'target']);
this.op = op ?? null;
this.target = target ?? null;
}
getTypeName() {
return 'UnaryOp';
}
}
export class BinOp extends Node {
op;
left;
right;
constructor(lineno, colno, op, left, right) {
super(lineno, colno, ['op', 'left', 'right']);
this.op = op ?? null;
this.left = left ?? null;
this.right = right ?? null;
}
getTypeName() {
return 'BinOp';
}
}
export class Literal extends Value {
getTypeName() {
return 'Literal';
}
}
export class Symbol extends Value {
getTypeName() {
return 'Symbol';
}
}
export class FunCall extends Node {
callee;
args;
constructor(lineno, colno, callee, args) {
super(lineno, colno, ['callee', 'args']);
this.callee = callee ?? null;
this.args = args ?? null;
}
getTypeName() {
return 'FunCall';
}
}
export class Member extends Node {
object;
property;
constructor(lineno, colno, object, property) {
super(lineno, colno, ['object', 'property']);
this.object = object ?? null;
this.property = property ?? null;
}
getTypeName() {
return 'Member';
}
}
export class Query extends Node {
table;
select;
where;
groupby;
calculated;
constructor(lineno, colno, table, select, where, groupby, calculated) {
super(lineno, colno, ['table', 'select', 'where', 'groupby', 'calculated']);
this.table = table ?? null;
this.select = select ?? null;
this.where = where ?? null;
this.groupby = groupby ?? null;
this.calculated = calculated ?? null;
}
getTypeName() {
return 'Query';
}
}
export class If extends Node {
cond;
body;
else_;
constructor(lineno, colno, cond, body, else_) {
super(lineno, colno, ['cond', 'body', 'else_']);
this.cond = cond ?? null;
this.body = body ?? null;
this.else_ = else_ ?? null;
}
getTypeName() {
return 'If';
}
}

View File

@@ -1,33 +0,0 @@
export const MOV = Symbol('mov');
export const CALL = Symbol('call');
export const QUERY = Symbol('query');
export const UOP = Symbol('uop');
export const BOP = Symbol('bop');
export const JUMPF = Symbol('jumpf');
export const JUMPT = Symbol('jumpt');
export const REG1 = { type: '__reg', index: 1 };
export function SP(n) {
return { type: '__stack', index: n };
}
export function VAR(name) {
return { type: '__var', name: name };
}
export function LABEL() {
let idx = null;
return {
get() {
if (idx === null) {
throw new Error('Attempted access of unresolved label');
}
return idx;
},
resolve(n) {
idx = n;
},
};
}

View File

@@ -1,462 +0,0 @@
import lex, * as types from './lexer';
import * as nodes from './nodes';
function nextToken(state, withWhitespace?: boolean) {
let tok;
let { peeked, tokens } = state;
if (peeked) {
if (!withWhitespace && peeked.type === types.TOKEN_WHITESPACE) {
state.peeked = null;
} else {
tok = state.peeked;
state.peeked = null;
return tok;
}
}
tok = tokens.nextToken();
if (!withWhitespace) {
while (tok && tok.type === types.TOKEN_WHITESPACE) {
tok = tokens.nextToken();
}
}
return tok;
}
function peekToken(state) {
state.peeked = state.peeked || nextToken(state);
return state.peeked;
}
function pushToken(state, tok) {
if (state.peeked) {
throw new Error('pushToken: can only push one token on between reads');
}
state.peeked = tok;
}
function fail(state, msg, lineno?: number, colno?: number) {
if (!peekToken(state)) {
throw new Error(msg + '\n\nSource:\n' + state.src + '\n');
}
if (lineno === undefined || colno === undefined) {
const tok = peekToken(state);
lineno = tok.lineno;
colno = tok.colno;
}
const lines = state.src.split('\n');
let space = '';
for (let i = 0; i < colno; i++) {
space += ' ';
}
throw new Error(
`[${lineno + 1}, ${colno + 1}] ${msg}:\n${lines[lineno]}\n${space}^`,
);
}
function skip(state, type) {
let tok = nextToken(state);
if (!tok || tok.type !== type) {
pushToken(state, tok);
return false;
}
return true;
}
function expectValue(state, type, value) {
let tok = nextToken(state);
if (tok.type !== type || tok.value !== value) {
fail(
state,
'expected ' + value + ', got ' + tok.value,
tok.lineno,
tok.colno,
);
}
return tok;
}
function expect(state, type) {
let tok = nextToken(state);
if (tok.type !== type) {
fail(
state,
'expected ' + type + ', got ' + tok.type,
tok.lineno,
tok.colno,
);
}
return tok;
}
function skipValue(state, type, val) {
let tok = nextToken(state);
if (!tok || tok.type !== type || tok.value !== val) {
pushToken(state, tok);
return false;
}
return true;
}
function skipSymbol(state, val) {
return skipValue(state, types.TOKEN_SYMBOL, val);
}
function parseExpression(state) {
return parseOr(state);
}
function parseOr(state) {
let left = parseAnd(state);
while (skipValue(state, types.TOKEN_OPERATOR, 'or')) {
const right = parseAnd(state);
left = new nodes.BinOp(left.lineno, left.colno, 'or', left, right);
}
return left;
}
function parseAnd(state) {
let left = parseNot(state);
while (skipValue(state, types.TOKEN_OPERATOR, 'and')) {
const right = parseNot(state);
left = new nodes.BinOp(left.lineno, left.colno, 'and', left, right);
}
return left;
}
function parseNot(state) {
let left = parseCompare(state);
while (skipValue(state, types.TOKEN_OPERATOR, 'not')) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const right = parseCompare(state);
left = new nodes.UnaryOp(left.lineno, left.colno, 'not', parseNot(state));
}
return left;
}
function parseCompare(state) {
let compareOps = ['=', '!=', '<', '>', '<=', '>=', '=~', '!=~'];
let node = parseAdd(state);
while (1) {
let tok = nextToken(state);
if (!tok) {
break;
} else if (compareOps.indexOf(tok.value) !== -1) {
node = new nodes.BinOp(
tok.lineno,
tok.colno,
tok.value,
node,
parseAdd(state),
);
} else {
pushToken(state, tok);
break;
}
}
return node;
}
function parseAdd(state) {
let left = parseSub(state);
while (skipValue(state, types.TOKEN_OPERATOR, '+')) {
const right = parseSub(state);
left = new nodes.BinOp(left.lineno, left.colno, '+', left, right);
}
return left;
}
function parseSub(state) {
let left = parseMul(state);
while (skipValue(state, types.TOKEN_OPERATOR, '-')) {
const right = parseMul(state);
left = new nodes.BinOp(left.lineno, left.colno, '-', left, right);
}
return left;
}
function parseMul(state) {
let left = parseDiv(state);
while (skipValue(state, types.TOKEN_OPERATOR, '*')) {
const right = parseDiv(state);
left = new nodes.BinOp(left.lineno, left.colno, '*', left, right);
}
return left;
}
function parseDiv(state) {
let left = parseUnary(state);
while (skipValue(state, types.TOKEN_OPERATOR, '/')) {
const right = parseUnary(state);
left = new nodes.BinOp(left.lineno, left.colno, '/', left, right);
}
return left;
}
function parseUnary(state) {
let tok = peekToken(state);
if (skipValue(state, types.TOKEN_OPERATOR, '-')) {
const nextTok = peekToken(state);
if (nextTok.type === types.TOKEN_INT) {
const number = parseInt(nextToken(state).value);
return new nodes.Literal(tok.lineno, tok.colno, -number);
} else if (nextTok.type === types.TOKEN_FLOAT) {
const number = parseFloat(nextToken(state).value);
return new nodes.Literal(tok.lineno, tok.colno, -number);
}
return new nodes.UnaryOp(tok.lineno, tok.colno, '-', parseUnary(state));
}
return parsePrimary(state);
}
function parsePrimary(state) {
let tok = nextToken(state);
let val: number | boolean | null = null;
if (!tok) {
fail(state, 'expected expression, got end of file');
} else if (tok.type === types.TOKEN_STRING) {
val = tok.value;
} else if (tok.type === types.TOKEN_INT) {
val = parseInt(tok.value, 10);
} else if (tok.type === types.TOKEN_FLOAT) {
val = parseFloat(tok.value);
} else if (tok.type === types.TOKEN_BOOLEAN) {
if (tok.value === 'true') {
val = true;
} else if (tok.value === 'false') {
val = false;
}
}
if (val !== null) {
return new nodes.Literal(tok.lineno, tok.colno, val);
} else if (tok.type === types.TOKEN_SYMBOL) {
if (tok.value === 'from') {
return parseQueryExpression(state);
} else if (tok.value === 'if') {
return parseIfExpression(state);
}
return parsePostfix(
state,
new nodes.Symbol(tok.lineno, tok.colno, tok.value),
);
} else if (tok.type === types.TOKEN_LEFT_PAREN) {
const node = parseExpression(state);
expect(state, types.TOKEN_RIGHT_PAREN);
return node;
}
fail(state, 'Unexpected token: ' + tok.value, tok.lineno, tok.colno);
}
function parseIfExpression(state) {
const tok = expect(state, types.TOKEN_LEFT_PAREN);
const cond = parseExpression(state);
expect(state, types.TOKEN_RIGHT_PAREN);
expect(state, types.TOKEN_LEFT_CURLY);
const body = parseExpression(state);
expect(state, types.TOKEN_RIGHT_CURLY);
let else_;
if (skipSymbol(state, 'else')) {
expect(state, types.TOKEN_LEFT_CURLY);
else_ = parseExpression(state);
expect(state, types.TOKEN_RIGHT_CURLY);
}
return new nodes.If(tok.lineno, tok.colno, cond, body, else_);
}
function parseQueryExpression(state) {
// The `from` keyword has already been parsed
const tok = expect(state, types.TOKEN_SYMBOL);
const table = tok.value;
let where = null;
if (skipSymbol(state, 'where')) {
where = parseQuerySubExpression(state);
}
let groupby = null;
if (skipSymbol(state, 'groupby')) {
groupby = parseQuerySubExpression(state);
}
let select: Array<{ expr: unknown; as?: unknown }> = [];
let calculated;
if (skipSymbol(state, 'select')) {
let checkComma = false;
calculated = false;
expectValue(state, types.TOKEN_LEFT_CURLY, '{');
while (!skipValue(state, types.TOKEN_RIGHT_CURLY, '}')) {
const tok = peekToken(state);
if (checkComma && !skip(state, types.TOKEN_COMMA)) {
fail(
state,
'Unexpected token in query select: ' + tok.value,
tok.lineno,
tok.colno,
);
}
const expr = parseQuerySubExpression(state);
let as = null;
if (skipSymbol(state, 'as')) {
const tok = expect(state, types.TOKEN_SYMBOL);
as = tok.value;
}
select.push({ expr, as });
checkComma = true;
}
} else if (skipSymbol(state, 'calculate')) {
calculated = true;
expectValue(state, types.TOKEN_LEFT_CURLY, '{');
select.push({ expr: parseQuerySubExpression(state) });
if (!skipValue(state, types.TOKEN_RIGHT_CURLY, '}')) {
fail(state, 'Only one expression allowed for `calculate`');
}
} else {
fail(state, 'Expected either the `select` or `calculate` keyword');
}
return new nodes.Query(
tok.lineno,
tok.colno,
table,
select,
where,
groupby,
calculated,
);
}
function parseQuerySubExpression(state) {
const node = parseExpression(state);
return node;
}
function parsePostfix(state, node) {
let tok;
while ((tok = nextToken(state))) {
if (tok.type === types.TOKEN_LEFT_PAREN) {
pushToken(state, tok);
let args = parseArgs(state);
node = new nodes.FunCall(tok.lineno, tok.colno, node, args);
} else if (tok.type === types.TOKEN_DOT) {
const val = nextToken(state);
node = new nodes.Member(
tok.lineno,
tok.colno,
node,
new nodes.Literal(val.lineno, val.colno, val.value),
);
} else if (tok.type === types.TOKEN_EXCLAIM) {
const name = nextToken(state);
if (name.type !== types.TOKEN_SYMBOL) {
fail(
state,
'Expected cell name in sheet reference',
name.lineno,
name.colno,
);
}
return new nodes.Symbol(
node.lineno,
node.colno,
node.value + '!' + name.value,
);
} else {
pushToken(state, tok);
break;
}
}
return node;
}
function parseArgs(state) {
let tok = peekToken(state);
if (tok.type !== types.TOKEN_LEFT_PAREN) {
fail(state, 'Expected arguments', tok.lineno, tok.colno);
}
nextToken(state);
let args = new nodes.NodeList(tok.lineno, tok.colno);
let checkComma = false;
while (1) {
tok = peekToken(state);
if (tok.type === types.TOKEN_RIGHT_PAREN) {
nextToken(state);
break;
}
if (checkComma && !skip(state, types.TOKEN_COMMA)) {
fail(
state,
'Expected comma after function argument',
tok.lineno,
tok.colno,
);
}
args.addChild(parseExpression(state));
checkComma = true;
}
return args;
}
export default function parse(src) {
let state = {
src: src,
tokens: lex(src),
peeked: null,
};
if (state.tokens.is_finished()) {
// If it's an empty string, return nothing
return new nodes.Root(0, 0, []);
} else {
const expr = parseExpression(state);
const tok = nextToken(state);
if (tok) {
fail(
state,
'Unexpected token after expression: ' + tok.value,
tok.lineno,
tok.colno,
);
}
return new nodes.Root(0, 0, [expr]);
}
}

View File

@@ -1,18 +0,0 @@
import { SCHEMA_PATHS } from './sqlgen';
export default function convert(table, item) {
if (SCHEMA_PATHS[table]) {
let fields = SCHEMA_PATHS[table];
let updates = {};
Object.keys(item).forEach(k => {
let mappedField = fields[k] && fields[k].field;
if (mappedField) {
updates[k] = item[mappedField];
}
});
return { ...item, ...updates };
}
return item;
}

View File

@@ -1,303 +0,0 @@
import * as nodes from './nodes';
type Lookup = { field: string; tableId?: string };
let _uid = 0;
function resetUid() {
_uid = 0;
}
function uid() {
_uid++;
return 't' + _uid;
}
function fail(node, message) {
const err = new Error(message);
// @ts-expect-error We should use error.cause to pass node info
err.node = node;
throw err;
}
function generateExpression(expr) {
if (typeof expr === 'string') {
// eslint-disable-next-line rulesdir/typography
return `"${expr}"`;
} else if (typeof expr === 'number') {
return expr;
}
switch (expr.getTypeName()) {
case 'FunCall':
return (
generateExpression(expr.callee) +
'(' +
expr.args.children.map(node => generateExpression(node)).join(',') +
')'
);
case 'Member':
return (
generateExpression(expr.object) +
'.' +
generateExpression(expr.property)
);
case 'BinOp':
const left = generateExpression(expr.left);
let str;
if (
expr.op === '=' &&
expr.right.getTypeName() === 'Symbol' &&
expr.right.value === 'null'
) {
str = left + ' IS NULL';
} else {
const right = generateExpression(expr.right);
switch (expr.op) {
case '=~':
str = `${left} LIKE ${right}`;
break;
case '!=~':
str = `${left} NOT LIKE ${right}`;
break;
default:
str = `${left} ${expr.op} ${right}`;
}
}
return '(' + str + ')';
case 'Literal':
if (typeof expr.value === 'string') {
// eslint-disable-next-line rulesdir/typography
return `"${expr.value}"`;
}
return expr.value;
case 'Symbol':
// if (expr.value.indexOf('!') !== -1) {
// fail(expr, 'SQL variable cannot contain cell lookup');
// }
return expr.value;
default:
throw new Error('Unknown query node: ' + expr.getTypeName());
}
}
function transformColumns(node, implicitTable) {
let transformed = node.traverse(n => {
if (n instanceof nodes.Symbol) {
let table = implicitTable;
let field = n.value;
if (SCHEMA_PATHS[table] && SCHEMA_PATHS[table][field]) {
let info = SCHEMA_PATHS[table][field];
if (info.field) {
// Map the field onto something else
return new nodes.Symbol(n.lineno, n.colno, info.field);
}
}
}
});
return transformed || node;
}
function transformLookups(node, implicitTable) {
let paths: Lookup[][] = [];
const transformed = node.traverse(n => {
if (n instanceof nodes.Member) {
let currentNode = n;
let lookups: Lookup[] = [];
while (currentNode instanceof nodes.Member) {
if (!(currentNode.property instanceof nodes.Value)) {
fail(currentNode, 'Invalid syntax for SQL reference');
}
lookups.push({ field: currentNode.property.value });
currentNode = currentNode.object;
}
// @ts-expect-error Node refinement missing
if (!(currentNode instanceof nodes.Symbol)) {
fail(currentNode, 'Invalid syntax for SQL reference');
}
// @ts-expect-error Node refinement missing
lookups.push({ field: currentNode.value });
lookups.reverse();
lookups = lookups.map((lookup, idx) => {
return {
field: lookup.field,
tableId: uid(),
};
});
let table = implicitTable;
// Skip the last field as we don't want to resolve to that
// table. The syntax to emit is `table.field`.
for (let i = 0; i < lookups.length - 1; i++) {
const lookup = lookups[i];
if (!SCHEMA_PATHS[table]) {
const err = new Error(
`Table “${table}” not joinable for field “${lookup}`,
);
// @ts-expect-error We should use error.cause to pass node info
err.node = node;
throw err;
}
if (!SCHEMA_PATHS[table][lookup.field]) {
const err = new Error(
`Unknown field “${lookup}” on table “${table}`,
);
// @ts-expect-error We should use error.cause to pass node info
err.node = node;
throw err;
}
table = SCHEMA_PATHS[table][lookup.field].table;
}
paths.push(lookups);
let tableId = lookups[lookups.length - 2].tableId;
let field = lookups[lookups.length - 1].field;
return new nodes.Member(
node.lineno,
node.colno,
new nodes.Symbol(node.lineno, node.colno, tableId),
new nodes.Symbol(node.lineno, node.colno, field),
);
}
});
return { paths, node: transformed || node };
}
export default function generate(table, where, groupby, select) {
// Figure out the dep tables here. Return the SQL and dependent
// tables
let allPaths: Lookup[][] = [];
resetUid();
if (!tables[table]) {
throw new Error('Table not found: ' + table);
}
const selectStr = select
.map(s => {
let { paths, node } = transformLookups(s.expr, table);
let as = s.as;
allPaths = allPaths.concat(paths);
let newNode = transformColumns(node, table);
// If the selected field was transformed, select it as the
// original name
if (node !== newNode && node instanceof nodes.Symbol && !as) {
as = node.value;
}
const exprStr = generateExpression(newNode);
return as ? `${exprStr} as ${as}` : exprStr;
})
.join(', ');
let whereStr = '';
let whereTransformed;
if (where) {
let { paths, node } = transformLookups(where, table);
allPaths = allPaths.concat(paths);
whereTransformed = node.copy();
// Where clauses provide a special hook to map a column onto
// something different, so you can represent something more
// complex internally. You are still required to provide the
// original name somehow; all other references use the original
// name, so make sure you do `JOIN table <original-name>` or
// something like that.
node = transformColumns(node, table);
whereStr = ' WHERE (' + generateExpression(node) + ')';
}
let groupByStr = '';
if (groupby) {
let { paths, node } = transformLookups(groupby, table);
allPaths = allPaths.concat(paths);
groupByStr = ' GROUP BY ' + generateExpression(node);
}
let dependencies: string[] = [];
let joins: string[] = [];
allPaths.forEach(path => {
let currentTable = { name: table, id: table };
for (let i = 0; i < path.length - 1; i++) {
let lookup = path[i];
let meta = SCHEMA_PATHS[currentTable.name][lookup.field];
if (meta.sql) {
joins.push(meta.sql(lookup.tableId));
} else {
joins.push(
`LEFT JOIN ${meta.table} ${lookup.tableId} ON ${lookup.tableId}.id = ${currentTable.id}.${lookup.field}`,
);
}
if (dependencies.indexOf(meta.table) === -1) {
dependencies.push(meta.table);
}
currentTable = { name: meta.table, id: lookup.tableId };
}
});
const sql =
tables[table](selectStr, whereStr, joins.join('\n')) + ' ' + groupByStr;
return {
sql,
where: whereTransformed,
dependencies,
};
}
export const SCHEMA_PATHS = {
transactions: {
category: {
table: 'categories',
sql: id => `LEFT JOIN categories ${id} ON __cm.transferId = ${id}.id`,
field: '__cm.transferId',
},
acct: { table: 'accounts' },
description: { table: 'payees' },
},
payees: {
transfer_acct: { table: 'accounts' },
},
accounts: {
bank: { table: 'banks' },
},
};
const tables = {
transactions: (select, where, join) => {
// Never take into account parent split transactions. Their
// children should sum up to be equal to it
// prettier-ignore
let whereStr = `${where === '' ? 'WHERE' : where + ' AND'} transactions.isParent = 0 AND transactions.tombstone = 0`;
return `
SELECT ${select} FROM transactions
LEFT JOIN category_mapping __cm ON __cm.id = transactions.category
${join}
${whereStr}
`;
},
};

View File

@@ -1,58 +0,0 @@
import { unresolveName } from '../util';
import VM from './vm';
const db = {
runQuery: sql => {
return Promise.resolve([{ 'sum(t.amount)': 1000 }]);
},
};
function makeScopes(vars) {
return {
getVariable: resolvedName => {
const { name } = unresolveName(resolvedName);
if (vars[resolvedName] !== undefined) {
return vars[resolvedName];
} else if (vars[name] !== undefined) {
return vars[name];
}
throw new Error(`${resolvedName}” is not defined`);
},
setVariable: (name, value) => {
vars[name] = value;
},
getAll: () => vars,
};
}
function run(src, vars = {}) {
const scopes = makeScopes(vars);
const vm = new VM(db, scopes);
return new Promise(resolve => {
vm.runSource(src, () => {
expect(scopes.getAll()).toMatchSnapshot();
resolve(undefined);
});
});
}
test('vm basic', async () => {
return run(`=-(1 + 2 + 3)`, {
number: x => {
return x;
},
firstValue: arr => {
return arr[0]['sum(t.amount)'];
},
});
});
test('vm boolean types', async () => {
return run('=if(true and (1 + 2 + 3 - 5)) { 0 } else { 1 } ');
});

View File

@@ -1,193 +0,0 @@
import { compile } from './compiler';
import { MOV, CALL, QUERY, UOP, BOP, JUMPF, JUMPT } from './ops';
export default class VM {
_onFinish;
db;
ops;
paused;
pc;
reg1;
scopes;
stack;
constructor(db, scopes) {
this.stack = new Array(1000);
this.reg1 = null;
this.pc = 0;
this.db = db;
this.scopes = scopes;
}
get(ref) {
if (ref && ref.type) {
if (ref.type === '__reg') {
return this.reg1;
} else if (ref.type === '__var') {
return this.scopes.getVariable(ref.name);
} else if (ref.type === '__stack') {
return this.stack[ref.index];
}
}
return ref;
}
set(ref, value) {
if (ref && ref.type) {
if (ref.type === '__reg') {
this.reg1 = this.get(value);
} else if (ref.type === '__var') {
this.scopes.setVariable(ref.name, this.get(value));
} else if (ref.type === '__stack') {
this.stack[ref.index] = this.get(value);
}
}
}
binaryOp(op, left, right) {
switch (op) {
case '+':
// TODO: Enforce these to be numbers
this.reg1 = this.get(left) + this.get(right);
break;
case '-':
this.reg1 = this.get(left) - this.get(right);
break;
case '*':
this.reg1 = this.get(left) * this.get(right);
break;
case '/':
this.reg1 = this.get(left) / this.get(right);
break;
case 'and':
this.reg1 = this.get(left) && this.get(right);
break;
case 'or':
this.reg1 = this.get(left) || this.get(right);
break;
default:
throw new Error('Unimplemented operator: ' + op);
}
}
unaryOp(op, target) {
switch (op) {
case '-':
this.reg1 = -this.get(target);
break;
default:
throw new Error('Unimplemented operator: ' + op);
}
}
call(callee, args) {
const func = this.get(callee);
this.reg1 = func.apply(
null,
args.map(arg => this.get(arg)),
);
}
query(sql, calculated) {
this.pause(
this.db.runQuery(sql, [], true).then(res => {
if (calculated) {
const keys = Object.keys(res[0]);
return res[0][keys[0]];
}
return res;
}),
'Running sql: ' + sql,
);
}
jump(value, loc, { test }) {
const result = this.get(value);
const falsy = result === false || result === 0 || result === '';
if ((test === 'true' && !falsy) || (test === 'false' && falsy)) {
this.pc = loc.get();
}
}
pause(promise, activityName) {
this.paused = true;
promise.then(
val => {
this.resume(val);
},
err => {
console.log('VM caught error during activity: ' + activityName);
console.log(err);
this.resume(null);
},
);
}
resume(val) {
this.reg1 = val;
this.paused = false;
this._run();
}
_run() {
while (this.pc < this.ops.length) {
const op = this.ops[this.pc];
switch (op[0]) {
case MOV:
this.set(op[2], op[1]);
break;
case CALL:
this.call(op[1], op[2]);
break;
case QUERY:
this.query(op[1], op[2]);
break;
case BOP:
this.binaryOp(op[1], op[2], op[3]);
break;
case UOP:
this.unaryOp(op[1], op[2]);
break;
case JUMPF:
this.jump(op[1], op[2], { test: 'false' });
break;
case JUMPT:
this.jump(op[1], op[2], { test: 'true' });
break;
default:
throw new Error('Unimplemented opcode: ' + op[0].toString());
}
this.pc++;
if (this.paused) {
break;
}
}
if (this.pc === this.ops.length && this._onFinish) {
this._onFinish(this.reg1);
}
}
onFinish(func) {
this._onFinish = func;
}
run(ops, onFinish) {
this.pc = 0;
this.ops = ops;
this._onFinish = onFinish;
this._run();
return this.reg1;
}
runSource(src, onFinish) {
const { ops } = compile(src);
return this.run(ops, onFinish);
}
}

View File

@@ -1,28 +0,0 @@
import { compile } from './new/compiler';
import sqlinterp from './sqlinterp';
test('sql interpretation works', async () => {
const transJan = {
date: 20170106,
amount: -5000,
acct: 'boa',
category: 1,
};
const transFeb = {
date: 20170215,
amount: -7620,
acct: 'boa',
category: 1,
};
const { sqlDependencies } = compile(`
=from transactions
where date >= 20170101 and date <= 20170131 and
category = 1
select { amount }
`);
const where = sqlDependencies[0].where;
expect(sqlinterp(where, transJan, 'transactions')).toBe(true);
expect(sqlinterp(where, transFeb, 'transactions')).toBe(false);
});

View File

@@ -12,28 +12,3 @@ export function unresolveName(name) {
export function resolveName(sheet, name) {
return sheet + '!' + name;
}
export function resolveNamesAsObjects(sheets) {
const cells = {};
Object.keys(sheets).forEach(sheetName => {
const sheet = sheets[sheetName];
Object.keys(sheet).forEach(name => {
const expr = sheet[name];
cells[resolveName(sheetName, name)] = expr;
});
});
return cells;
}
export function resolveNamesAsArrays(sheets) {
const cells = [];
Object.keys(sheets).forEach(sheetName => {
const sheet = sheets[sheetName];
sheet.forEach(name => {
cells.push(resolveName(sheetName, name));
});
});
return cells;
}

View File

@@ -76,19 +76,6 @@ export function getTestKeyError({ reason }) {
}
}
export function getSubscribeError({ reason }) {
switch (reason) {
case 'network':
return 'Unable to reach the server. Check your internet connection';
case 'exists':
return 'An account with that email already exists. Did you mean to login?';
case 'invalid-email':
return 'Invalid email';
default:
return 'An error occurred. Please try again later.';
}
}
export function getSyncError(error, id) {
if (error === 'out-of-sync-migrations' || error === 'out-of-sync-data') {
return 'This budget cannot be loaded with this version of the app.';

View File

@@ -25,19 +25,6 @@ export const TYPE_INFO = {
},
};
export type FieldTypes = {
imported_payee: string;
payee: string;
date: string;
notes: string;
amount: number;
amountInflow: number;
amountOutfow: number;
category: string;
account: string;
cleared: boolean;
};
export const FIELD_TYPES = new Map(
Object.entries({
imported_payee: 'string',

View File

@@ -1,50 +1,7 @@
export function cleanUUID(uuid) {
return uuid.replace(/-/g, '');
}
export function last(arr) {
return arr[arr.length - 1];
}
export function mergeObjects(objects) {
return Object.assign.apply(null, [{}, ...objects]);
}
export function composeCellChanges(objects) {
const merged = {};
Object.keys(objects).forEach(key => {
if (merged[key]) {
merged[key] = { ...merged[key], ...objects[key] };
} else {
merged[key] = objects[key];
}
});
}
export function flattenArray(arrays) {
return Array.prototype.concat.apply([], arrays);
}
export function shallowEqual(a, b) {
if (a === b) {
return true;
}
let numKeysA = 0,
numKeysB = 0,
key;
for (key in b) {
numKeysB++;
if (!a.hasOwnProperty(key) || a[key] !== b[key]) {
return false;
}
}
for (key in a) {
numKeysA++;
}
return numKeysA === numKeysB;
}
export function getChangedValues(obj1, obj2) {
// Keep the id field because this is mostly used to diff database
// objects
@@ -132,19 +89,6 @@ export function groupBy(data, field, mapper?: (v: unknown) => unknown) {
return res;
}
export function groupBySingle(data, field, mapper) {
let res = new Map();
for (let i = 0; i < data.length; i++) {
let item = data[i];
let key = item[field];
if (res.has(key)) {
throw new Error('groupBySingle found conflicting key: ' + key);
}
res.set(key, mapper ? mapper(item) : data[i]);
}
return res;
}
// This should replace the existing `groupById` function, since a
// `Map` is better, but we can't swap it out because `Map` has a
// different API and we need to go through and update everywhere that
@@ -192,14 +136,6 @@ export function groupById(data) {
return res;
}
export function debugMemoFailure(prevProps, nextProps) {
let changed = getChangedValues(prevProps, nextProps);
if (changed !== null) {
console.log(changed);
}
return changed === null;
}
export function setIn(map, keys, item) {
for (let i = 0; i < keys.length; i++) {
let key = keys[i];
@@ -228,11 +164,6 @@ export function getIn(map, keys) {
return item;
}
// Useful for throwing exception from expressions
export function throwError(err) {
throw err;
}
export function fastSetMerge(set1, set2) {
let finalSet = new Set(set1);
let iter = set2.values();
@@ -339,10 +270,6 @@ export function toRelaxedNumber(value) {
return integerToAmount(currencyToInteger(value) || 0);
}
export function toRelaxedInteger(value) {
return stringToInteger(value) || 0;
}
export function integerToCurrency(n) {
return numberFormat.formatter.format(safeNumber(n) / 100);
}
@@ -406,15 +333,3 @@ export function looselyParseAmount(amount) {
return safeNumber(parseFloat(left + '.' + right));
}
export function semverToNumber(str) {
return parseInt(
'1' +
str
.split('.')
.map(x => {
return ('000' + x.replace(/[^0-9]/g, '')).slice(-3);
})
.join(''),
);
}

View File

@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [Shazib]
---
Remove unused/legacy code from codebase