From e51ddd5506d71e8b40403d618b091347144a77a5 Mon Sep 17 00:00:00 2001 From: go-jet Date: Wed, 7 Feb 2024 11:07:50 +0100 Subject: [PATCH] Add support for FETCH FIRST clause. --- internal/jet/clause.go | 23 ++++++++++ postgres/select_statement.go | 41 +++++++++++++++-- tests/postgres/select_test.go | 84 +++++++++++++++++++++++++++++++++++ 3 files changed, 145 insertions(+), 3 deletions(-) diff --git a/internal/jet/clause.go b/internal/jet/clause.go index 1619124..ed899b3 100644 --- a/internal/jet/clause.go +++ b/internal/jet/clause.go @@ -234,6 +234,29 @@ func (o *ClauseOffset) Serialize(statementType StatementType, out *SQLBuilder, o } } +// ClauseFetch struct +type ClauseFetch struct { + Count IntegerExpression + WithTies bool +} + +// Serialize serializes ClauseFetch into sql builder output +func (o *ClauseFetch) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) { + if is.Nil(o.Count) { + return + } + + out.NewLine() + out.WriteString("FETCH FIRST") + o.Count.serialize(statementType, out, options...) + + if o.WithTies { + out.WriteString("ROWS WITH TIES") + } else { + out.WriteString("ROWS ONLY") + } +} + // ClauseFor struct type ClauseFor struct { Lock RowLock diff --git a/postgres/select_statement.go b/postgres/select_statement.go index d44d6aa..0ee80bf 100644 --- a/postgres/select_statement.go +++ b/postgres/select_statement.go @@ -53,6 +53,7 @@ type SelectStatement interface { ORDER_BY(orderByClauses ...OrderByClause) SelectStatement LIMIT(limit int64) SelectStatement OFFSET(offset int64) SelectStatement + FETCH_FIRST(count IntegerExpression) fetchExpand FOR(lock RowLock) SelectStatement UNION(rhs SelectStatement) setStatement @@ -72,9 +73,18 @@ func SELECT(projection Projection, projections ...Projection) SelectStatement { func newSelectStatement(table ReadableTable, projections []Projection) SelectStatement { newSelect := &selectStatementImpl{} - newSelect.ExpressionStatement = jet.NewExpressionStatementImpl(Dialect, jet.SelectStatementType, newSelect, &newSelect.Select, - &newSelect.From, &newSelect.Where, &newSelect.GroupBy, &newSelect.Having, &newSelect.Window, &newSelect.OrderBy, - &newSelect.Limit, &newSelect.Offset, &newSelect.For) + newSelect.ExpressionStatement = jet.NewExpressionStatementImpl(Dialect, jet.SelectStatementType, newSelect, + &newSelect.Select, + &newSelect.From, + &newSelect.Where, + &newSelect.GroupBy, + &newSelect.Having, + &newSelect.Window, + &newSelect.OrderBy, + &newSelect.Limit, + &newSelect.Offset, + &newSelect.Fetch, + &newSelect.For) newSelect.Select.ProjectionList = projections if table != nil { @@ -101,6 +111,7 @@ type selectStatementImpl struct { OrderBy jet.ClauseOrderBy Limit jet.ClauseLimit Offset jet.ClauseOffset + Fetch jet.ClauseFetch For jet.ClauseFor } @@ -150,6 +161,14 @@ func (s *selectStatementImpl) OFFSET(offset int64) SelectStatement { return s } +func (s *selectStatementImpl) FETCH_FIRST(count IntegerExpression) fetchExpand { + s.Fetch.Count = count + + return fetchExpand{ + selectStatement: s, + } +} + func (s *selectStatementImpl) FOR(lock RowLock) SelectStatement { s.For.Lock = lock return s @@ -188,3 +207,19 @@ func readableTablesToSerializerList(tables []ReadableTable) []jet.Serializer { } return ret } + +type fetchExpand struct { + selectStatement *selectStatementImpl +} + +func (f fetchExpand) ROWS_ONLY() SelectStatement { + f.selectStatement.Fetch.WithTies = false + + return f.selectStatement +} + +func (f fetchExpand) ROWS_WITH_TIES() SelectStatement { + f.selectStatement.Fetch.WithTies = true + + return f.selectStatement +} diff --git a/tests/postgres/select_test.go b/tests/postgres/select_test.go index dcf8993..d9b30ad 100644 --- a/tests/postgres/select_test.go +++ b/tests/postgres/select_test.go @@ -236,6 +236,90 @@ LIMIT 12; require.NoError(t, err) } +func TestFetchFirst(t *testing.T) { + + t.Run("rows only", func(t *testing.T) { + stmt := SELECT(Actor.AllColumns). + FROM(Actor). + ORDER_BY(Actor.ActorID). + OFFSET(2). + FETCH_FIRST(Int(3)).ROWS_ONLY() + + testutils.AssertStatementSql(t, stmt, ` +SELECT actor.actor_id AS "actor.actor_id", + actor.first_name AS "actor.first_name", + actor.last_name AS "actor.last_name", + actor.last_update AS "actor.last_update" +FROM dvds.actor +ORDER BY actor.actor_id +OFFSET $1 +FETCH FIRST $2 ROWS ONLY; +`) + + var dest []model.Actor + + err := stmt.Query(db, &dest) + require.NoError(t, err) + require.Len(t, dest, 3) + require.Equal(t, dest[0].ActorID, int32(3)) + require.Equal(t, dest[2].ActorID, int32(5)) + }) + + t.Run("rows with ties", func(t *testing.T) { + skipForCockroachDB(t) // ROWS_WITH_TIES is not supported on cockroachdb + + stmt := SELECT(Actor.AllColumns). + FROM(Actor). + ORDER_BY(Actor.LastUpdate). + FETCH_FIRST(Int(3)).ROWS_WITH_TIES() + + testutils.AssertStatementSql(t, stmt, ` +SELECT actor.actor_id AS "actor.actor_id", + actor.first_name AS "actor.first_name", + actor.last_name AS "actor.last_name", + actor.last_update AS "actor.last_update" +FROM dvds.actor +ORDER BY actor.last_update +FETCH FIRST $1 ROWS WITH TIES; +`) + + var dest []model.Actor + + err := stmt.Query(db, &dest) + require.NoError(t, err) + require.Len(t, dest, 200) + }) + + t.Run("complex expression", func(t *testing.T) { + stmt := SELECT(Actor.AllColumns). + FROM(Actor). + ORDER_BY(Actor.LastUpdate). + FETCH_FIRST(IntExp( + SELECT(MAX(Store.StoreID)). + FROM(Store), + )).ROWS_ONLY() + + testutils.AssertDebugStatementSql(t, stmt, ` +SELECT actor.actor_id AS "actor.actor_id", + actor.first_name AS "actor.first_name", + actor.last_name AS "actor.last_name", + actor.last_update AS "actor.last_update" +FROM dvds.actor +ORDER BY actor.last_update +FETCH FIRST ( + SELECT MAX(store.store_id) + FROM dvds.store +) ROWS ONLY; +`) + + var dest []model.Actor + + err := stmt.Query(db, &dest) + require.NoError(t, err) + require.Len(t, dest, 2) + }) +} + func TestJoinQueryStruct(t *testing.T) { expectedSQL := `