Skip to content

Commit

Permalink
chore: basic computed fields
Browse files Browse the repository at this point in the history
chore: sql func migrations, computed field sql gen, test

chore: migrations proper'

chore: computed fields optional in model api creates

chore: computed fields not required in create action inputs'

chore: cleanup, op precedence impro

chore: lint

chore: computed fns migrations reworked

chore: comments and clean up

chore: bool supports, more tests

chore: negation, fixed migration type

chore: prettier

chore: type compatibility between decimal and number

chore: consistent generation of migrations'

chore: completions

chore: validations
  • Loading branch information
davenewza committed Jan 10, 2025
1 parent 2c8bb21 commit 8bfcf47
Show file tree
Hide file tree
Showing 42 changed files with 1,654 additions and 124 deletions.
8 changes: 4 additions & 4 deletions expressions/options/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,16 @@ var typeCompatibilityMapping = map[string][][]*types.Type{
{typing.Date, typing.Timestamp, types.TimestampType},
},
operators.Add: {
{types.NewListType(types.IntType), types.NewListType(types.DoubleType), typing.Number, typing.Decimal},
{types.IntType, types.DoubleType, typing.Number, typing.Decimal},
},
operators.Subtract: {
{types.NewListType(types.IntType), types.NewListType(types.DoubleType), typing.Number, typing.Decimal},
{types.IntType, types.DoubleType, typing.Number, typing.Decimal},
},
operators.Multiply: {
{types.NewListType(types.IntType), types.NewListType(types.DoubleType), typing.Number, typing.Decimal},
{types.IntType, types.DoubleType, typing.Number, typing.Decimal},
},
operators.Divide: {
{types.NewListType(types.IntType), types.NewListType(types.DoubleType), typing.Number, typing.Decimal},
{types.IntType, types.DoubleType, typing.Number, typing.Decimal},
},
}

Expand Down
2 changes: 2 additions & 0 deletions expressions/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ func typesAssignable(expected *types.Type, actual *types.Type) bool {
typing.Markdown.String(): {mapType(typing.Text.String()), mapType(typing.Markdown.String())},
typing.ID.String(): {mapType(typing.Text.String()), mapType(typing.ID.String())},
typing.Text.String(): {mapType(typing.Text.String()), mapType(typing.Markdown.String()), mapType(typing.ID.String())},
typing.Number.String(): {mapType(typing.Number.String()), mapType(typing.Decimal.String())},
typing.Decimal.String(): {mapType(typing.Number.String()), mapType(typing.Decimal.String())},
}

