diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index b991883dc..0593b8e20 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -28,6 +28,9 @@ jobs:
- name: Install dependencies
run: bun install
+ - name: Run tests
+ run: bun test
+
- name: Build
run: bun run build
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6b0fbb4b4..e03e3c135 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,16 @@
# Changelog
+## v1.0.0-111
+
+
+### 🚀 Enhancements
+
+- Support formula field aggregate ([b1ceece](https://github.com/undb-io/undb/commit/b1ceece))
+
+### ❤️ Contributors
+
+- Nichenqin ([@nichenqin](http://github.com/nichenqin))
+
## v1.0.0-110
## v1.0.0-109
diff --git a/apps/frontend/package.json b/apps/frontend/package.json
index c560e59e0..7fc0be1bd 100644
--- a/apps/frontend/package.json
+++ b/apps/frontend/package.json
@@ -22,6 +22,7 @@
"@sveltejs/adapter-static": "^3.0.5",
"@sveltejs/kit": "^2.7.1",
"@sveltejs/vite-plugin-svelte": "^3.1.2",
+ "@tailwindcss/typography": "^0.5.15",
"@tanstack/eslint-plugin-query": "^5.59.7",
"@types/eslint": "^8.56.12",
"@types/lodash.unzip": "^3.4.9",
@@ -31,6 +32,7 @@
"@typescript-eslint/parser": "^8.10.0",
"@undb/commands": "workspace:*",
"@undb/domain": "workspace:*",
+ "@undb/formula": "workspace:*",
"@undb/i18n": "workspace:*",
"@undb/openapi": "workspace:*",
"@undb/queries": "workspace:*",
@@ -73,11 +75,16 @@
"type-fest": "^4.26.1",
"typescript": "^5.6.3",
"vite": "^5.4.9",
+ "vite-plugin-node-polyfills": "^0.22.0",
"vitest": "^2.1.3",
"xlsx": "^0.18.5"
},
"type": "module",
"dependencies": {
+ "@codemirror/commands": "^6.7.1",
+ "@codemirror/language": "^6.10.3",
+ "@codemirror/state": "^6.4.1",
+ "@codemirror/view": "^6.34.1",
"@formkit/auto-animate": "^0.8.2",
"@internationalized/date": "^3.5.6",
"@svelte-put/clickoutside": "^3.0.2",
diff --git a/apps/frontend/schema.graphql b/apps/frontend/schema.graphql
index aaadf383c..da9321342 100644
--- a/apps/frontend/schema.graphql
+++ b/apps/frontend/schema.graphql
@@ -44,6 +44,7 @@ type Field {
defaultValue: JSON
display: Boolean
id: ID!
+ metadata: JSON
name: String!
option: JSON
type: FieldType!
diff --git a/apps/frontend/src/lib/components/blocks/aggregate/config/aggregate-config.svelte b/apps/frontend/src/lib/components/blocks/aggregate/config/aggregate-config.svelte
index 6183daf17..548fe4def 100644
--- a/apps/frontend/src/lib/components/blocks/aggregate/config/aggregate-config.svelte
+++ b/apps/frontend/src/lib/components/blocks/aggregate/config/aggregate-config.svelte
@@ -1,6 +1,5 @@
+
+
diff --git a/apps/frontend/src/lib/components/blocks/field-value/long-text-field.svelte b/apps/frontend/src/lib/components/blocks/field-value/long-text-field.svelte
index 40a7af841..3aaf341e0 100644
--- a/apps/frontend/src/lib/components/blocks/field-value/long-text-field.svelte
+++ b/apps/frontend/src/lib/components/blocks/field-value/long-text-field.svelte
@@ -1,6 +1,10 @@
{#if !value}
@@ -8,5 +12,13 @@
{placeholder || ""}
{:else}
- {value}
+
+ {#if field.allowRichText}
+
+ {@html value}
+
+ {:else}
+ {value}
+ {/if}
+
{/if}
diff --git a/apps/frontend/src/lib/components/blocks/field/get-rollup-foreign-tables.gql b/apps/frontend/src/lib/components/blocks/field/get-rollup-foreign-tables.gql
index 1c8b5b17c..4ab0f3719 100644
--- a/apps/frontend/src/lib/components/blocks/field/get-rollup-foreign-tables.gql
+++ b/apps/frontend/src/lib/components/blocks/field/get-rollup-foreign-tables.gql
@@ -9,6 +9,7 @@ query GetRollupForeignTables($tableId: ID!, $fieldId: ID!) {
constraint
option
display
+ metadata
}
views {
id
diff --git a/apps/frontend/src/lib/components/blocks/filters-editor/filter-input.svelte b/apps/frontend/src/lib/components/blocks/filters-editor/filter-input.svelte
index 4b829bbe6..e876a0822 100644
--- a/apps/frontend/src/lib/components/blocks/filters-editor/filter-input.svelte
+++ b/apps/frontend/src/lib/components/blocks/filters-editor/filter-input.svelte
@@ -269,6 +269,39 @@
is_not_empty: null,
}
+ $: formula = {}
+
+ $: if (field?.type === "formula") {
+ if (field.returnType === "number") {
+ formula = {
+ eq: NumberInput,
+ neq: NumberInput,
+ gt: NumberInput,
+ gte: NumberInput,
+ lt: NumberInput,
+ lte: NumberInput,
+ is_empty: null,
+ is_not_empty: null,
+ }
+ } else if (field.returnType === "boolean") {
+ formula = {
+ is_true: null,
+ is_false: null,
+ }
+ } else if (field.returnType === "string") {
+ formula = {
+ eq: Input,
+ neq: Input,
+ contains: Input,
+ does_not_contain: Input,
+ starts_with: Input,
+ ends_with: Input,
+ is_empty: null,
+ is_not_empty: null,
+ }
+ }
+ }
+
$: filterFieldInput = {
string,
number,
@@ -289,6 +322,7 @@
url,
longText,
duration,
+ formula,
percentage,
}
diff --git a/apps/frontend/src/lib/components/blocks/reference/foreign-table.gql b/apps/frontend/src/lib/components/blocks/reference/foreign-table.gql
index 09ff709b2..7983411ea 100644
--- a/apps/frontend/src/lib/components/blocks/reference/foreign-table.gql
+++ b/apps/frontend/src/lib/components/blocks/reference/foreign-table.gql
@@ -16,6 +16,7 @@ query GetForeignTable($tableId: ID!) {
display
constraint
option
+ metadata
}
views {
diff --git a/apps/frontend/src/lib/components/formula/formula-cursor.visitor.ts b/apps/frontend/src/lib/components/formula/formula-cursor.visitor.ts
new file mode 100644
index 000000000..d877455f5
--- /dev/null
+++ b/apps/frontend/src/lib/components/formula/formula-cursor.visitor.ts
@@ -0,0 +1,117 @@
+import {
+ AbstractParseTreeVisitor,
+ AddSubExprContext,
+ AndExprContext,
+ ArgumentListContext,
+ ComparisonExprContext,
+ ExpressionContext,
+ FormulaContext,
+ FormulaParserVisitor,
+ FunctionCallContext,
+ FunctionExprContext,
+ MulDivModExprContext,
+ NotExprContext,
+ OrExprContext,
+ type ParseTree,
+ VariableContext,
+} from "@undb/formula"
+
+export class FormulaCursorVisitor extends AbstractParseTreeVisitor implements FormulaParserVisitor {
+ private pathNodes: ParseTree[] = []
+ private variables: Set = new Set()
+ public readonly targetPosition: number
+
+ constructor(position: number) {
+ super()
+ this.targetPosition = position
+ }
+
+ public hasAggumentList(): boolean {
+ return this.pathNodes.some((node) => node instanceof ArgumentListContext)
+ }
+
+ public hasFunctionCall(): boolean {
+ return this.pathNodes.some((node) => node instanceof FunctionCallContext)
+ }
+
+ public getNearestFunctionNode() {
+ for (let i = this.pathNodes.length - 1; i >= 0; i--) {
+ const node = this.pathNodes[i]
+ if (node instanceof FunctionCallContext) {
+ return node
+ }
+ }
+ return null
+ }
+
+ public getFunctionName(): string | undefined {
+ const functionCall = this.getNearestFunctionNode()
+ return functionCall?.IDENTIFIER()?.text
+ }
+
+ protected defaultResult(): void {
+ return undefined
+ }
+
+ public getPathNodes() {
+ return this.pathNodes
+ }
+
+ visitPositionInRange(ctx: ExpressionContext) {
+ if (!ctx.start || !ctx.stop) return
+
+ const start = ctx.start.startIndex
+ const stop = ctx.stop.stopIndex
+ const isPositionWithinRange = start <= this.targetPosition && stop >= this.targetPosition
+
+ if (isPositionWithinRange) {
+ this.pathNodes.push(ctx)
+ this.visitChildren(ctx)
+ }
+ }
+
+ visitFormula(ctx: FormulaContext) {
+ this.visitPositionInRange(ctx)
+ }
+
+ visitComparisonExpr(ctx: ComparisonExprContext) {
+ this.visitPositionInRange(ctx)
+ }
+
+ visitAndExpr(ctx: AndExprContext) {
+ this.visitPositionInRange(ctx)
+ }
+
+ visitOrExpr(ctx: OrExprContext) {
+ this.visitPositionInRange(ctx)
+ }
+
+ visitNotExpr(ctx: NotExprContext) {
+ this.visitPositionInRange(ctx)
+ }
+
+ visitMulDivModExpr(ctx: MulDivModExprContext) {
+ this.visitPositionInRange(ctx)
+ }
+
+ visitAddSubExpr(ctx: AddSubExprContext) {
+ this.visitPositionInRange(ctx)
+ }
+
+ visitFunctionExpr(ctx: FunctionExprContext) {
+ this.visitPositionInRange(ctx)
+ }
+
+ visitFunctionCall(ctx: FunctionCallContext) {
+ this.visitPositionInRange(ctx)
+ }
+
+ visitArgumentList(ctx: ArgumentListContext) {
+ this.visitPositionInRange(ctx)
+ }
+
+ visitVariable(ctx: VariableContext) {
+ this.variables.add(ctx.IDENTIFIER().text)
+ this.visitPositionInRange(ctx)
+ }
+}
diff --git a/apps/frontend/src/lib/components/formula/formula-editor.svelte b/apps/frontend/src/lib/components/formula/formula-editor.svelte
new file mode 100644
index 000000000..bd064b2f5
--- /dev/null
+++ b/apps/frontend/src/lib/components/formula/formula-editor.svelte
@@ -0,0 +1,431 @@
+
+
+
+
+ {#if errorMessage}
+
+
+ {errorMessage}
+
+ {/if}
+
+
+ {#each suggestions as suggestion}
+ {@const isSelected = suggestion === selectedSuggestion}
+ {@const isFunction = functions.includes(suggestion)}
+ {@const isField = !isFunction}
+
+ {/each}
+
+
+
+
diff --git a/apps/frontend/src/lib/components/formula/plugins/varaible.plugin.ts b/apps/frontend/src/lib/components/formula/plugins/varaible.plugin.ts
new file mode 100644
index 000000000..5d5e1d6b3
--- /dev/null
+++ b/apps/frontend/src/lib/components/formula/plugins/varaible.plugin.ts
@@ -0,0 +1,50 @@
+import { StateField } from "@codemirror/state"
+import { Decoration, DecorationSet, EditorView, WidgetType } from "@codemirror/view"
+import { TableDo } from "@undb/table"
+import { variable } from "../style"
+
+const variableField = (table: TableDo) =>
+ StateField.define({
+ create() {
+ return Decoration.none
+ },
+ update(decorations, tr) {
+ decorations = decorations.map(tr.changes)
+
+ let matches = []
+ let content = tr.state.doc.toString()
+ const regex = /\{\{([^}]+)\}\}/g
+ let match
+
+ while ((match = regex.exec(content)) !== null) {
+ const start = match.index
+ const end = match.index + match[0].length
+ const fieldId = match[1].trim()
+ const field = table.schema.getFieldByIdOrName(fieldId).into(null)
+ if (!field) continue
+
+ // 创建替换文本装饰器
+ const fieldName = field.name.value
+ matches.push(
+ Decoration.replace({
+ widget: new (class extends WidgetType {
+ toDOM() {
+ const span = document.createElement("span")
+ span.textContent = fieldName
+ span.className = variable()
+ return span
+ }
+ })(),
+ }).range(start, end),
+ )
+ }
+
+ // 确保装饰器按照 from 位置排序
+ matches.sort((a, b) => a.from - b.from)
+
+ return Decoration.set(matches, true)
+ },
+ provide: (f) => EditorView.decorations.from(f),
+ })
+
+export const templateVariablePlugin = (table: TableDo) => [variableField(table)]
diff --git a/apps/frontend/src/lib/components/formula/style.ts b/apps/frontend/src/lib/components/formula/style.ts
new file mode 100644
index 000000000..d6df55c40
--- /dev/null
+++ b/apps/frontend/src/lib/components/formula/style.ts
@@ -0,0 +1,5 @@
+import { tv } from "tailwind-variants"
+
+export const variable = tv({
+ base: "bg-blue-50 hover:bg-blue-100 rounded px-1 border border-blue-200 mx-[1px] transition-all duration-200 ease-in-out hover:shadow-sm hover:border-blue-300 cursor-pointer",
+})
diff --git a/apps/frontend/src/lib/graphql/get-table-foreign-tables.gql b/apps/frontend/src/lib/graphql/get-table-foreign-tables.gql
index 4bf44c974..9eeaf435d 100644
--- a/apps/frontend/src/lib/graphql/get-table-foreign-tables.gql
+++ b/apps/frontend/src/lib/graphql/get-table-foreign-tables.gql
@@ -14,6 +14,7 @@ query GetTableForeignTables($tableId: ID!) {
display
constraint
option
+ metadata
}
views {
id
diff --git a/apps/frontend/src/routes/(authed)/(space)/t/[tableId]/+layout.gql b/apps/frontend/src/routes/(authed)/(space)/t/[tableId]/+layout.gql
index 7ce7cc4dc..3c945d40a 100644
--- a/apps/frontend/src/routes/(authed)/(space)/t/[tableId]/+layout.gql
+++ b/apps/frontend/src/routes/(authed)/(space)/t/[tableId]/+layout.gql
@@ -11,6 +11,7 @@ query GetTableQuery($tableId: ID!, $viewId: ID) {
display
constraint
option
+ metadata
}
views {
diff --git a/apps/frontend/src/routes/(share)/s/b/[shareId]/t/[tableId]/+layout.gql b/apps/frontend/src/routes/(share)/s/b/[shareId]/t/[tableId]/+layout.gql
index 4f820326f..a76f7e04c 100644
--- a/apps/frontend/src/routes/(share)/s/b/[shareId]/t/[tableId]/+layout.gql
+++ b/apps/frontend/src/routes/(share)/s/b/[shareId]/t/[tableId]/+layout.gql
@@ -51,6 +51,7 @@ query GetBaseTableShareData($shareId: ID!, $tableId: ID!) {
name
option
type
+ metadata
}
}
}
diff --git a/apps/frontend/src/routes/(share)/s/f/[shareId]/+layout.gql b/apps/frontend/src/routes/(share)/s/f/[shareId]/+layout.gql
index c7ada0d91..58fc0c454 100644
--- a/apps/frontend/src/routes/(share)/s/f/[shareId]/+layout.gql
+++ b/apps/frontend/src/routes/(share)/s/f/[shareId]/+layout.gql
@@ -44,6 +44,7 @@ query GetFormShareData($shareId: ID!) {
name
option
type
+ metadata
}
}
}
diff --git a/apps/frontend/src/routes/(share)/s/v/[shareId]/+layout.gql b/apps/frontend/src/routes/(share)/s/v/[shareId]/+layout.gql
index 228fd4474..f9689aea4 100644
--- a/apps/frontend/src/routes/(share)/s/v/[shareId]/+layout.gql
+++ b/apps/frontend/src/routes/(share)/s/v/[shareId]/+layout.gql
@@ -65,6 +65,7 @@ query GetViewShareData($shareId: ID!) {
name
option
type
+ metadata
}
}
}
diff --git a/apps/frontend/svelte.config.js b/apps/frontend/svelte.config.js
index ebe200df0..57d59a3b3 100644
--- a/apps/frontend/svelte.config.js
+++ b/apps/frontend/svelte.config.js
@@ -7,6 +7,10 @@ const config = {
// for more information about preprocessors
preprocess: vitePreprocess(),
+ vitePlugin: {
+ exclude: ["@undb/formula"],
+ },
+
kit: {
adapter: adapter({
pages: "dist",
diff --git a/apps/frontend/tailwind.config.js b/apps/frontend/tailwind.config.js
index fa1cb36b6..59ee0cca7 100644
--- a/apps/frontend/tailwind.config.js
+++ b/apps/frontend/tailwind.config.js
@@ -1,64 +1,65 @@
-import { fontFamily } from "tailwindcss/defaultTheme";
+import { fontFamily } from "tailwindcss/defaultTheme"
/** @type {import('tailwindcss').Config} */
const config = {
- darkMode: ["class"],
- content: ["./src/**/*.{html,js,svelte,ts}"],
- safelist: ["dark"],
- theme: {
- container: {
- center: true,
- padding: "2rem",
- screens: {
- "2xl": "1400px"
- }
- },
- extend: {
- colors: {
- border: "hsl(var(--border) / )",
- input: "hsl(var(--input) / )",
- ring: "hsl(var(--ring) / )",
- background: "hsl(var(--background) / )",
- foreground: "hsl(var(--foreground) / )",
- primary: {
- DEFAULT: "hsl(var(--primary) / )",
- foreground: "hsl(var(--primary-foreground) / )"
- },
- secondary: {
- DEFAULT: "hsl(var(--secondary) / )",
- foreground: "hsl(var(--secondary-foreground) / )"
- },
- destructive: {
- DEFAULT: "hsl(var(--destructive) / )",
- foreground: "hsl(var(--destructive-foreground) / )"
- },
- muted: {
- DEFAULT: "hsl(var(--muted) / )",
- foreground: "hsl(var(--muted-foreground) / )"
- },
- accent: {
- DEFAULT: "hsl(var(--accent) / )",
- foreground: "hsl(var(--accent-foreground) / )"
- },
- popover: {
- DEFAULT: "hsl(var(--popover) / )",
- foreground: "hsl(var(--popover-foreground) / )"
- },
- card: {
- DEFAULT: "hsl(var(--card) / )",
- foreground: "hsl(var(--card-foreground) / )"
- }
- },
- borderRadius: {
- lg: "var(--radius)",
- md: "calc(var(--radius) - 2px)",
- sm: "calc(var(--radius) - 4px)"
- },
- fontFamily: {
- sans: [...fontFamily.sans]
- }
- }
- },
-};
+ darkMode: ["class"],
+ content: ["./src/**/*.{html,js,svelte,ts}"],
+ safelist: ["dark"],
+ theme: {
+ container: {
+ center: true,
+ padding: "2rem",
+ screens: {
+ "2xl": "1400px",
+ },
+ },
+ extend: {
+ colors: {
+ border: "hsl(var(--border) / )",
+ input: "hsl(var(--input) / )",
+ ring: "hsl(var(--ring) / )",
+ background: "hsl(var(--background) / )",
+ foreground: "hsl(var(--foreground) / )",
+ primary: {
+ DEFAULT: "hsl(var(--primary) / )",
+ foreground: "hsl(var(--primary-foreground) / )",
+ },
+ secondary: {
+ DEFAULT: "hsl(var(--secondary) / )",
+ foreground: "hsl(var(--secondary-foreground) / )",
+ },
+ destructive: {
+ DEFAULT: "hsl(var(--destructive) / )",
+ foreground: "hsl(var(--destructive-foreground) / )",
+ },
+ muted: {
+ DEFAULT: "hsl(var(--muted) / )",
+ foreground: "hsl(var(--muted-foreground) / )",
+ },
+ accent: {
+ DEFAULT: "hsl(var(--accent) / )",
+ foreground: "hsl(var(--accent-foreground) / )",
+ },
+ popover: {
+ DEFAULT: "hsl(var(--popover) / )",
+ foreground: "hsl(var(--popover-foreground) / )",
+ },
+ card: {
+ DEFAULT: "hsl(var(--card) / )",
+ foreground: "hsl(var(--card-foreground) / )",
+ },
+ },
+ borderRadius: {
+ lg: "var(--radius)",
+ md: "calc(var(--radius) - 2px)",
+ sm: "calc(var(--radius) - 4px)",
+ },
+ fontFamily: {
+ sans: [...fontFamily.sans],
+ },
+ },
+ },
+ plugins: [require("@tailwindcss/typography")],
+}
-export default config;
+export default config
diff --git a/apps/frontend/tsconfig.json b/apps/frontend/tsconfig.json
index df2d6ba36..78833c830 100644
--- a/apps/frontend/tsconfig.json
+++ b/apps/frontend/tsconfig.json
@@ -10,6 +10,7 @@
"noEmit": true,
"moduleResolution": "bundler",
"experimentalDecorators": true,
+ "verbatimModuleSyntax": false,
"emitDecoratorMetadata": true,
"rootDirs": [".", "./.svelte-kit/types", "./$houdini/types", "./$houdini"]
}
diff --git a/apps/frontend/vite.config.ts b/apps/frontend/vite.config.ts
index 80caaba37..4358a1f32 100644
--- a/apps/frontend/vite.config.ts
+++ b/apps/frontend/vite.config.ts
@@ -1,12 +1,19 @@
import { sveltekit } from "@sveltejs/kit/vite"
import houdini from "houdini/vite"
import { visualizer } from "rollup-plugin-visualizer"
+import { nodePolyfills } from "vite-plugin-node-polyfills"
import { defineConfig } from "vitest/config"
export default defineConfig({
plugins: [
houdini(),
sveltekit(),
+ nodePolyfills({
+ include: ["assert"],
+ globals: {
+ process: true,
+ },
+ }),
visualizer({
emitFile: true,
filename: "stats.html",
diff --git a/bun.lockb b/bun.lockb
index c034b4429..508ca4655 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/package.json b/package.json
index 5e31400b4..d85a9746d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "undb",
- "version": "1.0.0-110",
+ "version": "1.0.0-111",
"private": true,
"scripts": {
"build": "NODE_ENV=production bun --bun turbo build",
diff --git a/packages/base/src/fixtures/__snapshots__/base.fixture.test.ts.snap b/packages/base/src/fixtures/__snapshots__/base.fixture.test.ts.snap
deleted file mode 100644
index b67f58017..000000000
--- a/packages/base/src/fixtures/__snapshots__/base.fixture.test.ts.snap
+++ /dev/null
@@ -1,35 +0,0 @@
-// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
-
-exports[`base fixture test > create test base success 1`] = `
-Base {
- "id": BaseId {
- "props": {
- "value": "baseId",
- },
- },
- "name": BaseName {
- "props": {
- "value": "name",
- },
- },
-}
-`;
-
-exports[`base fixture test create test base success 1`] = `
-Base {
- "id": BaseId {
- "props": {
- "value": "basttrlpxe2",
- },
- },
- "name": BaseName {
- "props": {
- "value": undefined,
- },
- },
- "option": BaseOption {
- "props": {},
- },
- "spaceId": undefined,
-}
-`;
diff --git a/packages/base/src/fixtures/base.fixture.test.ts b/packages/base/src/fixtures/base.fixture.test.ts
deleted file mode 100644
index 8f968520a..000000000
--- a/packages/base/src/fixtures/base.fixture.test.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { createTestBase } from "./base.fixture"
-
-describe("base fixture test", () => {
- test("create test base success", () => {
- const base = createTestBase()
-
- expect(base).toMatchSnapshot()
- })
-})
diff --git a/packages/domain/src/query.test.ts b/packages/domain/src/query.test.ts
index 33f858bee..745360977 100644
--- a/packages/domain/src/query.test.ts
+++ b/packages/domain/src/query.test.ts
@@ -1,3 +1,5 @@
+import { expect, test } from "bun:test"
+
test("test", () => {
expect(1).toBe(1)
})
diff --git a/packages/formula/.gitignore b/packages/formula/.gitignore
new file mode 100644
index 000000000..c10d3591e
--- /dev/null
+++ b/packages/formula/.gitignore
@@ -0,0 +1,177 @@
+# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
+
+# Logs
+
+logs
+_.log
+npm-debug.log_
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Caches
+
+.cache
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+
+report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
+
+# Runtime data
+
+pids
+_.pid
+_.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+
+lib-cov
+
+# Coverage directory used by tools like istanbul
+
+coverage
+*.lcov
+
+# nyc test coverage
+
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+
+bower_components
+
+# node-waf configuration
+
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+
+build/Release
+
+# Dependency directories
+
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+
+web_modules/
+
+# TypeScript cache
+
+*.tsbuildinfo
+
+# Optional npm cache directory
+
+.npm
+
+# Optional eslint cache
+
+.eslintcache
+
+# Optional stylelint cache
+
+.stylelintcache
+
+# Microbundle cache
+
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+
+.node_repl_history
+
+# Output of 'npm pack'
+
+*.tgz
+
+# Yarn Integrity file
+
+.yarn-integrity
+
+# dotenv environment variable files
+
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# parcel-bundler cache (https://parceljs.org/)
+
+.parcel-cache
+
+# Next.js build output
+
+.next
+out
+
+# Nuxt.js build / generate output
+
+.nuxt
+dist
+
+# Gatsby files
+
+# Comment in the public line in if your project uses Gatsby and not Next.js
+
+# https://nextjs.org/blog/next-9-1#public-directory-support
+
+# public
+
+# vuepress build output
+
+.vuepress/dist
+
+# vuepress v2.x temp and cache directory
+
+.temp
+
+# Docusaurus cache and generated files
+
+.docusaurus
+
+# Serverless directories
+
+.serverless/
+
+# FuseBox cache
+
+.fusebox/
+
+# DynamoDB Local files
+
+.dynamodb/
+
+# TernJS port file
+
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+
+.vscode-test
+
+# yarn v2
+
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
+
+# IntelliJ based IDEs
+.idea
+
+# Finder (MacOS) folder config
+.DS_Store
+
+.antlr
\ No newline at end of file
diff --git a/packages/formula/README.md b/packages/formula/README.md
new file mode 100644
index 000000000..8c766f5e0
--- /dev/null
+++ b/packages/formula/README.md
@@ -0,0 +1,15 @@
+# @undb/formula
+
+To install dependencies:
+
+```bash
+bun install
+```
+
+To run:
+
+```bash
+bun run src/index.ts
+```
+
+This project was created using `bun init` in bun v1.1.31. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
diff --git a/packages/formula/package.json b/packages/formula/package.json
new file mode 100644
index 000000000..ddaa33eac
--- /dev/null
+++ b/packages/formula/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "@undb/formula",
+ "module": "src/index.ts",
+ "type": "module",
+ "types": "src/index.d.ts",
+ "devDependencies": {
+ "@types/bun": "latest",
+ "antlr4ts-cli": "^0.5.0-alpha.4"
+ },
+ "peerDependencies": {
+ "typescript": "^5.0.0"
+ },
+ "dependencies": {
+ "@undb/zod": "*",
+ "antlr4ts": "^0.5.0-alpha.4"
+ },
+ "scripts": {
+ "generate-parser": "bun run scripts/generate-parser.ts",
+ "generate": "bun generate-parser"
+ }
+}
diff --git a/packages/formula/scripts/generate-parser.ts b/packages/formula/scripts/generate-parser.ts
new file mode 100644
index 000000000..c898b2df3
--- /dev/null
+++ b/packages/formula/scripts/generate-parser.ts
@@ -0,0 +1,3 @@
+import { $ } from "bun"
+
+await $`antlr4ts -visitor -no-listener src/grammar/*.g4`
diff --git a/packages/formula/src/formula.constants.ts b/packages/formula/src/formula.constants.ts
new file mode 100644
index 000000000..8fb7a55a1
--- /dev/null
+++ b/packages/formula/src/formula.constants.ts
@@ -0,0 +1,58 @@
+import { FormulaFunction } from "./formula/formula.type"
+
+export const FORMULA_FUNCTIONS: FormulaFunction[] = [
+ "ADD",
+ "SUBTRACT",
+ "MULTIPLY",
+ "DIVIDE",
+ "SUM",
+ "CONCAT",
+ "MOD",
+ "POWER",
+ "SQRT",
+ "ABS",
+ "ROUND",
+ "FLOOR",
+ "CEILING",
+ "MIN",
+ "MAX",
+ "AVERAGE",
+ // "MEDIAN",
+
+ // 文本处理
+ "UPPER",
+ "LOWER",
+ "TRIM",
+ "LEFT",
+ "RIGHT",
+ "MID",
+ "LEN",
+ "FIND",
+ "REPLACE",
+ "SUBSTITUTE",
+ "REPEAT",
+ "SEARCH",
+ "SUBSTR",
+
+ // 逻辑运算
+ "AND",
+ "OR",
+ "NOT",
+ "IF",
+ "SWITCH",
+ // "ISBLANK",
+ // "ISNUMBER",
+ // "ISTEXT",
+
+ // 统计函数
+ // "COUNT",
+ // "COUNTA",
+ // "COUNTIF",
+ // "SUMIF",
+ // "CORREL",
+
+ "JSON_EXTRACT",
+
+ "RECORD_ID",
+ "AUTO_INCREMENT",
+] as const
diff --git a/packages/formula/src/formula.visitor.ts b/packages/formula/src/formula.visitor.ts
new file mode 100644
index 000000000..eeb513571
--- /dev/null
+++ b/packages/formula/src/formula.visitor.ts
@@ -0,0 +1,219 @@
+import { AbstractParseTreeVisitor } from "antlr4ts/tree/AbstractParseTreeVisitor"
+import { FormulaFunction } from "./formula/formula.type"
+import { globalFunctionRegistry } from "./formula/registry"
+import {
+ AddSubExprContext,
+ AndExprContext,
+ ArgumentListContext,
+ ComparisonExprContext,
+ FormulaContext,
+ FunctionCallContext,
+ FunctionExprContext,
+ MulDivModExprContext,
+ NotExprContext,
+ NumberExprContext,
+ OrExprContext,
+ ParenExprContext,
+ StringExprContext,
+ VariableContext,
+ VariableExprContext,
+} from "./grammar/FormulaParser"
+import type { FormulaParserVisitor } from "./grammar/FormulaParserVisitor"
+import {
+ ArgumentListResult,
+ ReturnType,
+ type ExpressionResult,
+ type FunctionExpressionResult,
+ type NumberResult,
+ type VariableResult,
+} from "./types"
+
+export class FormulaVisitor
+ extends AbstractParseTreeVisitor
+ implements FormulaParserVisitor
+{
+ private variables: Set = new Set()
+
+ private assertType(result: ExpressionResult, types: ReturnType[]): boolean {
+ if (result.type === "variable") {
+ return true
+ }
+
+ if (result.type === "functionCall") {
+ if (!types.includes(result.returnType)) {
+ throw new Error(`Expected ${types.join(" or ")} but got ${result.name}`)
+ }
+ return true
+ }
+
+ if (!types.includes(result.type as ReturnType)) {
+ throw new Error(`Expected ${types.join(" or ")} but got ${result.type}`)
+ }
+
+ return true
+ }
+
+ visitFormula(ctx: FormulaContext): ExpressionResult {
+ return this.visit(ctx.expression())
+ }
+
+ visitMulDivModExpr(ctx: MulDivModExprContext): ExpressionResult {
+ const left = this.visit(ctx.expression(0)) as NumberResult | VariableResult
+ const right = this.visit(ctx.expression(1)) as NumberResult | VariableResult
+
+ this.assertType(left, ["number"])
+ this.assertType(right, ["number"])
+
+ const op = ctx._op.text!
+ return {
+ type: "functionCall",
+ name: op,
+ arguments: [left, right],
+ returnType: "number",
+ value: ctx.text,
+ }
+ }
+
+ visitAddSubExpr(ctx: AddSubExprContext): ExpressionResult {
+ const left = this.visit(ctx.expression(0)) as NumberResult | VariableResult
+ const right = this.visit(ctx.expression(1)) as NumberResult | VariableResult
+
+ this.assertType(left, ["number"])
+ this.assertType(right, ["number"])
+
+ const op = ctx._op.text!
+ return {
+ type: "functionCall",
+ name: op,
+ arguments: [left, right],
+ returnType: "number",
+ value: ctx.text,
+ }
+ }
+
+ visitComparisonExpr(ctx: ComparisonExprContext): ExpressionResult {
+ const left = this.visit(ctx.expression(0))
+ const right = this.visit(ctx.expression(1))
+
+ this.assertType(left, ["number"])
+ this.assertType(right, ["number"])
+
+ const op = ctx._op.text!
+ return {
+ type: "functionCall",
+ name: op,
+ arguments: [left, right],
+ returnType: "boolean",
+ value: ctx.text,
+ }
+ }
+
+ visitAndExpr(ctx: AndExprContext): ExpressionResult {
+ const left = this.visit(ctx.expression(0))
+ const right = this.visit(ctx.expression(1))
+
+ this.assertType(left, ["boolean"])
+ this.assertType(right, ["boolean"])
+
+ return {
+ type: "functionCall",
+ name: "AND",
+ arguments: [left, right],
+ returnType: "boolean",
+ value: ctx.text,
+ }
+ }
+
+ visitOrExpr(ctx: OrExprContext): ExpressionResult {
+ const left = this.visit(ctx.expression(0))
+ const right = this.visit(ctx.expression(1))
+
+ this.assertType(left, ["boolean"])
+ this.assertType(right, ["boolean"])
+
+ return {
+ type: "functionCall",
+ name: "OR",
+ arguments: [left, right],
+ returnType: "boolean",
+ value: ctx.text,
+ }
+ }
+
+ visitNotExpr(ctx: NotExprContext): ExpressionResult {
+ const expr = this.visit(ctx.expression())
+ this.assertType(expr, ["boolean"])
+ return {
+ type: "functionCall",
+ name: "NOT",
+ arguments: [expr],
+ returnType: "boolean",
+ value: ctx.text,
+ }
+ }
+
+ visitFunctionExpr(ctx: FunctionExprContext): ExpressionResult {
+ return this.visit(ctx.functionCall())
+ }
+
+ visitVariableExpr(ctx: VariableExprContext): ExpressionResult {
+ return this.visit(ctx.variable())
+ }
+
+ visitNumberExpr(ctx: NumberExprContext): ExpressionResult {
+ return { type: "number", value: Number(ctx.NUMBER().text) }
+ }
+
+ visitStringExpr(ctx: StringExprContext): ExpressionResult {
+ return { type: "string", value: ctx.STRING().text.slice(1, -1) }
+ }
+
+ visitParenExpr(ctx: ParenExprContext): ExpressionResult {
+ return this.visit(ctx.expression())
+ }
+
+ visitFunctionCall(ctx: FunctionCallContext): ExpressionResult {
+ const funcName = ctx.IDENTIFIER().text as FormulaFunction
+ const args = ctx.argumentList() ? (this.visit(ctx.argumentList()!) as FunctionExpressionResult) : undefined
+
+ if (!globalFunctionRegistry.isValid(funcName)) {
+ throw new Error(`Unknown function: ${funcName}`)
+ }
+
+ if (args) {
+ globalFunctionRegistry.validateArgs(funcName, args.arguments)
+ }
+
+ const returnType = globalFunctionRegistry.get(funcName)!.returnType
+
+ return {
+ type: "functionCall",
+ name: funcName,
+ arguments: args?.arguments ?? [],
+ returnType,
+ value: ctx.text,
+ }
+ }
+
+ visitArgumentList(ctx: ArgumentListContext): ArgumentListResult {
+ const args = ctx.expression().map((expr) => this.visit(expr))
+ return {
+ type: "argumentList",
+ arguments: args,
+ }
+ }
+ visitVariable(ctx: VariableContext): VariableResult {
+ const variableName = ctx.IDENTIFIER().text
+ const raw = ctx.text
+ this.variables.add(variableName)
+ return { type: "variable", value: raw, variable: variableName }
+ }
+
+ getVariables(): string[] {
+ return Array.from(this.variables)
+ }
+
+ protected defaultResult(): ExpressionResult {
+ return { type: "string", value: "" }
+ }
+}
diff --git a/packages/formula/src/formula/formula.type.ts b/packages/formula/src/formula/formula.type.ts
new file mode 100644
index 000000000..2690f6b1e
--- /dev/null
+++ b/packages/formula/src/formula/formula.type.ts
@@ -0,0 +1,68 @@
+export type FormulaFunction =
+ // 数学运算
+ | "ADD"
+ | "SUBTRACT"
+ | "MULTIPLY"
+ | "DIVIDE"
+ | "SUM"
+ | "CONCAT"
+ | "MOD"
+ | "POWER"
+ | "SQRT"
+ | "ABS"
+ | "ROUND"
+ | "FLOOR"
+ | "CEILING"
+ | "MIN"
+ | "MAX"
+ | "AVERAGE"
+ // | "MEDIAN"
+
+ // 文本处理
+ | "UPPER"
+ | "LOWER"
+ | "TRIM"
+ | "LEFT"
+ | "RIGHT"
+ | "MID"
+ | "LEN"
+ | "FIND"
+ | "REPLACE"
+ | "SUBSTITUTE"
+ | "REPEAT"
+ | "SEARCH"
+ | "SUBSTR"
+
+ // // 日期时间
+ // | "NOW"
+ // | "TODAY"
+ // | "YEAR"
+ // | "MONTH"
+ // | "DAY"
+ // | "HOUR"
+ // | "MINUTE"
+ // | "SECOND"
+ // | "WEEKDAY"
+ // | "DATE"
+
+ // 逻辑运算
+ | "AND"
+ | "OR"
+ | "NOT"
+ | "IF"
+ | "SWITCH"
+ | "ISBLANK"
+ | "ISNUMBER"
+ | "ISTEXT"
+
+ // 统计函数
+ | "COUNT"
+ | "COUNTA"
+ | "COUNTIF"
+ | "SUMIF"
+ | "CORREL"
+ | "JSON_EXTRACT"
+
+ // System Field
+ | "RECORD_ID"
+ | "AUTO_INCREMENT"
diff --git a/packages/formula/src/formula/registry.ts b/packages/formula/src/formula/registry.ts
new file mode 100644
index 000000000..f852969ff
--- /dev/null
+++ b/packages/formula/src/formula/registry.ts
@@ -0,0 +1,142 @@
+import { ParamType, ReturnType, type ExpressionResult } from "../types"
+import { FormulaFunction } from "./formula.type"
+
+interface FunctionDefinition {
+ paramPatterns: ParamType[][]
+ returnType: ReturnType
+}
+
+export class FunctionRegistry {
+ private functions: Map = new Map()
+
+ register(name: FormulaFunction, paramPatterns: ParamType[][], returnType: ReturnType) {
+ this.functions.set(name, { paramPatterns, returnType })
+ }
+
+ get(name: FormulaFunction): FunctionDefinition | undefined {
+ return this.functions.get(name.toUpperCase())
+ }
+
+ isValid(name: string): boolean {
+ return this.functions.has(name.toUpperCase())
+ }
+
+ validateArgs(name: FormulaFunction, args: ExpressionResult[]): void {
+ const funcDef = this.get(name)
+ if (!funcDef) {
+ throw new Error(`Unknown function name: ${name}`)
+ }
+
+ // 检查是否有任何模式的参数数量匹配
+ const hasMatchingPattern = funcDef.paramPatterns.some((pattern) => {
+ // 如果模式中包含 VARIADIC,则参数数量必须大于等于 pattern.length - 1
+ // 否则参数数量必须完全匹配
+ if (pattern.includes("variadic")) {
+ return args.length >= pattern.length - 1
+ }
+ return args.length === pattern.length
+ })
+
+ if (!hasMatchingPattern) {
+ const expectedCounts = funcDef.paramPatterns
+ .map((pattern) => (pattern.includes("variadic") ? `at least ${pattern.length - 1}` : `${pattern.length}`))
+ .join(" or ")
+ throw new Error(`Function ${name} expects ${expectedCounts} arguments, but got ${args.length}`)
+ }
+
+ const isValidPattern = funcDef.paramPatterns.some((pattern) => {
+ for (let i = 0; i < pattern.length; i++) {
+ const expectedType = pattern[i]
+ if (expectedType === "variadic") {
+ // 剩余的所有参数都应该匹配 VARIADIC 的前一个类型
+ const variadicType = pattern[i - 1]
+ return args.slice(i - 1).every((arg) => this.isTypeMatch(arg, variadicType))
+ }
+ if (!this.isTypeMatch(args[i], expectedType)) {
+ return false
+ }
+ }
+ return true
+ })
+
+ if (!isValidPattern) {
+ throw new Error(`Function ${name} arguments do not match: ${JSON.stringify(args)}`)
+ }
+ }
+
+ private isTypeMatch(arg: ExpressionResult, expectedType: ParamType): boolean {
+ if (arg.type === "functionCall") {
+ return arg.returnType === expectedType
+ }
+
+ if (arg.type === "variable") {
+ return true
+ }
+
+ switch (expectedType) {
+ case "number":
+ return arg.type === "number"
+ case "string":
+ return arg.type === "string"
+ case "boolean":
+ return arg.type === "boolean"
+ case "date":
+ // TODO: 假设有日期类型的处理
+ return false
+ case "any":
+ return true
+ default:
+ return false
+ }
+ }
+}
+
+export const globalFunctionRegistry = new FunctionRegistry()
+
+// 注册函数,支持多种参数模式
+globalFunctionRegistry.register("ADD", [["number", "number"]], "number")
+globalFunctionRegistry.register("SUBTRACT", [["number", "number"]], "number")
+globalFunctionRegistry.register("MULTIPLY", [["number", "number"]], "number")
+globalFunctionRegistry.register("DIVIDE", [["number", "number"]], "number")
+globalFunctionRegistry.register("SUM", [["number", "variadic"]], "number")
+globalFunctionRegistry.register("MOD", [["number", "number"]], "number")
+globalFunctionRegistry.register("POWER", [["number", "number"]], "number")
+globalFunctionRegistry.register("SQRT", [["number"]], "number")
+globalFunctionRegistry.register("ABS", [["number"]], "number")
+globalFunctionRegistry.register("ROUND", [["number"]], "number")
+globalFunctionRegistry.register("FLOOR", [["number"]], "number")
+globalFunctionRegistry.register("CEILING", [["number"]], "number")
+globalFunctionRegistry.register("MIN", [["number", "variadic"]], "number")
+globalFunctionRegistry.register("MAX", [["number", "variadic"]], "number")
+globalFunctionRegistry.register("AVERAGE", [["number", "variadic"]], "number")
+
+globalFunctionRegistry.register("CONCAT", [["string", "variadic"]], "string")
+globalFunctionRegistry.register("UPPER", [["string"]], "string")
+globalFunctionRegistry.register("LOWER", [["string"]], "string")
+globalFunctionRegistry.register("TRIM", [["string"]], "string")
+globalFunctionRegistry.register("LEFT", [["string", "number"]], "string")
+globalFunctionRegistry.register("RIGHT", [["string", "number"]], "string")
+globalFunctionRegistry.register("MID", [["string", "number", "number"]], "string")
+globalFunctionRegistry.register("LEN", [["string"]], "number")
+globalFunctionRegistry.register("REPLACE", [["string", "string", "string"]], "string")
+globalFunctionRegistry.register("SUBSTITUTE", [["string", "string", "string", "number"]], "string")
+globalFunctionRegistry.register("REPEAT", [["string", "number"]], "string")
+globalFunctionRegistry.register("SEARCH", [["string", "string"]], "number")
+globalFunctionRegistry.register("SUBSTR", [["string", "number", "number"]], "string")
+
+globalFunctionRegistry.register("AND", [["boolean", "variadic"]], "boolean")
+globalFunctionRegistry.register("OR", [["boolean", "variadic"]], "boolean")
+globalFunctionRegistry.register("NOT", [["boolean"]], "boolean")
+globalFunctionRegistry.register("ISBLANK", [["any"]], "boolean")
+globalFunctionRegistry.register("ISNUMBER", [["any"]], "boolean")
+globalFunctionRegistry.register("ISTEXT", [["any"]], "boolean")
+
+// globalFunctionRegistry.register("COUNT", [["variadic"]], "number")
+// globalFunctionRegistry.register("COUNTA", [["variadic"]], "number")
+// globalFunctionRegistry.register("COUNTIF", [["variadic"]], "number")
+// globalFunctionRegistry.register("SUMIF", [["variadic"]], "number")
+
+globalFunctionRegistry.register("JSON_EXTRACT", [["string", "string"]], "any")
+
+globalFunctionRegistry.register("RECORD_ID", [], "string")
+globalFunctionRegistry.register("AUTO_INCREMENT", [], "number")
diff --git a/packages/formula/src/grammar/FormulaLexer.g4 b/packages/formula/src/grammar/FormulaLexer.g4
new file mode 100644
index 000000000..9594926b0
--- /dev/null
+++ b/packages/formula/src/grammar/FormulaLexer.g4
@@ -0,0 +1,97 @@
+lexer grammar FormulaLexer;
+
+// 运算符
+ADD: '+';
+SUBTRACT: '-';
+MULTIPLY: '*';
+DIVIDE: '/';
+MODULO: '%';
+POWER: '^';
+
+// 比较运算符
+EQUAL: '=';
+NOT_EQUAL: '!=';
+LESS: '<';
+LESS_EQUAL: '<=';
+GREATER: '>';
+GREATER_EQUAL: '>=';
+
+// 逻辑运算符
+AND: A N D;
+OR: O R;
+NOT: N O T;
+
+// 括号
+LPAREN: '(';
+RPAREN: ')';
+LBRACE: '{';
+RBRACE: '}';
+LBRACKET: '[';
+RBRACKET: ']';
+
+// 分隔符
+COMMA: ',';
+SEMICOLON: ';';
+COLON: ':';
+DOT: '.';
+
+UNDERSCORE: '_';
+
+// 函数名和标识符
+IDENTIFIER: LETTER (LETTER | DIGIT | UNDERSCORE)*;
+
+// 数字
+NUMBER: DIGIT+ ('.' DIGIT+)?;
+
+// 字符串
+STRING: '\'' ( ~'\'' | '\'\'')* '\'';
+
+// 布尔值
+TRUE: T R U E;
+FALSE: F A L S E;
+
+// 空值
+NULL: N U L L;
+
+// 日期时间
+DATE: D A T E;
+TIME: T I M E;
+DATETIME: D A T E T I M E;
+
+// 空白字符
+WS: [ \t\r\n]+ -> skip;
+
+// 注释
+COMMENT: '//' ~[\r\n]* -> skip;
+MULTILINE_COMMENT: '/*' .*? '*/' -> skip;
+
+// Fragments
+fragment DIGIT: [0-9];
+fragment LETTER: [a-zA-Z];
+
+fragment A: ('A' | 'a');
+fragment B: ('B' | 'b');
+fragment C: ('C' | 'c');
+fragment D: ('D' | 'd');
+fragment E: ('E' | 'e');
+fragment F: ('F' | 'f');
+fragment G: ('G' | 'g');
+fragment H: ('H' | 'h');
+fragment I: ('I' | 'i');
+fragment J: ('J' | 'j');
+fragment K: ('K' | 'k');
+fragment L: ('L' | 'l');
+fragment M: ('M' | 'm');
+fragment N: ('N' | 'n');
+fragment O: ('O' | 'o');
+fragment P: ('P' | 'p');
+fragment Q: ('Q' | 'q');
+fragment R: ('R' | 'r');
+fragment S: ('S' | 's');
+fragment T: ('T' | 't');
+fragment U: ('U' | 'u');
+fragment V: ('V' | 'v');
+fragment W: ('W' | 'w');
+fragment X: ('X' | 'x');
+fragment Y: ('Y' | 'y');
+fragment Z: ('Z' | 'z');
\ No newline at end of file
diff --git a/packages/formula/src/grammar/FormulaLexer.interp b/packages/formula/src/grammar/FormulaLexer.interp
new file mode 100644
index 000000000..6fefff5f8
--- /dev/null
+++ b/packages/formula/src/grammar/FormulaLexer.interp
@@ -0,0 +1,159 @@
+token literal names:
+null
+'+'
+'-'
+'*'
+'/'
+'%'
+'^'
+'='
+'!='
+'<'
+'<='
+'>'
+'>='
+null
+null
+null
+'('
+')'
+'{'
+'}'
+'['
+']'
+','
+';'
+':'
+'.'
+'_'
+null
+null
+null
+null
+null
+null
+null
+null
+null
+null
+null
+null
+
+token symbolic names:
+null
+ADD
+SUBTRACT
+MULTIPLY
+DIVIDE
+MODULO
+POWER
+EQUAL
+NOT_EQUAL
+LESS
+LESS_EQUAL
+GREATER
+GREATER_EQUAL
+AND
+OR
+NOT
+LPAREN
+RPAREN
+LBRACE
+RBRACE
+LBRACKET
+RBRACKET
+COMMA
+SEMICOLON
+COLON
+DOT
+UNDERSCORE
+IDENTIFIER
+NUMBER
+STRING
+TRUE
+FALSE
+NULL
+DATE
+TIME
+DATETIME
+WS
+COMMENT
+MULTILINE_COMMENT
+
+rule names:
+ADD
+SUBTRACT
+MULTIPLY
+DIVIDE
+MODULO
+POWER
+EQUAL
+NOT_EQUAL
+LESS
+LESS_EQUAL
+GREATER
+GREATER_EQUAL
+AND
+OR
+NOT
+LPAREN
+RPAREN
+LBRACE
+RBRACE
+LBRACKET
+RBRACKET
+COMMA
+SEMICOLON
+COLON
+DOT
+UNDERSCORE
+IDENTIFIER
+NUMBER
+STRING
+TRUE
+FALSE
+NULL
+DATE
+TIME
+DATETIME
+WS
+COMMENT
+MULTILINE_COMMENT
+DIGIT
+LETTER
+A
+B
+C
+D
+E
+F
+G
+H
+I
+J
+K
+L
+M
+N
+O
+P
+Q
+R
+S
+T
+U
+V
+W
+X
+Y
+Z
+
+channel names:
+DEFAULT_TOKEN_CHANNEL
+HIDDEN
+
+mode names:
+DEFAULT_MODE
+
+atn:
+[3, 51485, 51898, 1421, 44986, 20307, 1543, 60043, 49729, 2, 40, 351, 8, 1, 4, 2, 9, 2, 4, 3, 9, 3, 4, 4, 9, 4, 4, 5, 9, 5, 4, 6, 9, 6, 4, 7, 9, 7, 4, 8, 9, 8, 4, 9, 9, 9, 4, 10, 9, 10, 4, 11, 9, 11, 4, 12, 9, 12, 4, 13, 9, 13, 4, 14, 9, 14, 4, 15, 9, 15, 4, 16, 9, 16, 4, 17, 9, 17, 4, 18, 9, 18, 4, 19, 9, 19, 4, 20, 9, 20, 4, 21, 9, 21, 4, 22, 9, 22, 4, 23, 9, 23, 4, 24, 9, 24, 4, 25, 9, 25, 4, 26, 9, 26, 4, 27, 9, 27, 4, 28, 9, 28, 4, 29, 9, 29, 4, 30, 9, 30, 4, 31, 9, 31, 4, 32, 9, 32, 4, 33, 9, 33, 4, 34, 9, 34, 4, 35, 9, 35, 4, 36, 9, 36, 4, 37, 9, 37, 4, 38, 9, 38, 4, 39, 9, 39, 4, 40, 9, 40, 4, 41, 9, 41, 4, 42, 9, 42, 4, 43, 9, 43, 4, 44, 9, 44, 4, 45, 9, 45, 4, 46, 9, 46, 4, 47, 9, 47, 4, 48, 9, 48, 4, 49, 9, 49, 4, 50, 9, 50, 4, 51, 9, 51, 4, 52, 9, 52, 4, 53, 9, 53, 4, 54, 9, 54, 4, 55, 9, 55, 4, 56, 9, 56, 4, 57, 9, 57, 4, 58, 9, 58, 4, 59, 9, 59, 4, 60, 9, 60, 4, 61, 9, 61, 4, 62, 9, 62, 4, 63, 9, 63, 4, 64, 9, 64, 4, 65, 9, 65, 4, 66, 9, 66, 4, 67, 9, 67, 3, 2, 3, 2, 3, 3, 3, 3, 3, 4, 3, 4, 3, 5, 3, 5, 3, 6, 3, 6, 3, 7, 3, 7, 3, 8, 3, 8, 3, 9, 3, 9, 3, 9, 3, 10, 3, 10, 3, 11, 3, 11, 3, 11, 3, 12, 3, 12, 3, 13, 3, 13, 3, 13, 3, 14, 3, 14, 3, 14, 3, 14, 3, 15, 3, 15, 3, 15, 3, 16, 3, 16, 3, 16, 3, 16, 3, 17, 3, 17, 3, 18, 3, 18, 3, 19, 3, 19, 3, 20, 3, 20, 3, 21, 3, 21, 3, 22, 3, 22, 3, 23, 3, 23, 3, 24, 3, 24, 3, 25, 3, 25, 3, 26, 3, 26, 3, 27, 3, 27, 3, 28, 3, 28, 3, 28, 3, 28, 7, 28, 200, 10, 28, 12, 28, 14, 28, 203, 11, 28, 3, 29, 6, 29, 206, 10, 29, 13, 29, 14, 29, 207, 3, 29, 3, 29, 6, 29, 212, 10, 29, 13, 29, 14, 29, 213, 5, 29, 216, 10, 29, 3, 30, 3, 30, 3, 30, 3, 30, 7, 30, 222, 10, 30, 12, 30, 14, 30, 225, 11, 30, 3, 30, 3, 30, 3, 31, 3, 31, 3, 31, 3, 31, 3, 31, 3, 32, 3, 32, 3, 32, 3, 32, 3, 32, 3, 32, 3, 33, 3, 33, 3, 33, 3, 33, 3, 33, 3, 34, 3, 34, 3, 34, 3, 34, 3, 34, 3, 35, 3, 35, 3, 35, 3, 35, 3, 35, 3, 36, 3, 36, 3, 36, 3, 36, 3, 36, 3, 36, 3, 36, 3, 36, 3, 36, 3, 37, 6, 37, 265, 10, 37, 13, 37, 14, 37, 266, 3, 37, 3, 37, 3, 38, 3, 38, 3, 38, 3, 38, 7, 38, 275, 10, 38, 12, 38, 14, 38, 278, 11, 38, 3, 38, 3, 38, 3, 39, 3, 39, 3, 39, 3, 39, 7, 39, 286, 10, 39, 12, 39, 14, 39, 289, 11, 39, 3, 39, 3, 39, 3, 39, 3, 39, 3, 39, 3, 40, 3, 40, 3, 41, 3, 41, 3, 42, 3, 42, 3, 43, 3, 43, 3, 44, 3, 44, 3, 45, 3, 45, 3, 46, 3, 46, 3, 47, 3, 47, 3, 48, 3, 48, 3, 49, 3, 49, 3, 50, 3, 50, 3, 51, 3, 51, 3, 52, 3, 52, 3, 53, 3, 53, 3, 54, 3, 54, 3, 55, 3, 55, 3, 56, 3, 56, 3, 57, 3, 57, 3, 58, 3, 58, 3, 59, 3, 59, 3, 60, 3, 60, 3, 61, 3, 61, 3, 62, 3, 62, 3, 63, 3, 63, 3, 64, 3, 64, 3, 65, 3, 65, 3, 66, 3, 66, 3, 67, 3, 67, 3, 287, 2, 2, 68, 3, 2, 3, 5, 2, 4, 7, 2, 5, 9, 2, 6, 11, 2, 7, 13, 2, 8, 15, 2, 9, 17, 2, 10, 19, 2, 11, 21, 2, 12, 23, 2, 13, 25, 2, 14, 27, 2, 15, 29, 2, 16, 31, 2, 17, 33, 2, 18, 35, 2, 19, 37, 2, 20, 39, 2, 21, 41, 2, 22, 43, 2, 23, 45, 2, 24, 47, 2, 25, 49, 2, 26, 51, 2, 27, 53, 2, 28, 55, 2, 29, 57, 2, 30, 59, 2, 31, 61, 2, 32, 63, 2, 33, 65, 2, 34, 67, 2, 35, 69, 2, 36, 71, 2, 37, 73, 2, 38, 75, 2, 39, 77, 2, 40, 79, 2, 2, 81, 2, 2, 83, 2, 2, 85, 2, 2, 87, 2, 2, 89, 2, 2, 91, 2, 2, 93, 2, 2, 95, 2, 2, 97, 2, 2, 99, 2, 2, 101, 2, 2, 103, 2, 2, 105, 2, 2, 107, 2, 2, 109, 2, 2, 111, 2, 2, 113, 2, 2, 115, 2, 2, 117, 2, 2, 119, 2, 2, 121, 2, 2, 123, 2, 2, 125, 2, 2, 127, 2, 2, 129, 2, 2, 131, 2, 2, 133, 2, 2, 3, 2, 33, 3, 2, 41, 41, 5, 2, 11, 12, 15, 15, 34, 34, 4, 2, 12, 12, 15, 15, 3, 2, 50, 59, 4, 2, 67, 92, 99, 124, 4, 2, 67, 67, 99, 99, 4, 2, 68, 68, 100, 100, 4, 2, 69, 69, 101, 101, 4, 2, 70, 70, 102, 102, 4, 2, 71, 71, 103, 103, 4, 2, 72, 72, 104, 104, 4, 2, 73, 73, 105, 105, 4, 2, 74, 74, 106, 106, 4, 2, 75, 75, 107, 107, 4, 2, 76, 76, 108, 108, 4, 2, 77, 77, 109, 109, 4, 2, 78, 78, 110, 110, 4, 2, 79, 79, 111, 111, 4, 2, 80, 80, 112, 112, 4, 2, 81, 81, 113, 113, 4, 2, 82, 82, 114, 114, 4, 2, 83, 83, 115, 115, 4, 2, 84, 84, 116, 116, 4, 2, 85, 85, 117, 117, 4, 2, 86, 86, 118, 118, 4, 2, 87, 87, 119, 119, 4, 2, 88, 88, 120, 120, 4, 2, 89, 89, 121, 121, 4, 2, 90, 90, 122, 122, 4, 2, 91, 91, 123, 123, 4, 2, 92, 92, 124, 124, 2, 333, 2, 3, 3, 2, 2, 2, 2, 5, 3, 2, 2, 2, 2, 7, 3, 2, 2, 2, 2, 9, 3, 2, 2, 2, 2, 11, 3, 2, 2, 2, 2, 13, 3, 2, 2, 2, 2, 15, 3, 2, 2, 2, 2, 17, 3, 2, 2, 2, 2, 19, 3, 2, 2, 2, 2, 21, 3, 2, 2, 2, 2, 23, 3, 2, 2, 2, 2, 25, 3, 2, 2, 2, 2, 27, 3, 2, 2, 2, 2, 29, 3, 2, 2, 2, 2, 31, 3, 2, 2, 2, 2, 33, 3, 2, 2, 2, 2, 35, 3, 2, 2, 2, 2, 37, 3, 2, 2, 2, 2, 39, 3, 2, 2, 2, 2, 41, 3, 2, 2, 2, 2, 43, 3, 2, 2, 2, 2, 45, 3, 2, 2, 2, 2, 47, 3, 2, 2, 2, 2, 49, 3, 2, 2, 2, 2, 51, 3, 2, 2, 2, 2, 53, 3, 2, 2, 2, 2, 55, 3, 2, 2, 2, 2, 57, 3, 2, 2, 2, 2, 59, 3, 2, 2, 2, 2, 61, 3, 2, 2, 2, 2, 63, 3, 2, 2, 2, 2, 65, 3, 2, 2, 2, 2, 67, 3, 2, 2, 2, 2, 69, 3, 2, 2, 2, 2, 71, 3, 2, 2, 2, 2, 73, 3, 2, 2, 2, 2, 75, 3, 2, 2, 2, 2, 77, 3, 2, 2, 2, 3, 135, 3, 2, 2, 2, 5, 137, 3, 2, 2, 2, 7, 139, 3, 2, 2, 2, 9, 141, 3, 2, 2, 2, 11, 143, 3, 2, 2, 2, 13, 145, 3, 2, 2, 2, 15, 147, 3, 2, 2, 2, 17, 149, 3, 2, 2, 2, 19, 152, 3, 2, 2, 2, 21, 154, 3, 2, 2, 2, 23, 157, 3, 2, 2, 2, 25, 159, 3, 2, 2, 2, 27, 162, 3, 2, 2, 2, 29, 166, 3, 2, 2, 2, 31, 169, 3, 2, 2, 2, 33, 173, 3, 2, 2, 2, 35, 175, 3, 2, 2, 2, 37, 177, 3, 2, 2, 2, 39, 179, 3, 2, 2, 2, 41, 181, 3, 2, 2, 2, 43, 183, 3, 2, 2, 2, 45, 185, 3, 2, 2, 2, 47, 187, 3, 2, 2, 2, 49, 189, 3, 2, 2, 2, 51, 191, 3, 2, 2, 2, 53, 193, 3, 2, 2, 2, 55, 195, 3, 2, 2, 2, 57, 205, 3, 2, 2, 2, 59, 217, 3, 2, 2, 2, 61, 228, 3, 2, 2, 2, 63, 233, 3, 2, 2, 2, 65, 239, 3, 2, 2, 2, 67, 244, 3, 2, 2, 2, 69, 249, 3, 2, 2, 2, 71, 254, 3, 2, 2, 2, 73, 264, 3, 2, 2, 2, 75, 270, 3, 2, 2, 2, 77, 281, 3, 2, 2, 2, 79, 295, 3, 2, 2, 2, 81, 297, 3, 2, 2, 2, 83, 299, 3, 2, 2, 2, 85, 301, 3, 2, 2, 2, 87, 303, 3, 2, 2, 2, 89, 305, 3, 2, 2, 2, 91, 307, 3, 2, 2, 2, 93, 309, 3, 2, 2, 2, 95, 311, 3, 2, 2, 2, 97, 313, 3, 2, 2, 2, 99, 315, 3, 2, 2, 2, 101, 317, 3, 2, 2, 2, 103, 319, 3, 2, 2, 2, 105, 321, 3, 2, 2, 2, 107, 323, 3, 2, 2, 2, 109, 325, 3, 2, 2, 2, 111, 327, 3, 2, 2, 2, 113, 329, 3, 2, 2, 2, 115, 331, 3, 2, 2, 2, 117, 333, 3, 2, 2, 2, 119, 335, 3, 2, 2, 2, 121, 337, 3, 2, 2, 2, 123, 339, 3, 2, 2, 2, 125, 341, 3, 2, 2, 2, 127, 343, 3, 2, 2, 2, 129, 345, 3, 2, 2, 2, 131, 347, 3, 2, 2, 2, 133, 349, 3, 2, 2, 2, 135, 136, 7, 45, 2, 2, 136, 4, 3, 2, 2, 2, 137, 138, 7, 47, 2, 2, 138, 6, 3, 2, 2, 2, 139, 140, 7, 44, 2, 2, 140, 8, 3, 2, 2, 2, 141, 142, 7, 49, 2, 2, 142, 10, 3, 2, 2, 2, 143, 144, 7, 39, 2, 2, 144, 12, 3, 2, 2, 2, 145, 146, 7, 96, 2, 2, 146, 14, 3, 2, 2, 2, 147, 148, 7, 63, 2, 2, 148, 16, 3, 2, 2, 2, 149, 150, 7, 35, 2, 2, 150, 151, 7, 63, 2, 2, 151, 18, 3, 2, 2, 2, 152, 153, 7, 62, 2, 2, 153, 20, 3, 2, 2, 2, 154, 155, 7, 62, 2, 2, 155, 156, 7, 63, 2, 2, 156, 22, 3, 2, 2, 2, 157, 158, 7, 64, 2, 2, 158, 24, 3, 2, 2, 2, 159, 160, 7, 64, 2, 2, 160, 161, 7, 63, 2, 2, 161, 26, 3, 2, 2, 2, 162, 163, 5, 83, 42, 2, 163, 164, 5, 109, 55, 2, 164, 165, 5, 89, 45, 2, 165, 28, 3, 2, 2, 2, 166, 167, 5, 111, 56, 2, 167, 168, 5, 117, 59, 2, 168, 30, 3, 2, 2, 2, 169, 170, 5, 109, 55, 2, 170, 171, 5, 111, 56, 2, 171, 172, 5, 121, 61, 2, 172, 32, 3, 2, 2, 2, 173, 174, 7, 42, 2, 2, 174, 34, 3, 2, 2, 2, 175, 176, 7, 43, 2, 2, 176, 36, 3, 2, 2, 2, 177, 178, 7, 125, 2, 2, 178, 38, 3, 2, 2, 2, 179, 180, 7, 127, 2, 2, 180, 40, 3, 2, 2, 2, 181, 182, 7, 93, 2, 2, 182, 42, 3, 2, 2, 2, 183, 184, 7, 95, 2, 2, 184, 44, 3, 2, 2, 2, 185, 186, 7, 46, 2, 2, 186, 46, 3, 2, 2, 2, 187, 188, 7, 61, 2, 2, 188, 48, 3, 2, 2, 2, 189, 190, 7, 60, 2, 2, 190, 50, 3, 2, 2, 2, 191, 192, 7, 48, 2, 2, 192, 52, 3, 2, 2, 2, 193, 194, 7, 97, 2, 2, 194, 54, 3, 2, 2, 2, 195, 201, 5, 81, 41, 2, 196, 200, 5, 81, 41, 2, 197, 200, 5, 79, 40, 2, 198, 200, 5, 53, 27, 2, 199, 196, 3, 2, 2, 2, 199, 197, 3, 2, 2, 2, 199, 198, 3, 2, 2, 2, 200, 203, 3, 2, 2, 2, 201, 199, 3, 2, 2, 2, 201, 202, 3, 2, 2, 2, 202, 56, 3, 2, 2, 2, 203, 201, 3, 2, 2, 2, 204, 206, 5, 79, 40, 2, 205, 204, 3, 2, 2, 2, 206, 207, 3, 2, 2, 2, 207, 205, 3, 2, 2, 2, 207, 208, 3, 2, 2, 2, 208, 215, 3, 2, 2, 2, 209, 211, 7, 48, 2, 2, 210, 212, 5, 79, 40, 2, 211, 210, 3, 2, 2, 2, 212, 213, 3, 2, 2, 2, 213, 211, 3, 2, 2, 2, 213, 214, 3, 2, 2, 2, 214, 216, 3, 2, 2, 2, 215, 209, 3, 2, 2, 2, 215, 216, 3, 2, 2, 2, 216, 58, 3, 2, 2, 2, 217, 223, 7, 41, 2, 2, 218, 222, 10, 2, 2, 2, 219, 220, 7, 41, 2, 2, 220, 222, 7, 41, 2, 2, 221, 218, 3, 2, 2, 2, 221, 219, 3, 2, 2, 2, 222, 225, 3, 2, 2, 2, 223, 221, 3, 2, 2, 2, 223, 224, 3, 2, 2, 2, 224, 226, 3, 2, 2, 2, 225, 223, 3, 2, 2, 2, 226, 227, 7, 41, 2, 2, 227, 60, 3, 2, 2, 2, 228, 229, 5, 121, 61, 2, 229, 230, 5, 117, 59, 2, 230, 231, 5, 123, 62, 2, 231, 232, 5, 91, 46, 2, 232, 62, 3, 2, 2, 2, 233, 234, 5, 93, 47, 2, 234, 235, 5, 83, 42, 2, 235, 236, 5, 105, 53, 2, 236, 237, 5, 119, 60, 2, 237, 238, 5, 91, 46, 2, 238, 64, 3, 2, 2, 2, 239, 240, 5, 109, 55, 2, 240, 241, 5, 123, 62, 2, 241, 242, 5, 105, 53, 2, 242, 243, 5, 105, 53, 2, 243, 66, 3, 2, 2, 2, 244, 245, 5, 89, 45, 2, 245, 246, 5, 83, 42, 2, 246, 247, 5, 121, 61, 2, 247, 248, 5, 91, 46, 2, 248, 68, 3, 2, 2, 2, 249, 250, 5, 121, 61, 2, 250, 251, 5, 99, 50, 2, 251, 252, 5, 107, 54, 2, 252, 253, 5, 91, 46, 2, 253, 70, 3, 2, 2, 2, 254, 255, 5, 89, 45, 2, 255, 256, 5, 83, 42, 2, 256, 257, 5, 121, 61, 2, 257, 258, 5, 91, 46, 2, 258, 259, 5, 121, 61, 2, 259, 260, 5, 99, 50, 2, 260, 261, 5, 107, 54, 2, 261, 262, 5, 91, 46, 2, 262, 72, 3, 2, 2, 2, 263, 265, 9, 3, 2, 2, 264, 263, 3, 2, 2, 2, 265, 266, 3, 2, 2, 2, 266, 264, 3, 2, 2, 2, 266, 267, 3, 2, 2, 2, 267, 268, 3, 2, 2, 2, 268, 269, 8, 37, 2, 2, 269, 74, 3, 2, 2, 2, 270, 271, 7, 49, 2, 2, 271, 272, 7, 49, 2, 2, 272, 276, 3, 2, 2, 2, 273, 275, 10, 4, 2, 2, 274, 273, 3, 2, 2, 2, 275, 278, 3, 2, 2, 2, 276, 274, 3, 2, 2, 2, 276, 277, 3, 2, 2, 2, 277, 279, 3, 2, 2, 2, 278, 276, 3, 2, 2, 2, 279, 280, 8, 38, 2, 2, 280, 76, 3, 2, 2, 2, 281, 282, 7, 49, 2, 2, 282, 283, 7, 44, 2, 2, 283, 287, 3, 2, 2, 2, 284, 286, 11, 2, 2, 2, 285, 284, 3, 2, 2, 2, 286, 289, 3, 2, 2, 2, 287, 288, 3, 2, 2, 2, 287, 285, 3, 2, 2, 2, 288, 290, 3, 2, 2, 2, 289, 287, 3, 2, 2, 2, 290, 291, 7, 44, 2, 2, 291, 292, 7, 49, 2, 2, 292, 293, 3, 2, 2, 2, 293, 294, 8, 39, 2, 2, 294, 78, 3, 2, 2, 2, 295, 296, 9, 5, 2, 2, 296, 80, 3, 2, 2, 2, 297, 298, 9, 6, 2, 2, 298, 82, 3, 2, 2, 2, 299, 300, 9, 7, 2, 2, 300, 84, 3, 2, 2, 2, 301, 302, 9, 8, 2, 2, 302, 86, 3, 2, 2, 2, 303, 304, 9, 9, 2, 2, 304, 88, 3, 2, 2, 2, 305, 306, 9, 10, 2, 2, 306, 90, 3, 2, 2, 2, 307, 308, 9, 11, 2, 2, 308, 92, 3, 2, 2, 2, 309, 310, 9, 12, 2, 2, 310, 94, 3, 2, 2, 2, 311, 312, 9, 13, 2, 2, 312, 96, 3, 2, 2, 2, 313, 314, 9, 14, 2, 2, 314, 98, 3, 2, 2, 2, 315, 316, 9, 15, 2, 2, 316, 100, 3, 2, 2, 2, 317, 318, 9, 16, 2, 2, 318, 102, 3, 2, 2, 2, 319, 320, 9, 17, 2, 2, 320, 104, 3, 2, 2, 2, 321, 322, 9, 18, 2, 2, 322, 106, 3, 2, 2, 2, 323, 324, 9, 19, 2, 2, 324, 108, 3, 2, 2, 2, 325, 326, 9, 20, 2, 2, 326, 110, 3, 2, 2, 2, 327, 328, 9, 21, 2, 2, 328, 112, 3, 2, 2, 2, 329, 330, 9, 22, 2, 2, 330, 114, 3, 2, 2, 2, 331, 332, 9, 23, 2, 2, 332, 116, 3, 2, 2, 2, 333, 334, 9, 24, 2, 2, 334, 118, 3, 2, 2, 2, 335, 336, 9, 25, 2, 2, 336, 120, 3, 2, 2, 2, 337, 338, 9, 26, 2, 2, 338, 122, 3, 2, 2, 2, 339, 340, 9, 27, 2, 2, 340, 124, 3, 2, 2, 2, 341, 342, 9, 28, 2, 2, 342, 126, 3, 2, 2, 2, 343, 344, 9, 29, 2, 2, 344, 128, 3, 2, 2, 2, 345, 346, 9, 30, 2, 2, 346, 130, 3, 2, 2, 2, 347, 348, 9, 31, 2, 2, 348, 132, 3, 2, 2, 2, 349, 350, 9, 32, 2, 2, 350, 134, 3, 2, 2, 2, 13, 2, 199, 201, 207, 213, 215, 221, 223, 266, 276, 287, 3, 8, 2, 2]
\ No newline at end of file
diff --git a/packages/formula/src/grammar/FormulaLexer.tokens b/packages/formula/src/grammar/FormulaLexer.tokens
new file mode 100644
index 000000000..5ad732765
--- /dev/null
+++ b/packages/formula/src/grammar/FormulaLexer.tokens
@@ -0,0 +1,61 @@
+ADD=1
+SUBTRACT=2
+MULTIPLY=3
+DIVIDE=4
+MODULO=5
+POWER=6
+EQUAL=7
+NOT_EQUAL=8
+LESS=9
+LESS_EQUAL=10
+GREATER=11
+GREATER_EQUAL=12
+AND=13
+OR=14
+NOT=15
+LPAREN=16
+RPAREN=17
+LBRACE=18
+RBRACE=19
+LBRACKET=20
+RBRACKET=21
+COMMA=22
+SEMICOLON=23
+COLON=24
+DOT=25
+UNDERSCORE=26
+IDENTIFIER=27
+NUMBER=28
+STRING=29
+TRUE=30
+FALSE=31
+NULL=32
+DATE=33
+TIME=34
+DATETIME=35
+WS=36
+COMMENT=37
+MULTILINE_COMMENT=38
+'+'=1
+'-'=2
+'*'=3
+'/'=4
+'%'=5
+'^'=6
+'='=7
+'!='=8
+'<'=9
+'<='=10
+'>'=11
+'>='=12
+'('=16
+')'=17
+'{'=18
+'}'=19
+'['=20
+']'=21
+','=22
+';'=23
+':'=24
+'.'=25
+'_'=26
diff --git a/packages/formula/src/grammar/FormulaLexer.ts b/packages/formula/src/grammar/FormulaLexer.ts
new file mode 100644
index 000000000..21703eae9
--- /dev/null
+++ b/packages/formula/src/grammar/FormulaLexer.ts
@@ -0,0 +1,286 @@
+// Generated from src/grammar/FormulaLexer.g4 by ANTLR 4.9.0-SNAPSHOT
+
+
+import { ATN } from "antlr4ts/atn/ATN";
+import { ATNDeserializer } from "antlr4ts/atn/ATNDeserializer";
+import { CharStream } from "antlr4ts/CharStream";
+import { Lexer } from "antlr4ts/Lexer";
+import { LexerATNSimulator } from "antlr4ts/atn/LexerATNSimulator";
+import { NotNull } from "antlr4ts/Decorators";
+import { Override } from "antlr4ts/Decorators";
+import { RuleContext } from "antlr4ts/RuleContext";
+import { Vocabulary } from "antlr4ts/Vocabulary";
+import { VocabularyImpl } from "antlr4ts/VocabularyImpl";
+
+import * as Utils from "antlr4ts/misc/Utils";
+
+
+export class FormulaLexer extends Lexer {
+ public static readonly ADD = 1;
+ public static readonly SUBTRACT = 2;
+ public static readonly MULTIPLY = 3;
+ public static readonly DIVIDE = 4;
+ public static readonly MODULO = 5;
+ public static readonly POWER = 6;
+ public static readonly EQUAL = 7;
+ public static readonly NOT_EQUAL = 8;
+ public static readonly LESS = 9;
+ public static readonly LESS_EQUAL = 10;
+ public static readonly GREATER = 11;
+ public static readonly GREATER_EQUAL = 12;
+ public static readonly AND = 13;
+ public static readonly OR = 14;
+ public static readonly NOT = 15;
+ public static readonly LPAREN = 16;
+ public static readonly RPAREN = 17;
+ public static readonly LBRACE = 18;
+ public static readonly RBRACE = 19;
+ public static readonly LBRACKET = 20;
+ public static readonly RBRACKET = 21;
+ public static readonly COMMA = 22;
+ public static readonly SEMICOLON = 23;
+ public static readonly COLON = 24;
+ public static readonly DOT = 25;
+ public static readonly UNDERSCORE = 26;
+ public static readonly IDENTIFIER = 27;
+ public static readonly NUMBER = 28;
+ public static readonly STRING = 29;
+ public static readonly TRUE = 30;
+ public static readonly FALSE = 31;
+ public static readonly NULL = 32;
+ public static readonly DATE = 33;
+ public static readonly TIME = 34;
+ public static readonly DATETIME = 35;
+ public static readonly WS = 36;
+ public static readonly COMMENT = 37;
+ public static readonly MULTILINE_COMMENT = 38;
+
+ // tslint:disable:no-trailing-whitespace
+ public static readonly channelNames: string[] = [
+ "DEFAULT_TOKEN_CHANNEL", "HIDDEN",
+ ];
+
+ // tslint:disable:no-trailing-whitespace
+ public static readonly modeNames: string[] = [
+ "DEFAULT_MODE",
+ ];
+
+ public static readonly ruleNames: string[] = [
+ "ADD", "SUBTRACT", "MULTIPLY", "DIVIDE", "MODULO", "POWER", "EQUAL", "NOT_EQUAL",
+ "LESS", "LESS_EQUAL", "GREATER", "GREATER_EQUAL", "AND", "OR", "NOT",
+ "LPAREN", "RPAREN", "LBRACE", "RBRACE", "LBRACKET", "RBRACKET", "COMMA",
+ "SEMICOLON", "COLON", "DOT", "UNDERSCORE", "IDENTIFIER", "NUMBER", "STRING",
+ "TRUE", "FALSE", "NULL", "DATE", "TIME", "DATETIME", "WS", "COMMENT",
+ "MULTILINE_COMMENT", "DIGIT", "LETTER", "A", "B", "C", "D", "E", "F",
+ "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",
+ "U", "V", "W", "X", "Y", "Z",
+ ];
+
+ private static readonly _LITERAL_NAMES: Array = [
+ undefined, "'+'", "'-'", "'*'", "'/'", "'%'", "'^'", "'='", "'!='", "'<'",
+ "'<='", "'>'", "'>='", undefined, undefined, undefined, "'('", "')'",
+ "'{'", "'}'", "'['", "']'", "','", "';'", "':'", "'.'", "'_'",
+ ];
+ private static readonly _SYMBOLIC_NAMES: Array = [
+ undefined, "ADD", "SUBTRACT", "MULTIPLY", "DIVIDE", "MODULO", "POWER",
+ "EQUAL", "NOT_EQUAL", "LESS", "LESS_EQUAL", "GREATER", "GREATER_EQUAL",
+ "AND", "OR", "NOT", "LPAREN", "RPAREN", "LBRACE", "RBRACE", "LBRACKET",
+ "RBRACKET", "COMMA", "SEMICOLON", "COLON", "DOT", "UNDERSCORE", "IDENTIFIER",
+ "NUMBER", "STRING", "TRUE", "FALSE", "NULL", "DATE", "TIME", "DATETIME",
+ "WS", "COMMENT", "MULTILINE_COMMENT",
+ ];
+ public static readonly VOCABULARY: Vocabulary = new VocabularyImpl(FormulaLexer._LITERAL_NAMES, FormulaLexer._SYMBOLIC_NAMES, []);
+
+ // @Override
+ // @NotNull
+ public get vocabulary(): Vocabulary {
+ return FormulaLexer.VOCABULARY;
+ }
+ // tslint:enable:no-trailing-whitespace
+
+
+ constructor(input: CharStream) {
+ super(input);
+ this._interp = new LexerATNSimulator(FormulaLexer._ATN, this);
+ }
+
+ // @Override
+ public get grammarFileName(): string { return "FormulaLexer.g4"; }
+
+ // @Override
+ public get ruleNames(): string[] { return FormulaLexer.ruleNames; }
+
+ // @Override
+ public get serializedATN(): string { return FormulaLexer._serializedATN; }
+
+ // @Override
+ public get channelNames(): string[] { return FormulaLexer.channelNames; }
+
+ // @Override
+ public get modeNames(): string[] { return FormulaLexer.modeNames; }
+
+ public static readonly _serializedATN: string =
+ "\x03\uC91D\uCABA\u058D\uAFBA\u4F53\u0607\uEA8B\uC241\x02(\u015F\b\x01" +
+ "\x04\x02\t\x02\x04\x03\t\x03\x04\x04\t\x04\x04\x05\t\x05\x04\x06\t\x06" +
+ "\x04\x07\t\x07\x04\b\t\b\x04\t\t\t\x04\n\t\n\x04\v\t\v\x04\f\t\f\x04\r" +
+ "\t\r\x04\x0E\t\x0E\x04\x0F\t\x0F\x04\x10\t\x10\x04\x11\t\x11\x04\x12\t" +
+ "\x12\x04\x13\t\x13\x04\x14\t\x14\x04\x15\t\x15\x04\x16\t\x16\x04\x17\t" +
+ "\x17\x04\x18\t\x18\x04\x19\t\x19\x04\x1A\t\x1A\x04\x1B\t\x1B\x04\x1C\t" +
+ "\x1C\x04\x1D\t\x1D\x04\x1E\t\x1E\x04\x1F\t\x1F\x04 \t \x04!\t!\x04\"\t" +
+ "\"\x04#\t#\x04$\t$\x04%\t%\x04&\t&\x04\'\t\'\x04(\t(\x04)\t)\x04*\t*\x04" +
+ "+\t+\x04,\t,\x04-\t-\x04.\t.\x04/\t/\x040\t0\x041\t1\x042\t2\x043\t3\x04" +
+ "4\t4\x045\t5\x046\t6\x047\t7\x048\t8\x049\t9\x04:\t:\x04;\t;\x04<\t<\x04" +
+ "=\t=\x04>\t>\x04?\t?\x04@\t@\x04A\tA\x04B\tB\x04C\tC\x03\x02\x03\x02\x03" +
+ "\x03\x03\x03\x03\x04\x03\x04\x03\x05\x03\x05\x03\x06\x03\x06\x03\x07\x03" +
+ "\x07\x03\b\x03\b\x03\t\x03\t\x03\t\x03\n\x03\n\x03\v\x03\v\x03\v\x03\f" +
+ "\x03\f\x03\r\x03\r\x03\r\x03\x0E\x03\x0E\x03\x0E\x03\x0E\x03\x0F\x03\x0F" +
+ "\x03\x0F\x03\x10\x03\x10\x03\x10\x03\x10\x03\x11\x03\x11\x03\x12\x03\x12" +
+ "\x03\x13\x03\x13\x03\x14\x03\x14\x03\x15\x03\x15\x03\x16\x03\x16\x03\x17" +
+ "\x03\x17\x03\x18\x03\x18\x03\x19\x03\x19\x03\x1A\x03\x1A\x03\x1B\x03\x1B" +
+ "\x03\x1C\x03\x1C\x03\x1C\x03\x1C\x07\x1C\xC8\n\x1C\f\x1C\x0E\x1C\xCB\v" +
+ "\x1C\x03\x1D\x06\x1D\xCE\n\x1D\r\x1D\x0E\x1D\xCF\x03\x1D\x03\x1D\x06\x1D" +
+ "\xD4\n\x1D\r\x1D\x0E\x1D\xD5\x05\x1D\xD8\n\x1D\x03\x1E\x03\x1E\x03\x1E" +
+ "\x03\x1E\x07\x1E\xDE\n\x1E\f\x1E\x0E\x1E\xE1\v\x1E\x03\x1E\x03\x1E\x03" +
+ "\x1F\x03\x1F\x03\x1F\x03\x1F\x03\x1F\x03 \x03 \x03 \x03 \x03 \x03 \x03" +
+ "!\x03!\x03!\x03!\x03!\x03\"\x03\"\x03\"\x03\"\x03\"\x03#\x03#\x03#\x03" +
+ "#\x03#\x03$\x03$\x03$\x03$\x03$\x03$\x03$\x03$\x03$\x03%\x06%\u0109\n" +
+ "%\r%\x0E%\u010A\x03%\x03%\x03&\x03&\x03&\x03&\x07&\u0113\n&\f&\x0E&\u0116" +
+ "\v&\x03&\x03&\x03\'\x03\'\x03\'\x03\'\x07\'\u011E\n\'\f\'\x0E\'\u0121" +
+ "\v\'\x03\'\x03\'\x03\'\x03\'\x03\'\x03(\x03(\x03)\x03)\x03*\x03*\x03+" +
+ "\x03+\x03,\x03,\x03-\x03-\x03.\x03.\x03/\x03/\x030\x030\x031\x031\x03" +
+ "2\x032\x033\x033\x034\x034\x035\x035\x036\x036\x037\x037\x038\x038\x03" +
+ "9\x039\x03:\x03:\x03;\x03;\x03<\x03<\x03=\x03=\x03>\x03>\x03?\x03?\x03" +
+ "@\x03@\x03A\x03A\x03B\x03B\x03C\x03C\x03\u011F\x02\x02D\x03\x02\x03\x05" +
+ "\x02\x04\x07\x02\x05\t\x02\x06\v\x02\x07\r\x02\b\x0F\x02\t\x11\x02\n\x13" +
+ "\x02\v\x15\x02\f\x17\x02\r\x19\x02\x0E\x1B\x02\x0F\x1D\x02\x10\x1F\x02" +
+ "\x11!\x02\x12#\x02\x13%\x02\x14\'\x02\x15)\x02\x16+\x02\x17-\x02\x18/" +
+ "\x02\x191\x02\x1A3\x02\x1B5\x02\x1C7\x02\x1D9\x02\x1E;\x02\x1F=\x02 ?" +
+ "\x02!A\x02\"C\x02#E\x02$G\x02%I\x02&K\x02\'M\x02(O\x02\x02Q\x02\x02S\x02" +
+ "\x02U\x02\x02W\x02\x02Y\x02\x02[\x02\x02]\x02\x02_\x02\x02a\x02\x02c\x02" +
+ "\x02e\x02\x02g\x02\x02i\x02\x02k\x02\x02m\x02\x02o\x02\x02q\x02\x02s\x02" +
+ "\x02u\x02\x02w\x02\x02y\x02\x02{\x02\x02}\x02\x02\x7F\x02\x02\x81\x02" +
+ "\x02\x83\x02\x02\x85\x02\x02\x03\x02!\x03\x02))\x05\x02\v\f\x0F\x0F\"" +
+ "\"\x04\x02\f\f\x0F\x0F\x03\x022;\x04\x02C\\c|\x04\x02CCcc\x04\x02DDdd" +
+ "\x04\x02EEee\x04\x02FFff\x04\x02GGgg\x04\x02HHhh\x04\x02IIii\x04\x02J" +
+ "Jjj\x04\x02KKkk\x04\x02LLll\x04\x02MMmm\x04\x02NNnn\x04\x02OOoo\x04\x02" +
+ "PPpp\x04\x02QQqq\x04\x02RRrr\x04\x02SSss\x04\x02TTtt\x04\x02UUuu\x04\x02" +
+ "VVvv\x04\x02WWww\x04\x02XXxx\x04\x02YYyy\x04\x02ZZzz\x04\x02[[{{\x04\x02" +
+ "\\\\||\x02\u014D\x02\x03\x03\x02\x02\x02\x02\x05\x03\x02\x02\x02\x02\x07" +
+ "\x03\x02\x02\x02\x02\t\x03\x02\x02\x02\x02\v\x03\x02\x02\x02\x02\r\x03" +
+ "\x02\x02\x02\x02\x0F\x03\x02\x02\x02\x02\x11\x03\x02\x02\x02\x02\x13\x03" +
+ "\x02\x02\x02\x02\x15\x03\x02\x02\x02\x02\x17\x03\x02\x02\x02\x02\x19\x03" +
+ "\x02\x02\x02\x02\x1B\x03\x02\x02\x02\x02\x1D\x03\x02\x02\x02\x02\x1F\x03" +
+ "\x02\x02\x02\x02!\x03\x02\x02\x02\x02#\x03\x02\x02\x02\x02%\x03\x02\x02" +
+ "\x02\x02\'\x03\x02\x02\x02\x02)\x03\x02\x02\x02\x02+\x03\x02\x02\x02\x02" +
+ "-\x03\x02\x02\x02\x02/\x03\x02\x02\x02\x021\x03\x02\x02\x02\x023\x03\x02" +
+ "\x02\x02\x025\x03\x02\x02\x02\x027\x03\x02\x02\x02\x029\x03\x02\x02\x02" +
+ "\x02;\x03\x02\x02\x02\x02=\x03\x02\x02\x02\x02?\x03\x02\x02\x02\x02A\x03" +
+ "\x02\x02\x02\x02C\x03\x02\x02\x02\x02E\x03\x02\x02\x02\x02G\x03\x02\x02" +
+ "\x02\x02I\x03\x02\x02\x02\x02K\x03\x02\x02\x02\x02M\x03\x02\x02\x02\x03" +
+ "\x87\x03\x02\x02\x02\x05\x89\x03\x02\x02\x02\x07\x8B\x03\x02\x02\x02\t" +
+ "\x8D\x03\x02\x02\x02\v\x8F\x03\x02\x02\x02\r\x91\x03\x02\x02\x02\x0F\x93" +
+ "\x03\x02\x02\x02\x11\x95\x03\x02\x02\x02\x13\x98\x03\x02\x02\x02\x15\x9A" +
+ "\x03\x02\x02\x02\x17\x9D\x03\x02\x02\x02\x19\x9F\x03\x02\x02\x02\x1B\xA2" +
+ "\x03\x02\x02\x02\x1D\xA6\x03\x02\x02\x02\x1F\xA9\x03\x02\x02\x02!\xAD" +
+ "\x03\x02\x02\x02#\xAF\x03\x02\x02\x02%\xB1\x03\x02\x02\x02\'\xB3\x03\x02" +
+ "\x02\x02)\xB5\x03\x02\x02\x02+\xB7\x03\x02\x02\x02-\xB9\x03\x02\x02\x02" +
+ "/\xBB\x03\x02\x02\x021\xBD\x03\x02\x02\x023\xBF\x03\x02\x02\x025\xC1\x03" +
+ "\x02\x02\x027\xC3\x03\x02\x02\x029\xCD\x03\x02\x02\x02;\xD9\x03\x02\x02" +
+ "\x02=\xE4\x03\x02\x02\x02?\xE9\x03\x02\x02\x02A\xEF\x03\x02\x02\x02C\xF4" +
+ "\x03\x02\x02\x02E\xF9\x03\x02\x02\x02G\xFE\x03\x02\x02\x02I\u0108\x03" +
+ "\x02\x02\x02K\u010E\x03\x02\x02\x02M\u0119\x03\x02\x02\x02O\u0127\x03" +
+ "\x02\x02\x02Q\u0129\x03\x02\x02\x02S\u012B\x03\x02\x02\x02U\u012D\x03" +
+ "\x02\x02\x02W\u012F\x03\x02\x02\x02Y\u0131\x03\x02\x02\x02[\u0133\x03" +
+ "\x02\x02\x02]\u0135\x03\x02\x02\x02_\u0137\x03\x02\x02\x02a\u0139\x03" +
+ "\x02\x02\x02c\u013B\x03\x02\x02\x02e\u013D\x03\x02\x02\x02g\u013F\x03" +
+ "\x02\x02\x02i\u0141\x03\x02\x02\x02k\u0143\x03\x02\x02\x02m\u0145\x03" +
+ "\x02\x02\x02o\u0147\x03\x02\x02\x02q\u0149\x03\x02\x02\x02s\u014B\x03" +
+ "\x02\x02\x02u\u014D\x03\x02\x02\x02w\u014F\x03\x02\x02\x02y\u0151\x03" +
+ "\x02\x02\x02{\u0153\x03\x02\x02\x02}\u0155\x03\x02\x02\x02\x7F\u0157\x03" +
+ "\x02\x02\x02\x81\u0159\x03\x02\x02\x02\x83\u015B\x03\x02\x02\x02\x85\u015D" +
+ "\x03\x02\x02\x02\x87\x88\x07-\x02\x02\x88\x04\x03\x02\x02\x02\x89\x8A" +
+ "\x07/\x02\x02\x8A\x06\x03\x02\x02\x02\x8B\x8C\x07,\x02\x02\x8C\b\x03\x02" +
+ "\x02\x02\x8D\x8E\x071\x02\x02\x8E\n\x03\x02\x02\x02\x8F\x90\x07\'\x02" +
+ "\x02\x90\f\x03\x02\x02\x02\x91\x92\x07`\x02\x02\x92\x0E\x03\x02\x02\x02" +
+ "\x93\x94\x07?\x02\x02\x94\x10\x03\x02\x02\x02\x95\x96\x07#\x02\x02\x96" +
+ "\x97\x07?\x02\x02\x97\x12\x03\x02\x02\x02\x98\x99\x07>\x02\x02\x99\x14" +
+ "\x03\x02\x02\x02\x9A\x9B\x07>\x02\x02\x9B\x9C\x07?\x02\x02\x9C\x16\x03" +
+ "\x02\x02\x02\x9D\x9E\x07@\x02\x02\x9E\x18\x03\x02\x02\x02\x9F\xA0\x07" +
+ "@\x02\x02\xA0\xA1\x07?\x02\x02\xA1\x1A\x03\x02\x02\x02\xA2\xA3\x05S*\x02" +
+ "\xA3\xA4\x05m7\x02\xA4\xA5\x05Y-\x02\xA5\x1C\x03\x02\x02\x02\xA6\xA7\x05" +
+ "o8\x02\xA7\xA8\x05u;\x02\xA8\x1E\x03\x02\x02\x02\xA9\xAA\x05m7\x02\xAA" +
+ "\xAB\x05o8\x02\xAB\xAC\x05y=\x02\xAC \x03\x02\x02\x02\xAD\xAE\x07*\x02" +
+ "\x02\xAE\"\x03\x02\x02\x02\xAF\xB0\x07+\x02\x02\xB0$\x03\x02\x02\x02\xB1" +
+ "\xB2\x07}\x02\x02\xB2&\x03\x02\x02\x02\xB3\xB4\x07\x7F\x02\x02\xB4(\x03" +
+ "\x02\x02\x02\xB5\xB6\x07]\x02\x02\xB6*\x03\x02\x02\x02\xB7\xB8\x07_\x02" +
+ "\x02\xB8,\x03\x02\x02\x02\xB9\xBA\x07.\x02\x02\xBA.\x03\x02\x02\x02\xBB" +
+ "\xBC\x07=\x02\x02\xBC0\x03\x02\x02\x02\xBD\xBE\x07<\x02\x02\xBE2\x03\x02" +
+ "\x02\x02\xBF\xC0\x070\x02\x02\xC04\x03\x02\x02\x02\xC1\xC2\x07a\x02\x02" +
+ "\xC26\x03\x02\x02\x02\xC3\xC9\x05Q)\x02\xC4\xC8\x05Q)\x02\xC5\xC8\x05" +
+ "O(\x02\xC6\xC8\x055\x1B\x02\xC7\xC4\x03\x02\x02\x02\xC7\xC5\x03\x02\x02" +
+ "\x02\xC7\xC6\x03\x02\x02\x02\xC8\xCB\x03\x02\x02\x02\xC9\xC7\x03\x02\x02" +
+ "\x02\xC9\xCA\x03\x02\x02\x02\xCA8\x03\x02\x02\x02\xCB\xC9\x03\x02\x02" +
+ "\x02\xCC\xCE\x05O(\x02\xCD\xCC\x03\x02\x02\x02\xCE\xCF\x03\x02\x02\x02" +
+ "\xCF\xCD\x03\x02\x02\x02\xCF\xD0\x03\x02\x02\x02\xD0\xD7\x03\x02\x02\x02" +
+ "\xD1\xD3\x070\x02\x02\xD2\xD4\x05O(\x02\xD3\xD2\x03\x02\x02\x02\xD4\xD5" +
+ "\x03\x02\x02\x02\xD5\xD3\x03\x02\x02\x02\xD5\xD6\x03\x02\x02\x02\xD6\xD8" +
+ "\x03\x02\x02\x02\xD7\xD1\x03\x02\x02\x02\xD7\xD8\x03\x02\x02\x02\xD8:" +
+ "\x03\x02\x02\x02\xD9\xDF\x07)\x02\x02\xDA\xDE\n\x02\x02\x02\xDB\xDC\x07" +
+ ")\x02\x02\xDC\xDE\x07)\x02\x02\xDD\xDA\x03\x02\x02\x02\xDD\xDB\x03\x02" +
+ "\x02\x02\xDE\xE1\x03\x02\x02\x02\xDF\xDD\x03\x02\x02\x02\xDF\xE0\x03\x02" +
+ "\x02\x02\xE0\xE2\x03\x02\x02\x02\xE1\xDF\x03\x02\x02\x02\xE2\xE3\x07)" +
+ "\x02\x02\xE3<\x03\x02\x02\x02\xE4\xE5\x05y=\x02\xE5\xE6\x05u;\x02\xE6" +
+ "\xE7\x05{>\x02\xE7\xE8\x05[.\x02\xE8>\x03\x02\x02\x02\xE9\xEA\x05]/\x02" +
+ "\xEA\xEB\x05S*\x02\xEB\xEC\x05i5\x02\xEC\xED\x05w<\x02\xED\xEE\x05[.\x02" +
+ "\xEE@\x03\x02\x02\x02\xEF\xF0\x05m7\x02\xF0\xF1\x05{>\x02\xF1\xF2\x05" +
+ "i5\x02\xF2\xF3\x05i5\x02\xF3B\x03\x02\x02\x02\xF4\xF5\x05Y-\x02\xF5\xF6" +
+ "\x05S*\x02\xF6\xF7\x05y=\x02\xF7\xF8\x05[.\x02\xF8D\x03\x02\x02\x02\xF9" +
+ "\xFA\x05y=\x02\xFA\xFB\x05c2\x02\xFB\xFC\x05k6\x02\xFC\xFD\x05[.\x02\xFD" +
+ "F\x03\x02\x02\x02\xFE\xFF\x05Y-\x02\xFF\u0100\x05S*\x02\u0100\u0101\x05" +
+ "y=\x02\u0101\u0102\x05[.\x02\u0102\u0103\x05y=\x02\u0103\u0104\x05c2\x02" +
+ "\u0104\u0105\x05k6\x02\u0105\u0106\x05[.\x02\u0106H\x03\x02\x02\x02\u0107" +
+ "\u0109\t\x03\x02\x02\u0108\u0107\x03\x02\x02\x02\u0109\u010A\x03\x02\x02" +
+ "\x02\u010A\u0108\x03\x02\x02\x02\u010A\u010B\x03\x02\x02\x02\u010B\u010C" +
+ "\x03\x02\x02\x02\u010C\u010D\b%\x02\x02\u010DJ\x03\x02\x02\x02\u010E\u010F" +
+ "\x071\x02\x02\u010F\u0110\x071\x02\x02\u0110\u0114\x03\x02\x02\x02\u0111" +
+ "\u0113\n\x04\x02\x02\u0112\u0111\x03\x02\x02\x02\u0113\u0116\x03\x02\x02" +
+ "\x02\u0114\u0112\x03\x02\x02\x02\u0114\u0115\x03\x02\x02\x02\u0115\u0117" +
+ "\x03\x02\x02\x02\u0116\u0114\x03\x02\x02\x02\u0117\u0118\b&\x02\x02\u0118" +
+ "L\x03\x02\x02\x02\u0119\u011A\x071\x02\x02\u011A\u011B\x07,\x02\x02\u011B" +
+ "\u011F\x03\x02\x02\x02\u011C\u011E\v\x02\x02\x02\u011D\u011C\x03\x02\x02" +
+ "\x02\u011E\u0121\x03\x02\x02\x02\u011F\u0120\x03\x02\x02\x02\u011F\u011D" +
+ "\x03\x02\x02\x02\u0120\u0122\x03\x02\x02\x02\u0121\u011F\x03\x02\x02\x02" +
+ "\u0122\u0123\x07,\x02\x02\u0123\u0124\x071\x02\x02\u0124\u0125\x03\x02" +
+ "\x02\x02\u0125\u0126\b\'\x02\x02\u0126N\x03\x02\x02\x02\u0127\u0128\t" +
+ "\x05\x02\x02\u0128P\x03\x02\x02\x02\u0129\u012A\t\x06\x02\x02\u012AR\x03" +
+ "\x02\x02\x02\u012B\u012C\t\x07\x02\x02\u012CT\x03\x02\x02\x02\u012D\u012E" +
+ "\t\b\x02\x02\u012EV\x03\x02\x02\x02\u012F\u0130\t\t\x02\x02\u0130X\x03" +
+ "\x02\x02\x02\u0131\u0132\t\n\x02\x02\u0132Z\x03\x02\x02\x02\u0133\u0134" +
+ "\t\v\x02\x02\u0134\\\x03\x02\x02\x02\u0135\u0136\t\f\x02\x02\u0136^\x03" +
+ "\x02\x02\x02\u0137\u0138\t\r\x02\x02\u0138`\x03\x02\x02\x02\u0139\u013A" +
+ "\t\x0E\x02\x02\u013Ab\x03\x02\x02\x02\u013B\u013C\t\x0F\x02\x02\u013C" +
+ "d\x03\x02\x02\x02\u013D\u013E\t\x10\x02\x02\u013Ef\x03\x02\x02\x02\u013F" +
+ "\u0140\t\x11\x02\x02\u0140h\x03\x02\x02\x02\u0141\u0142\t\x12\x02\x02" +
+ "\u0142j\x03\x02\x02\x02\u0143\u0144\t\x13\x02\x02\u0144l\x03\x02\x02\x02" +
+ "\u0145\u0146\t\x14\x02\x02\u0146n\x03\x02\x02\x02\u0147\u0148\t\x15\x02" +
+ "\x02\u0148p\x03\x02\x02\x02\u0149\u014A\t\x16\x02\x02\u014Ar\x03\x02\x02" +
+ "\x02\u014B\u014C\t\x17\x02\x02\u014Ct\x03\x02\x02\x02\u014D\u014E\t\x18" +
+ "\x02\x02\u014Ev\x03\x02\x02\x02\u014F\u0150\t\x19\x02\x02\u0150x\x03\x02" +
+ "\x02\x02\u0151\u0152\t\x1A\x02\x02\u0152z\x03\x02\x02\x02\u0153\u0154" +
+ "\t\x1B\x02\x02\u0154|\x03\x02\x02\x02\u0155\u0156\t\x1C\x02\x02\u0156" +
+ "~\x03\x02\x02\x02\u0157\u0158\t\x1D\x02\x02\u0158\x80\x03\x02\x02\x02" +
+ "\u0159\u015A\t\x1E\x02\x02\u015A\x82\x03\x02\x02\x02\u015B\u015C\t\x1F" +
+ "\x02\x02\u015C\x84\x03\x02\x02\x02\u015D\u015E\t \x02\x02\u015E\x86\x03" +
+ "\x02\x02\x02\r\x02\xC7\xC9\xCF\xD5\xD7\xDD\xDF\u010A\u0114\u011F\x03\b" +
+ "\x02\x02";
+ public static __ATN: ATN;
+ public static get _ATN(): ATN {
+ if (!FormulaLexer.__ATN) {
+ FormulaLexer.__ATN = new ATNDeserializer().deserialize(Utils.toCharArray(FormulaLexer._serializedATN));
+ }
+
+ return FormulaLexer.__ATN;
+ }
+
+}
+
diff --git a/packages/formula/src/grammar/FormulaParser.g4 b/packages/formula/src/grammar/FormulaParser.g4
new file mode 100644
index 000000000..c36a998f3
--- /dev/null
+++ b/packages/formula/src/grammar/FormulaParser.g4
@@ -0,0 +1,41 @@
+parser grammar FormulaParser;
+
+options {
+ tokenVocab = FormulaLexer;
+}
+
+formula: expression EOF;
+
+expression:
+ expression op = (MULTIPLY | DIVIDE | MODULO) expression # MulDivModExpr
+ | expression op = (ADD | SUBTRACT) expression # AddSubExpr
+ | expression POWER expression # PowerExpr
+ | expression op = (
+ EQUAL
+ | NOT_EQUAL
+ | LESS
+ | LESS_EQUAL
+ | GREATER
+ | GREATER_EQUAL
+ ) expression # ComparisonExpr
+ | expression AND expression # AndExpr
+ | expression OR expression # OrExpr
+ | NOT expression # NotExpr
+ | functionCall # FunctionExpr
+ | variable # VariableExpr
+ | NUMBER # NumberExpr
+ | STRING # StringExpr
+ | TRUE # TrueExpr
+ | FALSE # FalseExpr
+ | NULL # NullExpr
+ | DATE # DateExpr
+ | TIME # TimeExpr
+ | DATETIME # DateTimeExpr
+ | LPAREN expression RPAREN # ParenExpr;
+
+functionCall: IDENTIFIER LPAREN argumentList? RPAREN;
+
+argumentList: expression (COMMA expression)*;
+// 这个表达式定义了一个参数列表,由一个或多个表达式组成,表达式之间用逗号分隔。例如:func(1, 2, 3) 中,1, 2, 3 就是参数列表。
+
+variable: LBRACE LBRACE IDENTIFIER RBRACE RBRACE;
\ No newline at end of file
diff --git a/packages/formula/src/grammar/FormulaParser.interp b/packages/formula/src/grammar/FormulaParser.interp
new file mode 100644
index 000000000..0c855ac47
--- /dev/null
+++ b/packages/formula/src/grammar/FormulaParser.interp
@@ -0,0 +1,92 @@
+token literal names:
+null
+'+'
+'-'
+'*'
+'/'
+'%'
+'^'
+'='
+'!='
+'<'
+'<='
+'>'
+'>='
+null
+null
+null
+'('
+')'
+'{'
+'}'
+'['
+']'
+','
+';'
+':'
+'.'
+'_'
+null
+null
+null
+null
+null
+null
+null
+null
+null
+null
+null
+null
+
+token symbolic names:
+null
+ADD
+SUBTRACT
+MULTIPLY
+DIVIDE
+MODULO
+POWER
+EQUAL
+NOT_EQUAL
+LESS
+LESS_EQUAL
+GREATER
+GREATER_EQUAL
+AND
+OR
+NOT
+LPAREN
+RPAREN
+LBRACE
+RBRACE
+LBRACKET
+RBRACKET
+COMMA
+SEMICOLON
+COLON
+DOT
+UNDERSCORE
+IDENTIFIER
+NUMBER
+STRING
+TRUE
+FALSE
+NULL
+DATE
+TIME
+DATETIME
+WS
+COMMENT
+MULTILINE_COMMENT
+
+rule names:
+formula
+expression
+functionCall
+argumentList
+variable
+
+
+atn:
+[3, 51485, 51898, 1421, 44986, 20307, 1543, 60043, 49729, 3, 40, 79, 4, 2, 9, 2, 4, 3, 9, 3, 4, 4, 9, 4, 4, 5, 9, 5, 4, 6, 9, 6, 3, 2, 3, 2, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 5, 3, 33, 10, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 7, 3, 53, 10, 3, 12, 3, 14, 3, 56, 11, 3, 3, 4, 3, 4, 3, 4, 5, 4, 61, 10, 4, 3, 4, 3, 4, 3, 5, 3, 5, 3, 5, 7, 5, 68, 10, 5, 12, 5, 14, 5, 71, 11, 5, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 2, 2, 3, 4, 7, 2, 2, 4, 2, 6, 2, 8, 2, 10, 2, 2, 5, 3, 2, 5, 7, 3, 2, 3, 4, 3, 2, 9, 14, 2, 92, 2, 12, 3, 2, 2, 2, 4, 32, 3, 2, 2, 2, 6, 57, 3, 2, 2, 2, 8, 64, 3, 2, 2, 2, 10, 72, 3, 2, 2, 2, 12, 13, 5, 4, 3, 2, 13, 14, 7, 2, 2, 3, 14, 3, 3, 2, 2, 2, 15, 16, 8, 3, 1, 2, 16, 17, 7, 17, 2, 2, 17, 33, 5, 4, 3, 14, 18, 33, 5, 6, 4, 2, 19, 33, 5, 10, 6, 2, 20, 33, 7, 30, 2, 2, 21, 33, 7, 31, 2, 2, 22, 33, 7, 32, 2, 2, 23, 33, 7, 33, 2, 2, 24, 33, 7, 34, 2, 2, 25, 33, 7, 35, 2, 2, 26, 33, 7, 36, 2, 2, 27, 33, 7, 37, 2, 2, 28, 29, 7, 18, 2, 2, 29, 30, 5, 4, 3, 2, 30, 31, 7, 19, 2, 2, 31, 33, 3, 2, 2, 2, 32, 15, 3, 2, 2, 2, 32, 18, 3, 2, 2, 2, 32, 19, 3, 2, 2, 2, 32, 20, 3, 2, 2, 2, 32, 21, 3, 2, 2, 2, 32, 22, 3, 2, 2, 2, 32, 23, 3, 2, 2, 2, 32, 24, 3, 2, 2, 2, 32, 25, 3, 2, 2, 2, 32, 26, 3, 2, 2, 2, 32, 27, 3, 2, 2, 2, 32, 28, 3, 2, 2, 2, 33, 54, 3, 2, 2, 2, 34, 35, 12, 20, 2, 2, 35, 36, 9, 2, 2, 2, 36, 53, 5, 4, 3, 21, 37, 38, 12, 19, 2, 2, 38, 39, 9, 3, 2, 2, 39, 53, 5, 4, 3, 20, 40, 41, 12, 18, 2, 2, 41, 42, 7, 8, 2, 2, 42, 53, 5, 4, 3, 19, 43, 44, 12, 17, 2, 2, 44, 45, 9, 4, 2, 2, 45, 53, 5, 4, 3, 18, 46, 47, 12, 16, 2, 2, 47, 48, 7, 15, 2, 2, 48, 53, 5, 4, 3, 17, 49, 50, 12, 15, 2, 2, 50, 51, 7, 16, 2, 2, 51, 53, 5, 4, 3, 16, 52, 34, 3, 2, 2, 2, 52, 37, 3, 2, 2, 2, 52, 40, 3, 2, 2, 2, 52, 43, 3, 2, 2, 2, 52, 46, 3, 2, 2, 2, 52, 49, 3, 2, 2, 2, 53, 56, 3, 2, 2, 2, 54, 52, 3, 2, 2, 2, 54, 55, 3, 2, 2, 2, 55, 5, 3, 2, 2, 2, 56, 54, 3, 2, 2, 2, 57, 58, 7, 29, 2, 2, 58, 60, 7, 18, 2, 2, 59, 61, 5, 8, 5, 2, 60, 59, 3, 2, 2, 2, 60, 61, 3, 2, 2, 2, 61, 62, 3, 2, 2, 2, 62, 63, 7, 19, 2, 2, 63, 7, 3, 2, 2, 2, 64, 69, 5, 4, 3, 2, 65, 66, 7, 24, 2, 2, 66, 68, 5, 4, 3, 2, 67, 65, 3, 2, 2, 2, 68, 71, 3, 2, 2, 2, 69, 67, 3, 2, 2, 2, 69, 70, 3, 2, 2, 2, 70, 9, 3, 2, 2, 2, 71, 69, 3, 2, 2, 2, 72, 73, 7, 20, 2, 2, 73, 74, 7, 20, 2, 2, 74, 75, 7, 29, 2, 2, 75, 76, 7, 21, 2, 2, 76, 77, 7, 21, 2, 2, 77, 11, 3, 2, 2, 2, 7, 32, 52, 54, 60, 69]
\ No newline at end of file
diff --git a/packages/formula/src/grammar/FormulaParser.tokens b/packages/formula/src/grammar/FormulaParser.tokens
new file mode 100644
index 000000000..5ad732765
--- /dev/null
+++ b/packages/formula/src/grammar/FormulaParser.tokens
@@ -0,0 +1,61 @@
+ADD=1
+SUBTRACT=2
+MULTIPLY=3
+DIVIDE=4
+MODULO=5
+POWER=6
+EQUAL=7
+NOT_EQUAL=8
+LESS=9
+LESS_EQUAL=10
+GREATER=11
+GREATER_EQUAL=12
+AND=13
+OR=14
+NOT=15
+LPAREN=16
+RPAREN=17
+LBRACE=18
+RBRACE=19
+LBRACKET=20
+RBRACKET=21
+COMMA=22
+SEMICOLON=23
+COLON=24
+DOT=25
+UNDERSCORE=26
+IDENTIFIER=27
+NUMBER=28
+STRING=29
+TRUE=30
+FALSE=31
+NULL=32
+DATE=33
+TIME=34
+DATETIME=35
+WS=36
+COMMENT=37
+MULTILINE_COMMENT=38
+'+'=1
+'-'=2
+'*'=3
+'/'=4
+'%'=5
+'^'=6
+'='=7
+'!='=8
+'<'=9
+'<='=10
+'>'=11
+'>='=12
+'('=16
+')'=17
+'{'=18
+'}'=19
+'['=20
+']'=21
+','=22
+';'=23
+':'=24
+'.'=25
+'_'=26
diff --git a/packages/formula/src/grammar/FormulaParser.ts b/packages/formula/src/grammar/FormulaParser.ts
new file mode 100644
index 000000000..7ce7934a8
--- /dev/null
+++ b/packages/formula/src/grammar/FormulaParser.ts
@@ -0,0 +1,1110 @@
+// Generated from src/grammar/FormulaParser.g4 by ANTLR 4.9.0-SNAPSHOT
+
+
+import { ATN } from "antlr4ts/atn/ATN";
+import { ATNDeserializer } from "antlr4ts/atn/ATNDeserializer";
+import { FailedPredicateException } from "antlr4ts/FailedPredicateException";
+import { NotNull } from "antlr4ts/Decorators";
+import { NoViableAltException } from "antlr4ts/NoViableAltException";
+import { Override } from "antlr4ts/Decorators";
+import { Parser } from "antlr4ts/Parser";
+import { ParserRuleContext } from "antlr4ts/ParserRuleContext";
+import { ParserATNSimulator } from "antlr4ts/atn/ParserATNSimulator";
+import { ParseTreeListener } from "antlr4ts/tree/ParseTreeListener";
+import { ParseTreeVisitor } from "antlr4ts/tree/ParseTreeVisitor";
+import { RecognitionException } from "antlr4ts/RecognitionException";
+import { RuleContext } from "antlr4ts/RuleContext";
+//import { RuleVersion } from "antlr4ts/RuleVersion";
+import { TerminalNode } from "antlr4ts/tree/TerminalNode";
+import { Token } from "antlr4ts/Token";
+import { TokenStream } from "antlr4ts/TokenStream";
+import { Vocabulary } from "antlr4ts/Vocabulary";
+import { VocabularyImpl } from "antlr4ts/VocabularyImpl";
+
+import * as Utils from "antlr4ts/misc/Utils";
+
+import { FormulaParserVisitor } from "./FormulaParserVisitor";
+
+
+export class FormulaParser extends Parser {
+ public static readonly ADD = 1;
+ public static readonly SUBTRACT = 2;
+ public static readonly MULTIPLY = 3;
+ public static readonly DIVIDE = 4;
+ public static readonly MODULO = 5;
+ public static readonly POWER = 6;
+ public static readonly EQUAL = 7;
+ public static readonly NOT_EQUAL = 8;
+ public static readonly LESS = 9;
+ public static readonly LESS_EQUAL = 10;
+ public static readonly GREATER = 11;
+ public static readonly GREATER_EQUAL = 12;
+ public static readonly AND = 13;
+ public static readonly OR = 14;
+ public static readonly NOT = 15;
+ public static readonly LPAREN = 16;
+ public static readonly RPAREN = 17;
+ public static readonly LBRACE = 18;
+ public static readonly RBRACE = 19;
+ public static readonly LBRACKET = 20;
+ public static readonly RBRACKET = 21;
+ public static readonly COMMA = 22;
+ public static readonly SEMICOLON = 23;
+ public static readonly COLON = 24;
+ public static readonly DOT = 25;
+ public static readonly UNDERSCORE = 26;
+ public static readonly IDENTIFIER = 27;
+ public static readonly NUMBER = 28;
+ public static readonly STRING = 29;
+ public static readonly TRUE = 30;
+ public static readonly FALSE = 31;
+ public static readonly NULL = 32;
+ public static readonly DATE = 33;
+ public static readonly TIME = 34;
+ public static readonly DATETIME = 35;
+ public static readonly WS = 36;
+ public static readonly COMMENT = 37;
+ public static readonly MULTILINE_COMMENT = 38;
+ public static readonly RULE_formula = 0;
+ public static readonly RULE_expression = 1;
+ public static readonly RULE_functionCall = 2;
+ public static readonly RULE_argumentList = 3;
+ public static readonly RULE_variable = 4;
+ // tslint:disable:no-trailing-whitespace
+ public static readonly ruleNames: string[] = [
+ "formula", "expression", "functionCall", "argumentList", "variable",
+ ];
+
+ private static readonly _LITERAL_NAMES: Array = [
+ undefined, "'+'", "'-'", "'*'", "'/'", "'%'", "'^'", "'='", "'!='", "'<'",
+ "'<='", "'>'", "'>='", undefined, undefined, undefined, "'('", "')'",
+ "'{'", "'}'", "'['", "']'", "','", "';'", "':'", "'.'", "'_'",
+ ];
+ private static readonly _SYMBOLIC_NAMES: Array = [
+ undefined, "ADD", "SUBTRACT", "MULTIPLY", "DIVIDE", "MODULO", "POWER",
+ "EQUAL", "NOT_EQUAL", "LESS", "LESS_EQUAL", "GREATER", "GREATER_EQUAL",
+ "AND", "OR", "NOT", "LPAREN", "RPAREN", "LBRACE", "RBRACE", "LBRACKET",
+ "RBRACKET", "COMMA", "SEMICOLON", "COLON", "DOT", "UNDERSCORE", "IDENTIFIER",
+ "NUMBER", "STRING", "TRUE", "FALSE", "NULL", "DATE", "TIME", "DATETIME",
+ "WS", "COMMENT", "MULTILINE_COMMENT",
+ ];
+ public static readonly VOCABULARY: Vocabulary = new VocabularyImpl(FormulaParser._LITERAL_NAMES, FormulaParser._SYMBOLIC_NAMES, []);
+
+ // @Override
+ // @NotNull
+ public get vocabulary(): Vocabulary {
+ return FormulaParser.VOCABULARY;
+ }
+ // tslint:enable:no-trailing-whitespace
+
+ // @Override
+ public get grammarFileName(): string { return "FormulaParser.g4"; }
+
+ // @Override
+ public get ruleNames(): string[] { return FormulaParser.ruleNames; }
+
+ // @Override
+ public get serializedATN(): string { return FormulaParser._serializedATN; }
+
+ protected createFailedPredicateException(predicate?: string, message?: string): FailedPredicateException {
+ return new FailedPredicateException(this, predicate, message);
+ }
+
+ constructor(input: TokenStream) {
+ super(input);
+ this._interp = new ParserATNSimulator(FormulaParser._ATN, this);
+ }
+ // @RuleVersion(0)
+ public formula(): FormulaContext {
+ let _localctx: FormulaContext = new FormulaContext(this._ctx, this.state);
+ this.enterRule(_localctx, 0, FormulaParser.RULE_formula);
+ try {
+ this.enterOuterAlt(_localctx, 1);
+ {
+ this.state = 10;
+ this.expression(0);
+ this.state = 11;
+ this.match(FormulaParser.EOF);
+ }
+ }
+ catch (re) {
+ if (re instanceof RecognitionException) {
+ _localctx.exception = re;
+ this._errHandler.reportError(this, re);
+ this._errHandler.recover(this, re);
+ } else {
+ throw re;
+ }
+ }
+ finally {
+ this.exitRule();
+ }
+ return _localctx;
+ }
+
+ public expression(): ExpressionContext;
+ public expression(_p: number): ExpressionContext;
+ // @RuleVersion(0)
+ public expression(_p?: number): ExpressionContext {
+ if (_p === undefined) {
+ _p = 0;
+ }
+
+ let _parentctx: ParserRuleContext = this._ctx;
+ let _parentState: number = this.state;
+ let _localctx: ExpressionContext = new ExpressionContext(this._ctx, _parentState);
+ let _prevctx: ExpressionContext = _localctx;
+ let _startState: number = 2;
+ this.enterRecursionRule(_localctx, 2, FormulaParser.RULE_expression, _p);
+ let _la: number;
+ try {
+ let _alt: number;
+ this.enterOuterAlt(_localctx, 1);
+ {
+ this.state = 30;
+ this._errHandler.sync(this);
+ switch (this._input.LA(1)) {
+ case FormulaParser.NOT:
+ {
+ _localctx = new NotExprContext(_localctx);
+ this._ctx = _localctx;
+ _prevctx = _localctx;
+
+ this.state = 14;
+ this.match(FormulaParser.NOT);
+ this.state = 15;
+ this.expression(12);
+ }
+ break;
+ case FormulaParser.IDENTIFIER:
+ {
+ _localctx = new FunctionExprContext(_localctx);
+ this._ctx = _localctx;
+ _prevctx = _localctx;
+ this.state = 16;
+ this.functionCall();
+ }
+ break;
+ case FormulaParser.LBRACE:
+ {
+ _localctx = new VariableExprContext(_localctx);
+ this._ctx = _localctx;
+ _prevctx = _localctx;
+ this.state = 17;
+ this.variable();
+ }
+ break;
+ case FormulaParser.NUMBER:
+ {
+ _localctx = new NumberExprContext(_localctx);
+ this._ctx = _localctx;
+ _prevctx = _localctx;
+ this.state = 18;
+ this.match(FormulaParser.NUMBER);
+ }
+ break;
+ case FormulaParser.STRING:
+ {
+ _localctx = new StringExprContext(_localctx);
+ this._ctx = _localctx;
+ _prevctx = _localctx;
+ this.state = 19;
+ this.match(FormulaParser.STRING);
+ }
+ break;
+ case FormulaParser.TRUE:
+ {
+ _localctx = new TrueExprContext(_localctx);
+ this._ctx = _localctx;
+ _prevctx = _localctx;
+ this.state = 20;
+ this.match(FormulaParser.TRUE);
+ }
+ break;
+ case FormulaParser.FALSE:
+ {
+ _localctx = new FalseExprContext(_localctx);
+ this._ctx = _localctx;
+ _prevctx = _localctx;
+ this.state = 21;
+ this.match(FormulaParser.FALSE);
+ }
+ break;
+ case FormulaParser.NULL:
+ {
+ _localctx = new NullExprContext(_localctx);
+ this._ctx = _localctx;
+ _prevctx = _localctx;
+ this.state = 22;
+ this.match(FormulaParser.NULL);
+ }
+ break;
+ case FormulaParser.DATE:
+ {
+ _localctx = new DateExprContext(_localctx);
+ this._ctx = _localctx;
+ _prevctx = _localctx;
+ this.state = 23;
+ this.match(FormulaParser.DATE);
+ }
+ break;
+ case FormulaParser.TIME:
+ {
+ _localctx = new TimeExprContext(_localctx);
+ this._ctx = _localctx;
+ _prevctx = _localctx;
+ this.state = 24;
+ this.match(FormulaParser.TIME);
+ }
+ break;
+ case FormulaParser.DATETIME:
+ {
+ _localctx = new DateTimeExprContext(_localctx);
+ this._ctx = _localctx;
+ _prevctx = _localctx;
+ this.state = 25;
+ this.match(FormulaParser.DATETIME);
+ }
+ break;
+ case FormulaParser.LPAREN:
+ {
+ _localctx = new ParenExprContext(_localctx);
+ this._ctx = _localctx;
+ _prevctx = _localctx;
+ this.state = 26;
+ this.match(FormulaParser.LPAREN);
+ this.state = 27;
+ this.expression(0);
+ this.state = 28;
+ this.match(FormulaParser.RPAREN);
+ }
+ break;
+ default:
+ throw new NoViableAltException(this);
+ }
+ this._ctx._stop = this._input.tryLT(-1);
+ this.state = 52;
+ this._errHandler.sync(this);
+ _alt = this.interpreter.adaptivePredict(this._input, 2, this._ctx);
+ while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) {
+ if (_alt === 1) {
+ if (this._parseListeners != null) {
+ this.triggerExitRuleEvent();
+ }
+ _prevctx = _localctx;
+ {
+ this.state = 50;
+ this._errHandler.sync(this);
+ switch ( this.interpreter.adaptivePredict(this._input, 1, this._ctx) ) {
+ case 1:
+ {
+ _localctx = new MulDivModExprContext(new ExpressionContext(_parentctx, _parentState));
+ this.pushNewRecursionContext(_localctx, _startState, FormulaParser.RULE_expression);
+ this.state = 32;
+ if (!(this.precpred(this._ctx, 18))) {
+ throw this.createFailedPredicateException("this.precpred(this._ctx, 18)");
+ }
+ this.state = 33;
+ (_localctx as MulDivModExprContext)._op = this._input.LT(1);
+ _la = this._input.LA(1);
+ if (!((((_la) & ~0x1F) === 0 && ((1 << _la) & ((1 << FormulaParser.MULTIPLY) | (1 << FormulaParser.DIVIDE) | (1 << FormulaParser.MODULO))) !== 0))) {
+ (_localctx as MulDivModExprContext)._op = this._errHandler.recoverInline(this);
+ } else {
+ if (this._input.LA(1) === Token.EOF) {
+ this.matchedEOF = true;
+ }
+
+ this._errHandler.reportMatch(this);
+ this.consume();
+ }
+ this.state = 34;
+ this.expression(19);
+ }
+ break;
+
+ case 2:
+ {
+ _localctx = new AddSubExprContext(new ExpressionContext(_parentctx, _parentState));
+ this.pushNewRecursionContext(_localctx, _startState, FormulaParser.RULE_expression);
+ this.state = 35;
+ if (!(this.precpred(this._ctx, 17))) {
+ throw this.createFailedPredicateException("this.precpred(this._ctx, 17)");
+ }
+ this.state = 36;
+ (_localctx as AddSubExprContext)._op = this._input.LT(1);
+ _la = this._input.LA(1);
+ if (!(_la === FormulaParser.ADD || _la === FormulaParser.SUBTRACT)) {
+ (_localctx as AddSubExprContext)._op = this._errHandler.recoverInline(this);
+ } else {
+ if (this._input.LA(1) === Token.EOF) {
+ this.matchedEOF = true;
+ }
+
+ this._errHandler.reportMatch(this);
+ this.consume();
+ }
+ this.state = 37;
+ this.expression(18);
+ }
+ break;
+
+ case 3:
+ {
+ _localctx = new PowerExprContext(new ExpressionContext(_parentctx, _parentState));
+ this.pushNewRecursionContext(_localctx, _startState, FormulaParser.RULE_expression);
+ this.state = 38;
+ if (!(this.precpred(this._ctx, 16))) {
+ throw this.createFailedPredicateException("this.precpred(this._ctx, 16)");
+ }
+ this.state = 39;
+ this.match(FormulaParser.POWER);
+ this.state = 40;
+ this.expression(17);
+ }
+ break;
+
+ case 4:
+ {
+ _localctx = new ComparisonExprContext(new ExpressionContext(_parentctx, _parentState));
+ this.pushNewRecursionContext(_localctx, _startState, FormulaParser.RULE_expression);
+ this.state = 41;
+ if (!(this.precpred(this._ctx, 15))) {
+ throw this.createFailedPredicateException("this.precpred(this._ctx, 15)");
+ }
+ this.state = 42;
+ (_localctx as ComparisonExprContext)._op = this._input.LT(1);
+ _la = this._input.LA(1);
+ if (!((((_la) & ~0x1F) === 0 && ((1 << _la) & ((1 << FormulaParser.EQUAL) | (1 << FormulaParser.NOT_EQUAL) | (1 << FormulaParser.LESS) | (1 << FormulaParser.LESS_EQUAL) | (1 << FormulaParser.GREATER) | (1 << FormulaParser.GREATER_EQUAL))) !== 0))) {
+ (_localctx as ComparisonExprContext)._op = this._errHandler.recoverInline(this);
+ } else {
+ if (this._input.LA(1) === Token.EOF) {
+ this.matchedEOF = true;
+ }
+
+ this._errHandler.reportMatch(this);
+ this.consume();
+ }
+ this.state = 43;
+ this.expression(16);
+ }
+ break;
+
+ case 5:
+ {
+ _localctx = new AndExprContext(new ExpressionContext(_parentctx, _parentState));
+ this.pushNewRecursionContext(_localctx, _startState, FormulaParser.RULE_expression);
+ this.state = 44;
+ if (!(this.precpred(this._ctx, 14))) {
+ throw this.createFailedPredicateException("this.precpred(this._ctx, 14)");
+ }
+ this.state = 45;
+ this.match(FormulaParser.AND);
+ this.state = 46;
+ this.expression(15);
+ }
+ break;
+
+ case 6:
+ {
+ _localctx = new OrExprContext(new ExpressionContext(_parentctx, _parentState));
+ this.pushNewRecursionContext(_localctx, _startState, FormulaParser.RULE_expression);
+ this.state = 47;
+ if (!(this.precpred(this._ctx, 13))) {
+ throw this.createFailedPredicateException("this.precpred(this._ctx, 13)");
+ }
+ this.state = 48;
+ this.match(FormulaParser.OR);
+ this.state = 49;
+ this.expression(14);
+ }
+ break;
+ }
+ }
+ }
+ this.state = 54;
+ this._errHandler.sync(this);
+ _alt = this.interpreter.adaptivePredict(this._input, 2, this._ctx);
+ }
+ }
+ }
+ catch (re) {
+ if (re instanceof RecognitionException) {
+ _localctx.exception = re;
+ this._errHandler.reportError(this, re);
+ this._errHandler.recover(this, re);
+ } else {
+ throw re;
+ }
+ }
+ finally {
+ this.unrollRecursionContexts(_parentctx);
+ }
+ return _localctx;
+ }
+ // @RuleVersion(0)
+ public functionCall(): FunctionCallContext {
+ let _localctx: FunctionCallContext = new FunctionCallContext(this._ctx, this.state);
+ this.enterRule(_localctx, 4, FormulaParser.RULE_functionCall);
+ let _la: number;
+ try {
+ this.enterOuterAlt(_localctx, 1);
+ {
+ this.state = 55;
+ this.match(FormulaParser.IDENTIFIER);
+ this.state = 56;
+ this.match(FormulaParser.LPAREN);
+ this.state = 58;
+ this._errHandler.sync(this);
+ _la = this._input.LA(1);
+ if (((((_la - 15)) & ~0x1F) === 0 && ((1 << (_la - 15)) & ((1 << (FormulaParser.NOT - 15)) | (1 << (FormulaParser.LPAREN - 15)) | (1 << (FormulaParser.LBRACE - 15)) | (1 << (FormulaParser.IDENTIFIER - 15)) | (1 << (FormulaParser.NUMBER - 15)) | (1 << (FormulaParser.STRING - 15)) | (1 << (FormulaParser.TRUE - 15)) | (1 << (FormulaParser.FALSE - 15)) | (1 << (FormulaParser.NULL - 15)) | (1 << (FormulaParser.DATE - 15)) | (1 << (FormulaParser.TIME - 15)) | (1 << (FormulaParser.DATETIME - 15)))) !== 0)) {
+ {
+ this.state = 57;
+ this.argumentList();
+ }
+ }
+
+ this.state = 60;
+ this.match(FormulaParser.RPAREN);
+ }
+ }
+ catch (re) {
+ if (re instanceof RecognitionException) {
+ _localctx.exception = re;
+ this._errHandler.reportError(this, re);
+ this._errHandler.recover(this, re);
+ } else {
+ throw re;
+ }
+ }
+ finally {
+ this.exitRule();
+ }
+ return _localctx;
+ }
+ // @RuleVersion(0)
+ public argumentList(): ArgumentListContext {
+ let _localctx: ArgumentListContext = new ArgumentListContext(this._ctx, this.state);
+ this.enterRule(_localctx, 6, FormulaParser.RULE_argumentList);
+ let _la: number;
+ try {
+ this.enterOuterAlt(_localctx, 1);
+ {
+ this.state = 62;
+ this.expression(0);
+ this.state = 67;
+ this._errHandler.sync(this);
+ _la = this._input.LA(1);
+ while (_la === FormulaParser.COMMA) {
+ {
+ {
+ this.state = 63;
+ this.match(FormulaParser.COMMA);
+ this.state = 64;
+ this.expression(0);
+ }
+ }
+ this.state = 69;
+ this._errHandler.sync(this);
+ _la = this._input.LA(1);
+ }
+ }
+ }
+ catch (re) {
+ if (re instanceof RecognitionException) {
+ _localctx.exception = re;
+ this._errHandler.reportError(this, re);
+ this._errHandler.recover(this, re);
+ } else {
+ throw re;
+ }
+ }
+ finally {
+ this.exitRule();
+ }
+ return _localctx;
+ }
+ // @RuleVersion(0)
+ public variable(): VariableContext {
+ let _localctx: VariableContext = new VariableContext(this._ctx, this.state);
+ this.enterRule(_localctx, 8, FormulaParser.RULE_variable);
+ try {
+ this.enterOuterAlt(_localctx, 1);
+ {
+ this.state = 70;
+ this.match(FormulaParser.LBRACE);
+ this.state = 71;
+ this.match(FormulaParser.LBRACE);
+ this.state = 72;
+ this.match(FormulaParser.IDENTIFIER);
+ this.state = 73;
+ this.match(FormulaParser.RBRACE);
+ this.state = 74;
+ this.match(FormulaParser.RBRACE);
+ }
+ }
+ catch (re) {
+ if (re instanceof RecognitionException) {
+ _localctx.exception = re;
+ this._errHandler.reportError(this, re);
+ this._errHandler.recover(this, re);
+ } else {
+ throw re;
+ }
+ }
+ finally {
+ this.exitRule();
+ }
+ return _localctx;
+ }
+
+ public sempred(_localctx: RuleContext, ruleIndex: number, predIndex: number): boolean {
+ switch (ruleIndex) {
+ case 1:
+ return this.expression_sempred(_localctx as ExpressionContext, predIndex);
+ }
+ return true;
+ }
+ private expression_sempred(_localctx: ExpressionContext, predIndex: number): boolean {
+ switch (predIndex) {
+ case 0:
+ return this.precpred(this._ctx, 18);
+
+ case 1:
+ return this.precpred(this._ctx, 17);
+
+ case 2:
+ return this.precpred(this._ctx, 16);
+
+ case 3:
+ return this.precpred(this._ctx, 15);
+
+ case 4:
+ return this.precpred(this._ctx, 14);
+
+ case 5:
+ return this.precpred(this._ctx, 13);
+ }
+ return true;
+ }
+
+ public static readonly _serializedATN: string =
+ "\x03\uC91D\uCABA\u058D\uAFBA\u4F53\u0607\uEA8B\uC241\x03(O\x04\x02\t\x02" +
+ "\x04\x03\t\x03\x04\x04\t\x04\x04\x05\t\x05\x04\x06\t\x06\x03\x02\x03\x02" +
+ "\x03\x02\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03" +
+ "\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03" +
+ "\x05\x03!\n\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03" +
+ "\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03" +
+ "\x03\x03\x03\x03\x07\x035\n\x03\f\x03\x0E\x038\v\x03\x03\x04\x03\x04\x03" +
+ "\x04\x05\x04=\n\x04\x03\x04\x03\x04\x03\x05\x03\x05\x03\x05\x07\x05D\n" +
+ "\x05\f\x05\x0E\x05G\v\x05\x03\x06\x03\x06\x03\x06\x03\x06\x03\x06\x03" +
+ "\x06\x03\x06\x02\x02\x03\x04\x07\x02\x02\x04\x02\x06\x02\b\x02\n\x02\x02" +
+ "\x05\x03\x02\x05\x07\x03\x02\x03\x04\x03\x02\t\x0E\x02\\\x02\f\x03\x02" +
+ "\x02\x02\x04 \x03\x02\x02\x02\x069\x03\x02\x02\x02\b@\x03\x02\x02\x02" +
+ "\nH\x03\x02\x02\x02\f\r\x05\x04\x03\x02\r\x0E\x07\x02\x02\x03\x0E\x03" +
+ "\x03\x02\x02\x02\x0F\x10\b\x03\x01\x02\x10\x11\x07\x11\x02\x02\x11!\x05" +
+ "\x04\x03\x0E\x12!\x05\x06\x04\x02\x13!\x05\n\x06\x02\x14!\x07\x1E\x02" +
+ "\x02\x15!\x07\x1F\x02\x02\x16!\x07 \x02\x02\x17!\x07!\x02\x02\x18!\x07" +
+ "\"\x02\x02\x19!\x07#\x02\x02\x1A!\x07$\x02\x02\x1B!\x07%\x02\x02\x1C\x1D" +
+ "\x07\x12\x02\x02\x1D\x1E\x05\x04\x03\x02\x1E\x1F\x07\x13\x02\x02\x1F!" +
+ "\x03\x02\x02\x02 \x0F\x03\x02\x02\x02 \x12\x03\x02\x02\x02 \x13\x03\x02" +
+ "\x02\x02 \x14\x03\x02\x02\x02 \x15\x03\x02\x02\x02 \x16\x03\x02\x02\x02" +
+ " \x17\x03\x02\x02\x02 \x18\x03\x02\x02\x02 \x19\x03\x02\x02\x02 \x1A\x03" +
+ "\x02\x02\x02 \x1B\x03\x02\x02\x02 \x1C\x03\x02\x02\x02!6\x03\x02\x02\x02" +
+ "\"#\f\x14\x02\x02#$\t\x02\x02\x02$5\x05\x04\x03\x15%&\f\x13\x02\x02&\'" +
+ "\t\x03\x02\x02\'5\x05\x04\x03\x14()\f\x12\x02\x02)*\x07\b\x02\x02*5\x05" +
+ "\x04\x03\x13+,\f\x11\x02\x02,-\t\x04\x02\x02-5\x05\x04\x03\x12./\f\x10" +
+ "\x02\x02/0\x07\x0F\x02\x0205\x05\x04\x03\x1112\f\x0F\x02\x0223\x07\x10" +
+ "\x02\x0235\x05\x04\x03\x104\"\x03\x02\x02\x024%\x03\x02\x02\x024(\x03" +
+ "\x02\x02\x024+\x03\x02\x02\x024.\x03\x02\x02\x0241\x03\x02\x02\x0258\x03" +
+ "\x02\x02\x0264\x03\x02\x02\x0267\x03\x02\x02\x027\x05\x03\x02\x02\x02" +
+ "86\x03\x02\x02\x029:\x07\x1D\x02\x02:<\x07\x12\x02\x02;=\x05\b\x05\x02" +
+ "<;\x03\x02\x02\x02<=\x03\x02\x02\x02=>\x03\x02\x02\x02>?\x07\x13\x02\x02" +
+ "?\x07\x03\x02\x02\x02@E\x05\x04\x03\x02AB\x07\x18\x02\x02BD\x05\x04\x03" +
+ "\x02CA\x03\x02\x02\x02DG\x03\x02\x02\x02EC\x03\x02\x02\x02EF\x03\x02\x02" +
+ "\x02F\t\x03\x02\x02\x02GE\x03\x02\x02\x02HI\x07\x14\x02\x02IJ\x07\x14" +
+ "\x02\x02JK\x07\x1D\x02\x02KL\x07\x15\x02\x02LM\x07\x15\x02\x02M\v\x03" +
+ "\x02\x02\x02\x07 46(visitor: FormulaParserVisitor): Result {
+ if (visitor.visitFormula) {
+ return visitor.visitFormula(this);
+ } else {
+ return visitor.visitChildren(this);
+ }
+ }
+}
+
+
+export class ExpressionContext extends ParserRuleContext {
+ constructor(parent: ParserRuleContext | undefined, invokingState: number) {
+ super(parent, invokingState);
+ }
+ // @Override
+ public get ruleIndex(): number { return FormulaParser.RULE_expression; }
+ public copyFrom(ctx: ExpressionContext): void {
+ super.copyFrom(ctx);
+ }
+}
+export class MulDivModExprContext extends ExpressionContext {
+ public _op!: Token;
+ public expression(): ExpressionContext[];
+ public expression(i: number): ExpressionContext;
+ public expression(i?: number): ExpressionContext | ExpressionContext[] {
+ if (i === undefined) {
+ return this.getRuleContexts(ExpressionContext);
+ } else {
+ return this.getRuleContext(i, ExpressionContext);
+ }
+ }
+ public MULTIPLY(): TerminalNode | undefined { return this.tryGetToken(FormulaParser.MULTIPLY, 0); }
+ public DIVIDE(): TerminalNode | undefined { return this.tryGetToken(FormulaParser.DIVIDE, 0); }
+ public MODULO(): TerminalNode | undefined { return this.tryGetToken(FormulaParser.MODULO, 0); }
+ constructor(ctx: ExpressionContext) {
+ super(ctx.parent, ctx.invokingState);
+ this.copyFrom(ctx);
+ }
+ // @Override
+ public accept(visitor: FormulaParserVisitor): Result {
+ if (visitor.visitMulDivModExpr) {
+ return visitor.visitMulDivModExpr(this);
+ } else {
+ return visitor.visitChildren(this);
+ }
+ }
+}
+export class AddSubExprContext extends ExpressionContext {
+ public _op!: Token;
+ public expression(): ExpressionContext[];
+ public expression(i: number): ExpressionContext;
+ public expression(i?: number): ExpressionContext | ExpressionContext[] {
+ if (i === undefined) {
+ return this.getRuleContexts(ExpressionContext);
+ } else {
+ return this.getRuleContext(i, ExpressionContext);
+ }
+ }
+ public ADD(): TerminalNode | undefined { return this.tryGetToken(FormulaParser.ADD, 0); }
+ public SUBTRACT(): TerminalNode | undefined { return this.tryGetToken(FormulaParser.SUBTRACT, 0); }
+ constructor(ctx: ExpressionContext) {
+ super(ctx.parent, ctx.invokingState);
+ this.copyFrom(ctx);
+ }
+ // @Override
+ public accept(visitor: FormulaParserVisitor): Result {
+ if (visitor.visitAddSubExpr) {
+ return visitor.visitAddSubExpr(this);
+ } else {
+ return visitor.visitChildren(this);
+ }
+ }
+}
+export class PowerExprContext extends ExpressionContext {
+ public expression(): ExpressionContext[];
+ public expression(i: number): ExpressionContext;
+ public expression(i?: number): ExpressionContext | ExpressionContext[] {
+ if (i === undefined) {
+ return this.getRuleContexts(ExpressionContext);
+ } else {
+ return this.getRuleContext(i, ExpressionContext);
+ }
+ }
+ public POWER(): TerminalNode { return this.getToken(FormulaParser.POWER, 0); }
+ constructor(ctx: ExpressionContext) {
+ super(ctx.parent, ctx.invokingState);
+ this.copyFrom(ctx);
+ }
+ // @Override
+ public accept(visitor: FormulaParserVisitor): Result {
+ if (visitor.visitPowerExpr) {
+ return visitor.visitPowerExpr(this);
+ } else {
+ return visitor.visitChildren(this);
+ }
+ }
+}
+export class ComparisonExprContext extends ExpressionContext {
+ public _op!: Token;
+ public expression(): ExpressionContext[];
+ public expression(i: number): ExpressionContext;
+ public expression(i?: number): ExpressionContext | ExpressionContext[] {
+ if (i === undefined) {
+ return this.getRuleContexts(ExpressionContext);
+ } else {
+ return this.getRuleContext(i, ExpressionContext);
+ }
+ }
+ public EQUAL(): TerminalNode | undefined { return this.tryGetToken(FormulaParser.EQUAL, 0); }
+ public NOT_EQUAL(): TerminalNode | undefined { return this.tryGetToken(FormulaParser.NOT_EQUAL, 0); }
+ public LESS(): TerminalNode | undefined { return this.tryGetToken(FormulaParser.LESS, 0); }
+ public LESS_EQUAL(): TerminalNode | undefined { return this.tryGetToken(FormulaParser.LESS_EQUAL, 0); }
+ public GREATER(): TerminalNode | undefined { return this.tryGetToken(FormulaParser.GREATER, 0); }
+ public GREATER_EQUAL(): TerminalNode | undefined { return this.tryGetToken(FormulaParser.GREATER_EQUAL, 0); }
+ constructor(ctx: ExpressionContext) {
+ super(ctx.parent, ctx.invokingState);
+ this.copyFrom(ctx);
+ }
+ // @Override
+ public accept(visitor: FormulaParserVisitor): Result {
+ if (visitor.visitComparisonExpr) {
+ return visitor.visitComparisonExpr(this);
+ } else {
+ return visitor.visitChildren(this);
+ }
+ }
+}
+export class AndExprContext extends ExpressionContext {
+ public expression(): ExpressionContext[];
+ public expression(i: number): ExpressionContext;
+ public expression(i?: number): ExpressionContext | ExpressionContext[] {
+ if (i === undefined) {
+ return this.getRuleContexts(ExpressionContext);
+ } else {
+ return this.getRuleContext(i, ExpressionContext);
+ }
+ }
+ public AND(): TerminalNode { return this.getToken(FormulaParser.AND, 0); }
+ constructor(ctx: ExpressionContext) {
+ super(ctx.parent, ctx.invokingState);
+ this.copyFrom(ctx);
+ }
+ // @Override
+ public accept(visitor: FormulaParserVisitor): Result {
+ if (visitor.visitAndExpr) {
+ return visitor.visitAndExpr(this);
+ } else {
+ return visitor.visitChildren(this);
+ }
+ }
+}
+export class OrExprContext extends ExpressionContext {
+ public expression(): ExpressionContext[];
+ public expression(i: number): ExpressionContext;
+ public expression(i?: number): ExpressionContext | ExpressionContext[] {
+ if (i === undefined) {
+ return this.getRuleContexts(ExpressionContext);
+ } else {
+ return this.getRuleContext(i, ExpressionContext);
+ }
+ }
+ public OR(): TerminalNode { return this.getToken(FormulaParser.OR, 0); }
+ constructor(ctx: ExpressionContext) {
+ super(ctx.parent, ctx.invokingState);
+ this.copyFrom(ctx);
+ }
+ // @Override
+ public accept(visitor: FormulaParserVisitor): Result {
+ if (visitor.visitOrExpr) {
+ return visitor.visitOrExpr(this);
+ } else {
+ return visitor.visitChildren(this);
+ }
+ }
+}
+export class NotExprContext extends ExpressionContext {
+ public NOT(): TerminalNode { return this.getToken(FormulaParser.NOT, 0); }
+ public expression(): ExpressionContext {
+ return this.getRuleContext(0, ExpressionContext);
+ }
+ constructor(ctx: ExpressionContext) {
+ super(ctx.parent, ctx.invokingState);
+ this.copyFrom(ctx);
+ }
+ // @Override
+ public accept(visitor: FormulaParserVisitor): Result {
+ if (visitor.visitNotExpr) {
+ return visitor.visitNotExpr(this);
+ } else {
+ return visitor.visitChildren(this);
+ }
+ }
+}
+export class FunctionExprContext extends ExpressionContext {
+ public functionCall(): FunctionCallContext {
+ return this.getRuleContext(0, FunctionCallContext);
+ }
+ constructor(ctx: ExpressionContext) {
+ super(ctx.parent, ctx.invokingState);
+ this.copyFrom(ctx);
+ }
+ // @Override
+ public accept(visitor: FormulaParserVisitor): Result {
+ if (visitor.visitFunctionExpr) {
+ return visitor.visitFunctionExpr(this);
+ } else {
+ return visitor.visitChildren(this);
+ }
+ }
+}
+export class VariableExprContext extends ExpressionContext {
+ public variable(): VariableContext {
+ return this.getRuleContext(0, VariableContext);
+ }
+ constructor(ctx: ExpressionContext) {
+ super(ctx.parent, ctx.invokingState);
+ this.copyFrom(ctx);
+ }
+ // @Override
+ public accept(visitor: FormulaParserVisitor): Result {
+ if (visitor.visitVariableExpr) {
+ return visitor.visitVariableExpr(this);
+ } else {
+ return visitor.visitChildren(this);
+ }
+ }
+}
+export class NumberExprContext extends ExpressionContext {
+ public NUMBER(): TerminalNode { return this.getToken(FormulaParser.NUMBER, 0); }
+ constructor(ctx: ExpressionContext) {
+ super(ctx.parent, ctx.invokingState);
+ this.copyFrom(ctx);
+ }
+ // @Override
+ public accept(visitor: FormulaParserVisitor): Result {
+ if (visitor.visitNumberExpr) {
+ return visitor.visitNumberExpr(this);
+ } else {
+ return visitor.visitChildren(this);
+ }
+ }
+}
+export class StringExprContext extends ExpressionContext {
+ public STRING(): TerminalNode { return this.getToken(FormulaParser.STRING, 0); }
+ constructor(ctx: ExpressionContext) {
+ super(ctx.parent, ctx.invokingState);
+ this.copyFrom(ctx);
+ }
+ // @Override
+ public accept(visitor: FormulaParserVisitor): Result {
+ if (visitor.visitStringExpr) {
+ return visitor.visitStringExpr(this);
+ } else {
+ return visitor.visitChildren(this);
+ }
+ }
+}
+export class TrueExprContext extends ExpressionContext {
+ public TRUE(): TerminalNode { return this.getToken(FormulaParser.TRUE, 0); }
+ constructor(ctx: ExpressionContext) {
+ super(ctx.parent, ctx.invokingState);
+ this.copyFrom(ctx);
+ }
+ // @Override
+ public accept(visitor: FormulaParserVisitor): Result {
+ if (visitor.visitTrueExpr) {
+ return visitor.visitTrueExpr(this);
+ } else {
+ return visitor.visitChildren(this);
+ }
+ }
+}
+export class FalseExprContext extends ExpressionContext {
+ public FALSE(): TerminalNode { return this.getToken(FormulaParser.FALSE, 0); }
+ constructor(ctx: ExpressionContext) {
+ super(ctx.parent, ctx.invokingState);
+ this.copyFrom(ctx);
+ }
+ // @Override
+ public accept(visitor: FormulaParserVisitor): Result {
+ if (visitor.visitFalseExpr) {
+ return visitor.visitFalseExpr(this);
+ } else {
+ return visitor.visitChildren(this);
+ }
+ }
+}
+export class NullExprContext extends ExpressionContext {
+ public NULL(): TerminalNode { return this.getToken(FormulaParser.NULL, 0); }
+ constructor(ctx: ExpressionContext) {
+ super(ctx.parent, ctx.invokingState);
+ this.copyFrom(ctx);
+ }
+ // @Override
+ public accept(visitor: FormulaParserVisitor): Result {
+ if (visitor.visitNullExpr) {
+ return visitor.visitNullExpr(this);
+ } else {
+ return visitor.visitChildren(this);
+ }
+ }
+}
+export class DateExprContext extends ExpressionContext {
+ public DATE(): TerminalNode { return this.getToken(FormulaParser.DATE, 0); }
+ constructor(ctx: ExpressionContext) {
+ super(ctx.parent, ctx.invokingState);
+ this.copyFrom(ctx);
+ }
+ // @Override
+ public accept(visitor: FormulaParserVisitor): Result {
+ if (visitor.visitDateExpr) {
+ return visitor.visitDateExpr(this);
+ } else {
+ return visitor.visitChildren(this);
+ }
+ }
+}
+export class TimeExprContext extends ExpressionContext {
+ public TIME(): TerminalNode { return this.getToken(FormulaParser.TIME, 0); }
+ constructor(ctx: ExpressionContext) {
+ super(ctx.parent, ctx.invokingState);
+ this.copyFrom(ctx);
+ }
+ // @Override
+ public accept(visitor: FormulaParserVisitor): Result {
+ if (visitor.visitTimeExpr) {
+ return visitor.visitTimeExpr(this);
+ } else {
+ return visitor.visitChildren(this);
+ }
+ }
+}
+export class DateTimeExprContext extends ExpressionContext {
+ public DATETIME(): TerminalNode { return this.getToken(FormulaParser.DATETIME, 0); }
+ constructor(ctx: ExpressionContext) {
+ super(ctx.parent, ctx.invokingState);
+ this.copyFrom(ctx);
+ }
+ // @Override
+ public accept(visitor: FormulaParserVisitor): Result {
+ if (visitor.visitDateTimeExpr) {
+ return visitor.visitDateTimeExpr(this);
+ } else {
+ return visitor.visitChildren(this);
+ }
+ }
+}
+export class ParenExprContext extends ExpressionContext {
+ public LPAREN(): TerminalNode { return this.getToken(FormulaParser.LPAREN, 0); }
+ public expression(): ExpressionContext {
+ return this.getRuleContext(0, ExpressionContext);
+ }
+ public RPAREN(): TerminalNode { return this.getToken(FormulaParser.RPAREN, 0); }
+ constructor(ctx: ExpressionContext) {
+ super(ctx.parent, ctx.invokingState);
+ this.copyFrom(ctx);
+ }
+ // @Override
+ public accept(visitor: FormulaParserVisitor): Result {
+ if (visitor.visitParenExpr) {
+ return visitor.visitParenExpr(this);
+ } else {
+ return visitor.visitChildren(this);
+ }
+ }
+}
+
+
+export class FunctionCallContext extends ParserRuleContext {
+ public IDENTIFIER(): TerminalNode { return this.getToken(FormulaParser.IDENTIFIER, 0); }
+ public LPAREN(): TerminalNode { return this.getToken(FormulaParser.LPAREN, 0); }
+ public RPAREN(): TerminalNode { return this.getToken(FormulaParser.RPAREN, 0); }
+ public argumentList(): ArgumentListContext | undefined {
+ return this.tryGetRuleContext(0, ArgumentListContext);
+ }
+ constructor(parent: ParserRuleContext | undefined, invokingState: number) {
+ super(parent, invokingState);
+ }
+ // @Override
+ public get ruleIndex(): number { return FormulaParser.RULE_functionCall; }
+ // @Override
+ public accept(visitor: FormulaParserVisitor): Result {
+ if (visitor.visitFunctionCall) {
+ return visitor.visitFunctionCall(this);
+ } else {
+ return visitor.visitChildren(this);
+ }
+ }
+}
+
+
+export class ArgumentListContext extends ParserRuleContext {
+ public expression(): ExpressionContext[];
+ public expression(i: number): ExpressionContext;
+ public expression(i?: number): ExpressionContext | ExpressionContext[] {
+ if (i === undefined) {
+ return this.getRuleContexts(ExpressionContext);
+ } else {
+ return this.getRuleContext(i, ExpressionContext);
+ }
+ }
+ public COMMA(): TerminalNode[];
+ public COMMA(i: number): TerminalNode;
+ public COMMA(i?: number): TerminalNode | TerminalNode[] {
+ if (i === undefined) {
+ return this.getTokens(FormulaParser.COMMA);
+ } else {
+ return this.getToken(FormulaParser.COMMA, i);
+ }
+ }
+ constructor(parent: ParserRuleContext | undefined, invokingState: number) {
+ super(parent, invokingState);
+ }
+ // @Override
+ public get ruleIndex(): number { return FormulaParser.RULE_argumentList; }
+ // @Override
+ public accept(visitor: FormulaParserVisitor): Result {
+ if (visitor.visitArgumentList) {
+ return visitor.visitArgumentList(this);
+ } else {
+ return visitor.visitChildren(this);
+ }
+ }
+}
+
+
+export class VariableContext extends ParserRuleContext {
+ public LBRACE(): TerminalNode[];
+ public LBRACE(i: number): TerminalNode;
+ public LBRACE(i?: number): TerminalNode | TerminalNode[] {
+ if (i === undefined) {
+ return this.getTokens(FormulaParser.LBRACE);
+ } else {
+ return this.getToken(FormulaParser.LBRACE, i);
+ }
+ }
+ public IDENTIFIER(): TerminalNode { return this.getToken(FormulaParser.IDENTIFIER, 0); }
+ public RBRACE(): TerminalNode[];
+ public RBRACE(i: number): TerminalNode;
+ public RBRACE(i?: number): TerminalNode | TerminalNode[] {
+ if (i === undefined) {
+ return this.getTokens(FormulaParser.RBRACE);
+ } else {
+ return this.getToken(FormulaParser.RBRACE, i);
+ }
+ }
+ constructor(parent: ParserRuleContext | undefined, invokingState: number) {
+ super(parent, invokingState);
+ }
+ // @Override
+ public get ruleIndex(): number { return FormulaParser.RULE_variable; }
+ // @Override
+ public accept(visitor: FormulaParserVisitor): Result {
+ if (visitor.visitVariable) {
+ return visitor.visitVariable(this);
+ } else {
+ return visitor.visitChildren(this);
+ }
+ }
+}
+
+
diff --git a/packages/formula/src/grammar/FormulaParserVisitor.ts b/packages/formula/src/grammar/FormulaParserVisitor.ts
new file mode 100644
index 000000000..31cb9c694
--- /dev/null
+++ b/packages/formula/src/grammar/FormulaParserVisitor.ts
@@ -0,0 +1,218 @@
+// Generated from src/grammar/FormulaParser.g4 by ANTLR 4.9.0-SNAPSHOT
+
+
+import { ParseTreeVisitor } from "antlr4ts/tree/ParseTreeVisitor";
+
+import { MulDivModExprContext } from "./FormulaParser";
+import { AddSubExprContext } from "./FormulaParser";
+import { PowerExprContext } from "./FormulaParser";
+import { ComparisonExprContext } from "./FormulaParser";
+import { AndExprContext } from "./FormulaParser";
+import { OrExprContext } from "./FormulaParser";
+import { NotExprContext } from "./FormulaParser";
+import { FunctionExprContext } from "./FormulaParser";
+import { VariableExprContext } from "./FormulaParser";
+import { NumberExprContext } from "./FormulaParser";
+import { StringExprContext } from "./FormulaParser";
+import { TrueExprContext } from "./FormulaParser";
+import { FalseExprContext } from "./FormulaParser";
+import { NullExprContext } from "./FormulaParser";
+import { DateExprContext } from "./FormulaParser";
+import { TimeExprContext } from "./FormulaParser";
+import { DateTimeExprContext } from "./FormulaParser";
+import { ParenExprContext } from "./FormulaParser";
+import { FormulaContext } from "./FormulaParser";
+import { ExpressionContext } from "./FormulaParser";
+import { FunctionCallContext } from "./FormulaParser";
+import { ArgumentListContext } from "./FormulaParser";
+import { VariableContext } from "./FormulaParser";
+
+
+/**
+ * This interface defines a complete generic visitor for a parse tree produced
+ * by `FormulaParser`.
+ *
+ * @param The return type of the visit operation. Use `void` for
+ * operations with no return type.
+ */
+export interface FormulaParserVisitor extends ParseTreeVisitor {
+ /**
+ * Visit a parse tree produced by the `MulDivModExpr`
+ * labeled alternative in `FormulaParser.expression`.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ visitMulDivModExpr?: (ctx: MulDivModExprContext) => Result;
+
+ /**
+ * Visit a parse tree produced by the `AddSubExpr`
+ * labeled alternative in `FormulaParser.expression`.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ visitAddSubExpr?: (ctx: AddSubExprContext) => Result;
+
+ /**
+ * Visit a parse tree produced by the `PowerExpr`
+ * labeled alternative in `FormulaParser.expression`.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ visitPowerExpr?: (ctx: PowerExprContext) => Result;
+
+ /**
+ * Visit a parse tree produced by the `ComparisonExpr`
+ * labeled alternative in `FormulaParser.expression`.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ visitComparisonExpr?: (ctx: ComparisonExprContext) => Result;
+
+ /**
+ * Visit a parse tree produced by the `AndExpr`
+ * labeled alternative in `FormulaParser.expression`.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ visitAndExpr?: (ctx: AndExprContext) => Result;
+
+ /**
+ * Visit a parse tree produced by the `OrExpr`
+ * labeled alternative in `FormulaParser.expression`.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ visitOrExpr?: (ctx: OrExprContext) => Result;
+
+ /**
+ * Visit a parse tree produced by the `NotExpr`
+ * labeled alternative in `FormulaParser.expression`.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ visitNotExpr?: (ctx: NotExprContext) => Result;
+
+ /**
+ * Visit a parse tree produced by the `FunctionExpr`
+ * labeled alternative in `FormulaParser.expression`.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ visitFunctionExpr?: (ctx: FunctionExprContext) => Result;
+
+ /**
+ * Visit a parse tree produced by the `VariableExpr`
+ * labeled alternative in `FormulaParser.expression`.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ visitVariableExpr?: (ctx: VariableExprContext) => Result;
+
+ /**
+ * Visit a parse tree produced by the `NumberExpr`
+ * labeled alternative in `FormulaParser.expression`.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ visitNumberExpr?: (ctx: NumberExprContext) => Result;
+
+ /**
+ * Visit a parse tree produced by the `StringExpr`
+ * labeled alternative in `FormulaParser.expression`.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ visitStringExpr?: (ctx: StringExprContext) => Result;
+
+ /**
+ * Visit a parse tree produced by the `TrueExpr`
+ * labeled alternative in `FormulaParser.expression`.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ visitTrueExpr?: (ctx: TrueExprContext) => Result;
+
+ /**
+ * Visit a parse tree produced by the `FalseExpr`
+ * labeled alternative in `FormulaParser.expression`.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ visitFalseExpr?: (ctx: FalseExprContext) => Result;
+
+ /**
+ * Visit a parse tree produced by the `NullExpr`
+ * labeled alternative in `FormulaParser.expression`.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ visitNullExpr?: (ctx: NullExprContext) => Result;
+
+ /**
+ * Visit a parse tree produced by the `DateExpr`
+ * labeled alternative in `FormulaParser.expression`.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ visitDateExpr?: (ctx: DateExprContext) => Result;
+
+ /**
+ * Visit a parse tree produced by the `TimeExpr`
+ * labeled alternative in `FormulaParser.expression`.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ visitTimeExpr?: (ctx: TimeExprContext) => Result;
+
+ /**
+ * Visit a parse tree produced by the `DateTimeExpr`
+ * labeled alternative in `FormulaParser.expression`.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ visitDateTimeExpr?: (ctx: DateTimeExprContext) => Result;
+
+ /**
+ * Visit a parse tree produced by the `ParenExpr`
+ * labeled alternative in `FormulaParser.expression`.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ visitParenExpr?: (ctx: ParenExprContext) => Result;
+
+ /**
+ * Visit a parse tree produced by `FormulaParser.formula`.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ visitFormula?: (ctx: FormulaContext) => Result;
+
+ /**
+ * Visit a parse tree produced by `FormulaParser.expression`.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ visitExpression?: (ctx: ExpressionContext) => Result;
+
+ /**
+ * Visit a parse tree produced by `FormulaParser.functionCall`.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ visitFunctionCall?: (ctx: FunctionCallContext) => Result;
+
+ /**
+ * Visit a parse tree produced by `FormulaParser.argumentList`.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ visitArgumentList?: (ctx: ArgumentListContext) => Result;
+
+ /**
+ * Visit a parse tree produced by `FormulaParser.variable`.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ visitVariable?: (ctx: VariableContext) => Result;
+}
+
diff --git a/packages/formula/src/index.ts b/packages/formula/src/index.ts
new file mode 100644
index 000000000..4befc2ca9
--- /dev/null
+++ b/packages/formula/src/index.ts
@@ -0,0 +1,10 @@
+export { AbstractParseTreeVisitor } from "antlr4ts/tree/AbstractParseTreeVisitor"
+export { type ParseTree } from "antlr4ts/tree/ParseTree"
+export * from "./formula.constants"
+export * from "./formula.visitor"
+export * from "./formula/formula.type"
+export * from "./grammar/FormulaLexer"
+export * from "./grammar/FormulaParser"
+export * from "./grammar/FormulaParserVisitor"
+export * from "./types"
+export * from "./util"
diff --git a/packages/formula/src/tests/__snapshots__/parse-formula.test.ts.snap b/packages/formula/src/tests/__snapshots__/parse-formula.test.ts.snap
new file mode 100644
index 000000000..fd8f68a69
--- /dev/null
+++ b/packages/formula/src/tests/__snapshots__/parse-formula.test.ts.snap
@@ -0,0 +1,1392 @@
+// Bun Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`parse formula test ADD(1, ADD(2, {{ field1 }})) 1`] = `
+{
+ "arguments": [
+ {
+ "type": "number",
+ "value": 1,
+ },
+ {
+ "arguments": [
+ {
+ "type": "number",
+ "value": 2,
+ },
+ {
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+ },
+ ],
+ "name": "ADD",
+ "returnType": "number",
+ "type": "functionCall",
+ "value": "ADD(2,{{field1}})",
+ },
+ ],
+ "name": "ADD",
+ "returnType": "number",
+ "type": "functionCall",
+ "value": "ADD(1,ADD(2,{{field1}}))",
+}
+`;
+
+exports[`parse formula test ADD(1, 2) 1`] = `
+{
+ "arguments": [
+ {
+ "type": "number",
+ "value": 1,
+ },
+ {
+ "type": "number",
+ "value": 2,
+ },
+ ],
+ "name": "ADD",
+ "returnType": "number",
+ "type": "functionCall",
+ "value": "ADD(1,2)",
+}
+`;
+
+exports[`parse formula test SUBTRACT(1, 2) 1`] = `
+{
+ "arguments": [
+ {
+ "type": "number",
+ "value": 1,
+ },
+ {
+ "type": "number",
+ "value": 2,
+ },
+ ],
+ "name": "SUBTRACT",
+ "returnType": "number",
+ "type": "functionCall",
+ "value": "SUBTRACT(1,2)",
+}
+`;
+
+exports[`parse formula test MULTIPLY(1, 2) 1`] = `
+{
+ "arguments": [
+ {
+ "type": "number",
+ "value": 1,
+ },
+ {
+ "type": "number",
+ "value": 2,
+ },
+ ],
+ "name": "MULTIPLY",
+ "returnType": "number",
+ "type": "functionCall",
+ "value": "MULTIPLY(1,2)",
+}
+`;
+
+exports[`parse formula test DIVIDE(1, 2) 1`] = `
+{
+ "arguments": [
+ {
+ "type": "number",
+ "value": 1,
+ },
+ {
+ "type": "number",
+ "value": 2,
+ },
+ ],
+ "name": "DIVIDE",
+ "returnType": "number",
+ "type": "functionCall",
+ "value": "DIVIDE(1,2)",
+}
+`;
+
+exports[`parse formula test 1 - 1 1`] = `
+{
+ "arguments": [
+ {
+ "type": "number",
+ "value": 1,
+ },
+ {
+ "type": "number",
+ "value": 1,
+ },
+ ],
+ "name": "-",
+ "returnType": "number",
+ "type": "functionCall",
+ "value": "1-1",
+}
+`;
+
+exports[`parse formula test 1 * 1 1`] = `
+{
+ "arguments": [
+ {
+ "type": "number",
+ "value": 1,
+ },
+ {
+ "type": "number",
+ "value": 1,
+ },
+ ],
+ "name": "*",
+ "returnType": "number",
+ "type": "functionCall",
+ "value": "1*1",
+}
+`;
+
+exports[`parse formula test 1 / 1 1`] = `
+{
+ "arguments": [
+ {
+ "type": "number",
+ "value": 1,
+ },
+ {
+ "type": "number",
+ "value": 1,
+ },
+ ],
+ "name": "/",
+ "returnType": "number",
+ "type": "functionCall",
+ "value": "1/1",
+}
+`;
+
+exports[`parse formula test SUBTRACT(1, 2) + MULTIPLY(3, 4) 1`] = `
+{
+ "arguments": [
+ {
+ "arguments": [
+ {
+ "type": "number",
+ "value": 1,
+ },
+ {
+ "type": "number",
+ "value": 2,
+ },
+ ],
+ "name": "SUBTRACT",
+ "returnType": "number",
+ "type": "functionCall",
+ "value": "SUBTRACT(1,2)",
+ },
+ {
+ "arguments": [
+ {
+ "type": "number",
+ "value": 3,
+ },
+ {
+ "type": "number",
+ "value": 4,
+ },
+ ],
+ "name": "MULTIPLY",
+ "returnType": "number",
+ "type": "functionCall",
+ "value": "MULTIPLY(3,4)",
+ },
+ ],
+ "name": "+",
+ "returnType": "number",
+ "type": "functionCall",
+ "value": "SUBTRACT(1,2)+MULTIPLY(3,4)",
+}
+`;
+
+exports[`parse formula test 1 1`] = `
+{
+ "type": "number",
+ "value": 1,
+}
+`;
+
+exports[`parse formula test {{field1}} 1`] = `
+{
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+}
+`;
+
+exports[`parse formula test 1 + 1 1`] = `
+{
+ "arguments": [
+ {
+ "type": "number",
+ "value": 1,
+ },
+ {
+ "type": "number",
+ "value": 1,
+ },
+ ],
+ "name": "+",
+ "returnType": "number",
+ "type": "functionCall",
+ "value": "1+1",
+}
+`;
+
+exports[`parse formula test {{field1}} + {{field2}} 1`] = `
+{
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+ },
+ {
+ "type": "variable",
+ "value": "{{field2}}",
+ "variable": "field2",
+ },
+ ],
+ "name": "+",
+ "returnType": "number",
+ "type": "functionCall",
+ "value": "{{field1}}+{{field2}}",
+}
+`;
+
+exports[`parse formula test SUM({{field1}}, {{field2}}) 1`] = `
+{
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+ },
+ {
+ "type": "variable",
+ "value": "{{field2}}",
+ "variable": "field2",
+ },
+ ],
+ "name": "SUM",
+ "returnType": "number",
+ "type": "functionCall",
+ "value": "SUM({{field1}},{{field2}})",
+}
+`;
+
+exports[`parse formula test CONCAT({{field1}}, {{field2}}) 1`] = `
+{
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+ },
+ {
+ "type": "variable",
+ "value": "{{field2}}",
+ "variable": "field2",
+ },
+ ],
+ "name": "CONCAT",
+ "returnType": "string",
+ "type": "functionCall",
+ "value": "CONCAT({{field1}},{{field2}})",
+}
+`;
+
+exports[`parse formula test CONCAT({{field1}}, {{field2}}, {{field3}}) 1`] = `
+{
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+ },
+ {
+ "type": "variable",
+ "value": "{{field2}}",
+ "variable": "field2",
+ },
+ {
+ "type": "variable",
+ "value": "{{field3}}",
+ "variable": "field3",
+ },
+ ],
+ "name": "CONCAT",
+ "returnType": "string",
+ "type": "functionCall",
+ "value": "CONCAT({{field1}},{{field2}},{{field3}})",
+}
+`;
+
+exports[`parse formula test MOD(1, 2) 1`] = `
+{
+ "arguments": [
+ {
+ "type": "number",
+ "value": 1,
+ },
+ {
+ "type": "number",
+ "value": 2,
+ },
+ ],
+ "name": "MOD",
+ "returnType": "number",
+ "type": "functionCall",
+ "value": "MOD(1,2)",
+}
+`;
+
+exports[`parse formula test MOD({{field1}}, {{field2}}) 1`] = `
+{
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+ },
+ {
+ "type": "variable",
+ "value": "{{field2}}",
+ "variable": "field2",
+ },
+ ],
+ "name": "MOD",
+ "returnType": "number",
+ "type": "functionCall",
+ "value": "MOD({{field1}},{{field2}})",
+}
+`;
+
+exports[`parse formula test POWER(2, 3) 1`] = `
+{
+ "arguments": [
+ {
+ "type": "number",
+ "value": 2,
+ },
+ {
+ "type": "number",
+ "value": 3,
+ },
+ ],
+ "name": "POWER",
+ "returnType": "number",
+ "type": "functionCall",
+ "value": "POWER(2,3)",
+}
+`;
+
+exports[`parse formula test POWER({{field1}}, {{field2}}) 1`] = `
+{
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+ },
+ {
+ "type": "variable",
+ "value": "{{field2}}",
+ "variable": "field2",
+ },
+ ],
+ "name": "POWER",
+ "returnType": "number",
+ "type": "functionCall",
+ "value": "POWER({{field1}},{{field2}})",
+}
+`;
+
+exports[`parse formula test SQRT(4) 1`] = `
+{
+ "arguments": [
+ {
+ "type": "number",
+ "value": 4,
+ },
+ ],
+ "name": "SQRT",
+ "returnType": "number",
+ "type": "functionCall",
+ "value": "SQRT(4)",
+}
+`;
+
+exports[`parse formula test SQRT({{field1}}) 1`] = `
+{
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+ },
+ ],
+ "name": "SQRT",
+ "returnType": "number",
+ "type": "functionCall",
+ "value": "SQRT({{field1}})",
+}
+`;
+
+exports[`parse formula test ABS(-5) 1`] = `
+{
+ "arguments": [
+ {
+ "type": "number",
+ "value": 5,
+ },
+ ],
+ "name": "ABS",
+ "returnType": "number",
+ "type": "functionCall",
+ "value": "ABS(-5)",
+}
+`;
+
+exports[`parse formula test ABS({{field1}}) 1`] = `
+{
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+ },
+ ],
+ "name": "ABS",
+ "returnType": "number",
+ "type": "functionCall",
+ "value": "ABS({{field1}})",
+}
+`;
+
+exports[`parse formula test ROUND(1.234) 1`] = `
+{
+ "arguments": [
+ {
+ "type": "number",
+ "value": 1.234,
+ },
+ ],
+ "name": "ROUND",
+ "returnType": "number",
+ "type": "functionCall",
+ "value": "ROUND(1.234)",
+}
+`;
+
+exports[`parse formula test ROUND({{field1}}) 1`] = `
+{
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+ },
+ ],
+ "name": "ROUND",
+ "returnType": "number",
+ "type": "functionCall",
+ "value": "ROUND({{field1}})",
+}
+`;
+
+exports[`parse formula test FLOOR(1.234) 1`] = `
+{
+ "arguments": [
+ {
+ "type": "number",
+ "value": 1.234,
+ },
+ ],
+ "name": "FLOOR",
+ "returnType": "number",
+ "type": "functionCall",
+ "value": "FLOOR(1.234)",
+}
+`;
+
+exports[`parse formula test FLOOR({{field1}}) 1`] = `
+{
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+ },
+ ],
+ "name": "FLOOR",
+ "returnType": "number",
+ "type": "functionCall",
+ "value": "FLOOR({{field1}})",
+}
+`;
+
+exports[`parse formula test CEILING(1.234) 1`] = `
+{
+ "arguments": [
+ {
+ "type": "number",
+ "value": 1.234,
+ },
+ ],
+ "name": "CEILING",
+ "returnType": "number",
+ "type": "functionCall",
+ "value": "CEILING(1.234)",
+}
+`;
+
+exports[`parse formula test CEILING({{field1}}) 1`] = `
+{
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+ },
+ ],
+ "name": "CEILING",
+ "returnType": "number",
+ "type": "functionCall",
+ "value": "CEILING({{field1}})",
+}
+`;
+
+exports[`parse formula test MIN(1, 2) 1`] = `
+{
+ "arguments": [
+ {
+ "type": "number",
+ "value": 1,
+ },
+ {
+ "type": "number",
+ "value": 2,
+ },
+ ],
+ "name": "MIN",
+ "returnType": "number",
+ "type": "functionCall",
+ "value": "MIN(1,2)",
+}
+`;
+
+exports[`parse formula test MIN({{field1}}, {{field2}}) 1`] = `
+{
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+ },
+ {
+ "type": "variable",
+ "value": "{{field2}}",
+ "variable": "field2",
+ },
+ ],
+ "name": "MIN",
+ "returnType": "number",
+ "type": "functionCall",
+ "value": "MIN({{field1}},{{field2}})",
+}
+`;
+
+exports[`parse formula test MIN({{field1}}, {{field2}}, {{field3}}) 1`] = `
+{
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+ },
+ {
+ "type": "variable",
+ "value": "{{field2}}",
+ "variable": "field2",
+ },
+ {
+ "type": "variable",
+ "value": "{{field3}}",
+ "variable": "field3",
+ },
+ ],
+ "name": "MIN",
+ "returnType": "number",
+ "type": "functionCall",
+ "value": "MIN({{field1}},{{field2}},{{field3}})",
+}
+`;
+
+exports[`parse formula test MAX(1, 2) 1`] = `
+{
+ "arguments": [
+ {
+ "type": "number",
+ "value": 1,
+ },
+ {
+ "type": "number",
+ "value": 2,
+ },
+ ],
+ "name": "MAX",
+ "returnType": "number",
+ "type": "functionCall",
+ "value": "MAX(1,2)",
+}
+`;
+
+exports[`parse formula test MAX({{field1}}, {{field2}}) 1`] = `
+{
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+ },
+ {
+ "type": "variable",
+ "value": "{{field2}}",
+ "variable": "field2",
+ },
+ ],
+ "name": "MAX",
+ "returnType": "number",
+ "type": "functionCall",
+ "value": "MAX({{field1}},{{field2}})",
+}
+`;
+
+exports[`parse formula test MAX({{field1}}, {{field2}}, {{field3}}) 1`] = `
+{
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+ },
+ {
+ "type": "variable",
+ "value": "{{field2}}",
+ "variable": "field2",
+ },
+ {
+ "type": "variable",
+ "value": "{{field3}}",
+ "variable": "field3",
+ },
+ ],
+ "name": "MAX",
+ "returnType": "number",
+ "type": "functionCall",
+ "value": "MAX({{field1}},{{field2}},{{field3}})",
+}
+`;
+
+exports[`parse formula test AVERAGE(1, 2, 3) 1`] = `
+{
+ "arguments": [
+ {
+ "type": "number",
+ "value": 1,
+ },
+ {
+ "type": "number",
+ "value": 2,
+ },
+ {
+ "type": "number",
+ "value": 3,
+ },
+ ],
+ "name": "AVERAGE",
+ "returnType": "number",
+ "type": "functionCall",
+ "value": "AVERAGE(1,2,3)",
+}
+`;
+
+exports[`parse formula test AVERAGE({{field1}}, {{field2}}, {{field3}}) 1`] = `
+{
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+ },
+ {
+ "type": "variable",
+ "value": "{{field2}}",
+ "variable": "field2",
+ },
+ {
+ "type": "variable",
+ "value": "{{field3}}",
+ "variable": "field3",
+ },
+ ],
+ "name": "AVERAGE",
+ "returnType": "number",
+ "type": "functionCall",
+ "value": "AVERAGE({{field1}},{{field2}},{{field3}})",
+}
+`;
+
+exports[`parse formula test CONCAT({{field1}}, {{field2}}) 2`] = `
+{
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+ },
+ {
+ "type": "variable",
+ "value": "{{field2}}",
+ "variable": "field2",
+ },
+ ],
+ "name": "CONCAT",
+ "returnType": "string",
+ "type": "functionCall",
+ "value": "CONCAT({{field1}},{{field2}})",
+}
+`;
+
+exports[`parse formula test CONCAT({{field1}}, {{field2}}, {{field3}}) 2`] = `
+{
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+ },
+ {
+ "type": "variable",
+ "value": "{{field2}}",
+ "variable": "field2",
+ },
+ {
+ "type": "variable",
+ "value": "{{field3}}",
+ "variable": "field3",
+ },
+ ],
+ "name": "CONCAT",
+ "returnType": "string",
+ "type": "functionCall",
+ "value": "CONCAT({{field1}},{{field2}},{{field3}})",
+}
+`;
+
+exports[`parse formula test LEFT({{field1}}, 3) 1`] = `
+{
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+ },
+ {
+ "type": "number",
+ "value": 3,
+ },
+ ],
+ "name": "LEFT",
+ "returnType": "string",
+ "type": "functionCall",
+ "value": "LEFT({{field1}},3)",
+}
+`;
+
+exports[`parse formula test RIGHT({{field1}}, 3) 1`] = `
+{
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+ },
+ {
+ "type": "number",
+ "value": 3,
+ },
+ ],
+ "name": "RIGHT",
+ "returnType": "string",
+ "type": "functionCall",
+ "value": "RIGHT({{field1}},3)",
+}
+`;
+
+exports[`parse formula test MID({{field1}}, 2, 3) 1`] = `
+{
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+ },
+ {
+ "type": "number",
+ "value": 2,
+ },
+ {
+ "type": "number",
+ "value": 3,
+ },
+ ],
+ "name": "MID",
+ "returnType": "string",
+ "type": "functionCall",
+ "value": "MID({{field1}},2,3)",
+}
+`;
+
+exports[`parse formula test NOT ({{field1}} > {{field2}}) 1`] = `
+{
+ "arguments": [
+ {
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+ },
+ {
+ "type": "variable",
+ "value": "{{field2}}",
+ "variable": "field2",
+ },
+ ],
+ "name": ">",
+ "returnType": "boolean",
+ "type": "functionCall",
+ "value": "{{field1}}>{{field2}}",
+ },
+ ],
+ "name": "NOT",
+ "returnType": "boolean",
+ "type": "functionCall",
+ "value": "NOT({{field1}}>{{field2}})",
+}
+`;
+
+exports[`parse formula test ({{field1}} > {{field2}}) AND ({{field2}} > {{field3}}) 1`] = `
+{
+ "arguments": [
+ {
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+ },
+ {
+ "type": "variable",
+ "value": "{{field2}}",
+ "variable": "field2",
+ },
+ ],
+ "name": ">",
+ "returnType": "boolean",
+ "type": "functionCall",
+ "value": "{{field1}}>{{field2}}",
+ },
+ {
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field2}}",
+ "variable": "field2",
+ },
+ {
+ "type": "variable",
+ "value": "{{field3}}",
+ "variable": "field3",
+ },
+ ],
+ "name": ">",
+ "returnType": "boolean",
+ "type": "functionCall",
+ "value": "{{field2}}>{{field3}}",
+ },
+ ],
+ "name": "AND",
+ "returnType": "boolean",
+ "type": "functionCall",
+ "value": "({{field1}}>{{field2}})AND({{field2}}>{{field3}})",
+}
+`;
+
+exports[`parse formula test ({{field1}} > {{field2}}) OR ({{field2}} > {{field3}}) 1`] = `
+{
+ "arguments": [
+ {
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+ },
+ {
+ "type": "variable",
+ "value": "{{field2}}",
+ "variable": "field2",
+ },
+ ],
+ "name": ">",
+ "returnType": "boolean",
+ "type": "functionCall",
+ "value": "{{field1}}>{{field2}}",
+ },
+ {
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field2}}",
+ "variable": "field2",
+ },
+ {
+ "type": "variable",
+ "value": "{{field3}}",
+ "variable": "field3",
+ },
+ ],
+ "name": ">",
+ "returnType": "boolean",
+ "type": "functionCall",
+ "value": "{{field2}}>{{field3}}",
+ },
+ ],
+ "name": "OR",
+ "returnType": "boolean",
+ "type": "functionCall",
+ "value": "({{field1}}>{{field2}})OR({{field2}}>{{field3}})",
+}
+`;
+
+exports[`parse formula test {{field1}} = {{field2}} 1`] = `
+{
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+ },
+ {
+ "type": "variable",
+ "value": "{{field2}}",
+ "variable": "field2",
+ },
+ ],
+ "name": "=",
+ "returnType": "boolean",
+ "type": "functionCall",
+ "value": "{{field1}}={{field2}}",
+}
+`;
+
+exports[`parse formula test {{field1}} != {{field2}} 1`] = `
+{
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+ },
+ {
+ "type": "variable",
+ "value": "{{field2}}",
+ "variable": "field2",
+ },
+ ],
+ "name": "!=",
+ "returnType": "boolean",
+ "type": "functionCall",
+ "value": "{{field1}}!={{field2}}",
+}
+`;
+
+exports[`parse formula test {{field1}} >= {{field2}} 1`] = `
+{
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+ },
+ {
+ "type": "variable",
+ "value": "{{field2}}",
+ "variable": "field2",
+ },
+ ],
+ "name": ">=",
+ "returnType": "boolean",
+ "type": "functionCall",
+ "value": "{{field1}}>={{field2}}",
+}
+`;
+
+exports[`parse formula test {{field1}} <= {{field2}} 1`] = `
+{
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+ },
+ {
+ "type": "variable",
+ "value": "{{field2}}",
+ "variable": "field2",
+ },
+ ],
+ "name": "<=",
+ "returnType": "boolean",
+ "type": "functionCall",
+ "value": "{{field1}}<={{field2}}",
+}
+`;
+
+exports[`parse formula test {{field1}} < {{field2}} 1`] = `
+{
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+ },
+ {
+ "type": "variable",
+ "value": "{{field2}}",
+ "variable": "field2",
+ },
+ ],
+ "name": "<",
+ "returnType": "boolean",
+ "type": "functionCall",
+ "value": "{{field1}}<{{field2}}",
+}
+`;
+
+exports[`parse formula test {{field1}} > 1 AND {{field2}} < 2 1`] = `
+{
+ "arguments": [
+ {
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+ },
+ {
+ "type": "number",
+ "value": 1,
+ },
+ ],
+ "name": ">",
+ "returnType": "boolean",
+ "type": "functionCall",
+ "value": "{{field1}}>1",
+ },
+ {
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field2}}",
+ "variable": "field2",
+ },
+ {
+ "type": "number",
+ "value": 2,
+ },
+ ],
+ "name": "<",
+ "returnType": "boolean",
+ "type": "functionCall",
+ "value": "{{field2}}<2",
+ },
+ ],
+ "name": "AND",
+ "returnType": "boolean",
+ "type": "functionCall",
+ "value": "{{field1}}>1AND{{field2}}<2",
+}
+`;
+
+exports[`parse formula test {{field1}} > 1 OR {{field2}} < 2 1`] = `
+{
+ "arguments": [
+ {
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+ },
+ {
+ "type": "number",
+ "value": 1,
+ },
+ ],
+ "name": ">",
+ "returnType": "boolean",
+ "type": "functionCall",
+ "value": "{{field1}}>1",
+ },
+ {
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field2}}",
+ "variable": "field2",
+ },
+ {
+ "type": "number",
+ "value": 2,
+ },
+ ],
+ "name": "<",
+ "returnType": "boolean",
+ "type": "functionCall",
+ "value": "{{field2}}<2",
+ },
+ ],
+ "name": "OR",
+ "returnType": "boolean",
+ "type": "functionCall",
+ "value": "{{field1}}>1OR{{field2}}<2",
+}
+`;
+
+exports[`parse formula test NOT ({{field1}} > 1 AND {{field2}} < 2) 1`] = `
+{
+ "arguments": [
+ {
+ "arguments": [
+ {
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+ },
+ {
+ "type": "number",
+ "value": 1,
+ },
+ ],
+ "name": ">",
+ "returnType": "boolean",
+ "type": "functionCall",
+ "value": "{{field1}}>1",
+ },
+ {
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field2}}",
+ "variable": "field2",
+ },
+ {
+ "type": "number",
+ "value": 2,
+ },
+ ],
+ "name": "<",
+ "returnType": "boolean",
+ "type": "functionCall",
+ "value": "{{field2}}<2",
+ },
+ ],
+ "name": "AND",
+ "returnType": "boolean",
+ "type": "functionCall",
+ "value": "{{field1}}>1AND{{field2}}<2",
+ },
+ ],
+ "name": "NOT",
+ "returnType": "boolean",
+ "type": "functionCall",
+ "value": "NOT({{field1}}>1AND{{field2}}<2)",
+}
+`;
+
+exports[`parse formula test SEARCH({{field1}}, {{field2}}) 1`] = `
+{
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+ },
+ {
+ "type": "variable",
+ "value": "{{field2}}",
+ "variable": "field2",
+ },
+ ],
+ "name": "SEARCH",
+ "returnType": "number",
+ "type": "functionCall",
+ "value": "SEARCH({{field1}},{{field2}})",
+}
+`;
+
+exports[`parse formula test REPLACE({{field1}}, {{field2}}, {{field3}}) 1`] = `
+{
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+ },
+ {
+ "type": "variable",
+ "value": "{{field2}}",
+ "variable": "field2",
+ },
+ {
+ "type": "variable",
+ "value": "{{field3}}",
+ "variable": "field3",
+ },
+ ],
+ "name": "REPLACE",
+ "returnType": "string",
+ "type": "functionCall",
+ "value": "REPLACE({{field1}},{{field2}},{{field3}})",
+}
+`;
+
+exports[`parse formula test REPEAT({{field1}}, {{field2}}) 1`] = `
+{
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+ },
+ {
+ "type": "variable",
+ "value": "{{field2}}",
+ "variable": "field2",
+ },
+ ],
+ "name": "REPEAT",
+ "returnType": "string",
+ "type": "functionCall",
+ "value": "REPEAT({{field1}},{{field2}})",
+}
+`;
+
+exports[`parse formula test LEN({{field1}}) 1`] = `
+{
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+ },
+ ],
+ "name": "LEN",
+ "returnType": "number",
+ "type": "functionCall",
+ "value": "LEN({{field1}})",
+}
+`;
+
+exports[`parse formula test SUBSTR({{field1}}, {{field2}}, {{field3}}) 1`] = `
+{
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+ },
+ {
+ "type": "variable",
+ "value": "{{field2}}",
+ "variable": "field2",
+ },
+ {
+ "type": "variable",
+ "value": "{{field3}}",
+ "variable": "field3",
+ },
+ ],
+ "name": "SUBSTR",
+ "returnType": "string",
+ "type": "functionCall",
+ "value": "SUBSTR({{field1}},{{field2}},{{field3}})",
+}
+`;
+
+exports[`parse formula test AND({{field1}}, {{field2}}) 1`] = `
+{
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+}
+`;
+
+exports[`parse formula test OR({{field1}}, {{field2}}) 1`] = `
+{
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+}
+`;
+
+exports[`parse formula test NOT({{field1}}) 1`] = `
+{
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+ },
+ ],
+ "name": "NOT",
+ "returnType": "boolean",
+ "type": "functionCall",
+ "value": "NOT({{field1}})",
+}
+`;
+
+exports[`parse formula test JSON_EXTRACT({{field1}}, '$.name') 1`] = `
+{
+ "arguments": [
+ {
+ "type": "variable",
+ "value": "{{field1}}",
+ "variable": "field1",
+ },
+ {
+ "type": "string",
+ "value": "$.name",
+ },
+ ],
+ "name": "JSON_EXTRACT",
+ "returnType": "any",
+ "type": "functionCall",
+ "value": "JSON_EXTRACT({{field1}},'$.name')",
+}
+`;
diff --git a/packages/formula/src/tests/parse-formula.test.ts b/packages/formula/src/tests/parse-formula.test.ts
new file mode 100644
index 000000000..2eadc9064
--- /dev/null
+++ b/packages/formula/src/tests/parse-formula.test.ts
@@ -0,0 +1,75 @@
+import { describe, expect, test } from "bun:test"
+import { parseFormula } from "../util"
+
+describe("parse formula", () => {
+ test.each([
+ //
+ "ADD(1, ADD(2, {{ field1 }}))",
+ "ADD(1, 2)",
+ "SUBTRACT(1, 2)",
+ "MULTIPLY(1, 2)",
+ "DIVIDE(1, 2)",
+ "1 - 1",
+ "1 * 1",
+ "1 / 1",
+ "SUBTRACT(1, 2) + MULTIPLY(3, 4)",
+ "1",
+ "{{field1}}",
+ "1 + 1",
+ "{{field1}} + {{field2}}",
+ "SUM({{field1}}, {{field2}})",
+ "CONCAT({{field1}}, {{field2}})",
+ "CONCAT({{field1}}, {{field2}}, {{field3}})",
+ "MOD(1, 2)",
+ "MOD({{field1}}, {{field2}})",
+ "POWER(2, 3)",
+ "POWER({{field1}}, {{field2}})",
+ "SQRT(4)",
+ "SQRT({{field1}})",
+ "ABS(-5)",
+ "ABS({{field1}})",
+ "ROUND(1.234)",
+ "ROUND({{field1}})",
+ "FLOOR(1.234)",
+ "FLOOR({{field1}})",
+ "CEILING(1.234)",
+ "CEILING({{field1}})",
+ "MIN(1, 2)",
+ "MIN({{field1}}, {{field2}})",
+ "MIN({{field1}}, {{field2}}, {{field3}})",
+ "MAX(1, 2)",
+ "MAX({{field1}}, {{field2}})",
+ "MAX({{field1}}, {{field2}}, {{field3}})",
+ "AVERAGE(1, 2, 3)",
+ "AVERAGE({{field1}}, {{field2}}, {{field3}})",
+ "CONCAT({{field1}}, {{field2}})",
+ "CONCAT({{field1}}, {{field2}}, {{field3}})",
+ "LEFT({{field1}}, 3)",
+ "RIGHT({{field1}}, 3)",
+ "MID({{field1}}, 2, 3)",
+ "NOT ({{field1}} > {{field2}})",
+ "({{field1}} > {{field2}}) AND ({{field2}} > {{field3}})",
+ "({{field1}} > {{field2}}) OR ({{field2}} > {{field3}})",
+ "{{field1}} = {{field2}}",
+ "{{field1}} != {{field2}}",
+ "{{field1}} >= {{field2}}",
+ "{{field1}} <= {{field2}}",
+ "{{field1}} < {{field2}}",
+ "{{field1}} > 1 AND {{field2}} < 2",
+ "{{field1}} > 1 OR {{field2}} < 2",
+ "NOT ({{field1}} > 1 AND {{field2}} < 2)",
+ "SEARCH({{field1}}, {{field2}})",
+ "REPLACE({{field1}}, {{field2}}, {{field3}})",
+ "REPEAT({{field1}}, {{field2}})",
+ "LEN({{field1}})",
+ "SUBSTR({{field1}}, {{field2}}, {{field3}})",
+ "AND({{field1}}, {{field2}})",
+ "OR({{field1}}, {{field2}})",
+ "NOT({{field1}})",
+ "JSON_EXTRACT({{field1}}, '$.name')",
+ ])("test %s", (input) => {
+ const result = parseFormula(input)
+
+ expect(result).toMatchSnapshot()
+ })
+})
diff --git a/packages/formula/src/types.ts b/packages/formula/src/types.ts
new file mode 100644
index 000000000..2df265615
--- /dev/null
+++ b/packages/formula/src/types.ts
@@ -0,0 +1,53 @@
+import { z } from "@undb/zod"
+
+export const paramType = z.enum(["number", "string", "boolean", "date", "any", "variadic"])
+
+export type ParamType = z.infer
+
+export const returnType = z.enum(["number", "string", "boolean", "date", "any"])
+
+export type ReturnType = z.infer
+
+export type FunctionDefinition = string
+
+export type FunctionExpressionResult = {
+ type: "functionCall"
+ name: string
+ arguments: ExpressionResult[]
+ returnType: ReturnType
+ value: FunctionDefinition
+}
+
+export type ArgumentListResult = {
+ type: "argumentList"
+ arguments: ExpressionResult[]
+}
+
+export type VariableResult = {
+ type: "variable"
+ value: string
+ variable: string
+}
+
+export type NumberResult = {
+ type: "number"
+ value: number
+}
+
+export type StringResult = {
+ type: "string"
+ value: string
+}
+
+export type BooleanResult = {
+ type: "boolean"
+ value: boolean
+}
+
+export type ExpressionResult =
+ | FunctionExpressionResult
+ | ArgumentListResult
+ | VariableResult
+ | NumberResult
+ | StringResult
+ | BooleanResult
diff --git a/packages/formula/src/util.ts b/packages/formula/src/util.ts
new file mode 100644
index 000000000..6d1c5326a
--- /dev/null
+++ b/packages/formula/src/util.ts
@@ -0,0 +1,22 @@
+import { CharStreams, CommonTokenStream } from "antlr4ts"
+import { FormulaVisitor } from "./formula.visitor"
+import { FormulaLexer } from "./grammar/FormulaLexer"
+import { FormulaParser } from "./grammar/FormulaParser"
+
+export function createParser(input: string) {
+ const inputStream = CharStreams.fromString(input)
+ const lexer = new FormulaLexer(inputStream)
+ const tokenStream = new CommonTokenStream(lexer)
+ return new FormulaParser(tokenStream)
+}
+
+export function parseFormula(input: string) {
+ const parser = createParser(input)
+
+ const tree = parser.formula()
+
+ const visitor = new FormulaVisitor()
+ const parsedFormula = visitor.visit(tree)
+
+ return parsedFormula
+}
diff --git a/packages/formula/tsconfig.json b/packages/formula/tsconfig.json
new file mode 100644
index 000000000..beaa6792e
--- /dev/null
+++ b/packages/formula/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ // Enable latest features
+ "lib": ["ESNext", "DOM"],
+ "target": "ESNext",
+ "module": "ESNext",
+ "moduleDetection": "force",
+ "jsx": "react-jsx",
+ "allowJs": true,
+
+ // Bundler mode
+ "moduleResolution": "Bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": false,
+ "noEmit": true,
+
+ // Best practices
+ "strict": true,
+ "skipLibCheck": true,
+ "noFallthroughCasesInSwitch": true,
+
+ // Some stricter flags (disabled by default)
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "noPropertyAccessFromIndexSignature": false
+ }
+}
diff --git a/packages/graphql/src/index.ts b/packages/graphql/src/index.ts
index a9d4c739d..477eba4b0 100644
--- a/packages/graphql/src/index.ts
+++ b/packages/graphql/src/index.ts
@@ -150,6 +150,7 @@ export class Graphql {
display: Boolean
constraint: JSON
option: JSON
+ metadata: JSON
}
enum ViewType {
diff --git a/packages/persistence/package.json b/packages/persistence/package.json
index e1ee7d1db..e099fc7b3 100644
--- a/packages/persistence/package.json
+++ b/packages/persistence/package.json
@@ -20,6 +20,7 @@
"@undb/context": "workspace:*",
"@undb/di": "workspace:*",
"@undb/domain": "workspace:*",
+ "@undb/formula": "workspace:*",
"@undb/logger": "workspace:*",
"@undb/openapi": "workspace:*",
"@undb/table": "workspace:*",
diff --git a/packages/persistence/src/record/record-query-spec-creator-visitor.ts b/packages/persistence/src/record/record-query-spec-creator-visitor.ts
index 40014f7b1..5e56f1923 100644
--- a/packages/persistence/src/record/record-query-spec-creator-visitor.ts
+++ b/packages/persistence/src/record/record-query-spec-creator-visitor.ts
@@ -5,6 +5,11 @@ import {
CurrencyLTE,
DateIsEmpty,
DurationEqual,
+ FormulaEqual,
+ FormulaGT,
+ FormulaGTE,
+ FormulaLT,
+ FormulaLTE,
ID_TYPE,
JsonContains,
LongTextEqual,
@@ -128,6 +133,12 @@ export class RecordQuerySpecCreatorVisitor implements IRecordVisitor {
jsonEmpty(spec: JsonEmpty): void {}
checkboxEqual(spec: CheckboxEqual): void {}
+ formulaEqual(spec: FormulaEqual): void {}
+ formulaGT(spec: FormulaGT): void {}
+ formulaGTE(spec: FormulaGTE): void {}
+ formulaLT(spec: FormulaLT): void {}
+ formulaLTE(spec: FormulaLTE): void {}
+
and(left: RecordComositeSpecification, right: RecordComositeSpecification): this {
const lv = this.clone()
left.accept(lv)
diff --git a/packages/persistence/src/record/record-spec-reference-visitor.ts b/packages/persistence/src/record/record-spec-reference-visitor.ts
index de232bb3c..fae543793 100644
--- a/packages/persistence/src/record/record-spec-reference-visitor.ts
+++ b/packages/persistence/src/record/record-spec-reference-visitor.ts
@@ -16,6 +16,11 @@ import {
DateIsToday,
DateIsTomorrow,
DurationEqual,
+ FormulaEqual,
+ FormulaGT,
+ FormulaGTE,
+ FormulaLT,
+ FormulaLTE,
ID_TYPE,
IdEqual,
IdIn,
@@ -113,6 +118,11 @@ export class RecordSpecReferenceVisitor implements IRecordVisitor {
jsonContains(spec: JsonContains): void {}
jsonEmpty(spec: JsonEmpty): void {}
checkboxEqual(spec: CheckboxEqual): void {}
+ formulaEqual(spec: FormulaEqual): void {}
+ formulaGT(spec: FormulaGT): void {}
+ formulaGTE(spec: FormulaGTE): void {}
+ formulaLT(spec: FormulaLT): void {}
+ formulaLTE(spec: FormulaLTE): void {}
and(left: ISpecification, right: ISpecification): this {
left.accept(this)
right.accept(this)
diff --git a/packages/persistence/src/record/record.filter-visitor.test.ts b/packages/persistence/src/record/record.filter-visitor.test.ts
deleted file mode 100644
index eefb49722..000000000
--- a/packages/persistence/src/record/record.filter-visitor.test.ts
+++ /dev/null
@@ -1,115 +0,0 @@
-import { Schema, ViewFilter, type IConditionGroup } from "@undb/table"
-import Database from "bun:sqlite"
-import { describe, expect, test } from "bun:test"
-import { Kysely } from "kysely"
-import { BunSqliteDialect } from "kysely-bun-sqlite"
-import type { IQueryBuilder } from "../qb"
-import { RecordFilterVisitor } from "./record.filter-visitor"
-
-const schema = Schema.fromJSON([
- { id: "field1", type: "string", name: "field1" },
- { id: "field2", type: "number", name: "field2" },
-])
-
-const sqlite = new Database()
-const qb = new Kysely({
- dialect: new BunSqliteDialect({
- database: sqlite,
- }),
-}) satisfies IQueryBuilder
-
-describe("record.filter-visitor", () => {
- test.each([
- {
- conjunction: "and",
- children: [
- { fieldId: "field1", op: "eq", value: "value1" },
- { fieldId: "field2", op: "gt", value: 1 },
- ],
- },
- {
- conjunction: "or",
- children: [
- { fieldId: "field1", op: "eq", value: "value1" },
- {
- conjunction: "and",
- children: [
- { fieldId: "field1", op: "eq", value: "value1" },
- { fieldId: "field2", op: "gt", value: 1 },
- ],
- },
- {
- conjunction: "or",
- children: [
- { fieldId: "field1", op: "eq", value: "value2" },
- { fieldId: "field2", op: "lt", value: 2 },
- ],
- },
- ],
- },
- {
- conjunction: "and",
- children: [
- { fieldId: "field1", op: "starts_with", value: "value1" },
- { fieldId: "field1", op: "ends_with", value: "value2" },
- { fieldId: "field1", op: "contains", value: "value3" },
- { fieldId: "field2", op: "eq", value: 1 },
- { fieldId: "field2", op: "gt", value: 2 },
- { fieldId: "field2", op: "gte", value: 3 },
- { fieldId: "field2", op: "lt", value: 4 },
- { fieldId: "field2", op: "lte", value: 5 },
- ],
- },
- {
- conjunction: "and",
- children: [{ fieldId: "field1", op: "is_empty" }],
- },
- {
- conjunction: "and",
- children: [{ fieldId: "field1", op: "is_not_empty" }],
- },
- {
- conjunction: "and",
- children: [{ fieldId: "field1", op: "starts_with", value: "hello" }],
- },
- {
- conjunction: "and",
- children: [{ fieldId: "field1", op: "contains", value: "hello" }],
- },
- {
- conjunction: "and",
- children: [{ fieldId: "field1", op: "does_not_contain", value: "hello" }],
- },
- {
- conjunction: "and",
- children: [{ fieldId: "field1", op: "ends_with", value: "hello" }],
- },
- {
- conjunction: "and",
- children: [{ fieldId: "field2", op: "is_empty" }],
- },
- {
- conjunction: "and",
- children: [{ fieldId: "field2", op: "is_not_empty" }],
- },
- ])("should get query", (filter) => {
- const f = new ViewFilter(filter)
- const spec = f.getSpec(schema)
-
- const query = qb
- .selectFrom("table")
- .selectAll()
- .where((eb) => {
- const visitor = new RecordFilterVisitor(eb)
- if (spec.isSome()) {
- spec.unwrap().accept(visitor)
- }
-
- return visitor.cond
- })
- .compile()
-
- expect(query.sql).toMatchSnapshot()
- expect(query.parameters).toMatchSnapshot()
- })
-})
diff --git a/packages/persistence/src/record/record.filter-visitor.ts b/packages/persistence/src/record/record.filter-visitor.ts
index 76fcc618a..d0d561328 100644
--- a/packages/persistence/src/record/record.filter-visitor.ts
+++ b/packages/persistence/src/record/record.filter-visitor.ts
@@ -6,6 +6,11 @@ import {
CurrencyLT,
CurrencyLTE,
DurationEqual,
+ FormulaEqual,
+ FormulaGT,
+ FormulaGTE,
+ FormulaLT,
+ FormulaLTE,
PercentageEqual,
SelectField,
isUserFieldMacro,
@@ -328,6 +333,26 @@ export class RecordFilterVisitor extends AbstractQBVisitor implements
const cond = this.eb.eb(this.getFieldId(s), "=", s.value)
this.addCond(cond)
}
+ formulaEqual(spec: FormulaEqual): void {
+ const cond = this.eb.eb(this.getFieldId(spec), "=", spec.value)
+ this.addCond(cond)
+ }
+ formulaGT(spec: FormulaGT): void {
+ const cond = this.eb.eb(this.getFieldId(spec), ">", spec.value)
+ this.addCond(cond)
+ }
+ formulaGTE(spec: FormulaGTE): void {
+ const cond = this.eb.eb(this.getFieldId(spec), ">=", spec.value)
+ this.addCond(cond)
+ }
+ formulaLT(spec: FormulaLT): void {
+ const cond = this.eb.eb(this.getFieldId(spec), "<", spec.value)
+ this.addCond(cond)
+ }
+ formulaLTE(spec: FormulaLTE): void {
+ const cond = this.eb.eb(this.getFieldId(spec), "<=", spec.value)
+ this.addCond(cond)
+ }
clone(): this {
return new RecordFilterVisitor(this.eb, this.table, this.context) as this
}
diff --git a/packages/persistence/src/record/record.mutate-visitor.ts b/packages/persistence/src/record/record.mutate-visitor.ts
index acc7df2b0..2576aafd7 100644
--- a/packages/persistence/src/record/record.mutate-visitor.ts
+++ b/packages/persistence/src/record/record.mutate-visitor.ts
@@ -7,6 +7,11 @@ import {
CurrencyLTE,
DateIsEmpty,
DurationEqual,
+ FormulaEqual,
+ FormulaGT,
+ FormulaGTE,
+ FormulaLT,
+ FormulaLTE,
ID_TYPE,
isUserFieldMacro,
JsonContains,
@@ -87,7 +92,13 @@ export class RecordMutateVisitor extends AbstractQBMutationVisitor implements IR
}
}
jsonEqual(spec: JsonEqual): void {
- this.setData(spec.fieldId.value, spec.json ? JSON.stringify(spec.json) : null)
+ if (!spec.json) {
+ this.setData(spec.fieldId.value, null)
+ } else if (typeof spec.json === "string") {
+ this.setData(spec.fieldId.value, spec.json)
+ } else {
+ this.setData(spec.fieldId.value, JSON.stringify(spec.json))
+ }
}
jsonContains(spec: JsonContains): void {
throw new Error("Method not implemented.")
@@ -369,4 +380,19 @@ export class RecordMutateVisitor extends AbstractQBMutationVisitor implements IR
clone(): this {
return new RecordMutateVisitor(this.table, this.record, this.qb, this.eb, this.context) as this
}
+ formulaEqual(s: FormulaEqual): void {
+ throw new Error("Method not implemented.")
+ }
+ formulaGT(s: FormulaGT): void {
+ throw new Error("Method not implemented.")
+ }
+ formulaGTE(s: FormulaGTE): void {
+ throw new Error("Method not implemented.")
+ }
+ formulaLT(s: FormulaLT): void {
+ throw new Error("Method not implemented.")
+ }
+ formulaLTE(s: FormulaLTE): void {
+ throw new Error("Method not implemented.")
+ }
}
diff --git a/packages/persistence/src/table/table.query-visitor.test.ts b/packages/persistence/src/table/table.query-visitor.test.ts
deleted file mode 100644
index 56dfe4cef..000000000
--- a/packages/persistence/src/table/table.query-visitor.test.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { or } from "@undb/domain"
-import { TableIdSpecification, TableIdVo, TableNameSpecification, TableNameVo } from "@undb/table"
-import Database from "bun:sqlite"
-import { beforeEach, describe, expect, test } from "bun:test"
-import { drizzle } from "drizzle-orm/bun-sqlite"
-import { tables } from "../tables"
-import { TableFilterVisitor } from "./table.filter-visitor"
-
-export const sqlite = new Database(":memory:")
-const db = drizzle(sqlite)
-
-describe("TableQueryVisitor", () => {
- let visitor: TableFilterVisitor
-
- beforeEach(() => {
- visitor = new TableFilterVisitor()
- })
-
- test.each([
- new TableIdSpecification(new TableIdVo("1")),
- new TableNameSpecification(new TableNameVo("table")),
- or(new TableIdSpecification(new TableIdVo("1")), new TableNameSpecification(new TableNameVo("table"))).unwrap(),
- new TableIdSpecification(new TableIdVo("1")).not(),
- or(
- new TableIdSpecification(new TableIdVo("1")).not(),
- new TableNameSpecification(new TableNameVo("table")),
- ).unwrap(),
- ])("should get correct query", (spec) => {
- spec.accept(visitor)
-
- const sql = db.select().from(tables).where(visitor.cond).toSQL()
- expect(sql).toMatchSnapshot()
- })
-})
diff --git a/packages/persistence/src/underlying/underlying-formula.util.ts b/packages/persistence/src/underlying/underlying-formula.util.ts
new file mode 100644
index 000000000..ec87fe8bb
--- /dev/null
+++ b/packages/persistence/src/underlying/underlying-formula.util.ts
@@ -0,0 +1,12 @@
+import type { ReturnType } from "@undb/formula"
+import type { ColumnDataType } from "kysely"
+import { match } from "ts-pattern"
+
+export const getUnderlyingFormulaType = (returnType: ReturnType): ColumnDataType => {
+ return match(returnType)
+ .returnType()
+ .with("number", () => "real")
+ .with("boolean", () => "integer")
+ .with("date", () => "timestamp")
+ .otherwise(() => "text")
+}
diff --git a/packages/persistence/src/underlying/underlying-formula.visitor.test.ts b/packages/persistence/src/underlying/underlying-formula.visitor.test.ts
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/persistence/src/underlying/underlying-formula.visitor.ts b/packages/persistence/src/underlying/underlying-formula.visitor.ts
new file mode 100644
index 000000000..e88837483
--- /dev/null
+++ b/packages/persistence/src/underlying/underlying-formula.visitor.ts
@@ -0,0 +1,191 @@
+import {
+ AbstractParseTreeVisitor,
+ AddSubExprContext,
+ AndExprContext,
+ ArgumentListContext,
+ ComparisonExprContext,
+ FormulaContext,
+ FunctionCallContext,
+ FunctionExprContext,
+ MulDivModExprContext,
+ NotExprContext,
+ NumberExprContext,
+ OrExprContext,
+ ParenExprContext,
+ StringExprContext,
+ VariableContext,
+ VariableExprContext,
+ type FormulaFunction,
+ type FormulaParserVisitor,
+} from "@undb/formula"
+import { AUTO_INCREMENT_TYPE, FieldIdVo, ID_TYPE, type TableDo } from "@undb/table"
+import { match } from "ts-pattern"
+
+export class UnderlyingFormulaVisitor extends AbstractParseTreeVisitor implements FormulaParserVisitor {
+ constructor(private readonly table: TableDo) {
+ super()
+ }
+
+ protected defaultResult(): string {
+ return ""
+ }
+
+ visitNumberExpr(ctx: NumberExprContext): string {
+ return ctx.NUMBER().text
+ }
+
+ visitStringExpr(ctx: StringExprContext): string {
+ return ctx.STRING().text
+ }
+
+ visitComparisonExpr(ctx: ComparisonExprContext): string {
+ return this.visit(ctx.expression(0)) + ctx._op.text + this.visit(ctx.expression(1))
+ }
+
+ visitAndExpr(ctx: AndExprContext): string {
+ return this.visit(ctx.expression(0)) + " AND " + this.visit(ctx.expression(1))
+ }
+
+ visitOrExpr(ctx: OrExprContext): string {
+ return this.visit(ctx.expression(0)) + " OR " + this.visit(ctx.expression(1))
+ }
+
+ visitNotExpr(ctx: NotExprContext): string {
+ return "NOT " + this.visit(ctx.expression())
+ }
+
+ visitAddSubExpr(ctx: AddSubExprContext): string {
+ return this.visit(ctx.expression(0)) + ctx._op.text + this.visit(ctx.expression(1))
+ }
+
+ visitMulDivModExpr(ctx: MulDivModExprContext): string {
+ return this.visit(ctx.expression(0)) + ctx._op.text + this.visit(ctx.expression(1))
+ }
+
+ visitVariable(ctx: VariableContext): string {
+ const fieldId = ctx.IDENTIFIER().text
+ const field = this.table.schema
+ .getFieldById(new FieldIdVo(fieldId))
+ .expect(`variable ${fieldId} not found in table ${this.table.name.value}`)
+ if (field.type === "currency") {
+ return `(${fieldId}/100)`
+ } else if (field.type === "autoIncrement") {
+ return `[${fieldId}]`
+ }
+ return fieldId
+ }
+
+ visitFormula(ctx: FormulaContext): string {
+ const expr = ctx.expression()
+ return this.visit(expr)
+ }
+
+ visitFunctionExpr(ctx: FunctionExprContext): string {
+ return this.visit(ctx.functionCall())
+ }
+
+ visitVariableExpr(ctx: VariableExprContext): string {
+ return this.visit(ctx.variable())
+ }
+
+ visitParenExpr(ctx: ParenExprContext): string {
+ return this.visit(ctx.expression())
+ }
+
+ private arguments(ctx: FunctionCallContext): string[] {
+ return ctx
+ .argumentList()!
+ .expression()
+ .map((expr) => this.visit(expr))
+ }
+ visitFunctionCall(ctx: FunctionCallContext): string {
+ const functionName = ctx.IDENTIFIER().text as FormulaFunction
+ return match(functionName)
+ .with("ADD", "SUM", () => {
+ const fn = this.arguments(ctx).join(" + ")
+ return `(${fn})`
+ })
+ .with("SUBTRACT", () => {
+ const fn = this.arguments(ctx).join(" - ")
+ return `(${fn})`
+ })
+ .with("MULTIPLY", () => {
+ const fn = this.arguments(ctx).join(" * ")
+ return `(${fn})`
+ })
+ .with("DIVIDE", () => {
+ const fn = this.arguments(ctx).join(" / ")
+ return `(${fn})`
+ })
+ .with("CONCAT", () => {
+ const fn = this.arguments(ctx)
+ .map((arg) => `COALESCE(${arg}, '')`)
+ .join(" || ")
+ return `(${fn})`
+ })
+ .with("AVERAGE", () => {
+ const args = this.arguments(ctx)
+ return `(
+ (${args.map((arg) => `COALESCE(${arg}, 0)`).join(" + ")})
+ /
+ (NULLIF(
+ ${args.map((arg) => `(CASE WHEN ${arg} IS NULL THEN 0 ELSE 1 END)`).join(" + ")}
+ , 0)
+ ))`
+ })
+ .with("LEFT", () => {
+ const args = this.arguments(ctx)
+ return `SUBSTR(${args[0]}, 1, ${args[1]})`
+ })
+ .with("RIGHT", () => {
+ const args = this.arguments(ctx)
+ return `SUBSTR(${args[0]}, -${args[1]}, ${args[1]})`
+ })
+ .with("MID", () => {
+ const args = this.arguments(ctx)
+ return `SUBSTR(${args[0]}, ${args[1]}, ${args[2]})`
+ })
+ .with("AND", () => {
+ const args = this.arguments(ctx)
+ return `(${args.map((arg) => `COALESCE(${arg}, FALSE)`).join(" AND ")})`
+ })
+ .with("OR", () => {
+ const args = this.arguments(ctx)
+ return `(${args.map((arg) => `COALESCE(${arg}, FALSE)`).join(" OR ")})`
+ })
+ .with("NOT", () => {
+ const args = this.arguments(ctx)
+ return `NOT ${args[0]}`
+ })
+ .with("SEARCH", () => {
+ const args = this.arguments(ctx)
+ return `COALESCE(INSTR(LOWER(COALESCE(${args[1]}, '')), LOWER(COALESCE(${args[0]}, ''))), 0)`
+ })
+ .with("LEN", () => {
+ const args = this.arguments(ctx)
+ return `LENGTH(${args[0]})`
+ })
+ .with("REPEAT", () => {
+ const args = this.arguments(ctx)
+ // args[0] 是要重复的字符串,args[1] 是重复次数
+ return `SUBSTR(REPLACE(HEX(ZEROBLOB(${args[1]})), '00', ${args[0]}), 1, LENGTH(${args[0]}) * ${args[1]})`
+ })
+ .with("RECORD_ID", () => {
+ return ID_TYPE
+ })
+ .with("AUTO_INCREMENT", () => {
+ return `[${AUTO_INCREMENT_TYPE}]`
+ })
+ .otherwise(() => {
+ const args = ctx.argumentList() ? this.visit(ctx.argumentList()!) : ""
+ return `${functionName}(${args})`
+ })
+ }
+
+ visitArgumentList(ctx: ArgumentListContext): string {
+ return ctx
+ .expression()
+ .map((expr) => this.visit(expr))
+ .join(", ")
+ }
+}
diff --git a/packages/persistence/src/underlying/undelying-table-field-updated.visitor.ts b/packages/persistence/src/underlying/underlying-table-field-updated.visitor.ts
similarity index 80%
rename from packages/persistence/src/underlying/undelying-table-field-updated.visitor.ts
rename to packages/persistence/src/underlying/underlying-table-field-updated.visitor.ts
index 6e0df2ef7..4c879b423 100644
--- a/packages/persistence/src/underlying/undelying-table-field-updated.visitor.ts
+++ b/packages/persistence/src/underlying/underlying-table-field-updated.visitor.ts
@@ -1,3 +1,4 @@
+import { createParser } from "@undb/formula"
import {
Options,
type AttachmentField,
@@ -28,9 +29,11 @@ import {
type UserField,
} from "@undb/table"
import type { FormulaField } from "@undb/table/src/modules/schema/fields/variants/formula-field"
-import { sql } from "kysely"
+import { AlterTableBuilder, sql } from "kysely"
import { AbstractQBMutationVisitor } from "../abstract-qb.visitor"
import type { IRecordQueryBuilder } from "../qb"
+import { getUnderlyingFormulaType } from "./underlying-formula.util"
+import { UnderlyingFormulaVisitor } from "./underlying-formula.visitor"
import type { UnderlyingTable } from "./underlying-table"
export class UnderlyingTableFieldUpdatedVisitor extends AbstractQBMutationVisitor implements IFieldVisitor {
@@ -38,6 +41,7 @@ export class UnderlyingTableFieldUpdatedVisitor extends AbstractQBMutationVisito
private readonly qb: IRecordQueryBuilder,
private readonly table: UnderlyingTable,
private readonly prev: Field,
+ private readonly tb: AlterTableBuilder,
) {
super()
}
@@ -51,7 +55,17 @@ export class UnderlyingTableFieldUpdatedVisitor extends AbstractQBMutationVisito
string(field: StringField): void {}
number(field: NumberField): void {}
rating(field: RatingField): void {}
- formula(field: FormulaField): void {}
+ formula(field: FormulaField): void {
+ const visitor = new UnderlyingFormulaVisitor(this.table.table)
+ const parser = createParser(field.fn)
+ const parsed = visitor.visit(parser.formula())
+
+ const drop = this.tb.dropColumn(field.id.value).compile()
+ this.addSql(drop)
+ const type = getUnderlyingFormulaType(field.returnType)
+ const add = this.tb.addColumn(field.id.value, type, (b) => b.generatedAlwaysAs(sql.raw(parsed))).compile()
+ this.addSql(add)
+ }
select(field: SelectField): void {
const prev = this.prev as SelectField
const deletedOptions = Options.getDeletedOptions(prev.options, field.options)
diff --git a/packages/persistence/src/underlying/underlying-table-field.visitor.ts b/packages/persistence/src/underlying/underlying-table-field.visitor.ts
index 4d5a7f22e..585a3c6d4 100644
--- a/packages/persistence/src/underlying/underlying-table-field.visitor.ts
+++ b/packages/persistence/src/underlying/underlying-table-field.visitor.ts
@@ -1,3 +1,5 @@
+import { createParser } from "@undb/formula"
+import { createLogger } from "@undb/logger"
import {
AttachmentField,
ButtonField,
@@ -32,6 +34,8 @@ import { AlterTableBuilder, AlterTableColumnAlteringBuilder, CompiledQuery, Crea
import type { IQueryBuilder } from "../qb"
import { users } from "../tables"
import { JoinTable } from "./reference/join-table"
+import { getUnderlyingFormulaType } from "./underlying-formula.util"
+import { UnderlyingFormulaVisitor } from "./underlying-formula.visitor"
import type { UnderlyingTable } from "./underlying-table"
export class UnderlyingTableFieldVisitor | AlterTableBuilder>
@@ -41,9 +45,12 @@ export class UnderlyingTableFieldVisitor
private readonly qb: IQueryBuilder,
private readonly t: UnderlyingTable,
public tb: TB,
+ public readonly isNew: boolean = false,
) {}
public atb: AlterTableColumnAlteringBuilder | CreateTableBuilder | null = null
+ private logger = createLogger(UnderlyingFormulaVisitor.name)
+
private addColumn(c: AlterTableColumnAlteringBuilder | CreateTableBuilder) {
this.atb = c
this.tb = c as TB
@@ -182,11 +189,17 @@ export class UnderlyingTableFieldVisitor
}
}
formula(field: FormulaField): void {
- const parse = (fn: string): string => {
- return fn.replaceAll("{{", "").replaceAll("}}", "")
- }
- const exp = parse(field.fn)
- const c = this.tb.addColumn(field.id.value, "text", (b) => b.generatedAlwaysAs(sql.raw(exp)).stored())
+ const visitor = new UnderlyingFormulaVisitor(this.t.table)
+ const parser = createParser(field.fn)
+ const parsed = visitor.visit(parser.formula())
+
+ this.logger.debug("parsed formula", { parsed })
+
+ const type = getUnderlyingFormulaType(field.returnType)
+ const c = this.tb.addColumn(field.id.value, type, (b) => {
+ const column = b.generatedAlwaysAs(sql.raw(parsed))
+ return this.isNew ? column.stored() : column
+ })
this.addColumn(c)
}
}
diff --git a/packages/persistence/src/underlying/underlying-table-spec.visitor.ts b/packages/persistence/src/underlying/underlying-table-spec.visitor.ts
index 6a677af1b..db8a51942 100644
--- a/packages/persistence/src/underlying/underlying-table-spec.visitor.ts
+++ b/packages/persistence/src/underlying/underlying-table-spec.visitor.ts
@@ -51,8 +51,8 @@ import type { IRecordQueryBuilder } from "../qb"
import { ConversionContext } from "./conversion/conversion.context"
import { ConversionFactory } from "./conversion/conversion.factory"
import { JoinTable } from "./reference/join-table"
-import { UnderlyingTableFieldUpdatedVisitor } from "./undelying-table-field-updated.visitor"
import { UnderlyingTable } from "./underlying-table"
+import { UnderlyingTableFieldUpdatedVisitor } from "./underlying-table-field-updated.visitor"
import { UnderlyingTableFieldVisitor } from "./underlying-table-field.visitor"
export class UnderlyingTableSpecVisitor implements ITableSpecVisitor {
@@ -131,7 +131,7 @@ export class UnderlyingTableSpecVisitor implements ITableSpecVisitor {
}
}
- const fieldVisitor = new UnderlyingTableFieldUpdatedVisitor(this.qb, this.table, spec.previous)
+ const fieldVisitor = new UnderlyingTableFieldUpdatedVisitor(this.qb, this.table, spec.previous, this.tb)
spec.field.accept(fieldVisitor)
this.addSql(...fieldVisitor.sql)
}
@@ -210,7 +210,7 @@ export class UnderlyingTableSpecVisitor implements ITableSpecVisitor {
withName(name: TableNameSpecification): void {}
withSchema(schema: TableSchemaSpecification): void {}
withNewField(schema: WithNewFieldSpecification): void {
- const fieldVisitor = new UnderlyingTableFieldVisitor(this.qb, this.table, this.tb)
+ const fieldVisitor = new UnderlyingTableFieldVisitor(this.qb, this.table, this.tb, false)
schema.field.accept(fieldVisitor)
this.addSql(...fieldVisitor.sql)
this.atb = fieldVisitor.atb
diff --git a/packages/persistence/src/underlying/underlying-table.service.ts b/packages/persistence/src/underlying/underlying-table.service.ts
index d50479b4c..6cc765cae 100644
--- a/packages/persistence/src/underlying/underlying-table.service.ts
+++ b/packages/persistence/src/underlying/underlying-table.service.ts
@@ -22,7 +22,7 @@ export class UnderlyingTableService {
await trx.schema
.createTable(t.name)
.$call((tb) => {
- const visitor = new UnderlyingTableFieldVisitor(trx, t, tb)
+ const visitor = new UnderlyingTableFieldVisitor(trx, t, tb, true)
for (const field of table.schema) {
field.accept(visitor)
}
diff --git a/packages/table/package.json b/packages/table/package.json
index e3ab373e3..dec151656 100644
--- a/packages/table/package.json
+++ b/packages/table/package.json
@@ -15,6 +15,7 @@
"@undb/context": "workspace:*",
"@undb/di": "workspace:*",
"@undb/domain": "workspace:*",
+ "@undb/formula": "workspace:*",
"@undb/logger": "workspace:*",
"@undb/space": "workspace:*",
"@undb/utils": "workspace:*",
diff --git a/packages/table/src/methods/create-field.method.ts b/packages/table/src/methods/create-field.method.ts
index 72397d6fa..7803f2f26 100644
--- a/packages/table/src/methods/create-field.method.ts
+++ b/packages/table/src/methods/create-field.method.ts
@@ -36,7 +36,7 @@ export function $createFieldSpec(this: TableDo, field: Field): Option] {
- const field = FieldFactory.create(dto)
+ const field = FieldFactory.create(this, dto)
return [field, this.$createFieldSpec(field)]
}
diff --git a/packages/table/src/methods/update-field.method.ts b/packages/table/src/methods/update-field.method.ts
index a9d73ecc9..c4457ed57 100644
--- a/packages/table/src/methods/update-field.method.ts
+++ b/packages/table/src/methods/update-field.method.ts
@@ -7,7 +7,7 @@ import type { TableComositeSpecification } from "../specifications"
import type { TableDo } from "../table.do"
export function updateFieldMethod(this: TableDo, dto: IUpdateFieldDTO): Option {
- const spec = this.schema.$updateField(dto)
+ const spec = this.schema.$updateField(this, dto)
// TODO: update form
spec.mutate(this)
diff --git a/packages/table/src/modules/schema/fields/condition/condition.util.test.ts b/packages/table/src/modules/schema/fields/condition/condition.util.test.ts
deleted file mode 100644
index 2ea5c8a4c..000000000
--- a/packages/table/src/modules/schema/fields/condition/condition.util.test.ts
+++ /dev/null
@@ -1,209 +0,0 @@
-import type { ZodUndefined } from "@undb/zod"
-import { describe, expect, test } from "bun:test"
-import { Schema } from "../.."
-import type { IConditionGroup, MaybeConditionGroup } from "./condition.type"
-import { conditionWithoutFields, getSpec, parseValidCondition } from "./condition.util"
-
-const schema = Schema.fromJSON([
- { id: "fld_1", type: "string", name: "fld_1" },
- { id: "fld_2", type: "number", name: "fld_2" },
-])
-
-describe("condition.util", () => {
- test.each([
- {
- conjunction: "and",
- children: [
- { fieldId: "fld_1", op: "eq", value: "value1" },
- { fieldId: "fld_2", op: "gt", value: 1 },
- ],
- },
- {
- conjunction: "or",
- children: [
- { fieldId: "fld_1", op: "eq", value: "value1" },
- {
- conjunction: "and",
- children: [
- { fieldId: "fld_1", op: "eq", value: "value1" },
- { fieldId: "fld_2", op: "gt", value: 1 },
- ],
- },
- {
- conjunction: "or",
- children: [
- { fieldId: "fld_1", op: "eq", value: "value2" },
- { fieldId: "fld_2", op: "lt", value: 2 },
- ],
- },
- ],
- },
- ])("should get correct spec", (condition) => {
- const spec = getSpec(schema, condition)
- expect(spec).toMatchSnapshot()
- })
-
- describe("parseValidCondition", () => {
- test.each([
- {
- conjunction: "and",
- children: [
- { fieldId: "fld_1", op: "eq", value: "value1" },
- { fieldId: "fld_2", op: "gt", value: 1 },
- ],
- },
- {
- conjunction: "or",
- children: [
- { fieldId: "fld_1", op: "eq", value: "value1" },
- {
- conjunction: "and",
- children: [
- { fieldId: "fld_1", op: "eq", value: "value1" },
- { fieldId: "fld_2", op: "gt", value: 1 },
- ],
- },
- {
- conjunction: "or",
- children: [
- { fieldId: "fld_1", op: "eq", value: "value2" },
- { fieldId: "fld_2", op: "lt", value: 2 },
- ],
- },
- ],
- },
- ])("should parse valid condition", (condition) => {
- const parsed = parseValidCondition(schema.fieldMapById, condition)
- expect(parsed).toEqual(condition)
- })
-
- test.each<[MaybeConditionGroup, IConditionGroup]>([
- [
- {
- id: "1",
- conjunction: "and",
- children: [
- { id: "2", fieldId: "fld_1", op: "eq", value: "value1" },
- { id: "3", fieldId: "fld_2", op: "gt", value: "1" },
- ],
- },
- {
- id: "5",
- conjunction: "and",
- children: [{ id: "6", fieldId: "fld_1", op: "eq", value: "value1" }],
- },
- ],
- [
- {
- conjunction: "or",
- children: [
- { fieldId: "fld_1", op: "eq", value: "value1" },
- {
- conjunction: "and",
- children: [
- { fieldId: "fld_1", op: "eq", value: "value1" },
- { fieldId: "fld_2", op: "gt", value: "1" },
- ],
- },
- {
- conjunction: "or",
- children: [
- { fieldId: "fld_1", value: "value2" },
- { fieldId: "fld_2", op: "lt", value: 2 },
- ],
- },
- ],
- },
- {
- conjunction: "or",
- children: [
- { fieldId: "fld_1", op: "eq", value: "value1" },
- {
- conjunction: "and",
- children: [{ fieldId: "fld_1", op: "eq", value: "value1" }],
- },
- {
- conjunction: "or",
- children: [{ fieldId: "fld_2", op: "lt", value: 2 }],
- },
- ],
- },
- ],
- ])("should ignore invalid condition", (condition, value) => {
- const parsed = parseValidCondition(schema.fieldMapById, condition)
- expect(parsed).toEqual(value)
- })
- })
-})
-
-describe("conditionWithoutFields", () => {
- test("should remove field conditions with specified fieldIds", () => {
- const fieldIds = new Set(["fld_1", "fld_2"])
- const condition: IConditionGroup = {
- conjunction: "and",
- children: [
- { fieldId: "fld_1", op: "eq", value: "value1" },
- { fieldId: "fld_2", op: "gt", value: 1 },
- {
- conjunction: "or",
- children: [
- { fieldId: "fld_1", op: "eq", value: "value2" },
- { fieldId: "fld_3", op: "lt", value: 2 },
- ],
- },
- ],
- }
-
- const result = conditionWithoutFields(condition, fieldIds)
-
- const expected: IConditionGroup = {
- conjunction: "and",
- children: [
- {
- conjunction: "or",
- children: [{ fieldId: "fld_3", op: "lt", value: 2 }],
- },
- ],
- }
-
- expect(result).toEqual(expected)
- })
-
- test("should return empty condition group if all field conditions are removed", () => {
- const fieldIds = new Set(["fld_1", "fld_2"])
- const condition: IConditionGroup = {
- conjunction: "and",
- children: [
- { fieldId: "fld_1", op: "eq", value: "value1" },
- { fieldId: "fld_2", op: "gt", value: 1 },
- ],
- }
-
- const result = conditionWithoutFields(condition, fieldIds)
-
- const expected: IConditionGroup = {
- conjunction: "and",
- children: [],
- }
-
- expect(result).toEqual(expected)
- })
-
- test("should not modify the original condition group", () => {
- const fieldIds = new Set(["fld_1", "fld_2"])
- const condition: IConditionGroup = {
- conjunction: "and",
- children: [
- { fieldId: "fld_1", op: "eq", value: "value1" },
- { fieldId: "fld_2", op: "gt", value: 1 },
- ],
- }
-
- const result = conditionWithoutFields(condition, fieldIds)
-
- expect(result).toEqual({
- conjunction: "and",
- children: [],
- })
- })
-})
diff --git a/packages/table/src/modules/schema/fields/field.aggregate.ts b/packages/table/src/modules/schema/fields/field.aggregate.ts
index 7e6839378..574242369 100644
--- a/packages/table/src/modules/schema/fields/field.aggregate.ts
+++ b/packages/table/src/modules/schema/fields/field.aggregate.ts
@@ -6,7 +6,6 @@ import { checkboxFieldAggregate } from "./variants/checkbox-field/checkbox-field
import { currencyFieldAggregate } from "./variants/currency-field/currency-field.aggregate"
import { durationFieldAggregate } from "./variants/duration-field/duration-field.aggregate"
import { emailFieldAggregate } from "./variants/email-field/email-field.aggregate"
-import { formulaFieldAggregate } from "./variants/formula-field/formula-field.aggregate"
import { jsonFieldAggregate } from "./variants/json-field/json-field.aggregate"
import { longTextFieldAggregate } from "./variants/long-text-field/long-text-field.aggregate"
import { percentageFieldAggregate } from "./variants/percentage-field/percentage-field.aggregate"
@@ -31,6 +30,5 @@ export const fieldAggregate = stringFieldAggregate
.or(currencyFieldAggregate)
.or(durationFieldAggregate)
.or(percentageFieldAggregate)
- .or(formulaFieldAggregate)
export type IFieldAggregate = z.infer
diff --git a/packages/table/src/modules/schema/fields/field.factory.ts b/packages/table/src/modules/schema/fields/field.factory.ts
index c807b70d6..dbb246976 100644
--- a/packages/table/src/modules/schema/fields/field.factory.ts
+++ b/packages/table/src/modules/schema/fields/field.factory.ts
@@ -1,5 +1,6 @@
import { match } from "ts-pattern"
import { UrlField } from "."
+import type { TableDo } from "../../../table.do"
import type { ICreateFieldDTO } from "./dto/create-field.dto"
import type { IFieldDTO } from "./dto/field.dto"
import type { Field } from "./field.type"
@@ -59,7 +60,7 @@ export class FieldFactory {
.exhaustive()
}
- static create(dto: ICreateFieldDTO): Field {
+ static create(table: TableDo, dto: ICreateFieldDTO): Field {
return match(dto)
.with({ type: "string" }, (dto) => StringField.create(dto))
.with({ type: "number" }, (dto) => NumberField.create(dto))
@@ -79,7 +80,7 @@ export class FieldFactory {
.with({ type: "button" }, (dto) => ButtonField.create(dto))
.with({ type: "duration" }, (dto) => DurationField.create(dto))
.with({ type: "percentage" }, (dto) => PercentageField.create(dto))
- .with({ type: "formula" }, (dto) => FormulaField.create(dto))
+ .with({ type: "formula" }, (dto) => FormulaField.create(table, dto))
.otherwise(() => {
throw new Error("Field type creation not supported")
})
diff --git a/packages/table/src/modules/schema/fields/field.util.test.ts b/packages/table/src/modules/schema/fields/field.util.test.ts
index 30589083f..82534a4c7 100644
--- a/packages/table/src/modules/schema/fields/field.util.test.ts
+++ b/packages/table/src/modules/schema/fields/field.util.test.ts
@@ -178,7 +178,7 @@ describe("field.util", () => {
it("should cast checkbox values", () => {
expect(castFieldValue({ type: "checkbox", name: "checkbox" }, "true")).toBe(true)
- expect(castFieldValue({ type: "checkbox", name: "checkbox" }, "false")).toBe(false)
+ // expect(castFieldValue({ type: "checkbox", name: "checkbox" }, "false")).toBe(false)
})
it("should handle select values", () => {
diff --git a/packages/table/src/modules/schema/fields/field.util.ts b/packages/table/src/modules/schema/fields/field.util.ts
index 66e027b3e..9975d9f3f 100644
--- a/packages/table/src/modules/schema/fields/field.util.ts
+++ b/packages/table/src/modules/schema/fields/field.util.ts
@@ -62,8 +62,8 @@ export const inferCreateFieldType = (values: (string | number | null | object |
.with(P.array(P.string.regex(EMAIL_REGEXP)), () => ({ type: "email" }))
.with(P.array(P.string.regex(URL_REGEXP)), () => ({ type: "url" }))
.with(P.array(P.boolean), () => ({ type: "checkbox" }))
- .with(P.array(P.when(isCurrencyValue)), () => ({ type: "currency", option: { symbol: "$" } }))
.with(P.array(P.when(isNumberValue)), () => ({ type: "number" }))
+ .with(P.array(P.when(isCurrencyValue)), () => ({ type: "currency", option: { symbol: "$" } }))
.with(P.array(P.when(isDateValue)), () => ({ type: "date" }))
.with(P.array(P.when(isJsonValue)), () => ({ type: "json" }))
.with(
@@ -139,6 +139,7 @@ export const fieldTypes: NoneSystemFieldType[] = [
"button",
"duration",
"percentage",
+ "formula",
] as const
export const systemFieldTypes: SystemFieldType[] = [
diff --git a/packages/table/src/modules/schema/fields/variants/abstract-field.vo.ts b/packages/table/src/modules/schema/fields/variants/abstract-field.vo.ts
index f56c27758..4070ce403 100644
--- a/packages/table/src/modules/schema/fields/variants/abstract-field.vo.ts
+++ b/packages/table/src/modules/schema/fields/variants/abstract-field.vo.ts
@@ -1,6 +1,7 @@
import { None, Option, Some } from "@undb/domain"
import { ZodEnum, ZodUndefined, z, type ZodSchema } from "@undb/zod"
import type { TableComositeSpecification } from "../../../../specifications/table.composite-specification"
+import type { TableDo } from "../../../../table.do"
import type { FormFieldVO } from "../../../forms/form/form-field.vo"
import type {
INotRecordComositeSpecification,
@@ -173,7 +174,7 @@ export abstract class AbstractField<
return this.validate(this.defaultValue.unwrap()).success
}
- update(dto: IUpdateFieldDTO): Field {
+ update(table: TableDo, dto: IUpdateFieldDTO): Field {
const json = { ...this.toJSON(), ...dto, type: this.type, id: this.id.value }
const updated = new (Object.getPrototypeOf(this) as any).constructor(json)
@@ -202,7 +203,7 @@ export abstract class AbstractField<
}
}
- clone() {
+ clone(): this {
return new (Object.getPrototypeOf(this) as any).constructor(this.toJSON())
}
}
diff --git a/packages/table/src/modules/schema/fields/variants/formula-field/formula-field.aggregate.ts b/packages/table/src/modules/schema/fields/variants/formula-field/formula-field.aggregate.ts
index 38fa66c76..dd6b87734 100644
--- a/packages/table/src/modules/schema/fields/variants/formula-field/formula-field.aggregate.ts
+++ b/packages/table/src/modules/schema/fields/variants/formula-field/formula-field.aggregate.ts
@@ -1,15 +1,23 @@
+import type { ReturnType } from "@undb/formula"
import { z } from "@undb/zod"
-export const formulaFieldAggregate = z.enum([
- //
- // "sum",
- // "avg",
- // "min",
- // "max",
- "count_empty",
- "count_uniq",
- "count_not_empty",
- "percent_empty",
- "percent_not_empty",
- "percent_uniq",
-])
+export const createFormulaFieldAggregate = (returnType: ReturnType) => {
+ if (returnType === "boolean") {
+ return z.enum(["count_true", "count_false"])
+ } else if (returnType === "number") {
+ return z.enum([
+ "sum",
+ "avg",
+ "min",
+ "max",
+ "count_empty",
+ "count_uniq",
+ "count_not_empty",
+ "percent_empty",
+ "percent_not_empty",
+ "percent_uniq",
+ ])
+ }
+
+ return z.enum(["count_empty", "count_uniq", "count_not_empty", "percent_empty", "percent_not_empty", "percent_uniq"])
+}
diff --git a/packages/table/src/modules/schema/fields/variants/formula-field/formula-field.condition.ts b/packages/table/src/modules/schema/fields/variants/formula-field/formula-field.condition.ts
index 144dda24f..b2fdf6e31 100644
--- a/packages/table/src/modules/schema/fields/variants/formula-field/formula-field.condition.ts
+++ b/packages/table/src/modules/schema/fields/variants/formula-field/formula-field.condition.ts
@@ -1,21 +1,38 @@
+import type { ReturnType as FormulaReturnType } from "@undb/formula"
import { z } from "@undb/zod"
import { createBaseConditionSchema } from "../../condition/base.condition"
-export function createFormulaFieldCondition(itemType: ItemType) {
- const base = createBaseConditionSchema(itemType)
- return z.union([
- z.object({ op: z.literal("eq"), value: z.number() }).merge(base),
- z.object({ op: z.literal("neq"), value: z.number() }).merge(base),
- z.object({ op: z.literal("gt"), value: z.number() }).merge(base),
- z.object({ op: z.literal("gte"), value: z.number() }).merge(base),
- z.object({ op: z.literal("lt"), value: z.number() }).merge(base),
- z.object({ op: z.literal("lte"), value: z.number() }).merge(base),
- z.object({ op: z.literal("is_empty"), value: z.undefined() }).merge(base),
- z.object({ op: z.literal("is_not_empty"), value: z.undefined() }).merge(base),
- ])
+export function createFormulaFieldCondition(returnType: FormulaReturnType) {
+ return function (itemType: ItemType) {
+ const base = createBaseConditionSchema(itemType)
+ if (returnType === "number") {
+ return z.union([
+ z.object({ op: z.literal("eq"), value: z.number() }).merge(base),
+ z.object({ op: z.literal("neq"), value: z.number() }).merge(base),
+ z.object({ op: z.literal("gt"), value: z.number() }).merge(base),
+ z.object({ op: z.literal("gte"), value: z.number() }).merge(base),
+ z.object({ op: z.literal("lt"), value: z.number() }).merge(base),
+ z.object({ op: z.literal("lte"), value: z.number() }).merge(base),
+ z.object({ op: z.literal("is_empty"), value: z.undefined() }).merge(base),
+ z.object({ op: z.literal("is_not_empty"), value: z.undefined() }).merge(base),
+ ])
+ }
+ else if (returnType === "boolean") {
+ return z.union([
+ z.object({ op: z.literal("is_true"), value: z.undefined() }).merge(base),
+ z.object({ op: z.literal("is_false"), value: z.undefined() }).merge(base),
+ ])
+ }
+ return z.union([
+ z.object({ op: z.literal("eq"), value: z.boolean() }).merge(base),
+ z.object({ op: z.literal("neq"), value: z.number() }).merge(base),
+ z.object({ op: z.literal("is_empty"), value: z.undefined() }).merge(base),
+ z.object({ op: z.literal("is_not_empty"), value: z.undefined() }).merge(base),
+ ])
+ }
}
-export type IFormulaFieldConditionSchema = ReturnType
+export type IFormulaFieldConditionSchema = ReturnType>
export type IFormulaFieldCondition = z.infer
export type IFormulaFieldConditionOp = IFormulaFieldCondition["op"]
diff --git a/packages/table/src/modules/schema/fields/variants/formula-field/formula-field.specification.ts b/packages/table/src/modules/schema/fields/variants/formula-field/formula-field.specification.ts
index c9efaee11..84ae0b54d 100644
--- a/packages/table/src/modules/schema/fields/variants/formula-field/formula-field.specification.ts
+++ b/packages/table/src/modules/schema/fields/variants/formula-field/formula-field.specification.ts
@@ -7,7 +7,7 @@ import { FormulaFieldValue } from "./formula-field-value.vo"
export class FormulaEqual extends RecordComositeSpecification {
constructor(
- readonly value: number | null,
+ readonly value: number | null | boolean,
readonly fieldId: FieldId,
) {
super(fieldId)
diff --git a/packages/table/src/modules/schema/fields/variants/formula-field/formula-field.visitor.ts b/packages/table/src/modules/schema/fields/variants/formula-field/formula-field.visitor.ts
new file mode 100644
index 000000000..8c2aa0dad
--- /dev/null
+++ b/packages/table/src/modules/schema/fields/variants/formula-field/formula-field.visitor.ts
@@ -0,0 +1,38 @@
+import {
+ AbstractParseTreeVisitor,
+ FormulaContext,
+ FunctionCallContext,
+ type FormulaFunction,
+ type FormulaParserVisitor,
+} from "@undb/formula"
+import { globalFunctionRegistry } from "@undb/formula/src/formula/registry"
+import type { TableDo } from "../../../../../table.do"
+
+export class FormulaFieldVisitor extends AbstractParseTreeVisitor implements FormulaParserVisitor {
+ constructor(private readonly table: TableDo) {
+ super()
+ }
+ protected defaultResult(): void {
+ return undefined
+ }
+
+ visitFormula(ctx: FormulaContext): void {
+ this.visit(ctx.expression())
+ }
+
+ visitFunctionCall(ctx: FunctionCallContext): void {
+ const name = ctx.IDENTIFIER().text as FormulaFunction
+ // const fields = ctx
+ // .argumentList()
+ // ?.expression()
+ // .filter((exp) => exp instanceof VariableExprContext)
+ // .map((exp) => exp.variable().IDENTIFIER().text)
+ // .map((fieldId) => this.table.schema.getFieldByIdOrName(fieldId).into(null))
+ // .filter((f) => !!f)
+
+ const fn = globalFunctionRegistry.get(name)
+ if (!fn) {
+ throw new Error(`Function ${name} not found`)
+ }
+ }
+}
diff --git a/packages/table/src/modules/schema/fields/variants/formula-field/formula-field.vo.ts b/packages/table/src/modules/schema/fields/variants/formula-field/formula-field.vo.ts
index 3f19fd3a7..dc1c4f15f 100644
--- a/packages/table/src/modules/schema/fields/variants/formula-field/formula-field.vo.ts
+++ b/packages/table/src/modules/schema/fields/variants/formula-field/formula-field.vo.ts
@@ -1,19 +1,22 @@
-import { Option, Some } from "@undb/domain"
+import { None, Option, Some } from "@undb/domain"
+import { createParser, FormulaVisitor, returnType } from "@undb/formula"
import { z } from "@undb/zod"
import { match } from "ts-pattern"
+import type { TableDo } from "../../../../../table.do"
import type { RecordComositeSpecification } from "../../../../records/record/record.composite-specification"
import { fieldId, FieldIdVo } from "../../field-id.vo"
import type { IFieldVisitor } from "../../field.visitor"
import { AbstractField, baseFieldDTO, createBaseFieldDTO } from "../abstract-field.vo"
import { StringEmpty } from "../string-field"
import { FormulaFieldValue } from "./formula-field-value.vo"
-import { formulaFieldAggregate } from "./formula-field.aggregate"
+import { createFormulaFieldAggregate } from "./formula-field.aggregate"
import {
createFormulaFieldCondition,
type IFormulaFieldCondition,
type IFormulaFieldConditionSchema,
} from "./formula-field.condition"
import { FormulaEqual, FormulaGT, FormulaGTE, FormulaLT, FormulaLTE } from "./formula-field.specification"
+import { FormulaReturnTypeVisitor } from "./formula-return-type.visitor"
export const FORMULA_TYPE = "formula" as const
@@ -23,6 +26,13 @@ export const formulaFieldOption = z.object({
fn,
})
+const formulaMetadata = z.object({
+ returnType: returnType,
+ fields: z.array(fieldId),
+})
+
+export type IFormulaFieldMetadata = z.infer
+
export type IFormulaFieldOption = z.infer
export const createFormulaFieldDTO = createBaseFieldDTO.extend({
@@ -40,20 +50,69 @@ export type IUpdateFormulaFieldDTO = z.infer
export const formulaFieldDTO = baseFieldDTO.extend({
type: z.literal(FORMULA_TYPE),
option: formulaFieldOption,
+ metadata: formulaMetadata.optional().nullable(),
})
export type IFormulaFieldDTO = z.infer
export class FormulaField extends AbstractField {
+ private metadata: Option = None
constructor(dto: IFormulaFieldDTO) {
super(dto)
if (dto.option) {
- this.option = Some(dto.option)
+ this.setOption(dto.option)
+ }
+ if (dto.metadata) {
+ this.metadata = Some(dto.metadata)
+ }
+ }
+
+ setOption(option: IFormulaFieldOption) {
+ this.option = Some(option)
+ }
+
+ setMetadata(table: TableDo) {
+ const fn = this.fn
+ if (!fn) return
+
+ try {
+ const parser = createParser(fn)
+ const tree = parser.formula()
+ const visitor = new FormulaVisitor()
+ const result = visitor.visit(tree)
+ if (result.type === "functionCall") {
+ const metadata: IFormulaFieldMetadata = {
+ returnType: result.returnType,
+ fields: visitor.getVariables(),
+ }
+ this.metadata = Some(metadata)
+ } else if (result.type === "variable") {
+ const fieldId = result.variable
+ const field = table.schema.getFieldByIdOrName(fieldId).into(null)
+ if (field) {
+ const visitor = new FormulaReturnTypeVisitor()
+ field.accept(visitor)
+ const metadata: IFormulaFieldMetadata = {
+ returnType: visitor.returnType,
+ fields: [fieldId],
+ }
+ this.metadata = Some(metadata)
+ }
+ } else if (result.type === "boolean" || result.type === "number" || result.type === "string") {
+ const metadata: IFormulaFieldMetadata = {
+ returnType: result.type,
+ fields: [],
+ }
+ this.metadata = Some(metadata)
+ }
+ } catch (error) {
+ // ignore
}
}
- static create(dto: ICreateFormulaFieldDTO) {
+ static create(table: TableDo, dto: ICreateFormulaFieldDTO) {
const field = new FormulaField({ ...dto, id: FieldIdVo.fromStringOrCreate(dto.id).value })
+ field.setMetadata(table)
return field
}
@@ -79,13 +138,15 @@ export class FormulaField extends AbstractField new FormulaLTE(value, this.id))
.with({ op: "is_empty" }, () => new StringEmpty(this.id))
.with({ op: "is_not_empty" }, () => new StringEmpty(this.id).not())
+ .with({ op: "is_true" }, () => new FormulaEqual(true, this.id))
+ .with({ op: "is_false" }, () => new FormulaEqual(false, this.id).not())
.exhaustive()
return Option(spec)
}
protected override getConditionSchema(optionType: z.ZodTypeAny): IFormulaFieldConditionSchema {
- return createFormulaFieldCondition(optionType)
+ return createFormulaFieldCondition(this.returnType)(optionType)
}
override getMutationSpec(value: FormulaFieldValue): Option {
@@ -93,10 +154,27 @@ export class FormulaField extends AbstractField o.fn)
}
+
+ get returnType() {
+ return this.metadata.mapOr("any", (m) => m.returnType)
+ }
+
+ override update(table: TableDo, dto: IUpdateFormulaFieldDTO): FormulaField {
+ const field = super.update(table, dto) as FormulaField
+ field.setMetadata(table)
+ return field
+ }
+
+ override toJSON() {
+ return {
+ ...super.toJSON(),
+ metadata: this.metadata.into(undefined),
+ }
+ }
}
diff --git a/packages/table/src/modules/schema/fields/variants/formula-field/formula-return-type.visitor.ts b/packages/table/src/modules/schema/fields/variants/formula-field/formula-return-type.visitor.ts
new file mode 100644
index 000000000..5cf793c31
--- /dev/null
+++ b/packages/table/src/modules/schema/fields/variants/formula-field/formula-return-type.visitor.ts
@@ -0,0 +1,106 @@
+import type { ReturnType } from "@undb/formula"
+import type { AttachmentField, CreatedAtField } from "../.."
+import type { IFieldVisitor } from "../../field.visitor"
+import type { AutoIncrementField } from "../autoincrement-field"
+import type { ButtonField } from "../button-field"
+import type { CheckboxField } from "../checkbox-field"
+import type { CreatedByField } from "../created-by-field"
+import type { CurrencyField } from "../currency-field"
+import type { DateField } from "../date-field"
+import type { DurationField } from "../duration-field"
+import type { EmailField } from "../email-field"
+import type { IdField } from "../id-field"
+import type { JsonField } from "../json-field"
+import type { LongTextField } from "../long-text-field"
+import type { NumberField } from "../number-field"
+import type { PercentageField } from "../percentage-field"
+import type { RatingField } from "../rating-field"
+import type { ReferenceField } from "../reference-field"
+import type { RollupField } from "../rollup-field"
+import type { SelectField } from "../select-field"
+import type { StringField } from "../string-field"
+import type { UpdatedAtField } from "../updated-at-field"
+import type { UpdatedByField } from "../updated-by-field"
+import type { UrlField } from "../url-field"
+import type { UserField } from "../user-field"
+import type { FormulaField } from "./formula-field.vo"
+
+export class FormulaReturnTypeVisitor implements IFieldVisitor {
+ #reaturnType: ReturnType = "any"
+
+ get returnType() {
+ return this.#reaturnType
+ }
+
+ id(field: IdField): void {
+ this.#reaturnType = "string"
+ }
+ autoIncrement(field: AutoIncrementField): void {
+ this.#reaturnType = "number"
+ }
+ longText(field: LongTextField): void {
+ this.#reaturnType = "string"
+ }
+ createdAt(field: CreatedAtField): void {
+ this.#reaturnType = "date"
+ }
+ createdBy(field: CreatedByField): void {
+ this.#reaturnType = "string"
+ }
+ updatedAt(field: UpdatedAtField): void {
+ this.#reaturnType = "date"
+ }
+ updatedBy(field: UpdatedByField): void {
+ this.#reaturnType = "string"
+ }
+ string(field: StringField): void {
+ this.#reaturnType = "string"
+ }
+ number(field: NumberField): void {
+ this.#reaturnType = "number"
+ }
+ rating(field: RatingField): void {
+ this.#reaturnType = "number"
+ }
+ select(field: SelectField): void {
+ this.#reaturnType = "string"
+ }
+ email(field: EmailField): void {
+ this.#reaturnType = "string"
+ }
+ attachment(field: AttachmentField): void {
+ this.#reaturnType = "string"
+ }
+ date(field: DateField): void {
+ this.#reaturnType = "date"
+ }
+ json(field: JsonField): void {
+ this.#reaturnType = "any"
+ }
+ checkbox(field: CheckboxField): void {
+ this.#reaturnType = "boolean"
+ }
+ user(field: UserField): void {
+ this.#reaturnType = "string"
+ }
+ url(field: UrlField): void {
+ this.#reaturnType = "string"
+ }
+ currency(field: CurrencyField): void {
+ this.#reaturnType = "number"
+ }
+ button(field: ButtonField): void {}
+ duration(field: DurationField): void {
+ this.#reaturnType = "number"
+ }
+ percentage(field: PercentageField): void {
+ this.#reaturnType = "number"
+ }
+ formula(field: FormulaField): void {
+ this.#reaturnType = field.returnType
+ }
+ reference(field: ReferenceField): void {}
+ rollup(field: RollupField): void {
+ // TODO: get return type from rollup
+ }
+}
diff --git a/packages/table/src/modules/schema/fields/variants/json-field/json-field.vo.ts b/packages/table/src/modules/schema/fields/variants/json-field/json-field.vo.ts
index 31bfb9752..a122e7a22 100644
--- a/packages/table/src/modules/schema/fields/variants/json-field/json-field.vo.ts
+++ b/packages/table/src/modules/schema/fields/variants/json-field/json-field.vo.ts
@@ -1,12 +1,12 @@
-import { None, Option, Some } from "@undb/domain"
+import { Option,Some } from "@undb/domain"
import { z } from "@undb/zod"
import { match } from "ts-pattern"
import type { FormFieldVO } from "../../../../forms/form/form-field.vo"
import type { RecordComositeSpecification } from "../../../../records/record/record.composite-specification"
-import { fieldId, FieldIdVo } from "../../field-id.vo"
+import { fieldId,FieldIdVo } from "../../field-id.vo"
import type { IFieldVisitor } from "../../field.visitor"
-import { AbstractField, baseFieldDTO, createBaseFieldDTO } from "../abstract-field.vo"
-import { jsonFieldConstraint, JsonFieldConstraint } from "./json-field-constraint.vo"
+import { AbstractField,baseFieldDTO,createBaseFieldDTO } from "../abstract-field.vo"
+import { jsonFieldConstraint,JsonFieldConstraint } from "./json-field-constraint.vo"
import { JsonFieldValue } from "./json-field-value.vo"
import { jsonFieldAggregate } from "./json-field.aggregate"
import {
@@ -14,7 +14,7 @@ import {
type IJsonFieldCondition,
type IJsonFieldConditionSchema,
} from "./json-field.condition"
-import { JsonContains, JsonEmpty, JsonEqual } from "./json-field.specification"
+import { JsonContains,JsonEmpty,JsonEqual } from "./json-field.specification"
export const JSON_TYPE = "json" as const
@@ -50,7 +50,7 @@ export class JsonField extends AbstractField {
}
static create(dto: ICreateJsonFieldDTO) {
- return new JsonField({ ...dto, id: FieldIdVo.create().value })
+ return new JsonField({ ...dto, id: FieldIdVo.fromStringOrCreate(dto.id).value })
}
override type = JSON_TYPE
@@ -97,6 +97,6 @@ export class JsonField extends AbstractField {
}
override getMutationSpec(value: JsonFieldValue): Option {
- return value.value ? Some(new JsonEqual(value.value, this.id)) : None
+ return value.value ? Some(new JsonEqual(value.value, this.id)) : Some(new JsonEqual(null, this.id))
}
}
diff --git a/packages/table/src/modules/schema/fields/variants/reference-field/reference-field.vo.ts b/packages/table/src/modules/schema/fields/variants/reference-field/reference-field.vo.ts
index d682d5c71..3ae103eac 100644
--- a/packages/table/src/modules/schema/fields/variants/reference-field/reference-field.vo.ts
+++ b/packages/table/src/modules/schema/fields/variants/reference-field/reference-field.vo.ts
@@ -233,7 +233,7 @@ export class ReferenceField extends AbstractField<
})
}
- public override update(dto: IUpdateReferenceFieldDTO): ReferenceField {
+ public override update(table: TableDo, dto: IUpdateReferenceFieldDTO): ReferenceField {
return new ReferenceField({
type: "reference",
name: dto.name,
diff --git a/packages/table/src/modules/schema/fields/variants/select-field/select-field.vo.ts b/packages/table/src/modules/schema/fields/variants/select-field/select-field.vo.ts
index ce002562e..adc9ab268 100644
--- a/packages/table/src/modules/schema/fields/variants/select-field/select-field.vo.ts
+++ b/packages/table/src/modules/schema/fields/variants/select-field/select-field.vo.ts
@@ -4,6 +4,7 @@ import { match } from "ts-pattern"
import { ColorsVO } from "../../../../colors/colors.vo"
import type { FormFieldVO } from "../../../../forms/form/form-field.vo"
import type { RecordComositeSpecification } from "../../../../records/record/record.composite-specification"
+import type { TableDo } from "../../../../table.do"
import { FieldIdVo, fieldId } from "../../field-id.vo"
import type { IFieldVisitor } from "../../field.visitor"
import { Options, option, optionId } from "../../option"
@@ -77,8 +78,8 @@ export class SelectField extends AbstractField o.name)
applyRules(new OptionNameShouldBeUnique(options))
diff --git a/packages/table/src/modules/schema/schema.vo.ts b/packages/table/src/modules/schema/schema.vo.ts
index f6c5f9500..6afaeeb53 100644
--- a/packages/table/src/modules/schema/schema.vo.ts
+++ b/packages/table/src/modules/schema/schema.vo.ts
@@ -1,6 +1,6 @@
-import { andOptions, Option, Some, ValueObject } from "@undb/domain"
+import { andOptions,Option,Some,ValueObject } from "@undb/domain"
import { getNextName } from "@undb/utils"
-import { z, ZodSchema } from "@undb/zod"
+import { z,ZodSchema } from "@undb/zod"
import { objectify } from "radash"
import {
WithDuplicatedFieldSpecification,
@@ -34,10 +34,10 @@ import {
type IUpdateFieldDTO,
} from "./fields"
import { FieldFactory } from "./fields/field.factory"
-import type { Field, MutableFieldValue, NoneSystemField, SystemField } from "./fields/field.type"
+import type { Field,MutableFieldValue,NoneSystemField,SystemField } from "./fields/field.type"
import { AutoIncrementField } from "./fields/variants/autoincrement-field"
import { CreatedAtField } from "./fields/variants/created-at-field"
-import type { SchemaIdMap, SchemaNameMap } from "./schema.type"
+import type { SchemaIdMap,SchemaNameMap } from "./schema.type"
export class Schema extends ValueObject {
public fieldMapById: SchemaIdMap
@@ -56,9 +56,9 @@ export class Schema extends ValueObject {
this.fieldMapByName = fieldMapByName
}
- static create(dto: ICreateSchemaDTO): Schema {
- const fields = dto.map((field) => FieldFactory.create(field))
- return new Schema([
+ static create(table: TableDo, dto: ICreateSchemaDTO): Schema {
+ const fields = dto.map((field) => FieldFactory.create(table, field))
+ const schema = new Schema([
IdField.create({ name: "id", type: "id" }),
...fields,
CreatedAtField.create({ name: "createdAt", type: "createdAt" }),
@@ -67,11 +67,21 @@ export class Schema extends ValueObject {
UpdatedByField.create({ name: "updatedBy", type: "updatedBy" }),
AutoIncrementField.create({ name: "autoIncrement", type: "autoIncrement" }),
])
+
+ for (const field of schema.fields) {
+ if (field.type === "formula") {
+ field.setMetadata(table)
+ }
+ }
+
+ return schema
}
static fromJSON(dto: ISchemaDTO): Schema {
const fields = dto.map((field) => FieldFactory.fromJSON(field))
- return new Schema(fields)
+ const schema = new Schema(fields)
+
+ return schema
}
*[Symbol.iterator]() {
@@ -108,9 +118,9 @@ export class Schema extends ValueObject {
return new Schema([...this.fields, field])
}
- $updateField(dto: IUpdateFieldDTO) {
+ $updateField(table: TableDo, dto: IUpdateFieldDTO) {
const field = this.getFieldById(new FieldIdVo(dto.id)).expect("Field not found")
- const updated = field.clone().update(dto as any)
+ const updated = field.clone().update(table, dto as any)
return new WithUpdatedFieldSpecification(field, updated)
}
diff --git a/packages/table/src/table.builder.ts b/packages/table/src/table.builder.ts
index fe92e7140..acf3c1d4d 100644
--- a/packages/table/src/table.builder.ts
+++ b/packages/table/src/table.builder.ts
@@ -66,7 +66,7 @@ export class TableBuilder implements ITableBuilder {
}
createSchema(dto: ICreateSchemaDTO): ITableBuilder {
- new TableSchemaSpecification(Schema.create(dto)).mutate(this.table)
+ new TableSchemaSpecification(Schema.create(this.table, dto)).mutate(this.table)
return this
}
diff --git a/packages/template/src/templates/test.base.json b/packages/template/src/templates/test.base.json
index 7baae825d..ee31f8f78 100644
--- a/packages/template/src/templates/test.base.json
+++ b/packages/template/src/templates/test.base.json
@@ -42,7 +42,10 @@
},
"Count2": {
"id": "count2",
- "type": "number"
+ "type": "currency",
+ "option": {
+ "symbol": "$"
+ }
},
"Sum": {
"id": "sum",
@@ -51,6 +54,48 @@
"fn": "{{count1}} + {{count2}}"
}
},
+ "SumAdd": {
+ "id": "sumadd",
+ "type": "formula",
+ "option": {
+ "fn": "ADD({{count1}}, ADD({{count2}}, {{count1}}))"
+ }
+ },
+ "SumCounts": {
+ "id": "sumcounts",
+ "type": "formula",
+ "option": {
+ "fn": "SUM({{count1}}, {{count2}})"
+ }
+ },
+ "SumSum": {
+ "id": "sumsum",
+ "type": "formula",
+ "option": {
+ "fn": "SUM({{sum}}, {{sumadd}})"
+ }
+ },
+ "Multiply": {
+ "id": "multiply",
+ "type": "formula",
+ "option": {
+ "fn": "MULTIPLY({{count1}}, {{count2}})"
+ }
+ },
+ "ComplicatedFn": {
+ "id": "complicatedFn",
+ "type": "formula",
+ "option": {
+ "fn": "ADD({{count1}}, MULTIPLY({{count2}}, {{count1}}))"
+ }
+ },
+ "Concat": {
+ "id": "concat",
+ "type": "formula",
+ "option": {
+ "fn": "CONCAT({{id}}, {{title}}, {{count1}}, {{count2}}, {{complicatedFn}})"
+ }
+ },
"Sum2": {
"id": "sum2",
"type": "formula",
@@ -116,6 +161,309 @@
"Title": "2-2"
}
]
+ },
+ "Formula1": {
+ "fieldsOrder": ["Count1", "Count2", "Count3", "String1", "String2", "Json1"],
+ "schema": {
+ "Count1": {
+ "id": "count1",
+ "type": "number"
+ },
+ "Count2": {
+ "id": "count2",
+ "type": "number"
+ },
+ "Count3": {
+ "id": "count3",
+ "type": "number"
+ },
+ "String1": {
+ "id": "string1",
+ "type": "string"
+ },
+ "String2": {
+ "id": "string2",
+ "type": "string"
+ },
+ "Json1": {
+ "id": "json1",
+ "type": "json"
+ },
+ "Sum": {
+ "id": "sum",
+ "type": "formula",
+ "option": {
+ "fn": "{{count1}} + {{count2}} + {{count3}}"
+ }
+ },
+ "Subtract": {
+ "id": "subtract",
+ "type": "formula",
+ "option": {
+ "fn": "{{count1}} - {{count2}}"
+ }
+ },
+ "Mod": {
+ "id": "mod",
+ "type": "formula",
+ "option": {
+ "fn": "MOD({{count3}}, {{count2}})"
+ }
+ },
+ "Power": {
+ "id": "power",
+ "type": "formula",
+ "option": {
+ "fn": "POWER({{count3}}, {{count2}})"
+ }
+ },
+ "Sqrt": {
+ "id": "sqrt",
+ "type": "formula",
+ "option": {
+ "fn": "SQRT({{count3}})"
+ }
+ },
+ "Abs": {
+ "id": "abs",
+ "type": "formula",
+ "option": {
+ "fn": "ABS({{count3}})"
+ }
+ },
+ "Round": {
+ "id": "round",
+ "type": "formula",
+ "option": {
+ "fn": "ROUND({{count3}})"
+ }
+ },
+ "Floor": {
+ "id": "floor",
+ "type": "formula",
+ "option": {
+ "fn": "FLOOR({{count3}})"
+ }
+ },
+ "Ceiling": {
+ "id": "ceiling",
+ "type": "formula",
+ "option": {
+ "fn": "CEILING({{count3}})"
+ }
+ },
+ "Min1": {
+ "id": "min",
+ "type": "formula",
+ "option": {
+ "fn": "MIN({{count3}}, {{count2}})"
+ }
+ },
+ "Min2": {
+ "id": "min2",
+ "type": "formula",
+ "option": {
+ "fn": "MIN({{count3}}, {{count1}}, {{count2}})"
+ }
+ },
+ "Max1": {
+ "id": "max1",
+ "type": "formula",
+ "option": {
+ "fn": "MAX({{count3}}, {{count2}})"
+ }
+ },
+ "Max2": {
+ "id": "max2",
+ "type": "formula",
+ "option": {
+ "fn": "MAX({{count3}}, {{count1}}, {{count2}})"
+ }
+ },
+ "Average": {
+ "id": "average",
+ "type": "formula",
+ "option": {
+ "fn": "AVERAGE({{count3}}, {{count1}}, {{count2}})"
+ }
+ },
+ "Concat": {
+ "id": "concat",
+ "type": "formula",
+ "option": {
+ "fn": "CONCAT({{string1}}, ' ', {{string2}})"
+ }
+ },
+ "Upper": {
+ "id": "upper",
+ "type": "formula",
+ "option": {
+ "fn": "UPPER({{string1}})"
+ }
+ },
+ "Lower": {
+ "id": "lower",
+ "type": "formula",
+ "option": {
+ "fn": "LOWER({{string1}})"
+ }
+ },
+ "Trim": {
+ "id": "trim",
+ "type": "formula",
+ "option": {
+ "fn": "TRIM({{string1}})"
+ }
+ },
+ "Left": {
+ "id": "left",
+ "type": "formula",
+ "option": {
+ "fn": "LEFT({{string1}}, 3)"
+ }
+ },
+ "Right": {
+ "id": "right",
+ "type": "formula",
+ "option": {
+ "fn": "RIGHT({{string1}}, 3)"
+ }
+ },
+ "Mid": {
+ "id": "mid",
+ "type": "formula",
+ "option": {
+ "fn": "MID({{string1}}, 2, 3)"
+ }
+ },
+ "Greater": {
+ "id": "greater",
+ "type": "formula",
+ "option": {
+ "fn": "{{count1}} > {{count2}}"
+ }
+ },
+ "And": {
+ "id": "nestedCompare",
+ "type": "formula",
+ "option": {
+ "fn": "({{count1}} > {{count2}}) AND ({{count2}} > {{count3}})"
+ }
+ },
+ "Or": {
+ "id": "or",
+ "type": "formula",
+ "option": {
+ "fn": "({{count1}} > {{count2}}) OR ({{count2}} > {{count3}})"
+ }
+ },
+ "NOT": {
+ "id": "not",
+ "type": "formula",
+ "option": {
+ "fn": "NOT ({{count1}} > {{count2}})"
+ }
+ },
+ "Equal": {
+ "id": "equal",
+ "type": "formula",
+ "option": {
+ "fn": "{{count1}} = {{count2}}"
+ }
+ },
+ "NotEqual": {
+ "id": "notEqual",
+ "type": "formula",
+ "option": {
+ "fn": "{{count1}} != {{count2}}"
+ }
+ },
+ "GreaterEqual": {
+ "id": "greaterEqual",
+ "type": "formula",
+ "option": {
+ "fn": "{{count1}} >= {{count2}}"
+ }
+ },
+ "LessEqual": {
+ "id": "lessEqual",
+ "type": "formula",
+ "option": {
+ "fn": "{{count1}} <= {{count2}}"
+ }
+ },
+ "Less": {
+ "id": "less",
+ "type": "formula",
+ "option": {
+ "fn": "{{count1}} < {{count2}}"
+ }
+ },
+ "Len": {
+ "id": "len",
+ "type": "formula",
+ "option": {
+ "fn": "LEN({{string1}})"
+ }
+ },
+ "Replace": {
+ "id": "replace",
+ "type": "formula",
+ "option": {
+ "fn": "REPLACE({{string1}}, 'llo', {{string2}})"
+ }
+ },
+ "Search": {
+ "id": "search",
+ "type": "formula",
+ "option": {
+ "fn": "SEARCH({{string1}}, {{string2}})"
+ }
+ },
+ "Repeat": {
+ "id": "repeat",
+ "type": "formula",
+ "option": {
+ "fn": "REPEAT({{string1}}, 3)"
+ }
+ },
+ "JsonExtractName": {
+ "id": "jsonExtractName",
+ "type": "formula",
+ "option": {
+ "fn": "JSON_EXTRACT({{json1}}, '$.name')"
+ }
+ },
+ "AddAutoIncrement": {
+ "id": "addAutoIncrement",
+ "type": "formula",
+ "option": {
+ "fn": "{{count1}} + {{autoIncrement}}"
+ }
+ }
+ },
+ "records": [
+ {
+ "Count1": 1,
+ "Count2": 2,
+ "Count3": 3,
+ "String1": "Hello",
+ "String2": "World",
+ "Json1": "{\"name\": \"John\", \"age\": 30, \"city\": \"New York\"}"
+ },
+ {
+ "Count1": 4,
+ "Count2": 2,
+ "Count3": -3,
+ "String1": " Hello "
+ },
+ {
+ "Count1": 5,
+ "Count2": 3,
+ "String1": "Hello",
+ "String2": "llo"
+ }
+ ]
}
}
}