From 605f1c8e3d6a0b4bc0508e2e0b3e5041e1282dcd Mon Sep 17 00:00:00 2001 From: go-jet Date: Tue, 7 Dec 2021 14:07:44 +0100 Subject: [PATCH] [Postgres] Add order set aggregate functions support. --- internal/jet/order_set_aggregate_functions.go | 60 +++++++++++++++++++ internal/jet/utils.go | 4 +- postgres/functions.go | 22 +++++++ tests/postgres/chinook_db_test.go | 50 ++++++++++++++++ 4 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 internal/jet/order_set_aggregate_functions.go diff --git a/internal/jet/order_set_aggregate_functions.go b/internal/jet/order_set_aggregate_functions.go new file mode 100644 index 0000000..8ce5d1e --- /dev/null +++ b/internal/jet/order_set_aggregate_functions.go @@ -0,0 +1,60 @@ +package jet + +// MODE computes the most frequent value of the aggregated argument +func MODE() *OrderSetAggregateFunc { + return newOrderSetAggregateFunction("MODE", nil) +} + +// PERCENTILE_CONT computes a value corresponding to the specified fraction within the ordered set of +// aggregated argument values. This will interpolate between adjacent input items if needed. +func PERCENTILE_CONT(fraction FloatExpression) *OrderSetAggregateFunc { + return newOrderSetAggregateFunction("PERCENTILE_CONT", fraction) +} + +// PERCENTILE_DISC computes the first value within the ordered set of aggregated argument values whose position +// in the ordering equals or exceeds the specified fraction. The aggregated argument must be of a sortable type. +func PERCENTILE_DISC(fraction FloatExpression) *OrderSetAggregateFunc { + return newOrderSetAggregateFunction("PERCENTILE_DISC", fraction) +} + +// OrderSetAggregateFunc implementation of order set aggregate function +type OrderSetAggregateFunc struct { + name string + fraction FloatExpression + orderBy Window +} + +func newOrderSetAggregateFunction(name string, fraction FloatExpression) *OrderSetAggregateFunc { + return &OrderSetAggregateFunc{ + name: name, + fraction: fraction, + } +} + +// WITHIN_GROUP_ORDER_BY specifies ordered set of aggregated argument values +func (p *OrderSetAggregateFunc) WITHIN_GROUP_ORDER_BY(orderBy OrderByClause) Expression { + p.orderBy = ORDER_BY(orderBy) + return newOrderSetAggregateFuncExpression(*p) +} + +func newOrderSetAggregateFuncExpression(aggFunc OrderSetAggregateFunc) *orderSetAggregateFuncExpression { + ret := &orderSetAggregateFuncExpression{ + OrderSetAggregateFunc: aggFunc, + } + + ret.ExpressionInterfaceImpl.Parent = ret + + return ret +} + +type orderSetAggregateFuncExpression struct { + ExpressionInterfaceImpl + OrderSetAggregateFunc +} + +func (p *orderSetAggregateFuncExpression) serialize(statement StatementType, out *SQLBuilder, options ...SerializeOption) { + out.WriteString(p.name) + WRAP(p.fraction).serialize(statement, out, FallTrough(options)...) + out.WriteString("WITHIN GROUP") + p.orderBy.serialize(statement, out) +} diff --git a/internal/jet/utils.go b/internal/jet/utils.go index d887f63..524c2c5 100644 --- a/internal/jet/utils.go +++ b/internal/jet/utils.go @@ -34,7 +34,9 @@ func serializeExpressionList( out.WriteString(separator) } - expression.serialize(statement, out, options...) + if expression != nil { + expression.serialize(statement, out, options...) + } } } diff --git a/postgres/functions.go b/postgres/functions.go index 34f8370..a20d1e1 100644 --- a/postgres/functions.go +++ b/postgres/functions.go @@ -336,3 +336,25 @@ func explicitLiteralCast(expresion Expression) jet.Expression { return expresion } + +// MODE computes the most frequent value of the aggregated argument +var MODE = jet.MODE + +// PERCENTILE_CONT computes a value corresponding to the specified fraction within the ordered set of +// aggregated argument values. This will interpolate between adjacent input items if needed. +func PERCENTILE_CONT(fraction FloatExpression) *jet.OrderSetAggregateFunc { + return jet.PERCENTILE_CONT(castFloatLiteral(fraction)) +} + +// PERCENTILE_DISC computes the first value within the ordered set of aggregated argument values whose position +// in the ordering equals or exceeds the specified fraction. The aggregated argument must be of a sortable type. +func PERCENTILE_DISC(fraction FloatExpression) *jet.OrderSetAggregateFunc { + return jet.PERCENTILE_DISC(castFloatLiteral(fraction)) +} + +func castFloatLiteral(fraction FloatExpression) FloatExpression { + if _, ok := fraction.(jet.LiteralExpression); ok { + return CAST(fraction).AS_DOUBLE() // to make postgres aware of the type + } + return fraction +} diff --git a/tests/postgres/chinook_db_test.go b/tests/postgres/chinook_db_test.go index 1e58a12..684abc2 100644 --- a/tests/postgres/chinook_db_test.go +++ b/tests/postgres/chinook_db_test.go @@ -743,3 +743,53 @@ var album347 = model.Album{ Title: "Koyaanisqatsi (Soundtrack from the Motion Picture)", ArtistId: 275, } + +func TestAggregateFunc(t *testing.T) { + stmt := SELECT( + PERCENTILE_DISC(Float(0.1)).WITHIN_GROUP_ORDER_BY(Invoice.InvoiceId).AS("percentile_disc_1"), + PERCENTILE_DISC(Invoice.Total.DIV(Float(100))).WITHIN_GROUP_ORDER_BY(Invoice.InvoiceDate.ASC()).AS("percentile_disc_2"), + PERCENTILE_DISC(RawFloat("(select array_agg(s) from generate_series(0, 1, 0.2) as s)")). + WITHIN_GROUP_ORDER_BY(Invoice.BillingAddress.DESC()).AS("percentile_disc_3"), + + PERCENTILE_CONT(Float(0.3)).WITHIN_GROUP_ORDER_BY(Invoice.Total).AS("percentile_cont_1"), + PERCENTILE_CONT(Float(0.2)).WITHIN_GROUP_ORDER_BY(INTERVAL(1, HOUR).DESC()).AS("percentile_cont_int"), + + MODE().WITHIN_GROUP_ORDER_BY(Invoice.BillingPostalCode.DESC()).AS("mode_1"), + ).FROM( + Invoice, + ).GROUP_BY( + Invoice.Total, + ) + + testutils.AssertStatementSql(t, stmt, ` +SELECT PERCENTILE_DISC ($1::double precision) WITHIN GROUP (ORDER BY "Invoice"."InvoiceId") AS "percentile_disc_1", + PERCENTILE_DISC ("Invoice"."Total" / $2) WITHIN GROUP (ORDER BY "Invoice"."InvoiceDate" ASC) AS "percentile_disc_2", + PERCENTILE_DISC ((select array_agg(s) from generate_series(0, 1, 0.2) as s)) WITHIN GROUP (ORDER BY "Invoice"."BillingAddress" DESC) AS "percentile_disc_3", + PERCENTILE_CONT ($3::double precision) WITHIN GROUP (ORDER BY "Invoice"."Total") AS "percentile_cont_1", + PERCENTILE_CONT ($4::double precision) WITHIN GROUP (ORDER BY INTERVAL '1 HOUR' DESC) AS "percentile_cont_int", + MODE () WITHIN GROUP (ORDER BY "Invoice"."BillingPostalCode" DESC) AS "mode_1" +FROM chinook."Invoice" +GROUP BY "Invoice"."Total"; +`, 0.1, 100.0, 0.3, 0.2) + + var dest struct { + PercentileDisc1 string + PercentileDisc2 string + PercentileDisc3 string + PercentileCont1 string + Mode1 string + } + + err := stmt.Query(db, &dest) + + require.NoError(t, err) + testutils.AssertJSON(t, dest, ` +{ + "PercentileDisc1": "41", + "PercentileDisc2": "2009-01-19T00:00:00Z", + "PercentileDisc3": "{\"Via Degli Scipioni, 43\",\"Qe 7 Bloco G\",\"Berger Stra�e 10\",\"696 Osborne Street\",\"2211 W Berry Street\",\"1033 N Park Ave\"}", + "PercentileCont1": "0.99", + "Mode1": "X1A 1N6" +} +`) +}