From 3c5e0a9be3e617a5cdd0b819430566689a014d8a Mon Sep 17 00:00:00 2001 From: Philip Salter -Hermes Date: Fri, 21 Aug 2026 17:36:56 +0000 Subject: [PATCH 1/2] Add MATERIALIZED support for postgres CTEs Expose AS_MATERIALIZED alongside AS_NOT_MATERIALIZED on the postgres CommonTableExpression, and emit the MATERIALIZED keyword in the WITH clause serializer. Unit-tested without a DB to lock down the SQL shape. --- internal/jet/with_statement.go | 6 +++- postgres/with_statement.go | 8 +++++ postgres/with_statement_test.go | 61 +++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 postgres/with_statement_test.go diff --git a/internal/jet/with_statement.go b/internal/jet/with_statement.go index 5fbf0671..07f6bc78 100644 --- a/internal/jet/with_statement.go +++ b/internal/jet/with_statement.go @@ -63,6 +63,7 @@ type CommonTableExpression struct { selectTableImpl NotMaterialized bool + Materialized bool Columns []ColumnExpression } @@ -90,8 +91,11 @@ func (c CommonTableExpression) serialize(statement StatementType, out *SQLBuilde } out.WriteString("AS") + if c.Materialized { + out.WriteString(" MATERIALIZED") + } if c.NotMaterialized { - out.WriteString("NOT MATERIALIZED") + out.WriteString(" NOT MATERIALIZED") } if c.Statement == nil { diff --git a/postgres/with_statement.go b/postgres/with_statement.go index de753ca3..89f758e9 100644 --- a/postgres/with_statement.go +++ b/postgres/with_statement.go @@ -7,6 +7,7 @@ type CommonTableExpression interface { SelectTable AS(statement jet.SerializerHasProjections) CommonTableExpression + AS_MATERIALIZED(statement jet.SerializerStatement) CommonTableExpression AS_NOT_MATERIALIZED(statement jet.SerializerStatement) CommonTableExpression // ALIAS is used to create another alias of the CTE, if a CTE needs to appear multiple times in the main query. ALIAS(alias string) SelectTable @@ -47,6 +48,13 @@ func (c *commonTableExpression) AS(statement jet.SerializerHasProjections) Commo return c } +// AS_MATERIALIZED is used to define a materialized CTE query +func (c *commonTableExpression) AS_MATERIALIZED(statement jet.SerializerStatement) CommonTableExpression { + c.CommonTableExpression.Materialized = true + c.CommonTableExpression.Statement = statement + return c +} + // AS_NOT_MATERIALIZED is used to define not materialized CTE query func (c *commonTableExpression) AS_NOT_MATERIALIZED(statement jet.SerializerStatement) CommonTableExpression { c.CommonTableExpression.NotMaterialized = true diff --git a/postgres/with_statement_test.go b/postgres/with_statement_test.go new file mode 100644 index 00000000..5ea360d3 --- /dev/null +++ b/postgres/with_statement_test.go @@ -0,0 +1,61 @@ +package postgres + +import ( + "testing" + + "github.com/go-jet/jet/v2/internal/testutils" +) + +func TestCTE_Materialized(t *testing.T) { + col := IntegerColumn("colo") + tbl := NewTable("db", "t", "", col) + + c1 := CTE("c1") + c2 := c1.ALIAS("c2") + + stmt := WITH( + c1.AS_MATERIALIZED( + SELECT(col).FROM(tbl), + ), + )( + SELECT( + c1.AllColumns().As("c1.*"), + c2.AllColumns().As("c2.*"), + ).FROM( + c1.INNER_JOIN(c2, Bool(true)), + ), + ) + + testutils.AssertStatementSql(t, stmt, ` +WITH c1 AS MATERIALIZED ( + SELECT t.colo AS "t.colo" + FROM db.t +) +SELECT c1."t.colo" AS "c1.colo", + c2."t.colo" AS "c2.colo" +FROM c1 + INNER JOIN c1 AS c2 ON $1::boolean; +`) +} + +func TestCTE_NotMaterialized(t *testing.T) { + col := IntegerColumn("colo") + tbl := NewTable("db", "t", "", col) + + n := CTE("n1") + + stmt := WITH( + n.AS_NOT_MATERIALIZED(SELECT(col).FROM(tbl)), + )( + SELECT(n.AllColumns().As("n1.*")).FROM(n), + ) + + testutils.AssertStatementSql(t, stmt, ` +WITH n1 AS NOT MATERIALIZED ( + SELECT t.colo AS "t.colo" + FROM db.t +) +SELECT n1."t.colo" AS "n1.colo" +FROM n1; +`) +} From f44a5e6956b03ea44e5c4e899c8c1459b6adcb5f Mon Sep 17 00:00:00 2001 From: Philip Salter -Hermes Date: Sun, 23 Aug 2026 09:34:01 +0000 Subject: [PATCH 2/2] Address review: enum for CTE materialization, widen statement types, add integration test - Replace the two booleans on CommonTableExpression with a CTEMaterialization enum (default / forced / disabled). - Accept jet.SerializerHasProjections on AS_MATERIALIZED and AS_NOT_MATERIALIZED so VALUES statements can be passed as CTE bodies, matching AS(). - Add TestWitStatement_CTE_Materialized integration test mirroring the existing NOT MATERIALIZED one. - Update sqlite AS_NOT_MATERIALIZED to the enum (keeps all dialects building). --- internal/jet/with_statement.go | 21 +++++++++--- postgres/with_statement.go | 12 +++---- sqlite/with_statement.go | 2 +- tests/postgres/with_test.go | 58 ++++++++++++++++++++++++++++++++++ 4 files changed, 81 insertions(+), 12 deletions(-) diff --git a/internal/jet/with_statement.go b/internal/jet/with_statement.go index 07f6bc78..49e2a092 100644 --- a/internal/jet/with_statement.go +++ b/internal/jet/with_statement.go @@ -58,12 +58,23 @@ func (w withImpl) projections() ProjectionList { return ProjectionList{} } +// CTEMaterialization is the materialization of a common table expression. +type CTEMaterialization int + +const ( + // CTEMaterializationDefault leaves materialization to the database. + CTEMaterializationDefault CTEMaterialization = iota + // CTEMaterializationForced forces materialization (MATERIALIZED). + CTEMaterializationForced + // CTEMaterializationDisabled disables materialization (NOT MATERIALIZED). + CTEMaterializationDisabled +) + // CommonTableExpression contains information about a CTE. type CommonTableExpression struct { selectTableImpl - NotMaterialized bool - Materialized bool + Materialization CTEMaterialization Columns []ColumnExpression } @@ -91,10 +102,10 @@ func (c CommonTableExpression) serialize(statement StatementType, out *SQLBuilde } out.WriteString("AS") - if c.Materialized { + switch c.Materialization { + case CTEMaterializationForced: out.WriteString(" MATERIALIZED") - } - if c.NotMaterialized { + case CTEMaterializationDisabled: out.WriteString(" NOT MATERIALIZED") } diff --git a/postgres/with_statement.go b/postgres/with_statement.go index 89f758e9..4acf55b1 100644 --- a/postgres/with_statement.go +++ b/postgres/with_statement.go @@ -7,8 +7,8 @@ type CommonTableExpression interface { SelectTable AS(statement jet.SerializerHasProjections) CommonTableExpression - AS_MATERIALIZED(statement jet.SerializerStatement) CommonTableExpression - AS_NOT_MATERIALIZED(statement jet.SerializerStatement) CommonTableExpression + AS_MATERIALIZED(statement jet.SerializerHasProjections) CommonTableExpression + AS_NOT_MATERIALIZED(statement jet.SerializerHasProjections) CommonTableExpression // ALIAS is used to create another alias of the CTE, if a CTE needs to appear multiple times in the main query. ALIAS(alias string) SelectTable @@ -49,15 +49,15 @@ func (c *commonTableExpression) AS(statement jet.SerializerHasProjections) Commo } // AS_MATERIALIZED is used to define a materialized CTE query -func (c *commonTableExpression) AS_MATERIALIZED(statement jet.SerializerStatement) CommonTableExpression { - c.CommonTableExpression.Materialized = true +func (c *commonTableExpression) AS_MATERIALIZED(statement jet.SerializerHasProjections) CommonTableExpression { + c.CommonTableExpression.Materialization = jet.CTEMaterializationForced c.CommonTableExpression.Statement = statement return c } // AS_NOT_MATERIALIZED is used to define not materialized CTE query -func (c *commonTableExpression) AS_NOT_MATERIALIZED(statement jet.SerializerStatement) CommonTableExpression { - c.CommonTableExpression.NotMaterialized = true +func (c *commonTableExpression) AS_NOT_MATERIALIZED(statement jet.SerializerHasProjections) CommonTableExpression { + c.CommonTableExpression.Materialization = jet.CTEMaterializationDisabled c.CommonTableExpression.Statement = statement return c } diff --git a/sqlite/with_statement.go b/sqlite/with_statement.go index 5693c8fd..5e274474 100644 --- a/sqlite/with_statement.go +++ b/sqlite/with_statement.go @@ -49,7 +49,7 @@ func (c *commonTableExpression) AS(statement jet.SerializerHasProjections) Commo // AS_NOT_MATERIALIZED is used to define not materialized CTE query func (c *commonTableExpression) AS_NOT_MATERIALIZED(statement jet.SerializerHasProjections) CommonTableExpression { - c.CommonTableExpression.NotMaterialized = true + c.CommonTableExpression.Materialization = jet.CTEMaterializationDisabled c.CommonTableExpression.Statement = statement return c } diff --git a/tests/postgres/with_test.go b/tests/postgres/with_test.go index 2348cd8b..d4124d0b 100644 --- a/tests/postgres/with_test.go +++ b/tests/postgres/with_test.go @@ -875,3 +875,61 @@ WHERE orders1."orders.order_id" < $1; require.NoError(t, err) require.Len(t, dest, 72) } + +func TestWitStatement_CTE_Materialized(t *testing.T) { + orders1 := CTE("orders1") + orders1ID := Orders.OrderID.From(orders1) + orders2 := orders1.ALIAS("orders2") + orders2ID := Orders.OrderID.From(orders2) + + stmt := WITH( + orders1.AS_MATERIALIZED( + SELECT( + Orders.OrderID, + Orders.EmployeeID, + Orders.ShipCity, + ).FROM( + Orders, + ), + ), + )( + SELECT( + orders1.AllColumns().As("orders1.*"), + orders2.AllColumns().As("orders2.*"), + ).FROM( + orders1. + INNER_JOIN(orders2, orders1ID.EQ(orders2ID)), + ).WHERE( + orders1ID.LT(Int(10320)), + ), + ) + + // fmt.Println(stmt.Sql()) + + testutils.AssertStatementSql(t, stmt, ` +WITH orders1 AS MATERIALIZED ( + SELECT orders.order_id AS "orders.order_id", + orders.employee_id AS "orders.employee_id", + orders.ship_city AS "orders.ship_city" + FROM northwind.orders +) +SELECT orders1."orders.order_id" AS "orders1.order_id", + orders1."orders.employee_id" AS "orders1.employee_id", + orders1."orders.ship_city" AS "orders1.ship_city", + orders2."orders.order_id" AS "orders2.order_id", + orders2."orders.employee_id" AS "orders2.employee_id", + orders2."orders.ship_city" AS "orders2.ship_city" +FROM orders1 + INNER JOIN orders1 AS orders2 ON (orders1."orders.order_id" = orders2."orders.order_id") +WHERE orders1."orders.order_id" < $1; +`) + + var dest []struct { + Orders1 model.Orders `alias:"orders1.*"` + Orders2 model.Orders `alias:"orders2.*"` + } + + err := stmt.Query(db, &dest) + require.NoError(t, err) + require.Len(t, dest, 72) +}