mirror of https://github.com/nocodb/nocodb
Browse Source
# Conflicts: # packages/nc-gui/components/project/spreadsheet/components/virtualHeaderCell.vue # packages/nc-gui/components/project/spreadsheet/views/xcGridView.vuepull/448/head
Pranav C
3 years ago
54 changed files with 1777 additions and 381 deletions
@ -0,0 +1,280 @@
|
||||
<template> |
||||
<div class="formula-wrapper"> |
||||
<v-menu |
||||
v-model="autocomplete" |
||||
bottom |
||||
offset-y |
||||
nudge-bottom="-25px" |
||||
allow-overflow |
||||
> |
||||
<template #activator="_args"> |
||||
<!-- todo: autocomplete based on available functions and metadata --> |
||||
<!-- <v-tooltip color="info" right>--> |
||||
<!-- <template #activator="{on}">--> |
||||
<v-text-field |
||||
ref="input" |
||||
v-model="formula.value" |
||||
dense |
||||
outlined |
||||
class="caption" |
||||
hide-details="auto" |
||||
label="Formula" |
||||
persistent-hint |
||||
hint="Available formulas are ADD, AVG, CONCAT, +, -, /" |
||||
:rules="[v => !!v || 'Required', v => parseAndValidateFormula(v)]" |
||||
autocomplete="off" |
||||
@input="handleInputDeb" |
||||
@keydown.down.prevent="suggestionListDown" |
||||
@keydown.up.prevent="suggestionListUp" |
||||
@keydown.enter.prevent="selectText" |
||||
/> |
||||
<!-- </template>--> |
||||
<!-- <span class="caption">Example: AVG(column1, column2, column3)</span>--> |
||||
<!-- </v-tooltip>--> |
||||
</template> |
||||
<v-list v-if="suggestion" ref="sugList" dense max-height="50vh" style="overflow: auto"> |
||||
<v-list-item-group |
||||
v-model="selected" |
||||
color="primary" |
||||
> |
||||
<v-list-item |
||||
v-for="(it,i) in suggestion" |
||||
:key="i" |
||||
ref="sugOptions" |
||||
dense |
||||
selectable |
||||
@mousedown.prevent="appendText(it)" |
||||
> |
||||
<span |
||||
class="caption" |
||||
:class="{ |
||||
'primary--text text--lighten-2 font-weight-bold': it.type ==='function' |
||||
}" |
||||
>{{ it.text }}<span v-if="it.type ==='function'">(...)</span></span> |
||||
</v-list-item> |
||||
</v-list-item-group> |
||||
</v-list> |
||||
</v-menu> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
|
||||
import NcAutocompleteTree from '@/helpers/NcAutocompleteTree' |
||||
import { getWordUntilCaret, insertAtCursor } from '@/helpers' |
||||
import debounce from 'debounce' |
||||
import jsep from 'jsep' |
||||
import formulaList, { validations } from '../../../../../helpers/formulaList' |
||||
|
||||
export default { |
||||
name: 'FormulaOptions', |
||||
props: ['nodes', 'column', 'meta', 'isSQLite', 'alias', 'value', 'sqlUi'], |
||||
data: () => ({ |
||||
formula: {}, |
||||
// formulas: ['AVERAGE()', 'COUNT()', 'COUNTA()', 'COUNTALL()', 'SUM()', 'MIN()', 'MAX()', 'AND()', 'OR()', 'TRUE()', 'FALSE()', 'NOT()', 'XOR()', 'ISERROR()', 'IF()', 'LEN()', 'MID()', 'LEFT()', 'RIGHT()', 'FIND()', 'CONCATENATE()', 'T()', 'VALUE()', 'ARRAYJOIN()', 'ARRAYUNIQUE()', 'ARRAYCOMPACT()', 'ARRAYFLATTEN()', 'ROUND()', 'ROUNDUP()', 'ROUNDDOWN()', 'INT()', 'EVEN()', 'ODD()', 'MOD()', 'LOG()', 'EXP()', 'POWER()', 'SQRT()', 'CEILING()', 'FLOOR()', 'ABS()', 'RECORD_ID()', 'CREATED_TIME()', 'ERROR()', 'BLANK()', 'YEAR()', 'MONTH()', 'DAY()', 'HOUR()', 'MINUTE()', 'SECOND()', 'TODAY()', 'NOW()', 'WORKDAY()', 'DATETIME_PARSE()', 'DATETIME_FORMAT()', 'SET_LOCALE()', 'SET_TIMEZONE()', 'DATESTR()', 'TIMESTR()', 'TONOW()', 'FROMNOW()', 'DATEADD()', 'WEEKDAY()', 'WEEKNUM()', 'DATETIME_DIFF()', 'WORKDAY_DIFF()', 'IS_BEFORE()', 'IS_SAME()', 'IS_AFTER()', 'REPLACE()', 'REPT()', 'LOWER()', 'UPPER()', 'TRIM()', 'SUBSTITUTE()', 'SEARCH()', 'SWITCH()', 'LAST_MODIFIED_TIME()', 'ENCODE_URL_COMPONENT()', 'REGEX_EXTRACT()', 'REGEX_MATCH()', 'REGEX_REPLACE()'] |
||||
availableFunctions: formulaList, |
||||
availableBinOps: ['+', '-', '*', '/', '>', '==', '<=', '>=', '!='], |
||||
autocomplete: false, |
||||
suggestion: null, |
||||
wordToComplete: '', |
||||
selected: 0, |
||||
tooltip: true |
||||
}), |
||||
computed: { |
||||
suggestionsList() { |
||||
console.log(this) |
||||
const unsupportedFnList = this.sqlUi.getUnsupportedFnList() |
||||
return [ |
||||
...this.availableFunctions.filter(fn => !unsupportedFnList.includes(fn)).map(fn => ({ |
||||
text: fn, |
||||
type: 'function' |
||||
})), |
||||
...this.meta.columns.map(c => ({ text: c._cn, type: 'column', c })), |
||||
...this.availableBinOps.map(op => ({ text: op, type: 'op' })) |
||||
] |
||||
}, |
||||
acTree() { |
||||
const ref = new NcAutocompleteTree() |
||||
for (const sug of this.suggestionsList) { |
||||
ref.add(sug) |
||||
} |
||||
return ref |
||||
} |
||||
}, |
||||
created() { |
||||
this.formula = this.value ? { ...this.value } : {} |
||||
}, |
||||
methods: { |
||||
async save() { |
||||
try { |
||||
await this.$store.dispatch('meta/ActLoadMeta', { |
||||
dbAlias: this.nodes.dbAlias, |
||||
env: this.nodes.env, |
||||
tn: this.meta.tn, |
||||
force: true |
||||
}) |
||||
const meta = JSON.parse(JSON.stringify(this.$store.state.meta.metas[this.meta.tn])) |
||||
|
||||
meta.v.push({ |
||||
_cn: this.alias, |
||||
formula: { |
||||
...this.formula, |
||||
tree: jsep(this.formula.value) |
||||
} |
||||
}) |
||||
|
||||
await this.$store.dispatch('sqlMgr/ActSqlOp', [{ |
||||
env: this.nodes.env, |
||||
dbAlias: this.nodes.dbAlias |
||||
}, 'xcModelSet', { |
||||
tn: this.nodes.tn, |
||||
meta |
||||
}]) |
||||
|
||||
this.$toast.success('Formula column saved successfully').goAway(3000) |
||||
return this.$emit('saved', this.alias) |
||||
} catch (e) { |
||||
this.$toast.error(e.message).goAway(3000) |
||||
} |
||||
}, |
||||
async update() { |
||||
try { |
||||
const meta = JSON.parse(JSON.stringify(this.$store.state.meta.metas[this.meta.tn])) |
||||
|
||||
const col = meta.v.find(c => c._cn === this.column._cn && c.formula) |
||||
|
||||
Object.assign(col, { |
||||
_cn: this.alias, |
||||
formula: { |
||||
...this.formula, |
||||
tree: jsep(this.formula.value), |
||||
error: undefined |
||||
} |
||||
}) |
||||
|
||||
await this.$store.dispatch('sqlMgr/ActSqlOp', [{ |
||||
env: this.nodes.env, |
||||
dbAlias: this.nodes.dbAlias |
||||
}, 'xcModelSet', { |
||||
tn: this.nodes.tn, |
||||
meta |
||||
}]) |
||||
this.$toast.success('Formula column updated successfully').goAway(3000) |
||||
} catch (e) { |
||||
this.$toast.error(e.message).goAway(3000) |
||||
} |
||||
}, |
||||
// todo: validate formula based on meta |
||||
parseAndValidateFormula(formula) { |
||||
try { |
||||
const pt = jsep(formula) |
||||
const err = this.validateAgainstMeta(pt) |
||||
if (err.length) { |
||||
return err.join(', ') |
||||
} |
||||
return true |
||||
} catch (e) { |
||||
return e.message |
||||
} |
||||
}, |
||||
validateAgainstMeta(pt, arr = []) { |
||||
if (pt.type === 'CallExpression') { |
||||
if (!this.availableFunctions.includes(pt.callee.name)) { |
||||
arr.push(`'${pt.callee.name}' function is not available`) |
||||
} |
||||
const validation = validations[pt.callee.name] && validations[pt.callee.name].validation |
||||
if (validation && validation.args) { |
||||
if (validation.args.rqd !== undefined && validation.args.rqd !== pt.arguments.length) { |
||||
arr.push(`'${pt.callee.name}' required ${validation.args.rqd} arguments`) |
||||
} else if (validation.args.min !== undefined && validation.args.min > pt.arguments.length) { |
||||
arr.push(`'${pt.callee.name}' required minimum ${validation.args.min} arguments`) |
||||
} else if (validation.args.max !== undefined && validation.args.max < pt.arguments.length) { |
||||
arr.push(`'${pt.callee.name}' required maximum ${validation.args.max} arguments`) |
||||
} |
||||
} |
||||
pt.arguments.map(arg => this.validateAgainstMeta(arg, arr)) |
||||
} else if (pt.type === 'Identifier') { |
||||
if (this.meta.columns.every(c => c._cn !== pt.name)) { |
||||
arr.push(`Column with name '${pt.name}' is not available`) |
||||
} |
||||
} else if (pt.type === 'BinaryExpression') { |
||||
if (!this.availableBinOps.includes(pt.operator)) { |
||||
arr.push(`'${pt.operator}' operation is not available`) |
||||
} |
||||
this.validateAgainstMeta(pt.left, arr) |
||||
this.validateAgainstMeta(pt.right, arr) |
||||
} |
||||
return arr |
||||
}, |
||||
appendText(it) { |
||||
const text = it.text |
||||
const len = this.wordToComplete.length |
||||
if (it.type === 'function') { |
||||
this.$set(this.formula, 'value', insertAtCursor(this.$refs.input.$el.querySelector('input'), text + '()', len, 1)) |
||||
} else { |
||||
this.$set(this.formula, 'value', insertAtCursor(this.$refs.input.$el.querySelector('input'), text, len)) |
||||
} |
||||
}, |
||||
_handleInputDeb: debounce(async function(self) { |
||||
await self.handleInput() |
||||
}, 250), |
||||
handleInputDeb() { |
||||
this._handleInputDeb(this) |
||||
}, |
||||
handleInput() { |
||||
this.selected = 0 |
||||
// const $fakeDiv = this.$refs.fakeDiv |
||||
this.suggestion = null |
||||
const query = getWordUntilCaret(this.$refs.input.$el.querySelector('input')) // this.formula.value |
||||
// if (query !== '') { |
||||
const parts = query.split(/\W+/) |
||||
|
||||
this.wordToComplete = parts.pop() |
||||
|
||||
// if (this.wordToComplete !== '') { |
||||
// get best match using popularity |
||||
this.suggestion = this.acTree.complete(this.wordToComplete) |
||||
|
||||
this.autocomplete = !!this.suggestion.length |
||||
// } else { |
||||
// // $span.textContent = '' // clear ghost span |
||||
// } |
||||
// } else { |
||||
// this.autocomplete = false |
||||
// // $time.textContent = '' |
||||
// // $span.textContent = '' // clear ghost span |
||||
// } |
||||
}, |
||||
selectText() { |
||||
if (this.selected > -1 && this.selected < this.suggestion.length) { |
||||
this.appendText(this.suggestion[this.selected]) |
||||
this.autocomplete = false |
||||
} |
||||
}, |
||||
suggestionListDown() { |
||||
this.selected = ++this.selected % this.suggestion.length |
||||
this.scrollToSelectedOption() |
||||
}, |
||||
suggestionListUp() { |
||||
this.selected = --this.selected > -1 ? this.selected : this.suggestion.length - 1 |
||||
this.scrollToSelectedOption() |
||||
}, |
||||
scrollToSelectedOption() { |
||||
this.$nextTick(() => { |
||||
if (this.$refs.sugOptions[this.selected]) { |
||||
try { |
||||
this.$refs.sugList.$el.scrollTo({ |
||||
top: this.$refs.sugOptions[this.selected].$el.offsetTop, |
||||
behavior: 'smooth' |
||||
}) |
||||
} catch (e) { |
||||
} |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style scoped lang="scss"> |
||||
</style> |
@ -0,0 +1,26 @@
|
||||
<template> |
||||
<v-tooltip |
||||
v-if="column.formula && column.formula.error && column.formula.error.length" |
||||
bottom |
||||
color="error" |
||||
> |
||||
<template #activator="{on}"> |
||||
<span class="caption" v-on="on">ERR<span class="error--text">!</span></span> |
||||
</template> |
||||
<span class=" font-weight-bold">{{ column.formula.error.join(', ') }}</span> |
||||
</v-tooltip> |
||||
<div v-else> |
||||
{{ row[column._cn] }} |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
export default { |
||||
name: 'FormulaCell', |
||||
props: ['column', 'row'] |
||||
} |
||||
</script> |
||||
|
||||
<style scoped> |
||||
|
||||
</style> |
@ -0,0 +1,79 @@
|
||||
// ref : https://medium.com/weekly-webtips/js-implementing-auto-complete-f4c5a5d5c009
|
||||
|
||||
class NcAutocompleteTree { |
||||
constructor() { |
||||
this.trie = null |
||||
this.suggestions = [] |
||||
} |
||||
|
||||
newNode() { |
||||
return { |
||||
isLeaf: false, |
||||
children: {} |
||||
} |
||||
} |
||||
|
||||
add(word) { |
||||
if (!this.trie) { |
||||
this.trie = this.newNode() |
||||
} |
||||
|
||||
let root = this.trie |
||||
for (const letter of word.text.toLowerCase()) { |
||||
if (!(letter in root.children)) { |
||||
root.children[letter] = this.newNode() |
||||
} |
||||
root = root.children[letter] |
||||
} |
||||
root.value = root.value || [] |
||||
root.value.push(word) |
||||
} |
||||
|
||||
find(word) { |
||||
let root = this.trie |
||||
for (const letter of word) { |
||||
if (letter in root.children) { |
||||
root = root.children[letter] |
||||
} else { |
||||
return null // if not found return null
|
||||
} |
||||
} |
||||
|
||||
return root // return the root where it ends search
|
||||
} |
||||
|
||||
traverse(root) { |
||||
if (root.value && root.value.length) { |
||||
this.suggestions.push(...root.value) |
||||
} |
||||
|
||||
for (const letter in root.children) { |
||||
this.traverse(root.children[letter]) |
||||
} |
||||
} |
||||
|
||||
complete(word, CHILDREN = null) { |
||||
this.suggestions = [] |
||||
const root = this.find(word.toLowerCase()) |
||||
|
||||
if (!root) { |
||||
return this.suggestions |
||||
} // cannot suggest anything
|
||||
|
||||
const children = root.children |
||||
|
||||
let spread = 0 |
||||
for (const letter in children) { |
||||
this.traverse(children[letter], word + letter) |
||||
spread++ |
||||
|
||||
if (CHILDREN && spread === CHILDREN) { |
||||
break |
||||
} |
||||
} |
||||
|
||||
return this.suggestions |
||||
} |
||||
} |
||||
|
||||
export default NcAutocompleteTree |
@ -0,0 +1,50 @@
|
||||
const validations = { |
||||
AVG: { |
||||
validation: { |
||||
args: { min: 1 } |
||||
} |
||||
}, |
||||
ADD: { |
||||
validation: { |
||||
args: { min: 1 } |
||||
} |
||||
}, |
||||
CONCAT: { |
||||
validation: { args: { min: 1 } } |
||||
}, |
||||
TRIM: { |
||||
validation: { args: { min: 1 } } |
||||
}, |
||||
UPPER: { |
||||
validation: { args: { rqd: 1 } } |
||||
}, |
||||
LOWER: { validation: { args: { rqd: 1 } } }, |
||||
LEN: { validation: { args: { rqd: 1 } } }, |
||||
MIN: { validation: { args: { min: 1 } } }, |
||||
MAX: { validation: { args: { min: 1 } } }, |
||||
CEILING: { validation: { args: { rqd: 1 } } }, |
||||
FLOOR: { validation: { args: { rqd: 1 } } }, |
||||
ROUND: { validation: { args: { rqd: 1 } } }, |
||||
MOD: { validation: { args: { rqd: 2 } } }, |
||||
REPEAT: { validation: { args: { rqd: 2 } } }, |
||||
LOG: { validation: {} }, |
||||
EXP: { validation: {} }, |
||||
POWER: { validation: { args: { rqd: 2 } } }, |
||||
SQRT: { validation: { args: { rqd: 1 } } }, |
||||
ABS: { validation: { args: { rqd: 1 } } }, |
||||
NOW: { validation: { args: { rqd: 0 } } }, |
||||
REPLACE: { validation: { args: { rqd: 2 } } }, |
||||
SEARCH: { validation: { args: { rqd: 2 } } }, |
||||
INT: { validation: { args: { rqd: 1 } } }, |
||||
RIGHT: { validation: { args: { rqd: 2 } } }, |
||||
LEFT: { |
||||
validation: { args: { rqd: 1 } } |
||||
}, |
||||
SUBSTR: { validation: { args: { min: 2, max: 3 } } }, |
||||
MID: { validation: { args: { rqd: 1 } } }, |
||||
IF: { validation: { args: { min: 2, max: 3 } } }, |
||||
SWITCH: { validation: { args: { min: 3 } } } |
||||
} |
||||
|
||||
export default Object.keys(validations) |
||||
export { validations } |
@ -0,0 +1,6 @@
|
||||
export * from './MysqlUi' |
||||
export * from './PgUi' |
||||
export * from './MssqlUi' |
||||
export * from './OracleUi' |
||||
export * from './SqliteUi' |
||||
export * from './SqlUiFactory' |
@ -0,0 +1,77 @@
|
||||
import {expect} from 'chai'; |
||||
import 'mocha'; |
||||
import knex from '../lib/dataMapper/lib/sql/CustomKnex'; |
||||
import formulaQueryBuilderFromString from "../lib/dataMapper/lib/sql/formulaQueryBuilderFromString"; |
||||
|
||||
process.env.TEST = 'test'; |
||||
|
||||
describe('{Auth, CRUD, HasMany, Belongs} Tests', () => { |
||||
|
||||
let knexMysqlRef; |
||||
let knexPgRef; |
||||
let knexMssqlRef; |
||||
let knexSqliteRef; |
||||
|
||||
|
||||
// Called once before any of the tests in this block begin.
|
||||
before(function (done) { |
||||
knexMysqlRef = knex({client:'mysql2'}) |
||||
knexMssqlRef = knex({client:'mssql'}) |
||||
knexPgRef = knex({client:'pg'}) |
||||
knexSqliteRef = knex({client:'sqlite3'}) |
||||
done() |
||||
}); |
||||
|
||||
|
||||
after((done) => { |
||||
done(); |
||||
}); |
||||
|
||||
|
||||
describe('Formulas', function () { |
||||
|
||||
it('Simple formula', function (done) { |
||||
expect(formulaQueryBuilderFromString("concat(city, ' _ ',city_id+country_id)", 'a',knexMysqlRef).toQuery()).eq('concat(`city`,\' _ \',`city_id` + `country_id`) as a') |
||||
expect(formulaQueryBuilderFromString("concat(city, ' _ ',city_id+country_id)", 'a',knexPgRef).toQuery()).eq('concat("city",\' _ \',"city_id" + "country_id") as a') |
||||
expect(formulaQueryBuilderFromString("concat(city, ' _ ',city_id+country_id)", 'a',knexMssqlRef).toQuery()).eq('concat([city],\' _ \',[city_id] + [country_id]) as a') |
||||
expect(formulaQueryBuilderFromString("concat(city, ' _ ',city_id+country_id)", 'a',knexSqliteRef).toQuery()).eq('`city` || \' _ \' || (`city_id` + `country_id`) as a') |
||||
done() |
||||
}); |
||||
it('Addition', function (done) { |
||||
expect(formulaQueryBuilderFromString("ADD(city_id,country_id,2,3,4,5,4)", 'a',knexMysqlRef).toQuery()).eq('`city_id` + `country_id` + 2 + 3 + 4 + 5 + 4 as a') |
||||
expect(formulaQueryBuilderFromString("ADD(city_id,country_id,2,3,4,5,4)", 'a',knexPgRef).toQuery()).eq('"city_id" + "country_id" + 2 + 3 + 4 + 5 + 4 as a') |
||||
expect(formulaQueryBuilderFromString("ADD(city_id,country_id,2,3,4,5,4)", 'a',knexMssqlRef).toQuery()).eq('[city_id] + [country_id] + 2 + 3 + 4 + 5 + 4 as a') |
||||
expect(formulaQueryBuilderFromString("ADD(city_id,country_id,2,3,4,5,4)", 'a',knexSqliteRef).toQuery()).eq('`city_id` + `country_id` + 2 + 3 + 4 + 5 + 4 as a') |
||||
done() |
||||
}); |
||||
it('Average', function (done) { |
||||
expect(formulaQueryBuilderFromString("AVG(city_id,country_id,2,3,4,5,4)", 'a',knexMysqlRef).toQuery()).eq('(`city_id` + `country_id` + 2 + 3 + 4 + 5 + 4) / 7 as a') |
||||
expect(formulaQueryBuilderFromString("AVG(city_id,country_id,2,3,4,5,4)", 'a',knexPgRef).toQuery()).eq('("city_id" + "country_id" + 2 + 3 + 4 + 5 + 4) / 7 as a') |
||||
expect(formulaQueryBuilderFromString("AVG(city_id,country_id,2,3,4,5,4)", 'a',knexMssqlRef).toQuery()).eq('([city_id] + [country_id] + 2 + 3 + 4 + 5 + 4) / 7 as a') |
||||
expect(formulaQueryBuilderFromString("AVG(city_id,country_id,2,3,4,5,4)", 'a',knexSqliteRef).toQuery()).eq('(`city_id` + `country_id` + 2 + 3 + 4 + 5 + 4) / 7 as a') |
||||
done() |
||||
}); |
||||
}); |
||||
|
||||
});/** |
||||
* @copyright Copyright (c) 2021, Xgene Cloud Ltd |
||||
* |
||||
* @author Naveen MR <oof1lab@gmail.com> |
||||
* @author Pranav C Balan <pranavxc@gmail.com> |
||||
* |
||||
* @license GNU AGPL version 3 or any later version |
||||
* |
||||
* This program is free software: you can redistribute it and/or modify |
||||
* it under the terms of the GNU Affero General Public License as |
||||
* published by the Free Software Foundation, either version 3 of the |
||||
* License, or (at your option) any later version. |
||||
*"concat(city, ' _ ',city_id+country_id)", 'a' |
||||
* This program is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU Affero General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU Affero General Public License |
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
||||
* |
||||
*/ |
@ -0,0 +1,84 @@
|
||||
import jsep from 'jsep'; |
||||
import mapFunctionName from "./mapFunctionName"; |
||||
|
||||
|
||||
// todo: switch function based on database
|
||||
|
||||
export function formulaQueryBuilderFromString(str, alias, knex) { |
||||
return formulaQueryBuilder(jsep(str), alias, knex) |
||||
} |
||||
|
||||
|
||||
export default function formulaQueryBuilder(tree, alias, knex, aliasToColumn = {}) { |
||||
const fn = (pt, a?, prevBinaryOp ?) => { |
||||
const colAlias = a ? ` as ${a}` : ''; |
||||
if (pt.type === 'CallExpression') { |
||||
switch (pt.callee.name) { |
||||
case 'ADD': |
||||
case 'SUM': |
||||
if (pt.arguments.length > 1) { |
||||
return fn({ |
||||
type: 'BinaryExpression', |
||||
operator: '+', |
||||
left: pt.arguments[0], |
||||
right: {...pt, arguments: pt.arguments.slice(1)} |
||||
}, a, prevBinaryOp) |
||||
} else { |
||||
return fn(pt.arguments[0], a, prevBinaryOp) |
||||
} |
||||
break; |
||||
case 'AVG': |
||||
if (pt.arguments.length > 1) { |
||||
return fn({ |
||||
type: 'BinaryExpression', |
||||
operator: '/', |
||||
left: {...pt, callee: {name: 'SUM'}}, |
||||
right: {type: 'Literal', value: pt.arguments.length} |
||||
}, a, prevBinaryOp) |
||||
} else { |
||||
return fn(pt.arguments[0], a, prevBinaryOp) |
||||
} |
||||
break; |
||||
case 'CONCAT': |
||||
if (knex.clientType() === 'sqlite3') { |
||||
if (pt.arguments.length > 1) { |
||||
return fn({ |
||||
type: 'BinaryExpression', |
||||
operator: '||', |
||||
left: pt.arguments[0], |
||||
right: {...pt, arguments: pt.arguments.slice(1)} |
||||
}, a, prevBinaryOp) |
||||
} else { |
||||
return fn(pt.arguments[0], a, prevBinaryOp) |
||||
} |
||||
} |
||||
break; |
||||
default: { |
||||
const res = mapFunctionName({pt, knex, alias, aliasToCol: aliasToColumn, fn, colAlias}) |
||||
if (res) return res; |
||||
} |
||||
break |
||||
} |
||||
|
||||
return knex.raw(`${pt.callee.name}(${pt.arguments.map(arg => fn(arg).toQuery()).join()})${colAlias}`) |
||||
} else if (pt.type === 'Literal') { |
||||
return knex.raw(`?${colAlias}`, [pt.value]); |
||||
} else if (pt.type === 'Identifier') { |
||||
return knex.raw(`??${colAlias}`, [aliasToColumn[pt.name] || pt.name]); |
||||
} else if (pt.type === 'BinaryExpression') { |
||||
if (pt.operator === '==') { |
||||
pt.operator = '=' |
||||
} |
||||
|
||||
const query = knex.raw(`${fn(pt.left, null, pt.operator).toQuery()} ${pt.operator} ${fn(pt.right, null, pt.operator).toQuery()}${colAlias}`) |
||||
if (prevBinaryOp && pt.operator !== prevBinaryOp) { |
||||
query.wrap('(', ')') |
||||
} |
||||
return query; |
||||
} |
||||
}; |
||||
return fn(tree, alias) |
||||
} |
||||
|
||||
|
||||
|
@ -0,0 +1,28 @@
|
||||
import {MapFnArgs} from "../mapFunctionName"; |
||||
|
||||
export default { |
||||
// todo: handle default case
|
||||
SWITCH: (args: MapFnArgs) => { |
||||
const count = Math.floor((args.pt.arguments.length-1) / 2) |
||||
let query = ''; |
||||
|
||||
const switchVal = args.fn(args.pt.arguments[0]).toQuery(); |
||||
|
||||
for (let i = 0; i < count; i++) { |
||||
query += args.knex.raw(`\n\tWHEN ${args.fn(args.pt.arguments[i * 2 + 1]).toQuery()} THEN ${args.fn(args.pt.arguments[i * 2 + 2]).toQuery()}`).toQuery() |
||||
} |
||||
if (args.pt.arguments.length % 2 === 0) { |
||||
query += args.knex.raw(`\n\tELSE ${args.fn(args.pt.arguments[args.pt.arguments.length - 1]).toQuery()}`).toQuery() |
||||
} |
||||
return args.knex.raw(`CASE ${switchVal} ${query}\n END${args.colAlias}`) |
||||
}, |
||||
IF: (args: MapFnArgs) => { |
||||
let query = args.knex.raw(`\n\tWHEN ${args.fn(args.pt.arguments[0]).toQuery()} THEN ${args.fn(args.pt.arguments[1]).toQuery()}`).toQuery(); |
||||
if (args.pt.arguments[2]) { |
||||
query += args.knex.raw(`\n\tELSE ${args.fn(args.pt.arguments[2]).toQuery()}`).toQuery() |
||||
} |
||||
return args.knex.raw(`CASE ${query}\n END${args.colAlias}`) |
||||
}, |
||||
TRUE:(_args) => 1, |
||||
FALSE:(_args) => 0 |
||||
} |
@ -0,0 +1,57 @@
|
||||
import {MapFnArgs} from "../mapFunctionName"; |
||||
import commonFns from "./commonFns"; |
||||
|
||||
const mssql = { |
||||
...commonFns, |
||||
MIN: (args: MapFnArgs) => { |
||||
if (args.pt.arguments.length === 1) { |
||||
return args.fn(args.pt.arguments[0]) |
||||
} |
||||
let query = ''; |
||||
for (const [i, arg] of Object.entries(args.pt.arguments)) { |
||||
if (+i === args.pt.arguments.length - 1) { |
||||
query += args.knex.raw(`\n\tElse ${args.fn(arg).toQuery()}`).toQuery() |
||||
} else { |
||||
query += args.knex.raw(`\n\tWhen ${args.pt.arguments.filter((_, j) => +i !== j).map(arg1 => `${args.fn(arg).toQuery()} < ${args.fn(arg1).toQuery()}`).join(' And ')} Then ${args.fn(arg).toQuery()}`).toQuery() |
||||
} |
||||
} |
||||
return args.knex.raw(`Case ${query}\n End${args.colAlias}`) |
||||
}, |
||||
MAX: (args: MapFnArgs) => { |
||||
if (args.pt.arguments.length === 1) { |
||||
return args.fn(args.pt.arguments[0]) |
||||
} |
||||
let query = ''; |
||||
for (const [i, arg] of Object.entries(args.pt.arguments)) { |
||||
if (+i === args.pt.arguments.length - 1) { |
||||
query += args.knex.raw(`\nElse ${args.fn(arg).toQuery()}`).toQuery() |
||||
} else { |
||||
query += args.knex.raw(`\nWhen ${args.pt.arguments.filter((_, j) => +i !== j).map(arg1 => `${args.fn(arg).toQuery()} > ${args.fn(arg1).toQuery()}`).join(' And ')} Then ${args.fn(arg).toQuery()}`).toQuery() |
||||
} |
||||
} |
||||
|
||||
return args.knex.raw(`Case ${query}\n End${args.colAlias}`) |
||||
}, |
||||
MOD: (pt) => { |
||||
Object.assign(pt, { |
||||
type: 'BinaryExpression', |
||||
operator: '%', |
||||
left: pt.arguments[0], |
||||
right: pt.arguments[1] |
||||
}) |
||||
}, |
||||
REPEAT: 'REPLICATE', |
||||
NOW: 'getdate', |
||||
SEARCH: (args: MapFnArgs) => { |
||||
args.pt.callee.name = 'CHARINDEX'; |
||||
const temp = args.pt.arguments[0] |
||||
args.pt.arguments[0] = args.pt.arguments[1] |
||||
args.pt.arguments[1] = temp; |
||||
}, |
||||
INT: (args: MapFnArgs) => { |
||||
return args.knex.raw(`CASE WHEN ISNUMERIC(${args.fn(args.pt.arguments[0]).toQuery()}) = 1 THEN FLOOR(${args.fn(args.pt.arguments[0]).toQuery()}) ELSE 0 END${args.colAlias}`) |
||||
}, |
||||
MID:'SUBSTR', |
||||
} |
||||
|
||||
export default mssql; |
@ -0,0 +1,30 @@
|
||||
import {MapFnArgs} from "../mapFunctionName"; |
||||
import commonFns from "./commonFns"; |
||||
|
||||
|
||||
const mysql2 = { |
||||
...commonFns, |
||||
LEN: 'CHAR_LENGTH', |
||||
MIN: 'LEAST', |
||||
MAX: 'GREATEST', |
||||
SEARCH: (args: MapFnArgs) => { |
||||
args.pt.callee.name = 'LOCATE'; |
||||
const temp = args.pt.arguments[0] |
||||
args.pt.arguments[0] = args.pt.arguments[1] |
||||
args.pt.arguments[1] = temp; |
||||
}, |
||||
INT:(args: MapFnArgs) =>{ |
||||
return args.knex.raw(`CAST(${args.fn(args.pt.arguments[0])} as SIGNED)${args.colAlias}`) |
||||
}, |
||||
LEFT:(args: MapFnArgs)=> { |
||||
return args.knex.raw(`SUBSTR(${args.fn(args.pt.arguments[0])},1,${args.fn(args.pt.arguments[1])})${args.colAlias}`) |
||||
}, |
||||
RIGHT:(args: MapFnArgs)=> { |
||||
return args.knex.raw(`SUBSTR(${args.fn(args.pt.arguments[0])},-${args.fn(args.pt.arguments[1])})${args.colAlias}`) |
||||
}, |
||||
MID:'SUBSTR', |
||||
} |
||||
|
||||
|
||||
export default mysql2; |
||||
|
@ -0,0 +1,24 @@
|
||||
import {MapFnArgs} from "../mapFunctionName"; |
||||
import commonFns from "./commonFns"; |
||||
|
||||
const pg = { |
||||
...commonFns, |
||||
LEN: 'length', |
||||
MIN: 'least', |
||||
MAX: 'greatest', |
||||
CEILING: 'ceil', |
||||
ROUND: 'round', |
||||
POWER: 'pow', |
||||
SQRT: 'sqrt', |
||||
SEARCH: (args: MapFnArgs) => { |
||||
return args.knex.raw(`POSITION(${args.knex.raw(args.fn(args.pt.arguments[1]).toQuery())} in ${args.knex.raw(args.fn(args.pt.arguments[0]).toQuery())})${args.colAlias}`) |
||||
}, |
||||
INT(args: MapFnArgs) { |
||||
// todo: correction
|
||||
return args.knex.raw(`REGEXP_REPLACE(COALESCE(${args.fn(args.pt.arguments[0])}::character varying, '0'), '[^0-9]+|\\.[0-9]+' ,'')${args.colAlias}`) |
||||
}, |
||||
MID: 'SUBSTR', |
||||
} |
||||
|
||||
|
||||
export default pg; |
@ -0,0 +1,39 @@
|
||||
import {MapFnArgs} from "../mapFunctionName"; |
||||
import commonFns from "./commonFns"; |
||||
|
||||
|
||||
const sqlite3 = { |
||||
...commonFns, |
||||
LEN: 'LENGTH', |
||||
CEILING(args) { |
||||
return args.knex.raw(`round(${args.fn(args.pt.arguments[0])} + 0.5)${args.colAlias}`) |
||||
}, FLOOR(args) { |
||||
return args.knex.raw(`round(${args.fn(args.pt.arguments[0])} - 0.5)${args.colAlias}`) |
||||
}, |
||||
MOD: (args: MapFnArgs) => { |
||||
return args.fn({ |
||||
type: 'BinaryExpression', |
||||
operator: '%', |
||||
left: args.pt.arguments[0], |
||||
right: args.pt.arguments[1] |
||||
}) |
||||
}, |
||||
REPEAT(args: MapFnArgs) { |
||||
return args.knex.raw(`replace(printf('%.' || ${args.fn(args.pt.arguments[1])} || 'c', '/'),'/',${args.fn(args.pt.arguments[0])})${args.colAlias}`) |
||||
}, |
||||
NOW: 'DATE', |
||||
SEARCH: 'INSTR', |
||||
INT(args: MapFnArgs) { |
||||
return args.knex.raw(`CAST(${args.fn(args.pt.arguments[0])} as INTEGER)${args.colAlias}`) |
||||
}, |
||||
LEFT: (args: MapFnArgs) => { |
||||
return args.knex.raw(`SUBSTR(${args.fn(args.pt.arguments[0])},1,${args.fn(args.pt.arguments[1])})${args.colAlias}`) |
||||
}, |
||||
RIGHT: (args: MapFnArgs) => { |
||||
return args.knex.raw(`SUBSTR(${args.fn(args.pt.arguments[0])},-${args.fn(args.pt.arguments[1])})${args.colAlias}`) |
||||
}, |
||||
MID: 'SUBSTR', |
||||
} |
||||
|
||||
|
||||
export default sqlite3; |
@ -0,0 +1,46 @@
|
||||
import {XKnex} from "../../index"; |
||||
import mssql from "./functionMappings/mssql"; |
||||
import mysql from "./functionMappings/mysql"; |
||||
import pg from "./functionMappings/pg"; |
||||
import sqlite from "./functionMappings/sqlite"; |
||||
import {QueryBuilder} from "knex"; |
||||
|
||||
export interface MapFnArgs { |
||||
pt: any, |
||||
aliasToCol: { [alias: string]: string }, |
||||
knex: XKnex, |
||||
alias: string, |
||||
fn: (...args: any) => QueryBuilder, |
||||
colAlias:string |
||||
} |
||||
|
||||
const mapFunctionName = (args: MapFnArgs): any => { |
||||
const name = args.pt.callee.name; |
||||
let val; |
||||
switch (args.knex.clientType()) { |
||||
case 'mysql': |
||||
case 'mysql2': |
||||
val = mysql[name] || name; |
||||
break; |
||||
case 'pg': |
||||
case 'postgre': |
||||
val = pg[name] || name; |
||||
break; |
||||
case 'mssql': |
||||
val = mssql[name] || name; |
||||
break; |
||||
case 'sqlite': |
||||
case 'sqlite3': |
||||
val = sqlite[name] || name; |
||||
break; |
||||
} |
||||
|
||||
if (typeof val === 'function') { |
||||
return val(args) |
||||
} else if (typeof val === 'string') { |
||||
args.pt.callee.name = val; |
||||
} |
||||
} |
||||
|
||||
|
||||
export default mapFunctionName; |
@ -0,0 +1,34 @@
|
||||
export default function (args: { |
||||
virtualColumns, |
||||
columnName: string |
||||
}): void | boolean { |
||||
|
||||
let modified = false; |
||||
|
||||
const fn = (pt, virtualColumn) => { |
||||
if (pt.type === 'CallExpression') { |
||||
pt.arguments.map(arg => fn(arg, virtualColumn)) |
||||
} else if (pt.type === 'Literal') { |
||||
} else if (pt.type === 'Identifier') { |
||||
if (pt.name === args.columnName) { |
||||
virtualColumn.formula.error = virtualColumn.formula.error || []; |
||||
virtualColumn.formula.error.push(`Column '${args.columnName}' was deleted`) |
||||
modified = true; |
||||
} |
||||
} else if (pt.type === 'BinaryExpression') { |
||||
fn(pt.left, virtualColumn); |
||||
fn(pt.right, virtualColumn); |
||||
} |
||||
}; |
||||
|
||||
if (!args.virtualColumns) { |
||||
return |
||||
} |
||||
for (const v of args.virtualColumns) { |
||||
if (!v.formula?.tree) { |
||||
continue; |
||||
} |
||||
fn(v.formula.tree, v) |
||||
} |
||||
return modified; |
||||
} |
@ -0,0 +1,43 @@
|
||||
export default function jsepTreeToFormula(node) { |
||||
if (node.type === 'BinaryExpression' || node.type === 'LogicalExpression') { |
||||
return '(' + jsepTreeToFormula(node.left) + ' ' + node.operator + ' ' + jsepTreeToFormula(node.right) + ')' |
||||
} |
||||
|
||||
if (node.type === 'UnaryExpression') { |
||||
return node.operator + jsepTreeToFormula(node.argument) |
||||
} |
||||
|
||||
if (node.type === 'MemberExpression') { |
||||
return jsepTreeToFormula(node.object) + '[' + jsepTreeToFormula(node.property) + ']' |
||||
} |
||||
|
||||
if (node.type === 'Identifier') { |
||||
return node.name |
||||
} |
||||
|
||||
if (node.type === 'Literal') { |
||||
if (typeof node.value === 'string') { |
||||
return '"' + node.value + '"' |
||||
} |
||||
|
||||
return '' + node.value |
||||
} |
||||
|
||||
if (node.type === 'CallExpression') { |
||||
return jsepTreeToFormula(node.callee) + '(' + node.arguments.map(jsepTreeToFormula).join(', ') + ')' |
||||
} |
||||
|
||||
if (node.type === 'ArrayExpression') { |
||||
return '[' + node.elements.map(jsepTreeToFormula).join(', ') + ']' |
||||
} |
||||
|
||||
if (node.type === 'Compound') { |
||||
return node.body.map(e => jsepTreeToFormula(e)).join(' ') |
||||
} |
||||
|
||||
if (node.type === 'ConditionalExpression') { |
||||
return jsepTreeToFormula(node.test) + ' ? ' + jsepTreeToFormula(node.consequent) + ' : ' + jsepTreeToFormula(node.alternate) |
||||
} |
||||
|
||||
return '' |
||||
} |
@ -0,0 +1,37 @@
|
||||
import jsepTreeToFormula from "./jsepTreeToFormula"; |
||||
|
||||
export default function (args: { |
||||
virtualColumns, |
||||
oldColumnName: string, |
||||
newColumnName: string, |
||||
}): void | boolean { |
||||
|
||||
let modified = false; |
||||
|
||||
const fn = (pt) => { |
||||
if (pt.type === 'CallExpression') { |
||||
pt.arguments.map(arg => fn(arg)) |
||||
} else if (pt.type === 'Literal') { |
||||
} else if (pt.type === 'Identifier') { |
||||
if (pt.name === args.oldColumnName) { |
||||
pt.name = args.newColumnName; |
||||
modified = true; |
||||
} |
||||
} else if (pt.type === 'BinaryExpression') { |
||||
fn(pt.left); |
||||
fn(pt.right); |
||||
} |
||||
}; |
||||
|
||||
if (!args.virtualColumns) { |
||||
return |
||||
} |
||||
for (const v of args.virtualColumns) { |
||||
if (!v.formula?.tree) { |
||||
continue; |
||||
} |
||||
fn(v.formula.tree) |
||||
v.formula.value = jsepTreeToFormula(v.formula.tree) |
||||
} |
||||
return modified; |
||||
} |
Loading…
Reference in new issue