// Check if there are specific compatibility rules for the expected type
Expand Down
70 changes: 47 additions & 23 deletions expressions/resolve/visitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,19 +74,19 @@ func (w *CelVisitor[T]) run(expression *parser.Expression) (T, error) {

w.ast = ast

if err := w.eval(checkedExpr.Expr, false); err != nil {
if err := w.eval(checkedExpr.Expr, isComplexOperatorWithRespectTo(operators.LogicalAnd, checkedExpr.Expr), false); err != nil {
return zero, err
}

return w.visitor.Result()
}

func (w *CelVisitor[T]) eval(expr *exprpb.Expr, inBinaryCondition bool) error {
func (w *CelVisitor[T]) eval(expr *exprpb.Expr, nested bool, inBinary bool) error {
var err error

switch expr.ExprKind.(type) {
case *exprpb.Expr_ConstExpr, *exprpb.Expr_ListExpr, *exprpb.Expr_SelectExpr, *exprpb.Expr_IdentExpr:
if !inBinaryCondition {
if !inBinary {
err := w.visitor.StartCondition(false)
if err != nil {
return err
Expand All @@ -96,10 +96,20 @@ func (w *CelVisitor[T]) eval(expr *exprpb.Expr, inBinaryCondition bool) error {

switch expr.ExprKind.(type) {
case *exprpb.Expr_CallExpr:
err = w.visitor.StartCondition(nested)
if err != nil {
return err
}

err := w.callExpr(expr)
if err != nil {
return err
}

err = w.visitor.EndCondition(nested)
if err != nil {
return err
}
case *exprpb.Expr_ConstExpr:
err := w.constExpr(expr)
if err != nil {
Expand All @@ -126,7 +136,7 @@ func (w *CelVisitor[T]) eval(expr *exprpb.Expr, inBinaryCondition bool) error {

switch expr.ExprKind.(type) {
case *exprpb.Expr_ConstExpr, *exprpb.Expr_ListExpr, *exprpb.Expr_SelectExpr, *exprpb.Expr_IdentExpr:
if !inBinaryCondition {
if !inBinary {
err := w.visitor.EndCondition(false)
if err != nil {
return err
Expand Down Expand Up @@ -173,18 +183,12 @@ func (w *CelVisitor[T]) binaryCall(expr *exprpb.Expr) error {
op := c.GetFunction()
args := c.GetArgs()
lhs := args[0]

isComplex := isComplexOperatorWithRespectTo(operators.LogicalAnd, expr)

err := w.visitor.StartCondition(isComplex)
if err != nil {
return err
}
lhsParen := isComplexOperatorWithRespectTo(op, lhs)
var err error

inBinary := !(op == operators.LogicalAnd || op == operators.LogicalOr)

rhs := args[1]
if err := w.eval(lhs, inBinary); err != nil {
if err := w.eval(lhs, lhsParen, inBinary); err != nil {
return err
}

Expand All @@ -200,11 +204,17 @@ func (w *CelVisitor[T]) binaryCall(expr *exprpb.Expr) error {
return err
}

if err := w.eval(rhs, inBinary); err != nil {
rhs := args[1]
rhsParen := isComplexOperatorWithRespectTo(op, rhs)
if !rhsParen && isLeftRecursive(op) {
rhsParen = isSamePrecedence(op, rhs)
}

if err := w.eval(rhs, rhsParen, inBinary); err != nil {
return err
}

return w.visitor.EndCondition(isComplex)
return nil
}

func (w *CelVisitor[T]) unaryCall(expr *exprpb.Expr) error {
Expand All @@ -224,16 +234,11 @@ func (w *CelVisitor[T]) unaryCall(expr *exprpb.Expr) error {
return fmt.Errorf("not implemented: %s", fun)
}

err := w.visitor.StartCondition(isComplex)
if err != nil {
return err
}

if err := w.eval(args[0], false); err != nil {
if err := w.eval(args[0], isComplex, false); err != nil {
return err
}

return w.visitor.EndCondition(isComplex)
return nil
}

func (w *CelVisitor[T]) constExpr(expr *exprpb.Expr) error {
Expand Down Expand Up @@ -362,7 +367,7 @@ func (w *CelVisitor[T]) SelectExpr(expr *exprpb.Expr) error {

switch expr.ExprKind.(type) {
case *exprpb.Expr_CallExpr:
err := w.eval(sel.GetOperand(), true)
err := w.eval(sel.GetOperand(), true, true)
if err != nil {
return err
}
Expand Down Expand Up @@ -449,6 +454,25 @@ func isComplexOperatorWithRespectTo(op string, expr *exprpb.Expr) bool {
return isLowerPrecedence(op, expr)
}

// isLeftRecursive indicates whether the parser resolves the call in a left-recursive manner as
// this can have an effect of how parentheses affect the order of operations in the AST.
func isLeftRecursive(op string) bool {
return op != operators.LogicalAnd && op != operators.LogicalOr
}

// isSamePrecedence indicates whether the precedence of the input operator is the same as the
// precedence of the (possible) operation represented in the input Expr.
//
// If the expr is not a Call, the result is false.
func isSamePrecedence(op string, expr *exprpb.Expr) bool {
if expr.GetCallExpr() == nil {
return false
}
c := expr.GetCallExpr()
other := c.GetFunction()
return operators.Precedence(op) == operators.Precedence(other)
}

func toNative(c *exprpb.Constant) (any, error) {
switch c.ConstantKind.(type) {
case *exprpb.Constant_BoolValue:
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ require (
github.com/spf13/viper v1.15.0
github.com/stretchr/testify v1.8.4
github.com/teamkeel/graphql v0.8.2-0.20230531102419-995b8ab035b6
github.com/test-go/testify v1.1.4
github.com/twitchtv/twirp v8.1.3+incompatible
github.com/vincent-petithory/dataurl v1.0.0
github.com/xeipuuv/gojsonschema v1.2.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,8 @@ github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/teamkeel/graphql v0.8.2-0.20230531102419-995b8ab035b6 h1:q8ZbAgqr7jJlZNJ4WAI+QMuZrcCBDOw9k7orYuy+Vqs=
github.com/teamkeel/graphql v0.8.2-0.20230531102419-995b8ab035b6/go.mod h1:5td34OA5ZUdckc2w3GgE7QQoaG8MK6hIVR3dFI+qaK4=
github.com/test-go/testify v1.1.4 h1:Tf9lntrKUMHiXQ07qBScBTSA0dhYQlu83hswqelv1iE=
github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU=
github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M=
github.com/thoas/go-funk v0.9.1/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q=
github.com/tkuchiki/go-timezone v0.2.0 h1:yyZVHtQRVZ+wvlte5HXvSpBkR0dPYnPEIgq9qqAqltk=
Expand Down
46 changes: 46 additions & 0 deletions integration/testdata/computed_fields/schema.keel
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
model ComputedDecimal {
fields {
price Decimal
quantity Number
total Decimal @computed(computedDecimal.quantity * computedDecimal.price)
totalWithShipping Decimal @computed(5 + computedDecimal.quantity * computedDecimal.price)
totalWithDiscount Decimal @computed(computedDecimal.quantity * (computedDecimal.price - (computedDecimal.price / 100 * 10)))
}
}

model ComputedNumber {
fields {
price Decimal
quantity Number
total Number @computed(computedNumber.quantity * computedNumber.price)
totalWithShipping Number @computed(5 + computedNumber.quantity * computedNumber.price)
totalWithDiscount Number @computed(computedNumber.quantity * (computedNumber.price - (computedNumber.price / 100 * 10)))
}
}

model ComputedBool {
fields {
price Decimal?
isActive Boolean
isExpensive Boolean @computed(computedBool.price > 100 && computedBool.isActive)
isCheap Boolean @computed(!computedBool.isExpensive)
}
}

model ComputedNulls {
fields {
price Decimal?
quantity Number?
total Decimal? @computed(computedNulls.quantity * computedNulls.price)
}
}

model ComputedDepends {
fields {
price Decimal
quantity Number
totalWithDiscount Decimal? @computed(computedDepends.totalWithShipping - (computedDepends.totalWithShipping / 100 * 10))
totalWithShipping Decimal? @computed(computedDepends.total + 5)
total Decimal? @computed(computedDepends.quantity * computedDepends.price)
}
}
139 changes: 139 additions & 0 deletions integration/testdata/computed_fields/tests.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { test, expect, beforeEach } from "vitest";
import { models, resetDatabase } from "@teamkeel/testing";

beforeEach(resetDatabase);

test("computed fields - decimal", async () => {
const item = await models.computedDecimal.create({ price: 5, quantity: 2 });
expect(item.total).toEqual(10);
expect(item.totalWithShipping).toEqual(15);
expect(item.totalWithDiscount).toEqual(9);

const get = await models.computedDecimal.findOne({ id: item.id });
expect(get!.total).toEqual(10);
expect(get!.totalWithShipping).toEqual(15);
expect(get!.totalWithDiscount).toEqual(9);

const updatePrice = await models.computedDecimal.update(
{ id: item.id },
{ price: 10 }
);
expect(updatePrice.total).toEqual(20);
expect(updatePrice.totalWithShipping).toEqual(25);
expect(updatePrice.totalWithDiscount).toEqual(18);

const updateQuantity = await models.computedDecimal.update(
{ id: item.id },
{ quantity: 3 }
);
expect(updateQuantity.total).toEqual(30);
expect(updateQuantity.totalWithShipping).toEqual(35);
expect(updateQuantity.totalWithDiscount).toEqual(27);

const updateBoth = await models.computedDecimal.update(
{ id: item.id },
{ price: 12, quantity: 4 }
);
expect(updateBoth.total).toEqual(48);
expect(updateBoth.totalWithShipping).toEqual(53);
expect(updateBoth.totalWithDiscount).toEqual(43.2);
});

test("computed fields - number", async () => {
const item = await models.computedNumber.create({ price: 5, quantity: 2 });
expect(item.total).toEqual(10);
expect(item.totalWithShipping).toEqual(15);
expect(item.totalWithDiscount).toEqual(9);

const get = await models.computedNumber.findOne({ id: item.id });
expect(get!.total).toEqual(10);
expect(get!.totalWithShipping).toEqual(15);
expect(get!.totalWithDiscount).toEqual(9);

const updatePrice = await models.computedNumber.update(
{ id: item.id },
{ price: 10 }
);
expect(updatePrice.total).toEqual(20);
expect(updatePrice.totalWithShipping).toEqual(25);
expect(updatePrice.totalWithDiscount).toEqual(18);

const updateQuantity = await models.computedNumber.update(
{ id: item.id },
{ quantity: 3 }
);
expect(updateQuantity.total).toEqual(30);
expect(updateQuantity.totalWithShipping).toEqual(35);
expect(updateQuantity.totalWithDiscount).toEqual(27);

const updateBoth = await models.computedNumber.update(
{ id: item.id },
{ price: 12, quantity: 4 }
);
expect(updateBoth.total).toEqual(48);
expect(updateBoth.totalWithShipping).toEqual(53);
expect(updateBoth.totalWithDiscount).toEqual(43);
});

test("computed fields - boolean", async () => {
const expensive = await models.computedBool.create({
price: 200,
isActive: true,
});
expect(expensive.isExpensive).toBeTruthy();
expect(expensive.isCheap).toBeFalsy();

const notExpensive = await models.computedBool.create({
price: 90,
isActive: true,
});
expect(notExpensive.isExpensive).toBeFalsy();
expect(notExpensive.isCheap).toBeTruthy();

const notActive = await models.computedBool.create({
price: 200,
isActive: false,
});
expect(notActive.isExpensive).toBeFalsy();
expect(notActive.isCheap).toBeTruthy();
});

test("computed fields - with nulls", async () => {
const item = await models.computedNulls.create({ price: 5 });
expect(item.total).toBeNull();

const updateQty = await models.computedNulls.update(
{ id: item.id },
{ quantity: 10 }
);
expect(updateQty!.total).toEqual(50);

const updatePrice2 = await models.computedNulls.update(
{ id: item.id },
{ price: null }
);
expect(updatePrice2!.total).toBeNull();
});

test("computed fields - with dependencies", async () => {
const item = await models.computedDepends.create({ price: 5, quantity: 2 });
expect(item.total).toEqual(10);
expect(item.totalWithShipping).toEqual(15);
expect(item.totalWithDiscount).toEqual(13.5);

const updatedQty = await models.computedDepends.update(
{ id: item.id },
{ quantity: 10 }
);
expect(updatedQty.total).toEqual(50);
expect(updatedQty.totalWithShipping).toEqual(55);
expect(updatedQty.totalWithDiscount).toEqual(49.5);

const updatePrice = await models.computedDepends.update(
{ id: item.id },
{ price: 8 }
);
expect(updatePrice.total).toEqual(80);
expect(updatePrice.totalWithShipping).toEqual(85);
expect(updatePrice.totalWithDiscount).toEqual(76.5);
});
8 changes: 8 additions & 0 deletions migrations/computed_functions.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
SELECT
routine_name
FROM
information_schema.routines
WHERE
routine_type = 'FUNCTION'
AND
routine_schema = 'public' AND routine_name LIKE '%__computed';
Loading

0 comments on commit 8bfcf47

Please sign in to comment.