diff --git a/engine.go b/engine.go index 9392539d1b..9ce1192440 100644 --- a/engine.go +++ b/engine.go @@ -579,9 +579,9 @@ func (e *Engine) bindExecuteQueryNode(ctx *sql.Context, query string, eq *plan.E // TODO: overwrite the current binding if bindings are not empty??? tempBindings := make(map[string]sql.Expression) - for i, name := range eq.BindVars { - if strings.HasPrefix(name.String(), "@") { - t, val, err := ctx.GetUserVariable(ctx, strings.TrimPrefix(name.String(), "@")) + for i, binding := range eq.BindVars { + if userVar, ok := binding.(*expression.UserVar); ok { + t, val, err := ctx.GetUserVariable(ctx, userVar.Name) if err != nil { return nil, nil } @@ -596,7 +596,7 @@ func (e *Engine) bindExecuteQueryNode(ctx *sql.Context, query string, eq *plan.E } tempBindings[fmt.Sprintf("v%d", i+1)] = expression.NewLiteral(val, t) } else { - tempBindings[fmt.Sprintf("v%d", i)] = name + tempBindings[fmt.Sprintf("v%d", i)] = binding } } diff --git a/enginetest/queries/imdb_plans.go b/enginetest/queries/imdb_plans.go index cd927b5951..59a4a9f09d 100644 --- a/enginetest/queries/imdb_plans.go +++ b/enginetest/queries/imdb_plans.go @@ -383,7 +383,7 @@ WHERE ci.note LIKE '%(voice)%' " └─ columns: [id role]\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [min(chn.name) as character, min(t.title) as russian_mov_with_actor_producer]\n" + + " ├─ columns: [min(chn.name) as `character`, min(t.title) as russian_mov_with_actor_producer]\n" + " └─ GroupBy\n" + " ├─ select: MIN(chn.name), MIN(t.title)\n" + " ├─ group: \n" + @@ -445,7 +445,7 @@ WHERE ci.note LIKE '%(voice)%' " └─ columns: [id role]\n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [min(chn.name) as character, min(t.title) as russian_mov_with_actor_producer]\n" + + " ├─ columns: [min(chn.name) as `character`, min(t.title) as russian_mov_with_actor_producer]\n" + " └─ GroupBy\n" + " ├─ select: MIN(chn.name), MIN(t.title)\n" + " ├─ group: \n" + @@ -621,7 +621,7 @@ WHERE ci.note LIKE '%(voice)%' " └─ columns: [id]\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [min(chn.name) as character, min(t.title) as movie_with_american_producer]\n" + + " ├─ columns: [min(chn.name) as `character`, min(t.title) as movie_with_american_producer]\n" + " └─ GroupBy\n" + " ├─ select: MIN(chn.name), MIN(t.title)\n" + " ├─ group: \n" + @@ -681,7 +681,7 @@ WHERE ci.note LIKE '%(voice)%' " └─ columns: [id]\n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [min(chn.name) as character, min(t.title) as movie_with_american_producer]\n" + + " ├─ columns: [min(chn.name) as `character`, min(t.title) as movie_with_american_producer]\n" + " └─ GroupBy\n" + " ├─ select: MIN(chn.name), MIN(t.title)\n" + " ├─ group: \n" + diff --git a/enginetest/queries/index_query_plans.go b/enginetest/queries/index_query_plans.go index 117f8e3e3b..d703e3a678 100644 --- a/enginetest/queries/index_query_plans.go +++ b/enginetest/queries/index_query_plans.go @@ -16369,7 +16369,7 @@ var IndexPlanTests = []QueryPlanTest{ "", ExpectedEstimates: "Limit(5)\n" + " └─ Project\n" + - " ├─ columns: [(comp_vector_index_t0.pk + 1) as pk+1]\n" + + " ├─ columns: [(comp_vector_index_t0.pk + 1) as `pk+1`]\n" + " └─ IndexedTableAccess(comp_vector_index_t0)\n" + " ├─ index: [comp_vector_index_t0.vector_column]\n" + " ├─ order: VEC_DISTANCE_L2_SQUARED('[50,50]', comp_vector_index_t0.vector_column) LIMIT 5 (bigint)\n" + @@ -16377,7 +16377,7 @@ var IndexPlanTests = []QueryPlanTest{ "", ExpectedAnalysis: "Limit(5)\n" + " └─ Project\n" + - " ├─ columns: [(comp_vector_index_t0.pk + 1) as pk+1]\n" + + " ├─ columns: [(comp_vector_index_t0.pk + 1) as `pk+1`]\n" + " └─ IndexedTableAccess(comp_vector_index_t0)\n" + " ├─ index: [comp_vector_index_t0.vector_column]\n" + " ├─ order: VEC_DISTANCE_L2_SQUARED('[50,50]', comp_vector_index_t0.vector_column) LIMIT 5 (bigint)\n" + @@ -16400,7 +16400,7 @@ var IndexPlanTests = []QueryPlanTest{ "", ExpectedEstimates: "Limit(5)\n" + " └─ Project\n" + - " ├─ columns: [(comp_vector_index_t0.v1 + 1) as v1+1]\n" + + " ├─ columns: [(comp_vector_index_t0.v1 + 1) as `v1+1`]\n" + " └─ IndexedTableAccess(comp_vector_index_t0)\n" + " ├─ index: [comp_vector_index_t0.vector_column]\n" + " ├─ order: VEC_DISTANCE_L2_SQUARED(comp_vector_index_t0.vector_column, '[50,50]') LIMIT 5 (bigint)\n" + @@ -16408,7 +16408,7 @@ var IndexPlanTests = []QueryPlanTest{ "", ExpectedAnalysis: "Limit(5)\n" + " └─ Project\n" + - " ├─ columns: [(comp_vector_index_t0.v1 + 1) as v1+1]\n" + + " ├─ columns: [(comp_vector_index_t0.v1 + 1) as `v1+1`]\n" + " └─ IndexedTableAccess(comp_vector_index_t0)\n" + " ├─ index: [comp_vector_index_t0.vector_column]\n" + " ├─ order: VEC_DISTANCE_L2_SQUARED(comp_vector_index_t0.vector_column, '[50,50]') LIMIT 5 (bigint)\n" + diff --git a/enginetest/queries/integration_plans.go b/enginetest/queries/integration_plans.go index 77da39697a..2c9469ff67 100644 --- a/enginetest/queries/integration_plans.go +++ b/enginetest/queries/integration_plans.go @@ -278,7 +278,7 @@ WHERE " ├─ columns: [nd.id as id, nd.ZH72S as ZH72S, Subquery\n" + " │ ├─ cacheable: false\n" + " │ └─ Project\n" + - " │ ├─ columns: [count(1) as COUNT(*)]\n" + + " │ ├─ columns: [count(1) as `COUNT(*)`]\n" + " │ └─ GroupBy\n" + " │ ├─ select: COUNT(1)\n" + " │ ├─ group: \n" + @@ -293,7 +293,7 @@ WHERE " ├─ columns: [nd.id, nd.DKCAJ, nd.KNG7T, nd.TW55N, nd.QRQXW, nd.ECXAJ, nd.FGG57, nd.ZH72S, nd.FSK67, nd.XQDYT, nd.TCE7A, nd.IWV2H, nd.HPCMS, nd.N5CC2, nd.FHCYT, nd.ETAQ7, nd.A75X7, nd.id as id, nd.ZH72S as ZH72S, Subquery\n" + " │ ├─ cacheable: false\n" + " │ └─ Project\n" + - " │ ├─ columns: [count(1) as COUNT(*)]\n" + + " │ ├─ columns: [count(1) as `COUNT(*)`]\n" + " │ └─ GroupBy\n" + " │ ├─ select: COUNT(1)\n" + " │ ├─ group: \n" + @@ -348,7 +348,7 @@ WHERE " ├─ columns: [nd.id as id, nd.ZH72S as ZH72S, Subquery\n" + " │ ├─ cacheable: false\n" + " │ └─ Project\n" + - " │ ├─ columns: [count(1) as COUNT(*)]\n" + + " │ ├─ columns: [count(1) as `COUNT(*)`]\n" + " │ └─ GroupBy\n" + " │ ├─ select: COUNT(1)\n" + " │ ├─ group: \n" + @@ -363,7 +363,7 @@ WHERE " ├─ columns: [nd.id, nd.DKCAJ, nd.KNG7T, nd.TW55N, nd.QRQXW, nd.ECXAJ, nd.FGG57, nd.ZH72S, nd.FSK67, nd.XQDYT, nd.TCE7A, nd.IWV2H, nd.HPCMS, nd.N5CC2, nd.FHCYT, nd.ETAQ7, nd.A75X7, nd.id as id, nd.ZH72S as ZH72S, Subquery\n" + " │ ├─ cacheable: false\n" + " │ └─ Project\n" + - " │ ├─ columns: [count(1) as COUNT(*)]\n" + + " │ ├─ columns: [count(1) as `COUNT(*)`]\n" + " │ └─ GroupBy\n" + " │ ├─ select: COUNT(1)\n" + " │ ├─ group: \n" + @@ -613,7 +613,58 @@ WHERE " └─ columns: [id fv24e uj6xy m22qn nz4mq etpqv pruv2 ykssu fhcyt]\n" + "", ExpectedEstimates: "Filter\n" + - " ├─ (((NOT(ism.PRUV2 IS NULL)) AND ((Subquery(select NHMXW.SWCQV from WGSDC as NHMXW where NHMXW.id = ism.PRUV2) = 1) OR (((NOT(ism.FV24E IS NULL)) AND (NOT((Subquery(select nd.id from E2I7U as nd where nd.TW55N = (select NHMXW.FZXV5 from WGSDC as NHMXW where NHMXW.id = ism.PRUV2)) = ism.FV24E)))) OR ((NOT(ism.UJ6XY IS NULL)) AND (NOT((Subquery(select nd.id from E2I7U as nd where nd.TW55N = (select NHMXW.DQYGV from WGSDC as NHMXW where NHMXW.id = ism.PRUV2)) = ism.UJ6XY))))))) OR ((NOT(ism.ETPQV IS NULL)) AND InSubquery\n" + + " ├─ (((NOT(ism.PRUV2 IS NULL)) AND ((Subquery\n" + + " │ ├─ cacheable: false\n" + + " │ └─ Project\n" + + " │ ├─ columns: [nhmxw.SWCQV]\n" + + " │ └─ Filter\n" + + " │ ├─ (nhmxw.id = ism.PRUV2)\n" + + " │ └─ TableAlias(nhmxw)\n" + + " │ └─ IndexedTableAccess(WGSDC)\n" + + " │ ├─ index: [WGSDC.id]\n" + + " │ ├─ columns: [id swcqv]\n" + + " │ └─ keys: ism.PRUV2\n" + + " │ = 1) OR (((NOT(ism.FV24E IS NULL)) AND (NOT((Subquery\n" + + " │ ├─ cacheable: false\n" + + " │ └─ Project\n" + + " │ ├─ columns: [nd.id]\n" + + " │ └─ Filter\n" + + " │ ├─ (nd.TW55N = Subquery\n" + + " │ │ ├─ cacheable: false\n" + + " │ │ └─ Project\n" + + " │ │ ├─ columns: [nhmxw.FZXV5]\n" + + " │ │ └─ Filter\n" + + " │ │ ├─ (nhmxw.id = ism.PRUV2)\n" + + " │ │ └─ TableAlias(nhmxw)\n" + + " │ │ └─ IndexedTableAccess(WGSDC)\n" + + " │ │ ├─ index: [WGSDC.id]\n" + + " │ │ ├─ columns: [id fzxv5]\n" + + " │ │ └─ keys: ism.PRUV2\n" + + " │ │ )\n" + + " │ └─ TableAlias(nd)\n" + + " │ └─ Table\n" + + " │ └─ name: E2I7U\n" + + " │ = ism.FV24E)))) OR ((NOT(ism.UJ6XY IS NULL)) AND (NOT((Subquery\n" + + " │ ├─ cacheable: false\n" + + " │ └─ Project\n" + + " │ ├─ columns: [nd.id]\n" + + " │ └─ Filter\n" + + " │ ├─ (nd.TW55N = Subquery\n" + + " │ │ ├─ cacheable: false\n" + + " │ │ └─ Project\n" + + " │ │ ├─ columns: [nhmxw.DQYGV]\n" + + " │ │ └─ Filter\n" + + " │ │ ├─ (nhmxw.id = ism.PRUV2)\n" + + " │ │ └─ TableAlias(nhmxw)\n" + + " │ │ └─ IndexedTableAccess(WGSDC)\n" + + " │ │ ├─ index: [WGSDC.id]\n" + + " │ │ ├─ columns: [id dqygv]\n" + + " │ │ └─ keys: ism.PRUV2\n" + + " │ │ )\n" + + " │ └─ TableAlias(nd)\n" + + " │ └─ Table\n" + + " │ └─ name: E2I7U\n" + + " │ = ism.UJ6XY))))))) OR ((NOT(ism.ETPQV IS NULL)) AND InSubquery\n" + " │ ├─ left: ism.ETPQV\n" + " │ └─ right: Subquery\n" + " │ ├─ cacheable: true\n" + @@ -623,9 +674,9 @@ WHERE " │ ├─ columns: [WGSDC.id, WGSDC.NOHHR, WGSDC.AVPYF, WGSDC.SYPKF, WGSDC.IDUT2, WGSDC.FZXV5, WGSDC.DQYGV, WGSDC.SWCQV, WGSDC.YKSSU, WGSDC.FHCYT, WRZVO.id, WRZVO.TVNW2, WRZVO.ZHITY, WRZVO.SYPKF, WRZVO.IDUT2, WRZVO.O6QJ3, WRZVO.NO2JA, WRZVO.YKSSU, WRZVO.FHCYT, WRZVO.QZ6VT]\n" + " │ └─ Filter\n" + " │ ├─ 1 IS NULL\n" + - " │ └─ LeftOuterHashJoinExcludingNulls\n" + + " │ └─ LeftOuterHashJoinExcludingNulls (estimated cost=2134.470 rows=39476)\n" + " │ ├─ (nhmxw.id = hddvb.PRUV2)\n" + - " │ ├─ LookupJoin\n" + + " │ ├─ LookupJoin (estimated cost=244.013 rows=71)\n" + " │ │ ├─ (((tizhk.TVNW2 = nhmxw.NOHHR) AND (tizhk.ZHITY = nhmxw.AVPYF)) AND (tizhk.IDUT2 = nhmxw.IDUT2))\n" + " │ │ ├─ Filter\n" + " │ │ │ ├─ (nhmxw.SWCQV = 0)\n" + @@ -651,7 +702,58 @@ WHERE " └─ name: HDDVB\n" + "", ExpectedAnalysis: "Filter\n" + - " ├─ (((NOT(ism.PRUV2 IS NULL)) AND ((Subquery(select NHMXW.SWCQV from WGSDC as NHMXW where NHMXW.id = ism.PRUV2) = 1) OR (((NOT(ism.FV24E IS NULL)) AND (NOT((Subquery(select nd.id from E2I7U as nd where nd.TW55N = (select NHMXW.FZXV5 from WGSDC as NHMXW where NHMXW.id = ism.PRUV2)) = ism.FV24E)))) OR ((NOT(ism.UJ6XY IS NULL)) AND (NOT((Subquery(select nd.id from E2I7U as nd where nd.TW55N = (select NHMXW.DQYGV from WGSDC as NHMXW where NHMXW.id = ism.PRUV2)) = ism.UJ6XY))))))) OR ((NOT(ism.ETPQV IS NULL)) AND InSubquery\n" + + " ├─ (((NOT(ism.PRUV2 IS NULL)) AND ((Subquery\n" + + " │ ├─ cacheable: false\n" + + " │ └─ Project\n" + + " │ ├─ columns: [nhmxw.SWCQV]\n" + + " │ └─ Filter\n" + + " │ ├─ (nhmxw.id = ism.PRUV2)\n" + + " │ └─ TableAlias(nhmxw)\n" + + " │ └─ IndexedTableAccess(WGSDC)\n" + + " │ ├─ index: [WGSDC.id]\n" + + " │ ├─ columns: [id swcqv]\n" + + " │ └─ keys: ism.PRUV2\n" + + " │ = 1) OR (((NOT(ism.FV24E IS NULL)) AND (NOT((Subquery\n" + + " │ ├─ cacheable: false\n" + + " │ └─ Project\n" + + " │ ├─ columns: [nd.id]\n" + + " │ └─ Filter\n" + + " │ ├─ (nd.TW55N = Subquery\n" + + " │ │ ├─ cacheable: false\n" + + " │ │ └─ Project\n" + + " │ │ ├─ columns: [nhmxw.FZXV5]\n" + + " │ │ └─ Filter\n" + + " │ │ ├─ (nhmxw.id = ism.PRUV2)\n" + + " │ │ └─ TableAlias(nhmxw)\n" + + " │ │ └─ IndexedTableAccess(WGSDC)\n" + + " │ │ ├─ index: [WGSDC.id]\n" + + " │ │ ├─ columns: [id fzxv5]\n" + + " │ │ └─ keys: ism.PRUV2\n" + + " │ │ )\n" + + " │ └─ TableAlias(nd)\n" + + " │ └─ Table\n" + + " │ └─ name: E2I7U\n" + + " │ = ism.FV24E)))) OR ((NOT(ism.UJ6XY IS NULL)) AND (NOT((Subquery\n" + + " │ ├─ cacheable: false\n" + + " │ └─ Project\n" + + " │ ├─ columns: [nd.id]\n" + + " │ └─ Filter\n" + + " │ ├─ (nd.TW55N = Subquery\n" + + " │ │ ├─ cacheable: false\n" + + " │ │ └─ Project\n" + + " │ │ ├─ columns: [nhmxw.DQYGV]\n" + + " │ │ └─ Filter\n" + + " │ │ ├─ (nhmxw.id = ism.PRUV2)\n" + + " │ │ └─ TableAlias(nhmxw)\n" + + " │ │ └─ IndexedTableAccess(WGSDC)\n" + + " │ │ ├─ index: [WGSDC.id]\n" + + " │ │ ├─ columns: [id dqygv]\n" + + " │ │ └─ keys: ism.PRUV2\n" + + " │ │ )\n" + + " │ └─ TableAlias(nd)\n" + + " │ └─ Table\n" + + " │ └─ name: E2I7U\n" + + " │ = ism.UJ6XY))))))) OR ((NOT(ism.ETPQV IS NULL)) AND InSubquery\n" + " │ ├─ left: ism.ETPQV\n" + " │ └─ right: Subquery\n" + " │ ├─ cacheable: true\n" + @@ -661,9 +763,9 @@ WHERE " │ ├─ columns: [WGSDC.id, WGSDC.NOHHR, WGSDC.AVPYF, WGSDC.SYPKF, WGSDC.IDUT2, WGSDC.FZXV5, WGSDC.DQYGV, WGSDC.SWCQV, WGSDC.YKSSU, WGSDC.FHCYT, WRZVO.id, WRZVO.TVNW2, WRZVO.ZHITY, WRZVO.SYPKF, WRZVO.IDUT2, WRZVO.O6QJ3, WRZVO.NO2JA, WRZVO.YKSSU, WRZVO.FHCYT, WRZVO.QZ6VT]\n" + " │ └─ Filter\n" + " │ ├─ 1 IS NULL\n" + - " │ └─ LeftOuterHashJoinExcludingNulls\n" + + " │ └─ LeftOuterHashJoinExcludingNulls (estimated cost=2134.470 rows=39476)\n" + " │ ├─ (nhmxw.id = hddvb.PRUV2)\n" + - " │ ├─ LookupJoin\n" + + " │ ├─ LookupJoin (estimated cost=244.013 rows=71)\n" + " │ │ ├─ (((tizhk.TVNW2 = nhmxw.NOHHR) AND (tizhk.ZHITY = nhmxw.AVPYF)) AND (tizhk.IDUT2 = nhmxw.IDUT2))\n" + " │ │ ├─ Filter\n" + " │ │ │ ├─ (nhmxw.SWCQV = 0)\n" + @@ -1315,7 +1417,7 @@ WHERE " ├─ columns: [nd.id as id, nd.ZH72S as ZH72S, Subquery\n" + " │ ├─ cacheable: false\n" + " │ └─ Project\n" + - " │ ├─ columns: [count(1) as COUNT(*)]\n" + + " │ ├─ columns: [count(1) as `COUNT(*)`]\n" + " │ └─ GroupBy\n" + " │ ├─ select: COUNT(1)\n" + " │ ├─ group: \n" + @@ -1330,7 +1432,7 @@ WHERE " ├─ columns: [nd.id, nd.DKCAJ, nd.KNG7T, nd.TW55N, nd.QRQXW, nd.ECXAJ, nd.FGG57, nd.ZH72S, nd.FSK67, nd.XQDYT, nd.TCE7A, nd.IWV2H, nd.HPCMS, nd.N5CC2, nd.FHCYT, nd.ETAQ7, nd.A75X7, nd.id as id, nd.ZH72S as ZH72S, Subquery\n" + " │ ├─ cacheable: false\n" + " │ └─ Project\n" + - " │ ├─ columns: [count(1) as COUNT(*)]\n" + + " │ ├─ columns: [count(1) as `COUNT(*)`]\n" + " │ └─ GroupBy\n" + " │ ├─ select: COUNT(1)\n" + " │ ├─ group: \n" + @@ -1385,7 +1487,7 @@ WHERE " ├─ columns: [nd.id as id, nd.ZH72S as ZH72S, Subquery\n" + " │ ├─ cacheable: false\n" + " │ └─ Project\n" + - " │ ├─ columns: [count(1) as COUNT(*)]\n" + + " │ ├─ columns: [count(1) as `COUNT(*)`]\n" + " │ └─ GroupBy\n" + " │ ├─ select: COUNT(1)\n" + " │ ├─ group: \n" + @@ -1400,7 +1502,7 @@ WHERE " ├─ columns: [nd.id, nd.DKCAJ, nd.KNG7T, nd.TW55N, nd.QRQXW, nd.ECXAJ, nd.FGG57, nd.ZH72S, nd.FSK67, nd.XQDYT, nd.TCE7A, nd.IWV2H, nd.HPCMS, nd.N5CC2, nd.FHCYT, nd.ETAQ7, nd.A75X7, nd.id as id, nd.ZH72S as ZH72S, Subquery\n" + " │ ├─ cacheable: false\n" + " │ └─ Project\n" + - " │ ├─ columns: [count(1) as COUNT(*)]\n" + + " │ ├─ columns: [count(1) as `COUNT(*)`]\n" + " │ └─ GroupBy\n" + " │ ├─ select: COUNT(1)\n" + " │ ├─ group: \n" + @@ -1648,7 +1750,38 @@ WHERE ExpectedEstimates: "Project\n" + " ├─ columns: [ct.id as id, ci.FTQLQ as VCGT3, nd.TW55N as UWBAI, aac.BTXC5 as TPXBU, ct.V5DPX as V5DPX, ct.S3Q3Y as S3Q3Y, ct.ZRV3B as ZRV3B]\n" + " └─ Filter\n" + - " ├─ (((NOT(ct.OCA7E IS NULL)) AND ((Subquery(select I7HCR.SWCQV from EPZU6 as I7HCR where I7HCR.id = ct.OCA7E) = 1) OR (NOT((Subquery(select nd.id from E2I7U as nd where nd.TW55N = (select I7HCR.FVUCX from EPZU6 as I7HCR where I7HCR.id = ct.OCA7E)) = ct.LUEVY))))) OR ((NOT(ct.NRURT IS NULL)) AND InSubquery\n" + + " ├─ (((NOT(ct.OCA7E IS NULL)) AND ((Subquery\n" + + " │ ├─ cacheable: false\n" + + " │ └─ Project\n" + + " │ ├─ columns: [i7hcr.SWCQV]\n" + + " │ └─ Filter\n" + + " │ ├─ (i7hcr.id = ct.OCA7E)\n" + + " │ └─ TableAlias(i7hcr)\n" + + " │ └─ IndexedTableAccess(EPZU6)\n" + + " │ ├─ index: [EPZU6.id]\n" + + " │ ├─ columns: [id swcqv]\n" + + " │ └─ keys: ct.OCA7E\n" + + " │ = 1) OR (NOT((Subquery\n" + + " │ ├─ cacheable: false\n" + + " │ └─ Project\n" + + " │ ├─ columns: [nd.id]\n" + + " │ └─ Filter\n" + + " │ ├─ (nd.TW55N = Subquery\n" + + " │ │ ├─ cacheable: false\n" + + " │ │ └─ Project\n" + + " │ │ ├─ columns: [i7hcr.FVUCX]\n" + + " │ │ └─ Filter\n" + + " │ │ ├─ (i7hcr.id = ct.OCA7E)\n" + + " │ │ └─ TableAlias(i7hcr)\n" + + " │ │ └─ IndexedTableAccess(EPZU6)\n" + + " │ │ ├─ index: [EPZU6.id]\n" + + " │ │ ├─ columns: [id fvucx]\n" + + " │ │ └─ keys: ct.OCA7E\n" + + " │ │ )\n" + + " │ └─ TableAlias(nd)\n" + + " │ └─ Table\n" + + " │ └─ name: E2I7U\n" + + " │ = ct.LUEVY))))) OR ((NOT(ct.NRURT IS NULL)) AND InSubquery\n" + " │ ├─ left: ct.NRURT\n" + " │ └─ right: Subquery\n" + " │ ├─ cacheable: true\n" + @@ -1658,9 +1791,9 @@ WHERE " │ ├─ columns: [EPZU6.id, EPZU6.TOFPN, EPZU6.SJYN2, EPZU6.BTXC5, EPZU6.FVUCX, EPZU6.SWCQV, EPZU6.YKSSU, EPZU6.FHCYT, OUBDL.id, OUBDL.FTQLQ, OUBDL.ZH72S, OUBDL.SFJ6L, OUBDL.V5DPX, OUBDL.LJLUM, OUBDL.IDPK7, OUBDL.NO52D, OUBDL.ZRV3B, OUBDL.VYO5E, OUBDL.YKSSU, OUBDL.FHCYT, OUBDL.QZ6VT]\n" + " │ └─ Filter\n" + " │ ├─ 1 IS NULL\n" + - " │ └─ LeftOuterHashJoinExcludingNulls\n" + + " │ └─ LeftOuterHashJoinExcludingNulls (estimated cost=39099.660 rows=1670140)\n" + " │ ├─ (i7hcr.id = flqlp.OCA7E)\n" + - " │ ├─ LookupJoin\n" + + " │ ├─ LookupJoin (estimated cost=730.387 rows=226)\n" + " │ │ ├─ ((uct.ZH72S = i7hcr.SJYN2) AND (uct.LJLUM = i7hcr.BTXC5))\n" + " │ │ ├─ Filter\n" + " │ │ │ ├─ (i7hcr.SWCQV = 0)\n" + @@ -1708,7 +1841,38 @@ WHERE ExpectedAnalysis: "Project\n" + " ├─ columns: [ct.id as id, ci.FTQLQ as VCGT3, nd.TW55N as UWBAI, aac.BTXC5 as TPXBU, ct.V5DPX as V5DPX, ct.S3Q3Y as S3Q3Y, ct.ZRV3B as ZRV3B]\n" + " └─ Filter\n" + - " ├─ (((NOT(ct.OCA7E IS NULL)) AND ((Subquery(select I7HCR.SWCQV from EPZU6 as I7HCR where I7HCR.id = ct.OCA7E) = 1) OR (NOT((Subquery(select nd.id from E2I7U as nd where nd.TW55N = (select I7HCR.FVUCX from EPZU6 as I7HCR where I7HCR.id = ct.OCA7E)) = ct.LUEVY))))) OR ((NOT(ct.NRURT IS NULL)) AND InSubquery\n" + + " ├─ (((NOT(ct.OCA7E IS NULL)) AND ((Subquery\n" + + " │ ├─ cacheable: false\n" + + " │ └─ Project\n" + + " │ ├─ columns: [i7hcr.SWCQV]\n" + + " │ └─ Filter\n" + + " │ ├─ (i7hcr.id = ct.OCA7E)\n" + + " │ └─ TableAlias(i7hcr)\n" + + " │ └─ IndexedTableAccess(EPZU6)\n" + + " │ ├─ index: [EPZU6.id]\n" + + " │ ├─ columns: [id swcqv]\n" + + " │ └─ keys: ct.OCA7E\n" + + " │ = 1) OR (NOT((Subquery\n" + + " │ ├─ cacheable: false\n" + + " │ └─ Project\n" + + " │ ├─ columns: [nd.id]\n" + + " │ └─ Filter\n" + + " │ ├─ (nd.TW55N = Subquery\n" + + " │ │ ├─ cacheable: false\n" + + " │ │ └─ Project\n" + + " │ │ ├─ columns: [i7hcr.FVUCX]\n" + + " │ │ └─ Filter\n" + + " │ │ ├─ (i7hcr.id = ct.OCA7E)\n" + + " │ │ └─ TableAlias(i7hcr)\n" + + " │ │ └─ IndexedTableAccess(EPZU6)\n" + + " │ │ ├─ index: [EPZU6.id]\n" + + " │ │ ├─ columns: [id fvucx]\n" + + " │ │ └─ keys: ct.OCA7E\n" + + " │ │ )\n" + + " │ └─ TableAlias(nd)\n" + + " │ └─ Table\n" + + " │ └─ name: E2I7U\n" + + " │ = ct.LUEVY))))) OR ((NOT(ct.NRURT IS NULL)) AND InSubquery\n" + " │ ├─ left: ct.NRURT\n" + " │ └─ right: Subquery\n" + " │ ├─ cacheable: true\n" + @@ -1718,9 +1882,9 @@ WHERE " │ ├─ columns: [EPZU6.id, EPZU6.TOFPN, EPZU6.SJYN2, EPZU6.BTXC5, EPZU6.FVUCX, EPZU6.SWCQV, EPZU6.YKSSU, EPZU6.FHCYT, OUBDL.id, OUBDL.FTQLQ, OUBDL.ZH72S, OUBDL.SFJ6L, OUBDL.V5DPX, OUBDL.LJLUM, OUBDL.IDPK7, OUBDL.NO52D, OUBDL.ZRV3B, OUBDL.VYO5E, OUBDL.YKSSU, OUBDL.FHCYT, OUBDL.QZ6VT]\n" + " │ └─ Filter\n" + " │ ├─ 1 IS NULL\n" + - " │ └─ LeftOuterHashJoinExcludingNulls\n" + + " │ └─ LeftOuterHashJoinExcludingNulls (estimated cost=39099.660 rows=1670140)\n" + " │ ├─ (i7hcr.id = flqlp.OCA7E)\n" + - " │ ├─ LookupJoin\n" + + " │ ├─ LookupJoin (estimated cost=730.387 rows=226)\n" + " │ │ ├─ ((uct.ZH72S = i7hcr.SJYN2) AND (uct.LJLUM = i7hcr.BTXC5))\n" + " │ │ ├─ Filter\n" + " │ │ │ ├─ (i7hcr.SWCQV = 0)\n" + @@ -3256,7 +3420,7 @@ WHERE " ├─ columns: [nd.id as id, nd.ZH72S as ZH72S, Subquery\n" + " │ ├─ cacheable: false\n" + " │ └─ Project\n" + - " │ ├─ columns: [count(1) as COUNT(*)]\n" + + " │ ├─ columns: [count(1) as `COUNT(*)`]\n" + " │ └─ GroupBy\n" + " │ ├─ select: COUNT(1)\n" + " │ ├─ group: \n" + @@ -3271,7 +3435,7 @@ WHERE " ├─ columns: [nd.id, nd.DKCAJ, nd.KNG7T, nd.TW55N, nd.QRQXW, nd.ECXAJ, nd.FGG57, nd.ZH72S, nd.FSK67, nd.XQDYT, nd.TCE7A, nd.IWV2H, nd.HPCMS, nd.N5CC2, nd.FHCYT, nd.ETAQ7, nd.A75X7, nd.id as id, nd.ZH72S as ZH72S, Subquery\n" + " │ ├─ cacheable: false\n" + " │ └─ Project\n" + - " │ ├─ columns: [count(1) as COUNT(*)]\n" + + " │ ├─ columns: [count(1) as `COUNT(*)`]\n" + " │ └─ GroupBy\n" + " │ ├─ select: COUNT(1)\n" + " │ ├─ group: \n" + @@ -3326,7 +3490,7 @@ WHERE " ├─ columns: [nd.id as id, nd.ZH72S as ZH72S, Subquery\n" + " │ ├─ cacheable: false\n" + " │ └─ Project\n" + - " │ ├─ columns: [count(1) as COUNT(*)]\n" + + " │ ├─ columns: [count(1) as `COUNT(*)`]\n" + " │ └─ GroupBy\n" + " │ ├─ select: COUNT(1)\n" + " │ ├─ group: \n" + @@ -3341,7 +3505,7 @@ WHERE " ├─ columns: [nd.id, nd.DKCAJ, nd.KNG7T, nd.TW55N, nd.QRQXW, nd.ECXAJ, nd.FGG57, nd.ZH72S, nd.FSK67, nd.XQDYT, nd.TCE7A, nd.IWV2H, nd.HPCMS, nd.N5CC2, nd.FHCYT, nd.ETAQ7, nd.A75X7, nd.id as id, nd.ZH72S as ZH72S, Subquery\n" + " │ ├─ cacheable: false\n" + " │ └─ Project\n" + - " │ ├─ columns: [count(1) as COUNT(*)]\n" + + " │ ├─ columns: [count(1) as `COUNT(*)`]\n" + " │ └─ GroupBy\n" + " │ ├─ select: COUNT(1)\n" + " │ ├─ group: \n" + @@ -4016,19 +4180,50 @@ WHERE ExpectedEstimates: "Project\n" + " ├─ columns: [mf.id as id, cla.FTQLQ as T4IBQ, nd.TW55N as UWBAI, aac.BTXC5 as TPXBU, mf.FSDY2 as FSDY2]\n" + " └─ Filter\n" + - " ├─ (((NOT(mf.QQV4M IS NULL)) AND ((Subquery(select TJ5D2.SWCQV from SZW6V as TJ5D2 where TJ5D2.id = mf.QQV4M) = 1) OR (NOT((Subquery(select nd.id from E2I7U as nd where nd.TW55N = (select TJ5D2.H4DMT from SZW6V as TJ5D2 where TJ5D2.id = mf.QQV4M)) = mf.LUEVY))))) OR ((NOT(mf.TEUJA IS NULL)) AND InSubquery\n" + + " ├─ (((NOT(mf.QQV4M IS NULL)) AND ((Subquery\n" + + " │ ├─ cacheable: false\n" + + " │ └─ Project\n" + + " │ ├─ columns: [tj5d2.SWCQV]\n" + + " │ └─ Filter\n" + + " │ ├─ (tj5d2.id = mf.QQV4M)\n" + + " │ └─ TableAlias(tj5d2)\n" + + " │ └─ IndexedTableAccess(SZW6V)\n" + + " │ ├─ index: [SZW6V.id]\n" + + " │ ├─ columns: [id swcqv]\n" + + " │ └─ keys: mf.QQV4M\n" + + " │ = 1) OR (NOT((Subquery\n" + + " │ ├─ cacheable: false\n" + + " │ └─ Project\n" + + " │ ├─ columns: [nd.id]\n" + + " │ └─ Filter\n" + + " │ ├─ (nd.TW55N = Subquery\n" + + " │ │ ├─ cacheable: false\n" + + " │ │ └─ Project\n" + + " │ │ ├─ columns: [tj5d2.H4DMT]\n" + + " │ │ └─ Filter\n" + + " │ │ ├─ (tj5d2.id = mf.QQV4M)\n" + + " │ │ └─ TableAlias(tj5d2)\n" + + " │ │ └─ IndexedTableAccess(SZW6V)\n" + + " │ │ ├─ index: [SZW6V.id]\n" + + " │ │ ├─ columns: [id h4dmt]\n" + + " │ │ └─ keys: mf.QQV4M\n" + + " │ │ )\n" + + " │ └─ TableAlias(nd)\n" + + " │ └─ Table\n" + + " │ └─ name: E2I7U\n" + + " │ = mf.LUEVY))))) OR ((NOT(mf.TEUJA IS NULL)) AND InSubquery\n" + " │ ├─ left: mf.TEUJA\n" + " │ └─ right: Subquery\n" + " │ ├─ cacheable: true\n" + " │ └─ Project\n" + " │ ├─ columns: [umf.id as ORB3K]\n" + - " │ └─ LookupJoin\n" + + " │ └─ LookupJoin (estimated cost=24.084 rows=0)\n" + " │ ├─ ((umf.FGG57 = tj5d2.V7UFH) AND (umf.SYPKF = tj5d2.SYPKF))\n" + " │ ├─ Project\n" + " │ │ ├─ columns: [SZW6V.id, SZW6V.T4IBQ, SZW6V.V7UFH, SZW6V.SYPKF, SZW6V.H4DMT, SZW6V.SWCQV, SZW6V.YKSSU, SZW6V.FHCYT]\n" + " │ │ └─ Filter\n" + " │ │ ├─ 1 IS NULL\n" + - " │ │ └─ LeftOuterLookupJoin\n" + + " │ │ └─ LeftOuterLookupJoin (estimated cost=24.062 rows=0)\n" + " │ │ ├─ Filter\n" + " │ │ │ ├─ (tj5d2.SWCQV = 0)\n" + " │ │ │ └─ TableAlias(tj5d2)\n" + @@ -4084,19 +4279,50 @@ WHERE ExpectedAnalysis: "Project\n" + " ├─ columns: [mf.id as id, cla.FTQLQ as T4IBQ, nd.TW55N as UWBAI, aac.BTXC5 as TPXBU, mf.FSDY2 as FSDY2]\n" + " └─ Filter\n" + - " ├─ (((NOT(mf.QQV4M IS NULL)) AND ((Subquery(select TJ5D2.SWCQV from SZW6V as TJ5D2 where TJ5D2.id = mf.QQV4M) = 1) OR (NOT((Subquery(select nd.id from E2I7U as nd where nd.TW55N = (select TJ5D2.H4DMT from SZW6V as TJ5D2 where TJ5D2.id = mf.QQV4M)) = mf.LUEVY))))) OR ((NOT(mf.TEUJA IS NULL)) AND InSubquery\n" + + " ├─ (((NOT(mf.QQV4M IS NULL)) AND ((Subquery\n" + + " │ ├─ cacheable: false\n" + + " │ └─ Project\n" + + " │ ├─ columns: [tj5d2.SWCQV]\n" + + " │ └─ Filter\n" + + " │ ├─ (tj5d2.id = mf.QQV4M)\n" + + " │ └─ TableAlias(tj5d2)\n" + + " │ └─ IndexedTableAccess(SZW6V)\n" + + " │ ├─ index: [SZW6V.id]\n" + + " │ ├─ columns: [id swcqv]\n" + + " │ └─ keys: mf.QQV4M\n" + + " │ = 1) OR (NOT((Subquery\n" + + " │ ├─ cacheable: false\n" + + " │ └─ Project\n" + + " │ ├─ columns: [nd.id]\n" + + " │ └─ Filter\n" + + " │ ├─ (nd.TW55N = Subquery\n" + + " │ │ ├─ cacheable: false\n" + + " │ │ └─ Project\n" + + " │ │ ├─ columns: [tj5d2.H4DMT]\n" + + " │ │ └─ Filter\n" + + " │ │ ├─ (tj5d2.id = mf.QQV4M)\n" + + " │ │ └─ TableAlias(tj5d2)\n" + + " │ │ └─ IndexedTableAccess(SZW6V)\n" + + " │ │ ├─ index: [SZW6V.id]\n" + + " │ │ ├─ columns: [id h4dmt]\n" + + " │ │ └─ keys: mf.QQV4M\n" + + " │ │ )\n" + + " │ └─ TableAlias(nd)\n" + + " │ └─ Table\n" + + " │ └─ name: E2I7U\n" + + " │ = mf.LUEVY))))) OR ((NOT(mf.TEUJA IS NULL)) AND InSubquery\n" + " │ ├─ left: mf.TEUJA\n" + " │ └─ right: Subquery\n" + " │ ├─ cacheable: true\n" + " │ └─ Project\n" + " │ ├─ columns: [umf.id as ORB3K]\n" + - " │ └─ LookupJoin\n" + + " │ └─ LookupJoin (estimated cost=24.084 rows=0)\n" + " │ ├─ ((umf.FGG57 = tj5d2.V7UFH) AND (umf.SYPKF = tj5d2.SYPKF))\n" + " │ ├─ Project\n" + " │ │ ├─ columns: [SZW6V.id, SZW6V.T4IBQ, SZW6V.V7UFH, SZW6V.SYPKF, SZW6V.H4DMT, SZW6V.SWCQV, SZW6V.YKSSU, SZW6V.FHCYT]\n" + " │ │ └─ Filter\n" + " │ │ ├─ 1 IS NULL\n" + - " │ │ └─ LeftOuterLookupJoin\n" + + " │ │ └─ LeftOuterLookupJoin (estimated cost=24.062 rows=0)\n" + " │ │ ├─ Filter\n" + " │ │ │ ├─ (tj5d2.SWCQV = 0)\n" + " │ │ │ └─ TableAlias(tj5d2)\n" + @@ -4959,7 +5185,16 @@ WHERE " │ │ │ ├─ (ci.id = ct.FZ2R5)\n" + " │ │ │ ├─ MergeJoin\n" + " │ │ │ │ ├─ cmp: (sn.BRQP2 = ct.LUEVY)\n" + - " │ │ │ │ ├─ sel: (ct.M22QN = Subquery(select aac.id from TPXBU as aac where BTXC5 = 'WT'))\n" + + " │ │ │ │ ├─ sel: (ct.M22QN = Subquery\n" + + " │ │ │ │ │ ├─ cacheable: true\n" + + " │ │ │ │ │ └─ Project\n" + + " │ │ │ │ │ ├─ columns: [aac.id]\n" + + " │ │ │ │ │ └─ TableAlias(aac)\n" + + " │ │ │ │ │ └─ IndexedTableAccess(TPXBU)\n" + + " │ │ │ │ │ ├─ index: [TPXBU.BTXC5]\n" + + " │ │ │ │ │ ├─ filters: [{[WT, WT]}]\n" + + " │ │ │ │ │ └─ columns: [id btxc5]\n" + + " │ │ │ │ │ )\n" + " │ │ │ │ ├─ TableAlias(sn)\n" + " │ │ │ │ │ └─ IndexedTableAccess(NOXN3)\n" + " │ │ │ │ │ ├─ index: [NOXN3.BRQP2]\n" + @@ -5175,7 +5410,16 @@ WHERE " │ │ │ ├─ (ci.id = ct.FZ2R5)\n" + " │ │ │ ├─ MergeJoin\n" + " │ │ │ │ ├─ cmp: (sn.BRQP2 = ct.LUEVY)\n" + - " │ │ │ │ ├─ sel: (ct.M22QN = Subquery(select aac.id from TPXBU as aac where BTXC5 = 'WT'))\n" + + " │ │ │ │ ├─ sel: (ct.M22QN = Subquery\n" + + " │ │ │ │ │ ├─ cacheable: true\n" + + " │ │ │ │ │ └─ Project\n" + + " │ │ │ │ │ ├─ columns: [aac.id]\n" + + " │ │ │ │ │ └─ TableAlias(aac)\n" + + " │ │ │ │ │ └─ IndexedTableAccess(TPXBU)\n" + + " │ │ │ │ │ ├─ index: [TPXBU.BTXC5]\n" + + " │ │ │ │ │ ├─ filters: [{[WT, WT]}]\n" + + " │ │ │ │ │ └─ columns: [id btxc5]\n" + + " │ │ │ │ │ )\n" + " │ │ │ │ ├─ TableAlias(sn)\n" + " │ │ │ │ │ └─ IndexedTableAccess(NOXN3)\n" + " │ │ │ │ │ ├─ index: [NOXN3.BRQP2]\n" + @@ -5823,7 +6067,16 @@ WHERE " │ │ │ ├─ (ci.id = ct.FZ2R5)\n" + " │ │ │ ├─ MergeJoin\n" + " │ │ │ │ ├─ cmp: (sn.BRQP2 = ct.LUEVY)\n" + - " │ │ │ │ ├─ sel: (ct.M22QN = Subquery(select aac.id from TPXBU as aac where BTXC5 = 'WT'))\n" + + " │ │ │ │ ├─ sel: (ct.M22QN = Subquery\n" + + " │ │ │ │ │ ├─ cacheable: true\n" + + " │ │ │ │ │ └─ Project\n" + + " │ │ │ │ │ ├─ columns: [aac.id]\n" + + " │ │ │ │ │ └─ TableAlias(aac)\n" + + " │ │ │ │ │ └─ IndexedTableAccess(TPXBU)\n" + + " │ │ │ │ │ ├─ index: [TPXBU.BTXC5]\n" + + " │ │ │ │ │ ├─ filters: [{[WT, WT]}]\n" + + " │ │ │ │ │ └─ columns: [id btxc5]\n" + + " │ │ │ │ │ )\n" + " │ │ │ │ ├─ TableAlias(sn)\n" + " │ │ │ │ │ └─ IndexedTableAccess(NOXN3)\n" + " │ │ │ │ │ ├─ index: [NOXN3.BRQP2]\n" + @@ -6034,7 +6287,16 @@ WHERE " │ │ │ ├─ (ci.id = ct.FZ2R5)\n" + " │ │ │ ├─ MergeJoin\n" + " │ │ │ │ ├─ cmp: (sn.BRQP2 = ct.LUEVY)\n" + - " │ │ │ │ ├─ sel: (ct.M22QN = Subquery(select aac.id from TPXBU as aac where BTXC5 = 'WT'))\n" + + " │ │ │ │ ├─ sel: (ct.M22QN = Subquery\n" + + " │ │ │ │ │ ├─ cacheable: true\n" + + " │ │ │ │ │ └─ Project\n" + + " │ │ │ │ │ ├─ columns: [aac.id]\n" + + " │ │ │ │ │ └─ TableAlias(aac)\n" + + " │ │ │ │ │ └─ IndexedTableAccess(TPXBU)\n" + + " │ │ │ │ │ ├─ index: [TPXBU.BTXC5]\n" + + " │ │ │ │ │ ├─ filters: [{[WT, WT]}]\n" + + " │ │ │ │ │ └─ columns: [id btxc5]\n" + + " │ │ │ │ │ )\n" + " │ │ │ │ ├─ TableAlias(sn)\n" + " │ │ │ │ │ └─ IndexedTableAccess(NOXN3)\n" + " │ │ │ │ │ ├─ index: [NOXN3.BRQP2]\n" + @@ -6098,15 +6360,15 @@ WHERE " └─ table_count(NOXN3) as COUNT(1)\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [count(1) as COUNT(*)]\n" + + " ├─ columns: [count(1) as `COUNT(*)`]\n" + " └─ Project\n" + - " ├─ columns: [NOXN3.COUNT(1) as COUNT(1)]\n" + + " ├─ columns: [NOXN3.COUNT(1) as `COUNT(1)`]\n" + " └─ table_count(NOXN3) as COUNT(1)\n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [count(1) as COUNT(*)]\n" + + " ├─ columns: [count(1) as `COUNT(*)`]\n" + " └─ Project\n" + - " ├─ columns: [NOXN3.COUNT(1) as COUNT(1)]\n" + + " ├─ columns: [NOXN3.COUNT(1) as `COUNT(1)`]\n" + " └─ table_count(NOXN3) as COUNT(1)\n" + "", }, @@ -16100,15 +16362,15 @@ SELECT COUNT(*) FROM E2I7U`, " └─ table_count(E2I7U) as COUNT(1)\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [count(1) as COUNT(*)]\n" + + " ├─ columns: [count(1) as `COUNT(*)`]\n" + " └─ Project\n" + - " ├─ columns: [E2I7U.COUNT(1) as COUNT(1)]\n" + + " ├─ columns: [E2I7U.COUNT(1) as `COUNT(1)`]\n" + " └─ table_count(E2I7U) as COUNT(1)\n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [count(1) as COUNT(*)]\n" + + " ├─ columns: [count(1) as `COUNT(*)`]\n" + " └─ Project\n" + - " ├─ columns: [E2I7U.COUNT(1) as COUNT(1)]\n" + + " ├─ columns: [E2I7U.COUNT(1) as `COUNT(1)`]\n" + " └─ table_count(E2I7U) as COUNT(1)\n" + "", }, diff --git a/enginetest/queries/prepared_statement_queries.go b/enginetest/queries/prepared_statement_queries.go index bb6da9b2d1..d780edf372 100644 --- a/enginetest/queries/prepared_statement_queries.go +++ b/enginetest/queries/prepared_statement_queries.go @@ -94,6 +94,7 @@ var PreparedScriptTests = []ScriptTest{ "set @a = 1", "set @b = 100", "set @c = 'abc'", + "set @schema = 'reserved name'", }, Assertions: []ScriptTestAssertion{ { @@ -134,6 +135,12 @@ var PreparedScriptTests = []ScriptTest{ {"abc"}, }, }, + { + Query: "execute s using @schema", + Expected: []sql.Row{ + {"reserved name"}, + }, + }, { Query: "deallocate prepare s", Expected: []sql.Row{ diff --git a/enginetest/queries/queries.go b/enginetest/queries/queries.go index 299bad8f4d..4665c9226e 100644 --- a/enginetest/queries/queries.go +++ b/enginetest/queries/queries.go @@ -9366,7 +9366,7 @@ from typestable`, {"Project"}, {" ├─ columns: [count(1)]"}, {" └─ Project"}, - {" ├─ columns: [mytable.COUNT(1) as COUNT(1)]"}, + {" ├─ columns: [mytable.COUNT(1) as `COUNT(1)`]"}, {" └─ table_count(mytable) as COUNT(1)"}, }, }, diff --git a/enginetest/queries/query_plans.go b/enginetest/queries/query_plans.go index 6c6145b43f..0d6243b8b6 100644 --- a/enginetest/queries/query_plans.go +++ b/enginetest/queries/query_plans.go @@ -93,7 +93,7 @@ offset 1;`, " ├─ columns: [cte.x, cte.y, Subquery\n" + " │ ├─ cacheable: true\n" + " │ └─ Project\n" + - " │ ├─ columns: [sum(cte.x) as SUM(x)]\n" + + " │ ├─ columns: [sum(cte.x) as `SUM(x)`]\n" + " │ └─ GroupBy\n" + " │ ├─ select: SUM(cte.x)\n" + " │ ├─ group: \n" + @@ -110,7 +110,7 @@ offset 1;`, " ├─ columns: [cte.x, cte.y, Subquery\n" + " │ ├─ cacheable: true\n" + " │ └─ Project\n" + - " │ ├─ columns: [sum(cte.x) as SUM(x)]\n" + + " │ ├─ columns: [sum(cte.x) as `SUM(x)`]\n" + " │ └─ GroupBy\n" + " │ ├─ select: SUM(cte.x)\n" + " │ ├─ group: \n" + @@ -210,7 +210,7 @@ offset 1;`, " │ └─ Project\n" + " │ ├─ columns: [t2.y]\n" + " │ └─ Filter\n" + - " │ ├─ EXISTS Subquery(select t3.y from xy as t3 where t3.y = xy.x)\n" + + " │ ├─ EXISTS (select t3.y from xy as t3 where t3.y = xy.x)\n" + " │ └─ TableAlias(t2)\n" + " │ └─ Table\n" + " │ └─ name: xy\n" + @@ -243,7 +243,7 @@ offset 1;`, " │ └─ Project\n" + " │ ├─ columns: [t2.y]\n" + " │ └─ Filter\n" + - " │ ├─ EXISTS Subquery(select t3.y from xy as t3 where t3.y = xy.x)\n" + + " │ ├─ EXISTS (select t3.y from xy as t3 where t3.y = xy.x)\n" + " │ └─ TableAlias(t2)\n" + " │ └─ Table\n" + " │ └─ name: xy\n" + @@ -260,15 +260,15 @@ offset 1;`, " └─ table_count(mytable) as COUNT(mytable.i)\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [count(mytable.i) as count(i)]\n" + + " ├─ columns: [count(mytable.i) as `count(i)`]\n" + " └─ Project\n" + - " ├─ columns: [mytable.COUNT(mytable.i) as COUNT(mytable.i)]\n" + + " ├─ columns: [mytable.COUNT(mytable.i) as `COUNT(mytable.i)`]\n" + " └─ table_count(mytable) as COUNT(mytable.i)\n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [count(mytable.i) as count(i)]\n" + + " ├─ columns: [count(mytable.i) as `count(i)`]\n" + " └─ Project\n" + - " ├─ columns: [mytable.COUNT(mytable.i) as COUNT(mytable.i)]\n" + + " ├─ columns: [mytable.COUNT(mytable.i) as `COUNT(mytable.i)`]\n" + " └─ table_count(mytable) as COUNT(mytable.i)\n" + "", }, @@ -285,7 +285,7 @@ offset 1;`, " └─ columns: [pk1]\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [count(two_pk.pk1) as count(pk1)]\n" + + " ├─ columns: [count(two_pk.pk1) as `count(pk1)`]\n" + " └─ GroupBy\n" + " ├─ select: COUNT(two_pk.pk1)\n" + " ├─ group: \n" + @@ -294,7 +294,7 @@ offset 1;`, " └─ columns: [pk1]\n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [count(two_pk.pk1) as count(pk1)]\n" + + " ├─ columns: [count(two_pk.pk1) as `count(pk1)`]\n" + " └─ GroupBy\n" + " ├─ select: COUNT(two_pk.pk1)\n" + " ├─ group: \n" + @@ -657,7 +657,7 @@ From xy;`, " └─ columns: [x y]\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [count(1) as count(*)]\n" + + " ├─ columns: [count(1) as `count(*)`]\n" + " └─ GroupBy\n" + " ├─ select: COUNT(1)\n" + " ├─ group: \n" + @@ -674,7 +674,7 @@ From xy;`, " └─ keys: uv.v\n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [count(1) as count(*)]\n" + + " ├─ columns: [count(1) as `count(*)`]\n" + " └─ GroupBy\n" + " ├─ select: COUNT(1)\n" + " ├─ group: \n" + @@ -719,7 +719,7 @@ From xy;`, " └─ columns: [x y]\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [count(1) as count(*)]\n" + + " ├─ columns: [count(1) as `count(*)`]\n" + " └─ GroupBy\n" + " ├─ select: COUNT(1)\n" + " ├─ group: \n" + @@ -735,7 +735,7 @@ From xy;`, " └─ keys: uv.u\n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [count(1) as count(*)]\n" + + " ├─ columns: [count(1) as `count(*)`]\n" + " └─ GroupBy\n" + " ├─ select: COUNT(1)\n" + " ├─ group: \n" + @@ -760,15 +760,15 @@ From xy;`, " └─ table_count(mytable) as COUNT(1)\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [count(1) as count(*)]\n" + + " ├─ columns: [count(1) as `count(*)`]\n" + " └─ Project\n" + - " ├─ columns: [mytable.COUNT(1) as COUNT(1)]\n" + + " ├─ columns: [mytable.COUNT(1) as `COUNT(1)`]\n" + " └─ table_count(mytable) as COUNT(1)\n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [count(1) as count(*)]\n" + + " ├─ columns: [count(1) as `count(*)`]\n" + " └─ Project\n" + - " ├─ columns: [mytable.COUNT(1) as COUNT(1)]\n" + + " ├─ columns: [mytable.COUNT(1) as `COUNT(1)`]\n" + " └─ table_count(mytable) as COUNT(1)\n" + "", }, @@ -783,13 +783,13 @@ From xy;`, ExpectedEstimates: "Project\n" + " ├─ columns: [count(1) as cnt]\n" + " └─ Project\n" + - " ├─ columns: [mytable.COUNT(1) as COUNT(1)]\n" + + " ├─ columns: [mytable.COUNT(1) as `COUNT(1)`]\n" + " └─ table_count(mytable) as COUNT(1)\n" + "", ExpectedAnalysis: "Project\n" + " ├─ columns: [count(1) as cnt]\n" + " └─ Project\n" + - " ├─ columns: [mytable.COUNT(1) as COUNT(1)]\n" + + " ├─ columns: [mytable.COUNT(1) as `COUNT(1)`]\n" + " └─ table_count(mytable) as COUNT(1)\n" + "", }, @@ -806,7 +806,7 @@ From xy;`, " └─ columns: []\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [count(1) as count(*)]\n" + + " ├─ columns: [count(1) as `count(*)`]\n" + " └─ GroupBy\n" + " ├─ select: COUNT(1)\n" + " ├─ group: \n" + @@ -815,7 +815,7 @@ From xy;`, " └─ columns: []\n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [count(1) as count(*)]\n" + + " ├─ columns: [count(1) as `count(*)`]\n" + " └─ GroupBy\n" + " ├─ select: COUNT(1)\n" + " ├─ group: \n" + @@ -833,15 +833,15 @@ From xy;`, " └─ table_count(xy) as COUNT(1)\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [count(1) as count(*)]\n" + + " ├─ columns: [count(1) as `count(*)`]\n" + " └─ Project\n" + - " ├─ columns: [xy.COUNT(1) as COUNT(1)]\n" + + " ├─ columns: [xy.COUNT(1) as `COUNT(1)`]\n" + " └─ table_count(xy) as COUNT(1)\n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [count(1) as count(*)]\n" + + " ├─ columns: [count(1) as `count(*)`]\n" + " └─ Project\n" + - " ├─ columns: [xy.COUNT(1) as COUNT(1)]\n" + + " ├─ columns: [xy.COUNT(1) as `COUNT(1)`]\n" + " └─ table_count(xy) as COUNT(1)\n" + "", }, @@ -856,13 +856,13 @@ From xy;`, ExpectedEstimates: "Project\n" + " ├─ columns: [count(1)]\n" + " └─ Project\n" + - " ├─ columns: [mytable.COUNT(1) as COUNT(1)]\n" + + " ├─ columns: [mytable.COUNT(1) as `COUNT(1)`]\n" + " └─ table_count(mytable) as COUNT(1)\n" + "", ExpectedAnalysis: "Project\n" + " ├─ columns: [count(1)]\n" + " └─ Project\n" + - " ├─ columns: [mytable.COUNT(1) as COUNT(1)]\n" + + " ├─ columns: [mytable.COUNT(1) as `COUNT(1)`]\n" + " └─ table_count(mytable) as COUNT(1)\n" + "", }, @@ -877,13 +877,13 @@ From xy;`, ExpectedEstimates: "Project\n" + " ├─ columns: [count(1)]\n" + " └─ Project\n" + - " ├─ columns: [xy.COUNT(1) as COUNT(1)]\n" + + " ├─ columns: [xy.COUNT(1) as `COUNT(1)`]\n" + " └─ table_count(xy) as COUNT(1)\n" + "", ExpectedAnalysis: "Project\n" + " ├─ columns: [count(1)]\n" + " └─ Project\n" + - " ├─ columns: [xy.COUNT(1) as COUNT(1)]\n" + + " ├─ columns: [xy.COUNT(1) as `COUNT(1)`]\n" + " └─ table_count(xy) as COUNT(1)\n" + "", }, @@ -954,9 +954,9 @@ From xy;`, " ├─ colSet: (4)\n" + " ├─ tableId: 2\n" + " └─ Project\n" + - " ├─ columns: [count(1) as count(*)]\n" + + " ├─ columns: [count(1) as `count(*)`]\n" + " └─ Project\n" + - " ├─ columns: [xy.COUNT(1) as COUNT(1)]\n" + + " ├─ columns: [xy.COUNT(1) as `COUNT(1)`]\n" + " └─ table_count(xy) as COUNT(1)\n" + "", ExpectedAnalysis: "SubqueryAlias\n" + @@ -967,9 +967,9 @@ From xy;`, " ├─ colSet: (4)\n" + " ├─ tableId: 2\n" + " └─ Project\n" + - " ├─ columns: [count(1) as count(*)]\n" + + " ├─ columns: [count(1) as `count(*)`]\n" + " └─ Project\n" + - " ├─ columns: [xy.COUNT(1) as COUNT(1)]\n" + + " ├─ columns: [xy.COUNT(1) as `COUNT(1)`]\n" + " └─ table_count(xy) as COUNT(1)\n" + "", }, @@ -1004,18 +1004,18 @@ From xy;`, " ├─ columns: [Subquery\n" + " │ ├─ cacheable: true\n" + " │ └─ Project\n" + - " │ ├─ columns: [count(1) as count(*)]\n" + + " │ ├─ columns: [count(1) as `count(*)`]\n" + " │ └─ Project\n" + - " │ ├─ columns: [xy.COUNT(1) as COUNT(1)]\n" + + " │ ├─ columns: [xy.COUNT(1) as `COUNT(1)`]\n" + " │ └─ table_count(xy) as COUNT(1)\n" + - " │ as (select count(*) from xy), Subquery\n" + + " │ as `(select count(*) from xy)`, Subquery\n" + " │ ├─ cacheable: true\n" + " │ └─ Project\n" + - " │ ├─ columns: [count(1) as count(*)]\n" + + " │ ├─ columns: [count(1) as `count(*)`]\n" + " │ └─ Project\n" + - " │ ├─ columns: [uv.COUNT(1) as COUNT(1)]\n" + + " │ ├─ columns: [uv.COUNT(1) as `COUNT(1)`]\n" + " │ └─ table_count(uv) as COUNT(1)\n" + - " │ as (select count(*) from uv)]\n" + + " │ as `(select count(*) from uv)`]\n" + " └─ Project\n" + " ├─ columns: [dual.]\n" + " └─ Table\n" + @@ -1025,18 +1025,18 @@ From xy;`, " ├─ columns: [Subquery\n" + " │ ├─ cacheable: true\n" + " │ └─ Project\n" + - " │ ├─ columns: [count(1) as count(*)]\n" + + " │ ├─ columns: [count(1) as `count(*)`]\n" + " │ └─ Project\n" + - " │ ├─ columns: [xy.COUNT(1) as COUNT(1)]\n" + + " │ ├─ columns: [xy.COUNT(1) as `COUNT(1)`]\n" + " │ └─ table_count(xy) as COUNT(1)\n" + - " │ as (select count(*) from xy), Subquery\n" + + " │ as `(select count(*) from xy)`, Subquery\n" + " │ ├─ cacheable: true\n" + " │ └─ Project\n" + - " │ ├─ columns: [count(1) as count(*)]\n" + + " │ ├─ columns: [count(1) as `count(*)`]\n" + " │ └─ Project\n" + - " │ ├─ columns: [uv.COUNT(1) as COUNT(1)]\n" + + " │ ├─ columns: [uv.COUNT(1) as `COUNT(1)`]\n" + " │ └─ table_count(uv) as COUNT(1)\n" + - " │ as (select count(*) from uv)]\n" + + " │ as `(select count(*) from uv)`]\n" + " └─ Project\n" + " ├─ columns: [dual.]\n" + " └─ Table\n" + @@ -1071,40 +1071,40 @@ From xy;`, " ├─ columns: [Subquery\n" + " │ ├─ cacheable: true\n" + " │ └─ Project\n" + - " │ ├─ columns: [count(1) as count(*)]\n" + + " │ ├─ columns: [count(1) as `count(*)`]\n" + " │ └─ Project\n" + - " │ ├─ columns: [xy.COUNT(1) as COUNT(1)]\n" + + " │ ├─ columns: [xy.COUNT(1) as `COUNT(1)`]\n" + " │ └─ table_count(xy) as COUNT(1)\n" + - " │ as (select count(*) from xy), Subquery\n" + + " │ as `(select count(*) from xy)`, Subquery\n" + " │ ├─ cacheable: true\n" + " │ └─ Project\n" + - " │ ├─ columns: [count(1) as count(*)]\n" + + " │ ├─ columns: [count(1) as `count(*)`]\n" + " │ └─ Project\n" + - " │ ├─ columns: [uv.COUNT(1) as COUNT(1)]\n" + + " │ ├─ columns: [uv.COUNT(1) as `COUNT(1)`]\n" + " │ └─ table_count(uv) as COUNT(1)\n" + - " │ as (select count(*) from uv), count(1) as count(*)]\n" + + " │ as `(select count(*) from uv)`, count(1) as `count(*)`]\n" + " └─ Project\n" + - " ├─ columns: [ab.COUNT(1) as COUNT(1)]\n" + + " ├─ columns: [ab.COUNT(1) as `COUNT(1)`]\n" + " └─ table_count(ab) as COUNT(1)\n" + "", ExpectedAnalysis: "Project\n" + " ├─ columns: [Subquery\n" + " │ ├─ cacheable: true\n" + " │ └─ Project\n" + - " │ ├─ columns: [count(1) as count(*)]\n" + + " │ ├─ columns: [count(1) as `count(*)`]\n" + " │ └─ Project\n" + - " │ ├─ columns: [xy.COUNT(1) as COUNT(1)]\n" + + " │ ├─ columns: [xy.COUNT(1) as `COUNT(1)`]\n" + " │ └─ table_count(xy) as COUNT(1)\n" + - " │ as (select count(*) from xy), Subquery\n" + + " │ as `(select count(*) from xy)`, Subquery\n" + " │ ├─ cacheable: true\n" + " │ └─ Project\n" + - " │ ├─ columns: [count(1) as count(*)]\n" + + " │ ├─ columns: [count(1) as `count(*)`]\n" + " │ └─ Project\n" + - " │ ├─ columns: [uv.COUNT(1) as COUNT(1)]\n" + + " │ ├─ columns: [uv.COUNT(1) as `COUNT(1)`]\n" + " │ └─ table_count(uv) as COUNT(1)\n" + - " │ as (select count(*) from uv), count(1) as count(*)]\n" + + " │ as `(select count(*) from uv)`, count(1) as `count(*)`]\n" + " └─ Project\n" + - " ├─ columns: [ab.COUNT(1) as COUNT(1)]\n" + + " ├─ columns: [ab.COUNT(1) as `COUNT(1)`]\n" + " └─ table_count(ab) as COUNT(1)\n" + "", }, @@ -1121,9 +1121,9 @@ WHERE s_i_id=ol_i_id AND s_quantity < 15;`, ExpectedPlan: "Project\n" + - " ├─ columns: [countdistinct([stock1.s_i_id]):0!null->COUNT(DISTINCT (s_i_id)):0]\n" + + " ├─ columns: [count(distinct stock1.s_i_id):0!null->COUNT(DISTINCT (s_i_id)):0]\n" + " └─ GroupBy\n" + - " ├─ select: COUNTDISTINCT([stock1.s_i_id])\n" + + " ├─ select: COUNT(DISTINCT stock1.s_i_id)\n" + " ├─ group: \n" + " └─ HashJoin\n" + " ├─ Eq\n" + @@ -1154,9 +1154,9 @@ WHERE " └─ columns: [s_i_id s_w_id s_quantity]\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [countdistinct([stock1.s_i_id]) as COUNT(DISTINCT (s_i_id))]\n" + + " ├─ columns: [count(distinct stock1.s_i_id) as `COUNT(DISTINCT (s_i_id))`]\n" + " └─ GroupBy\n" + - " ├─ select: COUNTDISTINCT([stock1.s_i_id])\n" + + " ├─ select: COUNT(DISTINCT stock1.s_i_id)\n" + " ├─ group: \n" + " └─ HashJoin (estimated cost=2808089.580 rows=1503029)\n" + " ├─ (stock1.s_i_id = order_line1.ol_i_id)\n" + @@ -1175,9 +1175,9 @@ WHERE " └─ columns: [s_i_id s_w_id s_quantity]\n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [countdistinct([stock1.s_i_id]) as COUNT(DISTINCT (s_i_id))]\n" + + " ├─ columns: [count(distinct stock1.s_i_id) as `COUNT(DISTINCT (s_i_id))`]\n" + " └─ GroupBy\n" + - " ├─ select: COUNTDISTINCT([stock1.s_i_id])\n" + + " ├─ select: COUNT(DISTINCT stock1.s_i_id)\n" + " ├─ group: \n" + " └─ HashJoin (estimated cost=2808089.580 rows=1503029) (actual rows=0 loops=1)\n" + " ├─ (stock1.s_i_id = order_line1.ol_i_id)\n" + @@ -1492,7 +1492,7 @@ where " └─ tableId: 0\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [json_extract(dt.col1, '$.key1') as col1->'$.key1']\n" + + " ├─ columns: [json_extract(dt.col1, '$.key1') as `col1->'$.key1'`]\n" + " └─ SubqueryAlias\n" + " ├─ name: dt\n" + " ├─ outerVisibility: false\n" + @@ -1501,12 +1501,12 @@ where " ├─ colSet: (1)\n" + " ├─ tableId: 1\n" + " └─ Project\n" + - " ├─ columns: [json_object('key1',1,'key2','abc') as JSON_OBJECT('key1', 1, 'key2', 'abc')]\n" + + " ├─ columns: [json_object('key1',1,'key2','abc') as `JSON_OBJECT('key1', 1, 'key2', 'abc')`]\n" + " └─ Table\n" + " └─ name: \n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [json_extract(dt.col1, '$.key1') as col1->'$.key1']\n" + + " ├─ columns: [json_extract(dt.col1, '$.key1') as `col1->'$.key1'`]\n" + " └─ SubqueryAlias\n" + " ├─ name: dt\n" + " ├─ outerVisibility: false\n" + @@ -1515,7 +1515,7 @@ where " ├─ colSet: (1)\n" + " ├─ tableId: 1\n" + " └─ Project\n" + - " ├─ columns: [json_object('key1',1,'key2','abc') as JSON_OBJECT('key1', 1, 'key2', 'abc')]\n" + + " ├─ columns: [json_object('key1',1,'key2','abc') as `JSON_OBJECT('key1', 1, 'key2', 'abc')`]\n" + " └─ Table\n" + " └─ name: \n" + "", @@ -1540,7 +1540,7 @@ where " └─ tableId: 0\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [json_unquote(json_extract(dt.col1, '$.key1')) as col1->>'$.key1']\n" + + " ├─ columns: [json_unquote(json_extract(dt.col1, '$.key1')) as `col1->>'$.key1'`]\n" + " └─ SubqueryAlias\n" + " ├─ name: dt\n" + " ├─ outerVisibility: false\n" + @@ -1549,12 +1549,12 @@ where " ├─ colSet: (1)\n" + " ├─ tableId: 1\n" + " └─ Project\n" + - " ├─ columns: [json_object('key1',1,'key2','abc') as JSON_OBJECT('key1', 1, 'key2', 'abc')]\n" + + " ├─ columns: [json_object('key1',1,'key2','abc') as `JSON_OBJECT('key1', 1, 'key2', 'abc')`]\n" + " └─ Table\n" + " └─ name: \n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [json_unquote(json_extract(dt.col1, '$.key1')) as col1->>'$.key1']\n" + + " ├─ columns: [json_unquote(json_extract(dt.col1, '$.key1')) as `col1->>'$.key1'`]\n" + " └─ SubqueryAlias\n" + " ├─ name: dt\n" + " ├─ outerVisibility: false\n" + @@ -1563,7 +1563,7 @@ where " ├─ colSet: (1)\n" + " ├─ tableId: 1\n" + " └─ Project\n" + - " ├─ columns: [json_object('key1',1,'key2','abc') as JSON_OBJECT('key1', 1, 'key2', 'abc')]\n" + + " ├─ columns: [json_object('key1',1,'key2','abc') as `JSON_OBJECT('key1', 1, 'key2', 'abc')`]\n" + " └─ Table\n" + " └─ name: \n" + "", @@ -2349,14 +2349,14 @@ Select * from ( " └─ columns: [x y]\n" + "", ExpectedEstimates: "AntiJoinIncludingNulls (estimated cost=2024.000 rows=5)\n" + - " ├─ ((NOT(EXISTS Subquery(select * from xy where not (u = 1)))) OR (uv.u = 1))\n" + + " ├─ ((NOT(EXISTS (select * from xy where not (u = 1)))) OR (uv.u = 1))\n" + " ├─ Table\n" + " │ └─ name: uv\n" + " └─ Table\n" + " └─ name: xy\n" + "", ExpectedAnalysis: "AntiJoinIncludingNulls (estimated cost=2024.000 rows=5) (actual rows=3 loops=1)\n" + - " ├─ ((NOT(EXISTS Subquery(select * from xy where not (u = 1)))) OR (uv.u = 1))\n" + + " ├─ ((NOT(EXISTS (select * from xy where not (u = 1)))) OR (uv.u = 1))\n" + " ├─ Table\n" + " │ └─ name: uv\n" + " └─ Table\n" + @@ -2420,7 +2420,7 @@ Select * from ( " │ │ ├─ index: [uv.u]\n" + " │ │ ├─ columns: [u]\n" + " │ │ └─ keys: sq.p\n" + - " │ │ as (select u from uv where u = sq.p)]\n" + + " │ │ as `(select u from uv where u = sq.p)`]\n" + " │ └─ TableAlias(sq)\n" + " │ └─ Table\n" + " │ ├─ name: pq\n" + @@ -2444,7 +2444,7 @@ Select * from ( " │ │ ├─ index: [uv.u]\n" + " │ │ ├─ columns: [u]\n" + " │ │ └─ keys: sq.p\n" + - " │ │ as (select u from uv where u = sq.p)]\n" + + " │ │ as `(select u from uv where u = sq.p)`]\n" + " │ └─ TableAlias(sq)\n" + " │ └─ Table\n" + " │ ├─ name: pq\n" + @@ -3227,7 +3227,23 @@ Select * from ( " ├─ columns: [ab.a, ab.b]\n" + " └─ Sort(ab.a ASC)\n" + " └─ Filter\n" + - " ├─ (ab.b = Subquery(select y from xy where y in (select v from uv where v = b)))\n" + + " ├─ (ab.b = Subquery\n" + + " │ ├─ cacheable: false\n" + + " │ └─ Project\n" + + " │ ├─ columns: [xy.y]\n" + + " │ └─ Filter\n" + + " │ ├─ InSubquery\n" + + " │ │ ├─ left: xy.y\n" + + " │ │ └─ right: Subquery\n" + + " │ │ ├─ cacheable: false\n" + + " │ │ └─ Filter\n" + + " │ │ ├─ (uv.v = ab.b)\n" + + " │ │ └─ Table\n" + + " │ │ ├─ name: uv\n" + + " │ │ └─ columns: [v]\n" + + " │ └─ Table\n" + + " │ └─ name: xy\n" + + " │ )\n" + " └─ LookupJoin (estimated cost=13.338 rows=4)\n" + " ├─ Table\n" + " │ └─ name: pq\n" + @@ -3239,7 +3255,23 @@ Select * from ( " ├─ columns: [ab.a, ab.b]\n" + " └─ Sort(ab.a ASC)\n" + " └─ Filter\n" + - " ├─ (ab.b = Subquery(select y from xy where y in (select v from uv where v = b)))\n" + + " ├─ (ab.b = Subquery\n" + + " │ ├─ cacheable: false\n" + + " │ └─ Project\n" + + " │ ├─ columns: [xy.y]\n" + + " │ └─ Filter\n" + + " │ ├─ InSubquery\n" + + " │ │ ├─ left: xy.y\n" + + " │ │ └─ right: Subquery\n" + + " │ │ ├─ cacheable: false\n" + + " │ │ └─ Filter\n" + + " │ │ ├─ (uv.v = ab.b)\n" + + " │ │ └─ Table\n" + + " │ │ ├─ name: uv\n" + + " │ │ └─ columns: [v]\n" + + " │ └─ Table\n" + + " │ └─ name: xy\n" + + " │ )\n" + " └─ LookupJoin (estimated cost=13.338 rows=4) (actual rows=4 loops=1)\n" + " ├─ Table\n" + " │ └─ name: pq\n" + @@ -3976,16 +4008,16 @@ Select * from ( " └─ columns: [i s]\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [(mytable.i + (0.0 / lag(mytable.i, 1) over ( order by mytable.s asc))) as i+0.0/(lag(i) over (order by s))]\n" + - " └─ Sort((mytable.i + (0.0 / lag(mytable.i, 1) over ( order by mytable.s asc))) as i+0.0/(lag(i) over (order by s)) ASC)\n" + + " ├─ columns: [(mytable.i + (0.0 / lag(mytable.i, 1) over ( order by mytable.s asc))) as `i+0.0/(lag(i) over (order by s))`]\n" + + " └─ Sort((mytable.i + (0.0 / lag(mytable.i, 1) over ( order by mytable.s asc))) as `i+0.0/(lag(i) over (order by s))` ASC)\n" + " └─ Window(lag(mytable.i, 1) over ( order by mytable.s ASC), mytable.i)\n" + " └─ Table\n" + " ├─ name: mytable\n" + " └─ columns: [i s]\n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [(mytable.i + (0.0 / lag(mytable.i, 1) over ( order by mytable.s asc))) as i+0.0/(lag(i) over (order by s))]\n" + - " └─ Sort((mytable.i + (0.0 / lag(mytable.i, 1) over ( order by mytable.s asc))) as i+0.0/(lag(i) over (order by s)) ASC)\n" + + " ├─ columns: [(mytable.i + (0.0 / lag(mytable.i, 1) over ( order by mytable.s asc))) as `i+0.0/(lag(i) over (order by s))`]\n" + + " └─ Sort((mytable.i + (0.0 / lag(mytable.i, 1) over ( order by mytable.s asc))) as `i+0.0/(lag(i) over (order by s))` ASC)\n" + " └─ Window(lag(mytable.i, 1) over ( order by mytable.s ASC), mytable.i)\n" + " └─ Table\n" + " ├─ name: mytable\n" + @@ -4007,16 +4039,16 @@ Select * from ( " └─ columns: [i f32 f64]\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [(floattable.f64 / floattable.f32) as f64/f32, (floattable.f32 / lag(floattable.i, 1) over ( order by floattable.f64 asc)) as f32/(lag(i) over (order by f64))]\n" + - " └─ Sort((floattable.f64 / floattable.f32) as f64/f32 ASC, (floattable.f32 / lag(floattable.i, 1) over ( order by floattable.f64 asc)) as f32/(lag(i) over (order by f64)) ASC)\n" + + " ├─ columns: [(floattable.f64 / floattable.f32) as `f64/f32`, (floattable.f32 / lag(floattable.i, 1) over ( order by floattable.f64 asc)) as `f32/(lag(i) over (order by f64))`]\n" + + " └─ Sort((floattable.f64 / floattable.f32) as `f64/f32` ASC, (floattable.f32 / lag(floattable.i, 1) over ( order by floattable.f64 asc)) as `f32/(lag(i) over (order by f64))` ASC)\n" + " └─ Window(lag(floattable.i, 1) over ( order by floattable.f64 ASC), floattable.f64, floattable.f32)\n" + " └─ Table\n" + " ├─ name: floattable\n" + " └─ columns: [i f32 f64]\n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [(floattable.f64 / floattable.f32) as f64/f32, (floattable.f32 / lag(floattable.i, 1) over ( order by floattable.f64 asc)) as f32/(lag(i) over (order by f64))]\n" + - " └─ Sort((floattable.f64 / floattable.f32) as f64/f32 ASC, (floattable.f32 / lag(floattable.i, 1) over ( order by floattable.f64 asc)) as f32/(lag(i) over (order by f64)) ASC)\n" + + " ├─ columns: [(floattable.f64 / floattable.f32) as `f64/f32`, (floattable.f32 / lag(floattable.i, 1) over ( order by floattable.f64 asc)) as `f32/(lag(i) over (order by f64))`]\n" + + " └─ Sort((floattable.f64 / floattable.f32) as `f64/f32` ASC, (floattable.f32 / lag(floattable.i, 1) over ( order by floattable.f64 asc)) as `f32/(lag(i) over (order by f64))` ASC)\n" + " └─ Window(lag(floattable.i, 1) over ( order by floattable.f64 ASC), floattable.f64, floattable.f32)\n" + " └─ Table\n" + " ├─ name: floattable\n" + @@ -4932,7 +4964,7 @@ Select * from ( " │ └─ Project\n" + " │ ├─ columns: [count(1) as u, 123 as v]\n" + " │ └─ Project\n" + - " │ ├─ columns: [emptytable.COUNT(1) as COUNT(1)]\n" + + " │ ├─ columns: [emptytable.COUNT(1) as `COUNT(1)`]\n" + " │ └─ table_count(emptytable) as COUNT(1)\n" + " └─ HashLookup\n" + " ├─ left-key: (uv.u)\n" + @@ -4955,7 +4987,7 @@ Select * from ( " │ └─ Project\n" + " │ ├─ columns: [count(1) as u, 123 as v]\n" + " │ └─ Project\n" + - " │ ├─ columns: [emptytable.COUNT(1) as COUNT(1)]\n" + + " │ ├─ columns: [emptytable.COUNT(1) as `COUNT(1)`]\n" + " │ └─ table_count(emptytable) as COUNT(1)\n" + " └─ HashLookup\n" + " ├─ left-key: (uv.u)\n" + @@ -5073,7 +5105,7 @@ Select * from ( " │ └─ Project\n" + " │ ├─ columns: [count(1) as u, 123 as v]\n" + " │ └─ Project\n" + - " │ ├─ columns: [emptytable.COUNT(1) as COUNT(1)]\n" + + " │ ├─ columns: [emptytable.COUNT(1) as `COUNT(1)`]\n" + " │ └─ table_count(emptytable) as COUNT(1)\n" + " └─ Table\n" + " └─ name: one_pk\n" + @@ -5088,7 +5120,7 @@ Select * from ( " │ └─ Project\n" + " │ ├─ columns: [count(1) as u, 123 as v]\n" + " │ └─ Project\n" + - " │ ├─ columns: [emptytable.COUNT(1) as COUNT(1)]\n" + + " │ ├─ columns: [emptytable.COUNT(1) as `COUNT(1)`]\n" + " │ └─ table_count(emptytable) as COUNT(1)\n" + " └─ Table\n" + " └─ name: one_pk\n" + @@ -5186,7 +5218,7 @@ Select * from ( " │ └─ Project\n" + " │ ├─ columns: [count(1) as u, 123 as v]\n" + " │ └─ Project\n" + - " │ ├─ columns: [emptytable.COUNT(1) as COUNT(1)]\n" + + " │ ├─ columns: [emptytable.COUNT(1) as `COUNT(1)`]\n" + " │ └─ table_count(emptytable) as COUNT(1)\n" + " └─ HashLookup\n" + " ├─ left-key: ()\n" + @@ -5206,7 +5238,7 @@ Select * from ( " │ └─ Project\n" + " │ ├─ columns: [count(1) as u, 123 as v]\n" + " │ └─ Project\n" + - " │ ├─ columns: [emptytable.COUNT(1) as COUNT(1)]\n" + + " │ ├─ columns: [emptytable.COUNT(1) as `COUNT(1)`]\n" + " │ └─ table_count(emptytable) as COUNT(1)\n" + " └─ HashLookup\n" + " ├─ left-key: ()\n" + @@ -5242,7 +5274,7 @@ Select * from ( " └─ columns: []\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [count(1) as count(*), Subquery\n" + + " ├─ columns: [count(1) as `count(*)`, Subquery\n" + " │ ├─ cacheable: true\n" + " │ └─ GroupBy\n" + " │ ├─ select: mytable.i\n" + @@ -5251,7 +5283,7 @@ Select * from ( " │ ├─ index: [mytable.i]\n" + " │ ├─ filters: [{[1, 1]}]\n" + " │ └─ columns: [i]\n" + - " │ as (SELECT i FROM mytable WHERE i = 1 group by i)]\n" + + " │ as `(SELECT i FROM mytable WHERE i = 1 group by i)`]\n" + " └─ GroupBy\n" + " ├─ select: COUNT(1)\n" + " ├─ group: \n" + @@ -5259,7 +5291,7 @@ Select * from ( " └─ name: \n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [count(1) as count(*), Subquery\n" + + " ├─ columns: [count(1) as `count(*)`, Subquery\n" + " │ ├─ cacheable: true\n" + " │ └─ GroupBy\n" + " │ ├─ select: mytable.i\n" + @@ -5268,7 +5300,7 @@ Select * from ( " │ ├─ index: [mytable.i]\n" + " │ ├─ filters: [{[1, 1]}]\n" + " │ └─ columns: [i]\n" + - " │ as (SELECT i FROM mytable WHERE i = 1 group by i)]\n" + + " │ as `(SELECT i FROM mytable WHERE i = 1 group by i)`]\n" + " └─ GroupBy\n" + " ├─ select: COUNT(1)\n" + " ├─ group: \n" + @@ -5307,12 +5339,12 @@ Select * from ( " └─ columns: [x y]\n" + "", ExpectedEstimates: "Filter\n" + - " ├─ EXISTS Subquery(select * from cte where a = x)\n" + + " ├─ EXISTS (select * from cte where a = x)\n" + " └─ Table\n" + " └─ name: xy\n" + "", ExpectedAnalysis: "Filter\n" + - " ├─ EXISTS Subquery(select * from cte where a = x)\n" + + " ├─ EXISTS (select * from cte where a = x)\n" + " └─ Table\n" + " └─ name: xy\n" + "", @@ -6526,8 +6558,8 @@ inner join pq on true " └─ columns: [i2]\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [row_number() over ( order by mytable.i desc) as row_number() over (order by i desc), mytable.i as i2]\n" + - " └─ Sort(row_number() over ( order by mytable.i desc) as row_number() over (order by i desc) ASC)\n" + + " ├─ columns: [row_number() over ( order by mytable.i desc) as `row_number() over (order by i desc)`, mytable.i as i2]\n" + + " └─ Sort(row_number() over ( order by mytable.i desc) as `row_number() over (order by i desc)` ASC)\n" + " └─ Window(row_number() over ( order by mytable.i DESC), mytable.i)\n" + " └─ MergeJoin\n" + " ├─ cmp: (mytable.i = othertable.i2)\n" + @@ -6541,8 +6573,8 @@ inner join pq on true " └─ columns: [i2]\n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [row_number() over ( order by mytable.i desc) as row_number() over (order by i desc), mytable.i as i2]\n" + - " └─ Sort(row_number() over ( order by mytable.i desc) as row_number() over (order by i desc) ASC)\n" + + " ├─ columns: [row_number() over ( order by mytable.i desc) as `row_number() over (order by i desc)`, mytable.i as i2]\n" + + " └─ Sort(row_number() over ( order by mytable.i desc) as `row_number() over (order by i desc)` ASC)\n" + " └─ Window(row_number() over ( order by mytable.i DESC), mytable.i)\n" + " └─ MergeJoin\n" + " ├─ cmp: (mytable.i = othertable.i2)\n" + @@ -6681,8 +6713,8 @@ inner join pq on true " └─ columns: [i2]\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [row_number() over ( order by mytable.i desc) as row_number() over (order by i desc), mytable.i as i2]\n" + - " └─ Sort(row_number() over ( order by mytable.i desc) as row_number() over (order by i desc) ASC)\n" + + " ├─ columns: [row_number() over ( order by mytable.i desc) as `row_number() over (order by i desc)`, mytable.i as i2]\n" + + " └─ Sort(row_number() over ( order by mytable.i desc) as `row_number() over (order by i desc)` ASC)\n" + " └─ Window(row_number() over ( order by mytable.i DESC), mytable.i)\n" + " └─ LookupJoin\n" + " ├─ IndexedTableAccess(mytable)\n" + @@ -6695,8 +6727,8 @@ inner join pq on true " └─ keys: mytable.i\n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [row_number() over ( order by mytable.i desc) as row_number() over (order by i desc), mytable.i as i2]\n" + - " └─ Sort(row_number() over ( order by mytable.i desc) as row_number() over (order by i desc) ASC)\n" + + " ├─ columns: [row_number() over ( order by mytable.i desc) as `row_number() over (order by i desc)`, mytable.i as i2]\n" + + " └─ Sort(row_number() over ( order by mytable.i desc) as `row_number() over (order by i desc)` ASC)\n" + " └─ Window(row_number() over ( order by mytable.i DESC), mytable.i)\n" + " └─ LookupJoin\n" + " ├─ IndexedTableAccess(mytable)\n" + @@ -16103,7 +16135,23 @@ inner join pq on true ExpectedEstimates: "Project\n" + " ├─ columns: [mt.i]\n" + " └─ Filter\n" + - " ├─ ((NOT(Subquery(select i from mytable where i = mt.i and i > 2) IS NULL)) AND (NOT(Subquery(select i2 from othertable where i2 = i) IS NULL)))\n" + + " ├─ ((NOT(Subquery\n" + + " │ ├─ cacheable: false\n" + + " │ └─ Filter\n" + + " │ ├─ (mytable.i = mt.i)\n" + + " │ └─ IndexedTableAccess(mytable)\n" + + " │ ├─ index: [mytable.i]\n" + + " │ ├─ filters: [{(2, ∞)}]\n" + + " │ └─ columns: [i]\n" + + " │ IS NULL)) AND (NOT(Subquery\n" + + " │ ├─ cacheable: false\n" + + " │ └─ Filter\n" + + " │ ├─ (othertable.i2 = mt.i)\n" + + " │ └─ IndexedTableAccess(othertable)\n" + + " │ ├─ index: [othertable.i2]\n" + + " │ ├─ columns: [i2]\n" + + " │ └─ keys: mt.i\n" + + " │ IS NULL)))\n" + " └─ TableAlias(mt)\n" + " └─ Table\n" + " └─ name: mytable\n" + @@ -16111,7 +16159,23 @@ inner join pq on true ExpectedAnalysis: "Project\n" + " ├─ columns: [mt.i]\n" + " └─ Filter\n" + - " ├─ ((NOT(Subquery(select i from mytable where i = mt.i and i > 2) IS NULL)) AND (NOT(Subquery(select i2 from othertable where i2 = i) IS NULL)))\n" + + " ├─ ((NOT(Subquery\n" + + " │ ├─ cacheable: false\n" + + " │ └─ Filter\n" + + " │ ├─ (mytable.i = mt.i)\n" + + " │ └─ IndexedTableAccess(mytable)\n" + + " │ ├─ index: [mytable.i]\n" + + " │ ├─ filters: [{(2, ∞)}]\n" + + " │ └─ columns: [i]\n" + + " │ IS NULL)) AND (NOT(Subquery\n" + + " │ ├─ cacheable: false\n" + + " │ └─ Filter\n" + + " │ ├─ (othertable.i2 = mt.i)\n" + + " │ └─ IndexedTableAccess(othertable)\n" + + " │ ├─ index: [othertable.i2]\n" + + " │ ├─ columns: [i2]\n" + + " │ └─ keys: mt.i\n" + + " │ IS NULL)))\n" + " └─ TableAlias(mt)\n" + " └─ Table\n" + " └─ name: mytable\n" + @@ -16172,7 +16236,23 @@ inner join pq on true ExpectedEstimates: "Project\n" + " ├─ columns: [mt.i]\n" + " └─ Filter\n" + - " ├─ ((NOT(Subquery(select i from mytable where i = mt.i) IS NULL)) AND (NOT(Subquery(select i2 from othertable where i2 = i and i > 2) IS NULL)))\n" + + " ├─ ((NOT(Subquery\n" + + " │ ├─ cacheable: false\n" + + " │ └─ Filter\n" + + " │ ├─ (mytable.i = mt.i)\n" + + " │ └─ IndexedTableAccess(mytable)\n" + + " │ ├─ index: [mytable.i]\n" + + " │ ├─ columns: [i]\n" + + " │ └─ keys: mt.i\n" + + " │ IS NULL)) AND (NOT(Subquery\n" + + " │ ├─ cacheable: false\n" + + " │ └─ Filter\n" + + " │ ├─ ((mt.i > 2) AND (othertable.i2 = mt.i))\n" + + " │ └─ IndexedTableAccess(othertable)\n" + + " │ ├─ index: [othertable.i2]\n" + + " │ ├─ columns: [i2]\n" + + " │ └─ keys: mt.i\n" + + " │ IS NULL)))\n" + " └─ TableAlias(mt)\n" + " └─ Table\n" + " └─ name: mytable\n" + @@ -16180,7 +16260,23 @@ inner join pq on true ExpectedAnalysis: "Project\n" + " ├─ columns: [mt.i]\n" + " └─ Filter\n" + - " ├─ ((NOT(Subquery(select i from mytable where i = mt.i) IS NULL)) AND (NOT(Subquery(select i2 from othertable where i2 = i and i > 2) IS NULL)))\n" + + " ├─ ((NOT(Subquery\n" + + " │ ├─ cacheable: false\n" + + " │ └─ Filter\n" + + " │ ├─ (mytable.i = mt.i)\n" + + " │ └─ IndexedTableAccess(mytable)\n" + + " │ ├─ index: [mytable.i]\n" + + " │ ├─ columns: [i]\n" + + " │ └─ keys: mt.i\n" + + " │ IS NULL)) AND (NOT(Subquery\n" + + " │ ├─ cacheable: false\n" + + " │ └─ Filter\n" + + " │ ├─ ((mt.i > 2) AND (othertable.i2 = mt.i))\n" + + " │ └─ IndexedTableAccess(othertable)\n" + + " │ ├─ index: [othertable.i2]\n" + + " │ ├─ columns: [i2]\n" + + " │ └─ keys: mt.i\n" + + " │ IS NULL)))\n" + " └─ TableAlias(mt)\n" + " └─ Table\n" + " └─ name: mytable\n" + @@ -16231,7 +16327,7 @@ inner join pq on true " │ ├─ index: [one_pk.pk]\n" + " │ ├─ filters: [{[1, 1]}]\n" + " │ └─ columns: [pk]\n" + - " │ as (SELECT pk from one_pk where pk = 1 limit 1)]\n" + + " │ as `(SELECT pk from one_pk where pk = 1 limit 1)`]\n" + " └─ Sort(t1.pk ASC, t2.pk2 ASC)\n" + " └─ CrossJoin (estimated cost=4.030 rows=3)\n" + " ├─ Filter\n" + @@ -16252,7 +16348,7 @@ inner join pq on true " │ ├─ index: [one_pk.pk]\n" + " │ ├─ filters: [{[1, 1]}]\n" + " │ └─ columns: [pk]\n" + - " │ as (SELECT pk from one_pk where pk = 1 limit 1)]\n" + + " │ as `(SELECT pk from one_pk where pk = 1 limit 1)`]\n" + " └─ Sort(t1.pk ASC, t2.pk2 ASC)\n" + " └─ CrossJoin (estimated cost=4.030 rows=3) (actual rows=2 loops=1)\n" + " ├─ Filter\n" + @@ -16481,14 +16577,14 @@ inner join pq on true " └─ columns: [t n]\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [bigtable.t, bigtable.n, lag(bigtable.t, 1, (bigtable.t + 1)) over ( partition by bigtable.n rows between unbounded preceding and unbounded following) as lag(t, 1, t+1) over (partition by n)]\n" + + " ├─ columns: [bigtable.t, bigtable.n, lag(bigtable.t, 1, (bigtable.t + 1)) over ( partition by bigtable.n rows between unbounded preceding and unbounded following) as `lag(t, 1, t+1) over (partition by n)`]\n" + " └─ Window(lag(bigtable.t, 1, (bigtable.t + 1)) over ( partition by bigtable.n ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING), bigtable.t, bigtable.n)\n" + " └─ Table\n" + " ├─ name: bigtable\n" + " └─ columns: [t n]\n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [bigtable.t, bigtable.n, lag(bigtable.t, 1, (bigtable.t + 1)) over ( partition by bigtable.n rows between unbounded preceding and unbounded following) as lag(t, 1, t+1) over (partition by n)]\n" + + " ├─ columns: [bigtable.t, bigtable.n, lag(bigtable.t, 1, (bigtable.t + 1)) over ( partition by bigtable.n rows between unbounded preceding and unbounded following) as `lag(t, 1, t+1) over (partition by n)`]\n" + " └─ Window(lag(bigtable.t, 1, (bigtable.t + 1)) over ( partition by bigtable.n ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING), bigtable.t, bigtable.n)\n" + " └─ Table\n" + " ├─ name: bigtable\n" + @@ -16508,14 +16604,14 @@ inner join pq on true " └─ columns: [i]\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [mytable.i, row_number() over ( rows between unbounded preceding and unbounded following) as row_number() over (w3)]\n" + + " ├─ columns: [mytable.i, row_number() over ( rows between unbounded preceding and unbounded following) as `row_number() over (w3)`]\n" + " └─ Window(row_number() over ( ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING), mytable.i)\n" + " └─ Table\n" + " ├─ name: mytable\n" + " └─ columns: [i]\n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [mytable.i, row_number() over ( rows between unbounded preceding and unbounded following) as row_number() over (w3)]\n" + + " ├─ columns: [mytable.i, row_number() over ( rows between unbounded preceding and unbounded following) as `row_number() over (w3)`]\n" + " └─ Window(row_number() over ( ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING), mytable.i)\n" + " └─ Table\n" + " ├─ name: mytable\n" + @@ -16535,14 +16631,14 @@ inner join pq on true " └─ columns: [i s]\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [mytable.i, row_number() over ( partition by mytable.s order by mytable.i asc) as row_number() over (w1 partition by s)]\n" + + " ├─ columns: [mytable.i, row_number() over ( partition by mytable.s order by mytable.i asc) as `row_number() over (w1 partition by s)`]\n" + " └─ Window(row_number() over ( partition by mytable.s order by mytable.i ASC), mytable.i)\n" + " └─ Table\n" + " ├─ name: mytable\n" + " └─ columns: [i s]\n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [mytable.i, row_number() over ( partition by mytable.s order by mytable.i asc) as row_number() over (w1 partition by s)]\n" + + " ├─ columns: [mytable.i, row_number() over ( partition by mytable.s order by mytable.i asc) as `row_number() over (w1 partition by s)`]\n" + " └─ Window(row_number() over ( partition by mytable.s order by mytable.i ASC), mytable.i)\n" + " └─ Table\n" + " ├─ name: mytable\n" + @@ -19804,7 +19900,7 @@ inner join pq on true " └─ RecursiveTable(n)\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [count(n.i) as count(i)]\n" + + " ├─ columns: [count(n.i) as `count(i)`]\n" + " └─ GroupBy\n" + " ├─ select: COUNT(n.i)\n" + " ├─ group: \n" + @@ -19823,13 +19919,13 @@ inner join pq on true " │ └─ Table\n" + " │ └─ name: \n" + " └─ Project\n" + - " ├─ columns: [(n.i + 1) as i + 1]\n" + + " ├─ columns: [(n.i + 1) as `i + 1`]\n" + " └─ Filter\n" + " ├─ ((n.i + 1) <= 10)\n" + " └─ RecursiveTable(n)\n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [count(n.i) as count(i)]\n" + + " ├─ columns: [count(n.i) as `count(i)`]\n" + " └─ GroupBy\n" + " ├─ select: COUNT(n.i)\n" + " ├─ group: \n" + @@ -19848,7 +19944,7 @@ inner join pq on true " │ └─ Table\n" + " │ └─ name: \n" + " └─ Project\n" + - " ├─ columns: [(n.i + 1) as i + 1]\n" + + " ├─ columns: [(n.i + 1) as `i + 1`]\n" + " └─ Filter\n" + " ├─ ((n.i + 1) <= 10)\n" + " └─ RecursiveTable(n)\n" + @@ -19889,7 +19985,7 @@ inner join pq on true " └─ RecursiveTable(n)\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [count(n.i) as count(i)]\n" + + " ├─ columns: [count(n.i) as `count(i)`]\n" + " └─ GroupBy\n" + " ├─ select: COUNT(n.i)\n" + " ├─ group: \n" + @@ -19907,7 +20003,7 @@ inner join pq on true " │ └─ Table\n" + " │ └─ name: \n" + " └─ Project\n" + - " ├─ columns: [(n.i + 1) as i + 1]\n" + + " ├─ columns: [(n.i + 1) as `i + 1`]\n" + " └─ Having(((n.i + 1) <= 10))\n" + " └─ GroupBy\n" + " ├─ select: n.i\n" + @@ -19915,7 +20011,7 @@ inner join pq on true " └─ RecursiveTable(n)\n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [count(n.i) as count(i)]\n" + + " ├─ columns: [count(n.i) as `count(i)`]\n" + " └─ GroupBy\n" + " ├─ select: COUNT(n.i)\n" + " ├─ group: \n" + @@ -19933,7 +20029,7 @@ inner join pq on true " │ └─ Table\n" + " │ └─ name: \n" + " └─ Project\n" + - " ├─ columns: [(n.i + 1) as i + 1]\n" + + " ├─ columns: [(n.i + 1) as `i + 1`]\n" + " └─ Having(((n.i + 1) <= 10))\n" + " └─ GroupBy\n" + " ├─ select: n.i\n" + @@ -19974,7 +20070,7 @@ inner join pq on true " └─ RecursiveTable(n)\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [count(n.i) as count(i)]\n" + + " ├─ columns: [count(n.i) as `count(i)`]\n" + " └─ GroupBy\n" + " ├─ select: COUNT(n.i)\n" + " ├─ group: \n" + @@ -19993,13 +20089,13 @@ inner join pq on true " │ └─ Table\n" + " │ └─ name: \n" + " └─ Project\n" + - " ├─ columns: [(n.i + 1) as i + 1]\n" + + " ├─ columns: [(n.i + 1) as `i + 1`]\n" + " └─ Filter\n" + " ├─ ((n.i + 1) <= 10)\n" + " └─ RecursiveTable(n)\n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [count(n.i) as count(i)]\n" + + " ├─ columns: [count(n.i) as `count(i)`]\n" + " └─ GroupBy\n" + " ├─ select: COUNT(n.i)\n" + " ├─ group: \n" + @@ -20018,7 +20114,7 @@ inner join pq on true " │ └─ Table\n" + " │ └─ name: \n" + " └─ Project\n" + - " ├─ columns: [(n.i + 1) as i + 1]\n" + + " ├─ columns: [(n.i + 1) as `i + 1`]\n" + " └─ Filter\n" + " ├─ ((n.i + 1) <= 10)\n" + " └─ RecursiveTable(n)\n" + @@ -20372,7 +20468,7 @@ inner join pq on true " └─ RecursiveTable(a)\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [count(1) as count(*)]\n" + + " ├─ columns: [count(1) as `count(*)`]\n" + " └─ GroupBy\n" + " ├─ select: COUNT(1)\n" + " ├─ group: \n" + @@ -20410,13 +20506,13 @@ inner join pq on true " │ └─ Table\n" + " │ └─ name: \n" + " └─ Project\n" + - " ├─ columns: [(a.x + 1) as x+1]\n" + + " ├─ columns: [(a.x + 1) as `x+1`]\n" + " └─ Filter\n" + " ├─ (a.x < 10)\n" + " └─ RecursiveTable(a)\n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [count(1) as count(*)]\n" + + " ├─ columns: [count(1) as `count(*)`]\n" + " └─ GroupBy\n" + " ├─ select: COUNT(1)\n" + " ├─ group: \n" + @@ -20454,7 +20550,7 @@ inner join pq on true " │ └─ Table\n" + " │ └─ name: \n" + " └─ Project\n" + - " ├─ columns: [(a.x + 1) as x+1]\n" + + " ├─ columns: [(a.x + 1) as `x+1`]\n" + " └─ Filter\n" + " ├─ (a.x < 10)\n" + " └─ RecursiveTable(a)\n" + @@ -22625,7 +22721,7 @@ WHERE keyless.c0 IN ( " └─ columns: [pk1 pk2 c1]\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [two_pk.pk1, two_pk.pk2, row_number() over ( partition by two_pk.pk1 order by two_pk.c1 desc) as row_number() over (partition by pk1 order by c1 desc)]\n" + + " ├─ columns: [two_pk.pk1, two_pk.pk2, row_number() over ( partition by two_pk.pk1 order by two_pk.c1 desc) as `row_number() over (partition by pk1 order by c1 desc)`]\n" + " └─ Sort(two_pk.pk1 ASC, two_pk.pk2 ASC)\n" + " └─ Window(row_number() over ( partition by two_pk.pk1 order by two_pk.c1 DESC), two_pk.pk1, two_pk.pk2)\n" + " └─ Table\n" + @@ -22633,7 +22729,7 @@ WHERE keyless.c0 IN ( " └─ columns: [pk1 pk2 c1]\n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [two_pk.pk1, two_pk.pk2, row_number() over ( partition by two_pk.pk1 order by two_pk.c1 desc) as row_number() over (partition by pk1 order by c1 desc)]\n" + + " ├─ columns: [two_pk.pk1, two_pk.pk2, row_number() over ( partition by two_pk.pk1 order by two_pk.c1 desc) as `row_number() over (partition by pk1 order by c1 desc)`]\n" + " └─ Sort(two_pk.pk1 ASC, two_pk.pk2 ASC)\n" + " └─ Window(row_number() over ( partition by two_pk.pk1 order by two_pk.c1 DESC), two_pk.pk1, two_pk.pk2)\n" + " └─ Table\n" + @@ -23227,7 +23323,7 @@ WHERE keyless.c0 IN ( "", ExpectedEstimates: "Limit(1)\n" + " └─ Project\n" + - " ├─ columns: [xy.x as max(x)]\n" + + " ├─ columns: [xy.x as `max(x)`]\n" + " └─ IndexedTableAccess(xy)\n" + " ├─ index: [xy.x]\n" + " ├─ filters: [{[NULL, ∞)}]\n" + @@ -23236,7 +23332,7 @@ WHERE keyless.c0 IN ( "", ExpectedAnalysis: "Limit(1)\n" + " └─ Project\n" + - " ├─ columns: [xy.x as max(x)]\n" + + " ├─ columns: [xy.x as `max(x)`]\n" + " └─ IndexedTableAccess(xy)\n" + " ├─ index: [xy.x]\n" + " ├─ filters: [{[NULL, ∞)}]\n" + @@ -23260,7 +23356,7 @@ WHERE keyless.c0 IN ( "", ExpectedEstimates: "Limit(1)\n" + " └─ Project\n" + - " ├─ columns: [xy.x as min(x)]\n" + + " ├─ columns: [xy.x as `min(x)`]\n" + " └─ IndexedTableAccess(xy)\n" + " ├─ index: [xy.x]\n" + " ├─ filters: [{[NULL, ∞)}]\n" + @@ -23268,7 +23364,7 @@ WHERE keyless.c0 IN ( "", ExpectedAnalysis: "Limit(1)\n" + " └─ Project\n" + - " ├─ columns: [xy.x as min(x)]\n" + + " ├─ columns: [xy.x as `min(x)`]\n" + " └─ IndexedTableAccess(xy)\n" + " ├─ index: [xy.x]\n" + " ├─ filters: [{[NULL, ∞)}]\n" + @@ -23288,7 +23384,7 @@ WHERE keyless.c0 IN ( " └─ columns: [y]\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [max(xy.y) as max(y)]\n" + + " ├─ columns: [max(xy.y) as `max(y)`]\n" + " └─ GroupBy\n" + " ├─ select: MAX(xy.y)\n" + " ├─ group: \n" + @@ -23297,7 +23393,7 @@ WHERE keyless.c0 IN ( " └─ columns: [y]\n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [max(xy.y) as max(y)]\n" + + " ├─ columns: [max(xy.y) as `max(y)`]\n" + " └─ GroupBy\n" + " ├─ select: MAX(xy.y)\n" + " ├─ group: \n" + @@ -23323,7 +23419,7 @@ WHERE keyless.c0 IN ( "", ExpectedEstimates: "Limit(1)\n" + " └─ Project\n" + - " ├─ columns: [(xy.x + 100) as max(x)+100]\n" + + " ├─ columns: [(xy.x + 100) as `max(x)+100`]\n" + " └─ IndexedTableAccess(xy)\n" + " ├─ index: [xy.x]\n" + " ├─ filters: [{[NULL, ∞)}]\n" + @@ -23332,7 +23428,7 @@ WHERE keyless.c0 IN ( "", ExpectedAnalysis: "Limit(1)\n" + " └─ Project\n" + - " ├─ columns: [(xy.x + 100) as max(x)+100]\n" + + " ├─ columns: [(xy.x + 100) as `max(x)+100`]\n" + " └─ IndexedTableAccess(xy)\n" + " ├─ index: [xy.x]\n" + " ├─ filters: [{[NULL, ∞)}]\n" + @@ -23391,7 +23487,7 @@ WHERE keyless.c0 IN ( "", ExpectedEstimates: "Limit(1)\n" + " └─ Project\n" + - " ├─ columns: [1, 2.0, '3', xy.x as max(x)]\n" + + " ├─ columns: [1, 2.0, '3', xy.x as `max(x)`]\n" + " └─ IndexedTableAccess(xy)\n" + " ├─ index: [xy.x]\n" + " ├─ filters: [{[NULL, ∞)}]\n" + @@ -23400,7 +23496,7 @@ WHERE keyless.c0 IN ( "", ExpectedAnalysis: "Limit(1)\n" + " └─ Project\n" + - " ├─ columns: [1, 2.0, '3', xy.x as max(x)]\n" + + " ├─ columns: [1, 2.0, '3', xy.x as `max(x)`]\n" + " └─ IndexedTableAccess(xy)\n" + " ├─ index: [xy.x]\n" + " ├─ filters: [{[NULL, ∞)}]\n" + @@ -23424,7 +23520,7 @@ WHERE keyless.c0 IN ( "", ExpectedEstimates: "Limit(1)\n" + " └─ Project\n" + - " ├─ columns: [xy.x as min(x)]\n" + + " ├─ columns: [xy.x as `min(x)`]\n" + " └─ IndexedTableAccess(xy)\n" + " ├─ index: [xy.x]\n" + " ├─ filters: [{(0, ∞)}]\n" + @@ -23432,7 +23528,7 @@ WHERE keyless.c0 IN ( "", ExpectedAnalysis: "Limit(1)\n" + " └─ Project\n" + - " ├─ columns: [xy.x as min(x)]\n" + + " ├─ columns: [xy.x as `min(x)`]\n" + " └─ IndexedTableAccess(xy)\n" + " ├─ index: [xy.x]\n" + " ├─ filters: [{(0, ∞)}]\n" + @@ -23456,7 +23552,7 @@ WHERE keyless.c0 IN ( "", ExpectedEstimates: "Limit(1)\n" + " └─ Project\n" + - " ├─ columns: [xy.x as max(x)]\n" + + " ├─ columns: [xy.x as `max(x)`]\n" + " └─ IndexedTableAccess(xy)\n" + " ├─ index: [xy.x]\n" + " ├─ filters: [{(NULL, 3)}]\n" + @@ -23465,7 +23561,7 @@ WHERE keyless.c0 IN ( "", ExpectedAnalysis: "Limit(1)\n" + " └─ Project\n" + - " ├─ columns: [xy.x as max(x)]\n" + + " ├─ columns: [xy.x as `max(x)`]\n" + " └─ IndexedTableAccess(xy)\n" + " ├─ index: [xy.x]\n" + " ├─ filters: [{(NULL, 3)}]\n" + @@ -23490,7 +23586,7 @@ WHERE keyless.c0 IN ( " └─ columns: [x y]\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [min(xy.x) as min(x)]\n" + + " ├─ columns: [min(xy.x) as `min(x)`]\n" + " └─ GroupBy\n" + " ├─ select: MIN(xy.x)\n" + " ├─ group: \n" + @@ -23500,7 +23596,7 @@ WHERE keyless.c0 IN ( " └─ columns: [x y]\n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [min(xy.x) as min(x)]\n" + + " ├─ columns: [min(xy.x) as `min(x)`]\n" + " └─ GroupBy\n" + " ├─ select: MIN(xy.x)\n" + " ├─ group: \n" + @@ -23527,7 +23623,7 @@ WHERE keyless.c0 IN ( " └─ columns: [x y]\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [max(xy.x) as max(x)]\n" + + " ├─ columns: [max(xy.x) as `max(x)`]\n" + " └─ GroupBy\n" + " ├─ select: MAX(xy.x)\n" + " ├─ group: \n" + @@ -23537,7 +23633,7 @@ WHERE keyless.c0 IN ( " └─ columns: [x y]\n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [max(xy.x) as max(x)]\n" + + " ├─ columns: [max(xy.x) as `max(x)`]\n" + " └─ GroupBy\n" + " ├─ select: MAX(xy.x)\n" + " ├─ group: \n" + @@ -23578,7 +23674,7 @@ WHERE keyless.c0 IN ( " ├─ tableId: 2\n" + " └─ Limit(1)\n" + " └─ Project\n" + - " ├─ columns: [xy.x as max(x)]\n" + + " ├─ columns: [xy.x as `max(x)`]\n" + " └─ IndexedTableAccess(xy)\n" + " ├─ index: [xy.x]\n" + " ├─ filters: [{[NULL, ∞)}]\n" + @@ -23594,7 +23690,7 @@ WHERE keyless.c0 IN ( " ├─ tableId: 2\n" + " └─ Limit(1)\n" + " └─ Project\n" + - " ├─ columns: [xy.x as max(x)]\n" + + " ├─ columns: [xy.x as `max(x)`]\n" + " └─ IndexedTableAccess(xy)\n" + " ├─ index: [xy.x]\n" + " ├─ filters: [{[NULL, ∞)}]\n" + @@ -23627,7 +23723,7 @@ WHERE keyless.c0 IN ( " └─ columns: [x]\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [(cte.i + 100) as i + 100]\n" + + " ├─ columns: [(cte.i + 100) as `i + 100`]\n" + " └─ SubqueryAlias\n" + " ├─ name: cte\n" + " ├─ outerVisibility: false\n" + @@ -23637,7 +23733,7 @@ WHERE keyless.c0 IN ( " ├─ tableId: 3\n" + " └─ Limit(1)\n" + " └─ Project\n" + - " ├─ columns: [xy.x as max(x)]\n" + + " ├─ columns: [xy.x as `max(x)`]\n" + " └─ IndexedTableAccess(xy)\n" + " ├─ index: [xy.x]\n" + " ├─ filters: [{[NULL, ∞)}]\n" + @@ -23645,7 +23741,7 @@ WHERE keyless.c0 IN ( " └─ reverse: true\n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [(cte.i + 100) as i + 100]\n" + + " ├─ columns: [(cte.i + 100) as `i + 100`]\n" + " └─ SubqueryAlias\n" + " ├─ name: cte\n" + " ├─ outerVisibility: false\n" + @@ -23655,7 +23751,7 @@ WHERE keyless.c0 IN ( " ├─ tableId: 3\n" + " └─ Limit(1)\n" + " └─ Project\n" + - " ├─ columns: [xy.x as max(x)]\n" + + " ├─ columns: [xy.x as `max(x)`]\n" + " └─ IndexedTableAccess(xy)\n" + " ├─ index: [xy.x]\n" + " ├─ filters: [{[NULL, ∞)}]\n" + @@ -23684,7 +23780,7 @@ WHERE keyless.c0 IN ( " └─ tableId: 1\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [max(cte.i) as max(i)]\n" + + " ├─ columns: [max(cte.i) as `max(i)`]\n" + " └─ GroupBy\n" + " ├─ select: MAX(cte.i)\n" + " ├─ group: \n" + @@ -23700,7 +23796,7 @@ WHERE keyless.c0 IN ( " └─ columns: [x]\n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [max(cte.i) as max(i)]\n" + + " ├─ columns: [max(cte.i) as `max(i)`]\n" + " └─ GroupBy\n" + " ├─ select: MAX(cte.i)\n" + " ├─ group: \n" + @@ -23729,7 +23825,7 @@ WHERE keyless.c0 IN ( " └─ columns: [x y]\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [max(xy.x) as max(x)]\n" + + " ├─ columns: [max(xy.x) as `max(x)`]\n" + " └─ GroupBy\n" + " ├─ select: MAX(xy.x)\n" + " ├─ group: xy.y\n" + @@ -23738,7 +23834,7 @@ WHERE keyless.c0 IN ( " └─ columns: [x y]\n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [max(xy.x) as max(x)]\n" + + " ├─ columns: [max(xy.x) as `max(x)`]\n" + " └─ GroupBy\n" + " ├─ select: MAX(xy.x)\n" + " ├─ group: xy.y\n" + @@ -23769,7 +23865,7 @@ WHERE keyless.c0 IN ( " └─ columns: [x]\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [max(xy.x) as max(x)]\n" + + " ├─ columns: [max(xy.x) as `max(x)`]\n" + " └─ GroupBy\n" + " ├─ select: MAX(xy.x)\n" + " ├─ group: \n" + @@ -23783,7 +23879,7 @@ WHERE keyless.c0 IN ( " └─ keys: uv.u\n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [max(xy.x) as max(x)]\n" + + " ├─ columns: [max(xy.x) as `max(x)`]\n" + " └─ GroupBy\n" + " ├─ select: MAX(xy.x)\n" + " ├─ group: \n" + @@ -24094,7 +24190,20 @@ order by xy.x, xy.y, uv.u, uv.v;`, " ├─ columns: [xy.x, xy.y, uv.u, uv.v]\n" + " └─ Sort(xy.x ASC, xy.y ASC, uv.u ASC, uv.v ASC)\n" + " └─ LookupJoin (estimated cost=13.338 rows=4)\n" + - " ├─ (uv.v = Subquery(select max(v) from uv where xy.x = uv.u))\n" + + " ├─ (uv.v = Subquery\n" + + " │ ├─ cacheable: false\n" + + " │ └─ Project\n" + + " │ ├─ columns: [max(uv.v) as `max(v)`]\n" + + " │ └─ GroupBy\n" + + " │ ├─ select: MAX(uv.v)\n" + + " │ ├─ group: \n" + + " │ └─ Filter\n" + + " │ ├─ (xy.x = uv.u)\n" + + " │ └─ IndexedTableAccess(uv)\n" + + " │ ├─ index: [uv.u]\n" + + " │ ├─ columns: [u v]\n" + + " │ └─ keys: xy.x\n" + + " │ )\n" + " ├─ Table\n" + " │ └─ name: uv\n" + " └─ IndexedTableAccess(xy)\n" + @@ -24105,7 +24214,20 @@ order by xy.x, xy.y, uv.u, uv.v;`, " ├─ columns: [xy.x, xy.y, uv.u, uv.v]\n" + " └─ Sort(xy.x ASC, xy.y ASC, uv.u ASC, uv.v ASC)\n" + " └─ LookupJoin (estimated cost=13.338 rows=4) (actual rows=4 loops=1)\n" + - " ├─ (uv.v = Subquery(select max(v) from uv where xy.x = uv.u))\n" + + " ├─ (uv.v = Subquery\n" + + " │ ├─ cacheable: false\n" + + " │ └─ Project\n" + + " │ ├─ columns: [max(uv.v) as `max(v)`]\n" + + " │ └─ GroupBy\n" + + " │ ├─ select: MAX(uv.v)\n" + + " │ ├─ group: \n" + + " │ └─ Filter\n" + + " │ ├─ (xy.x = uv.u)\n" + + " │ └─ IndexedTableAccess(uv)\n" + + " │ ├─ index: [uv.u]\n" + + " │ ├─ columns: [u v]\n" + + " │ └─ keys: xy.x\n" + + " │ )\n" + " ├─ Table\n" + " │ └─ name: uv\n" + " └─ IndexedTableAccess(xy)\n" + @@ -24162,7 +24284,19 @@ where exists ( " └─ tableId: 2\n" + "", ExpectedEstimates: "SemiJoin (estimated cost=506000.000 rows=1250)\n" + - " ├─ (ab.b = Subquery(select max(v) from uv where uv.v = ab2.a and uv.v = ab.a))\n" + + " ├─ (ab.b = Subquery\n" + + " │ ├─ cacheable: false\n" + + " │ └─ Project\n" + + " │ ├─ columns: [max(uv.v) as `max(v)`]\n" + + " │ └─ GroupBy\n" + + " │ ├─ select: MAX(uv.v)\n" + + " │ ├─ group: \n" + + " │ └─ Filter\n" + + " │ ├─ ((uv.v = ab2.a) AND (uv.v = ab.a))\n" + + " │ └─ Table\n" + + " │ ├─ name: uv\n" + + " │ └─ columns: [v]\n" + + " │ )\n" + " ├─ TableAlias(ab2)\n" + " │ └─ Table\n" + " │ └─ name: ab\n" + @@ -24170,7 +24304,19 @@ where exists ( " └─ name: ab\n" + "", ExpectedAnalysis: "SemiJoin (estimated cost=506000.000 rows=1250) (actual rows=1 loops=1)\n" + - " ├─ (ab.b = Subquery(select max(v) from uv where uv.v = ab2.a and uv.v = ab.a))\n" + + " ├─ (ab.b = Subquery\n" + + " │ ├─ cacheable: false\n" + + " │ └─ Project\n" + + " │ ├─ columns: [max(uv.v) as `max(v)`]\n" + + " │ └─ GroupBy\n" + + " │ ├─ select: MAX(uv.v)\n" + + " │ ├─ group: \n" + + " │ └─ Filter\n" + + " │ ├─ ((uv.v = ab2.a) AND (uv.v = ab.a))\n" + + " │ └─ Table\n" + + " │ ├─ name: uv\n" + + " │ └─ columns: [v]\n" + + " │ )\n" + " ├─ TableAlias(ab2)\n" + " │ └─ Table\n" + " │ └─ name: ab\n" + @@ -24231,7 +24377,19 @@ order by x, y; "", ExpectedEstimates: "Sort(xy2.x ASC, xy2.y ASC)\n" + " └─ SemiJoin (estimated cost=506000.000 rows=1250)\n" + - " ├─ (xy.y = Subquery(select max(v) from uv where uv.v = xy2.x and uv.v = xy.x))\n" + + " ├─ (xy.y = Subquery\n" + + " │ ├─ cacheable: false\n" + + " │ └─ Project\n" + + " │ ├─ columns: [max(uv.v) as `max(v)`]\n" + + " │ └─ GroupBy\n" + + " │ ├─ select: MAX(uv.v)\n" + + " │ ├─ group: \n" + + " │ └─ Filter\n" + + " │ ├─ ((uv.v = xy2.x) AND (uv.v = xy.x))\n" + + " │ └─ Table\n" + + " │ ├─ name: uv\n" + + " │ └─ columns: [v]\n" + + " │ )\n" + " ├─ TableAlias(xy2)\n" + " │ └─ Table\n" + " │ └─ name: xy\n" + @@ -24240,7 +24398,19 @@ order by x, y; "", ExpectedAnalysis: "Sort(xy2.x ASC, xy2.y ASC)\n" + " └─ SemiJoin (estimated cost=506000.000 rows=1250) (actual rows=0 loops=1)\n" + - " ├─ (xy.y = Subquery(select max(v) from uv where uv.v = xy2.x and uv.v = xy.x))\n" + + " ├─ (xy.y = Subquery\n" + + " │ ├─ cacheable: false\n" + + " │ └─ Project\n" + + " │ ├─ columns: [max(uv.v) as `max(v)`]\n" + + " │ └─ GroupBy\n" + + " │ ├─ select: MAX(uv.v)\n" + + " │ ├─ group: \n" + + " │ └─ Filter\n" + + " │ ├─ ((uv.v = xy2.x) AND (uv.v = xy.x))\n" + + " │ └─ Table\n" + + " │ ├─ name: uv\n" + + " │ └─ columns: [v]\n" + + " │ )\n" + " ├─ TableAlias(xy2)\n" + " │ └─ Table\n" + " │ └─ name: xy\n" + @@ -24619,13 +24789,13 @@ order by x, y; " └─ columns: [x y]\n" + "", ExpectedEstimates: "Filter\n" + - " ├─ (xy.x = {})\n" + + " ├─ (xy.x = CAST('{}' AS JSON))\n" + " └─ Table\n" + " ├─ name: xy\n" + " └─ columns: [x y]\n" + "", ExpectedAnalysis: "Filter\n" + - " ├─ (xy.x = {})\n" + + " ├─ (xy.x = CAST('{}' AS JSON))\n" + " └─ Table\n" + " ├─ name: xy\n" + " └─ columns: [x y]\n" + @@ -24643,13 +24813,13 @@ order by x, y; " └─ columns: [x y]\n" + "", ExpectedEstimates: "Filter\n" + - " ├─ (xy.x = [])\n" + + " ├─ (xy.x = CAST('[]' AS JSON))\n" + " └─ Table\n" + " ├─ name: xy\n" + " └─ columns: [x y]\n" + "", ExpectedAnalysis: "Filter\n" + - " ├─ (xy.x = [])\n" + + " ├─ (xy.x = CAST('[]' AS JSON))\n" + " └─ Table\n" + " ├─ name: xy\n" + " └─ columns: [x y]\n" + @@ -24927,7 +25097,7 @@ order by x, y; "", ExpectedEstimates: "Distinct\n" + " └─ Project\n" + - " ├─ columns: [(two_pk.pk1 + 1) as pk1 + 1]\n" + + " ├─ columns: [(two_pk.pk1 + 1) as `pk1 + 1`]\n" + " └─ Sort((two_pk.pk1 + 1) ASC)\n" + " └─ Table\n" + " ├─ name: two_pk\n" + @@ -24935,7 +25105,7 @@ order by x, y; "", ExpectedAnalysis: "Distinct\n" + " └─ Project\n" + - " ├─ columns: [(two_pk.pk1 + 1) as pk1 + 1]\n" + + " ├─ columns: [(two_pk.pk1 + 1) as `pk1 + 1`]\n" + " └─ Sort((two_pk.pk1 + 1) ASC)\n" + " └─ Table\n" + " ├─ name: two_pk\n" + @@ -24955,7 +25125,7 @@ order by x, y; "", ExpectedEstimates: "Distinct\n" + " └─ Project\n" + - " ├─ columns: [(two_pk.pk2 + 1) as pk2 + 1]\n" + + " ├─ columns: [(two_pk.pk2 + 1) as `pk2 + 1`]\n" + " └─ Sort((two_pk.pk2 + 1) ASC)\n" + " └─ Table\n" + " ├─ name: two_pk\n" + @@ -24963,7 +25133,7 @@ order by x, y; "", ExpectedAnalysis: "Distinct\n" + " └─ Project\n" + - " ├─ columns: [(two_pk.pk2 + 1) as pk2 + 1]\n" + + " ├─ columns: [(two_pk.pk2 + 1) as `pk2 + 1`]\n" + " └─ Sort((two_pk.pk2 + 1) ASC)\n" + " └─ Table\n" + " ├─ name: two_pk\n" + diff --git a/enginetest/queries/sysbench_plans.go b/enginetest/queries/sysbench_plans.go index 00b8de0812..54488b8e7e 100644 --- a/enginetest/queries/sysbench_plans.go +++ b/enginetest/queries/sysbench_plans.go @@ -146,7 +146,7 @@ var SysbenchPlanTests = []QueryPlanTest{ " └─ columns: [id small_int_col big_int_col year_col]\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [sbtest1.year_col, count(sbtest1.year_col) as count(year_col), max(sbtest1.big_int_col) as max(big_int_col), avg(sbtest1.small_int_col) as avg(small_int_col)]\n" + + " ├─ columns: [sbtest1.year_col, count(sbtest1.year_col) as `count(year_col)`, max(sbtest1.big_int_col) as `max(big_int_col)`, avg(sbtest1.small_int_col) as `avg(small_int_col)`]\n" + " └─ Sort(sbtest1.year_col ASC)\n" + " └─ GroupBy\n" + " ├─ select: AVG(sbtest1.small_int_col), COUNT(sbtest1.year_col), MAX(sbtest1.big_int_col), sbtest1.year_col\n" + @@ -157,7 +157,7 @@ var SysbenchPlanTests = []QueryPlanTest{ " └─ columns: [id small_int_col big_int_col year_col]\n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [sbtest1.year_col, count(sbtest1.year_col) as count(year_col), max(sbtest1.big_int_col) as max(big_int_col), avg(sbtest1.small_int_col) as avg(small_int_col)]\n" + + " ├─ columns: [sbtest1.year_col, count(sbtest1.year_col) as `count(year_col)`, max(sbtest1.big_int_col) as `max(big_int_col)`, avg(sbtest1.small_int_col) as `avg(small_int_col)`]\n" + " └─ Sort(sbtest1.year_col ASC)\n" + " └─ GroupBy\n" + " ├─ select: AVG(sbtest1.small_int_col), COUNT(sbtest1.year_col), MAX(sbtest1.big_int_col), sbtest1.year_col\n" + @@ -185,7 +185,7 @@ var SysbenchPlanTests = []QueryPlanTest{ " └─ columns: [id big_int_col]\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [count(sbtest1.id) as count(id)]\n" + + " ├─ columns: [count(sbtest1.id) as `count(id)`]\n" + " └─ GroupBy\n" + " ├─ select: COUNT(sbtest1.id)\n" + " ├─ group: \n" + @@ -195,7 +195,7 @@ var SysbenchPlanTests = []QueryPlanTest{ " └─ columns: [id big_int_col]\n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [count(sbtest1.id) as count(id)]\n" + + " ├─ columns: [count(sbtest1.id) as `count(id)`]\n" + " └─ GroupBy\n" + " ├─ select: COUNT(sbtest1.id)\n" + " ├─ group: \n" + diff --git a/enginetest/queries/tpcc_plans.go b/enginetest/queries/tpcc_plans.go index 3a4f26e6c9..1239ada6cd 100644 --- a/enginetest/queries/tpcc_plans.go +++ b/enginetest/queries/tpcc_plans.go @@ -687,9 +687,9 @@ SELECT d_next_o_id FROM district2 WHERE d_id = 5 AND d_w_id= 1`, { Query: `SELECT COUNT(DISTINCT (s_i_id)) FROM order_line2, stock2 WHERE ol_w_id = 1 AND ol_d_id = 5 AND ol_o_id < 3003 AND ol_o_id >= 2983 AND s_w_id= 1 AND s_i_id=ol_i_id AND s_quantity < 18`, ExpectedPlan: "Project\n" + - " ├─ columns: [countdistinct([stock2.s_i_id]):0!null->COUNT(DISTINCT (s_i_id)):0]\n" + + " ├─ columns: [count(distinct stock2.s_i_id):0!null->COUNT(DISTINCT (s_i_id)):0]\n" + " └─ GroupBy\n" + - " ├─ select: COUNTDISTINCT([stock2.s_i_id])\n" + + " ├─ select: COUNT(DISTINCT stock2.s_i_id)\n" + " ├─ group: \n" + " └─ LookupJoin\n" + " ├─ IndexedTableAccess(order_line2)\n" + @@ -718,9 +718,9 @@ SELECT d_next_o_id FROM district2 WHERE d_id = 5 AND d_w_id= 1`, " └─ columns: [s_i_id s_w_id s_quantity]\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [countdistinct([stock2.s_i_id]) as COUNT(DISTINCT (s_i_id))]\n" + + " ├─ columns: [count(distinct stock2.s_i_id) as `COUNT(DISTINCT (s_i_id))`]\n" + " └─ GroupBy\n" + - " ├─ select: COUNTDISTINCT([stock2.s_i_id])\n" + + " ├─ select: COUNT(DISTINCT stock2.s_i_id)\n" + " ├─ group: \n" + " └─ LookupJoin (estimated cost=132440.651 rows=39455)\n" + " ├─ IndexedTableAccess(order_line2)\n" + @@ -735,9 +735,9 @@ SELECT d_next_o_id FROM district2 WHERE d_id = 5 AND d_w_id= 1`, " └─ keys: 1, order_line2.ol_i_id\n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [countdistinct([stock2.s_i_id]) as COUNT(DISTINCT (s_i_id))]\n" + + " ├─ columns: [count(distinct stock2.s_i_id) as `COUNT(DISTINCT (s_i_id))`]\n" + " └─ GroupBy\n" + - " ├─ select: COUNTDISTINCT([stock2.s_i_id])\n" + + " ├─ select: COUNT(DISTINCT stock2.s_i_id)\n" + " ├─ group: \n" + " └─ LookupJoin (estimated cost=132440.651 rows=39455) (actual rows=0 loops=1)\n" + " ├─ IndexedTableAccess(order_line2)\n" + @@ -793,17 +793,39 @@ WHERE " └─ columns: [o_id o_d_id o_w_id o_c_id o_entry_d o_carrier_id o_ol_cnt o_all_local]\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [orders2.o_id, orders2.o_entry_d, coalesce(orders2.o_carrier_id,0) as COALESCE(o_carrier_id,0)]\n" + + " ├─ columns: [orders2.o_id, orders2.o_entry_d, coalesce(orders2.o_carrier_id,0) as `COALESCE(o_carrier_id,0)`]\n" + " └─ Filter\n" + - " ├─ (orders2.o_id = Subquery(select MAX(o_id) from orders2 where o_w_id = 1 and o_d_id = 3 and o_c_id = 20001))\n" + + " ├─ (orders2.o_id = Subquery\n" + + " │ ├─ cacheable: true\n" + + " │ └─ Project\n" + + " │ ├─ columns: [max(orders2.o_id) as `MAX(o_id)`]\n" + + " │ └─ GroupBy\n" + + " │ ├─ select: MAX(orders2.o_id)\n" + + " │ ├─ group: \n" + + " │ └─ IndexedTableAccess(orders2)\n" + + " │ ├─ index: [orders2.o_w_id,orders2.o_d_id,orders2.o_c_id,orders2.o_id]\n" + + " │ ├─ filters: [{[1, 1], [3, 3], [20001, 20001], [NULL, ∞)}]\n" + + " │ └─ columns: [o_id o_d_id o_w_id o_c_id]\n" + + " │ )\n" + " └─ IndexedTableAccess(orders2)\n" + " ├─ index: [orders2.o_w_id,orders2.o_d_id,orders2.o_c_id,orders2.o_id]\n" + " └─ filters: [{[1, 1], [3, 3], [20001, 20001], [NULL, ∞)}]\n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [orders2.o_id, orders2.o_entry_d, coalesce(orders2.o_carrier_id,0) as COALESCE(o_carrier_id,0)]\n" + + " ├─ columns: [orders2.o_id, orders2.o_entry_d, coalesce(orders2.o_carrier_id,0) as `COALESCE(o_carrier_id,0)`]\n" + " └─ Filter\n" + - " ├─ (orders2.o_id = Subquery(select MAX(o_id) from orders2 where o_w_id = 1 and o_d_id = 3 and o_c_id = 20001))\n" + + " ├─ (orders2.o_id = Subquery\n" + + " │ ├─ cacheable: true\n" + + " │ └─ Project\n" + + " │ ├─ columns: [max(orders2.o_id) as `MAX(o_id)`]\n" + + " │ └─ GroupBy\n" + + " │ ├─ select: MAX(orders2.o_id)\n" + + " │ ├─ group: \n" + + " │ └─ IndexedTableAccess(orders2)\n" + + " │ ├─ index: [orders2.o_w_id,orders2.o_d_id,orders2.o_c_id,orders2.o_id]\n" + + " │ ├─ filters: [{[1, 1], [3, 3], [20001, 20001], [NULL, ∞)}]\n" + + " │ └─ columns: [o_id o_d_id o_w_id o_c_id]\n" + + " │ )\n" + " └─ IndexedTableAccess(orders2)\n" + " ├─ index: [orders2.o_w_id,orders2.o_d_id,orders2.o_c_id,orders2.o_id]\n" + " └─ filters: [{[1, 1], [3, 3], [20001, 20001], [NULL, ∞)}]\n" + @@ -840,13 +862,13 @@ from " │ ├─ tableId: 3\n" + " │ └─ Limit(1)\n" + " │ └─ Project\n" + - " │ ├─ columns: [orders2.o_c_id:1, orders2.o_w_id:2!null, orders2.o_d_id:3!null, countdistinct([orders2.o_id]):0!null->count(distinct o_id):0]\n" + + " │ ├─ columns: [orders2.o_c_id:1, orders2.o_w_id:2!null, orders2.o_d_id:3!null, count(distinct orders2.o_id):0!null->count(distinct o_id):0]\n" + " │ └─ Having\n" + " │ ├─ GreaterThan\n" + - " │ │ ├─ countdistinct([orders2.o_id]):0!null\n" + + " │ │ ├─ count(distinct orders2.o_id):0!null\n" + " │ │ └─ 1 (bigint)\n" + " │ └─ GroupBy\n" + - " │ ├─ select: COUNTDISTINCT([orders2.o_id]), orders2.o_c_id:3, orders2.o_w_id:2!null, orders2.o_d_id:1!null, orders2.o_id:0!null\n" + + " │ ├─ select: COUNT(DISTINCT orders2.o_id), orders2.o_c_id:3, orders2.o_w_id:2!null, orders2.o_d_id:1!null, orders2.o_id:0!null\n" + " │ ├─ group: orders2.o_c_id:3, orders2.o_d_id:1!null, orders2.o_w_id:2!null\n" + " │ └─ IndexedTableAccess(orders2)\n" + " │ ├─ index: [orders2.o_w_id,orders2.o_d_id,orders2.o_id]\n" + @@ -879,10 +901,10 @@ from " │ ├─ tableId: 3\n" + " │ └─ Limit(1)\n" + " │ └─ Project\n" + - " │ ├─ columns: [orders2.o_c_id, orders2.o_w_id, orders2.o_d_id, countdistinct([orders2.o_id]) as count(distinct o_id)]\n" + - " │ └─ Having((countdistinct([orders2.o_id]) > 1))\n" + + " │ ├─ columns: [orders2.o_c_id, orders2.o_w_id, orders2.o_d_id, count(distinct orders2.o_id) as `count(distinct o_id)`]\n" + + " │ └─ Having((count(distinct orders2.o_id) > 1))\n" + " │ └─ GroupBy\n" + - " │ ├─ select: COUNTDISTINCT([orders2.o_id]), orders2.o_c_id, orders2.o_w_id, orders2.o_d_id, orders2.o_id\n" + + " │ ├─ select: COUNT(DISTINCT orders2.o_id), orders2.o_c_id, orders2.o_w_id, orders2.o_d_id, orders2.o_id\n" + " │ ├─ group: orders2.o_c_id, orders2.o_d_id, orders2.o_w_id\n" + " │ └─ IndexedTableAccess(orders2)\n" + " │ ├─ index: [orders2.o_w_id,orders2.o_d_id,orders2.o_id]\n" + @@ -906,10 +928,10 @@ from " │ ├─ tableId: 3\n" + " │ └─ Limit(1)\n" + " │ └─ Project\n" + - " │ ├─ columns: [orders2.o_c_id, orders2.o_w_id, orders2.o_d_id, countdistinct([orders2.o_id]) as count(distinct o_id)]\n" + - " │ └─ Having((countdistinct([orders2.o_id]) > 1))\n" + + " │ ├─ columns: [orders2.o_c_id, orders2.o_w_id, orders2.o_d_id, count(distinct orders2.o_id) as `count(distinct o_id)`]\n" + + " │ └─ Having((count(distinct orders2.o_id) > 1))\n" + " │ └─ GroupBy\n" + - " │ ├─ select: COUNTDISTINCT([orders2.o_id]), orders2.o_c_id, orders2.o_w_id, orders2.o_d_id, orders2.o_id\n" + + " │ ├─ select: COUNT(DISTINCT orders2.o_id), orders2.o_c_id, orders2.o_w_id, orders2.o_d_id, orders2.o_id\n" + " │ ├─ group: orders2.o_c_id, orders2.o_d_id, orders2.o_w_id\n" + " │ └─ IndexedTableAccess(orders2)\n" + " │ ├─ index: [orders2.o_w_id,orders2.o_d_id,orders2.o_id]\n" + diff --git a/enginetest/queries/tpch_plans.go b/enginetest/queries/tpch_plans.go index 767193050f..edfc7a0007 100644 --- a/enginetest/queries/tpch_plans.go +++ b/enginetest/queries/tpch_plans.go @@ -63,7 +63,7 @@ order by " ├─ select: AVG(lineitem.l_discount), AVG(lineitem.l_extendedprice), AVG(lineitem.l_quantity), COUNT(1), SUM(((lineitem.l_extendedprice * (1 - lineitem.l_discount)) * (1 + lineitem.l_tax))), SUM((lineitem.l_extendedprice * (1 - lineitem.l_discount))), SUM(lineitem.l_extendedprice), SUM(lineitem.l_quantity), lineitem.l_returnflag, lineitem.l_linestatus\n" + " ├─ group: lineitem.l_returnflag, lineitem.l_linestatus\n" + " └─ Filter\n" + - " ├─ (lineitem.l_shipdate <= 1998-09-02 00:00:00 +0000 UTC)\n" + + " ├─ (lineitem.l_shipdate <= '1998-09-02 00:00:00')\n" + " └─ Table\n" + " ├─ name: lineitem\n" + " └─ columns: [l_quantity l_extendedprice l_discount l_tax l_returnflag l_linestatus l_shipdate]\n" + @@ -75,7 +75,7 @@ order by " ├─ select: AVG(lineitem.l_discount), AVG(lineitem.l_extendedprice), AVG(lineitem.l_quantity), COUNT(1), SUM(((lineitem.l_extendedprice * (1 - lineitem.l_discount)) * (1 + lineitem.l_tax))), SUM((lineitem.l_extendedprice * (1 - lineitem.l_discount))), SUM(lineitem.l_extendedprice), SUM(lineitem.l_quantity), lineitem.l_returnflag, lineitem.l_linestatus\n" + " ├─ group: lineitem.l_returnflag, lineitem.l_linestatus\n" + " └─ Filter\n" + - " ├─ (lineitem.l_shipdate <= 1998-09-02 00:00:00 +0000 UTC)\n" + + " ├─ (lineitem.l_shipdate <= '1998-09-02 00:00:00')\n" + " └─ Table\n" + " ├─ name: lineitem\n" + " └─ columns: [l_quantity l_extendedprice l_discount l_tax l_returnflag l_linestatus l_shipdate]\n" + @@ -243,7 +243,36 @@ order by " ├─ columns: [supplier.s_acctbal, supplier.s_name, nation.n_name, part.p_partkey, part.p_mfgr, supplier.s_address, supplier.s_phone, supplier.s_comment]\n" + " └─ Sort(supplier.s_acctbal DESC, nation.n_name ASC, supplier.s_name ASC, part.p_partkey ASC)\n" + " └─ Filter\n" + - " ├─ (partsupp.ps_supplycost = Subquery(select min(ps_supplycost) from partsupp, supplier, nation, region where p_partkey = ps_partkey and s_suppkey = ps_suppkey and s_nationkey = n_nationkey and n_regionkey = r_regionkey and r_name = 'EUROPE'))\n" + + " ├─ (partsupp.ps_supplycost = Subquery\n" + + " │ ├─ cacheable: false\n" + + " │ └─ Project\n" + + " │ ├─ columns: [min(partsupp.ps_supplycost) as `min(ps_supplycost)`]\n" + + " │ └─ GroupBy\n" + + " │ ├─ select: MIN(partsupp.ps_supplycost)\n" + + " │ ├─ group: \n" + + " │ └─ LookupJoin (estimated cost=2833.667 rows=850)\n" + + " │ ├─ LookupJoin (estimated cost=2834.358 rows=850)\n" + + " │ │ ├─ LookupJoin (estimated cost=2834.358 rows=850)\n" + + " │ │ │ ├─ Filter\n" + + " │ │ │ │ ├─ (part.p_partkey = partsupp.ps_partkey)\n" + + " │ │ │ │ └─ Table\n" + + " │ │ │ │ ├─ name: partsupp\n" + + " │ │ │ │ └─ columns: [ps_partkey ps_suppkey ps_supplycost]\n" + + " │ │ │ └─ IndexedTableAccess(supplier)\n" + + " │ │ │ ├─ index: [supplier.S_SUPPKEY]\n" + + " │ │ │ ├─ columns: [s_suppkey s_nationkey]\n" + + " │ │ │ └─ keys: partsupp.ps_suppkey\n" + + " │ │ └─ IndexedTableAccess(nation)\n" + + " │ │ ├─ index: [nation.N_NATIONKEY]\n" + + " │ │ ├─ columns: [n_nationkey n_regionkey]\n" + + " │ │ └─ keys: supplier.s_nationkey\n" + + " │ └─ Filter\n" + + " │ ├─ (region.r_name = 'EUROPE')\n" + + " │ └─ IndexedTableAccess(region)\n" + + " │ ├─ index: [region.R_REGIONKEY]\n" + + " │ ├─ columns: [r_regionkey r_name]\n" + + " │ └─ keys: nation.n_regionkey\n" + + " │ )\n" + " └─ LookupJoin (estimated cost=3333.726 rows=1000)\n" + " ├─ LookupJoin (estimated cost=3334.539 rows=1000)\n" + " │ ├─ LookupJoin (estimated cost=3334.539 rows=1000)\n" + @@ -273,7 +302,36 @@ order by " ├─ columns: [supplier.s_acctbal, supplier.s_name, nation.n_name, part.p_partkey, part.p_mfgr, supplier.s_address, supplier.s_phone, supplier.s_comment]\n" + " └─ Sort(supplier.s_acctbal DESC, nation.n_name ASC, supplier.s_name ASC, part.p_partkey ASC)\n" + " └─ Filter\n" + - " ├─ (partsupp.ps_supplycost = Subquery(select min(ps_supplycost) from partsupp, supplier, nation, region where p_partkey = ps_partkey and s_suppkey = ps_suppkey and s_nationkey = n_nationkey and n_regionkey = r_regionkey and r_name = 'EUROPE'))\n" + + " ├─ (partsupp.ps_supplycost = Subquery\n" + + " │ ├─ cacheable: false\n" + + " │ └─ Project\n" + + " │ ├─ columns: [min(partsupp.ps_supplycost) as `min(ps_supplycost)`]\n" + + " │ └─ GroupBy\n" + + " │ ├─ select: MIN(partsupp.ps_supplycost)\n" + + " │ ├─ group: \n" + + " │ └─ LookupJoin (estimated cost=2833.667 rows=850)\n" + + " │ ├─ LookupJoin (estimated cost=2834.358 rows=850)\n" + + " │ │ ├─ LookupJoin (estimated cost=2834.358 rows=850)\n" + + " │ │ │ ├─ Filter\n" + + " │ │ │ │ ├─ (part.p_partkey = partsupp.ps_partkey)\n" + + " │ │ │ │ └─ Table\n" + + " │ │ │ │ ├─ name: partsupp\n" + + " │ │ │ │ └─ columns: [ps_partkey ps_suppkey ps_supplycost]\n" + + " │ │ │ └─ IndexedTableAccess(supplier)\n" + + " │ │ │ ├─ index: [supplier.S_SUPPKEY]\n" + + " │ │ │ ├─ columns: [s_suppkey s_nationkey]\n" + + " │ │ │ └─ keys: partsupp.ps_suppkey\n" + + " │ │ └─ IndexedTableAccess(nation)\n" + + " │ │ ├─ index: [nation.N_NATIONKEY]\n" + + " │ │ ├─ columns: [n_nationkey n_regionkey]\n" + + " │ │ └─ keys: supplier.s_nationkey\n" + + " │ └─ Filter\n" + + " │ ├─ (region.r_name = 'EUROPE')\n" + + " │ └─ IndexedTableAccess(region)\n" + + " │ ├─ index: [region.R_REGIONKEY]\n" + + " │ ├─ columns: [r_regionkey r_name]\n" + + " │ └─ keys: nation.n_regionkey\n" + + " │ )\n" + " └─ LookupJoin (estimated cost=3333.726 rows=1000) (actual rows=0 loops=1)\n" + " ├─ LookupJoin (estimated cost=3334.539 rows=1000) (actual rows=0 loops=1)\n" + " │ ├─ LookupJoin (estimated cost=3334.539 rows=1000) (actual rows=0 loops=1)\n" + @@ -515,7 +573,7 @@ order by " └─ MergeJoin (estimated cost=1725.500 rows=850)\n" + " ├─ cmp: (orders.o_orderkey = lineitem.l_orderkey)\n" + " ├─ Filter\n" + - " │ ├─ ((orders.o_orderdate >= '1993-07-01') AND (orders.o_orderdate < 1993-10-01 00:00:00 +0000 UTC))\n" + + " │ ├─ ((orders.o_orderdate >= '1993-07-01') AND (orders.o_orderdate < '1993-10-01 00:00:00'))\n" + " │ └─ IndexedTableAccess(orders)\n" + " │ ├─ index: [orders.O_ORDERKEY]\n" + " │ └─ filters: [{[NULL, ∞)}]\n" + @@ -540,7 +598,7 @@ order by " └─ MergeJoin (estimated cost=1725.500 rows=850) (actual rows=0 loops=1)\n" + " ├─ cmp: (orders.o_orderkey = lineitem.l_orderkey)\n" + " ├─ Filter\n" + - " │ ├─ ((orders.o_orderdate >= '1993-07-01') AND (orders.o_orderdate < 1993-10-01 00:00:00 +0000 UTC))\n" + + " │ ├─ ((orders.o_orderdate >= '1993-07-01') AND (orders.o_orderdate < '1993-10-01 00:00:00'))\n" + " │ └─ IndexedTableAccess(orders)\n" + " │ ├─ index: [orders.O_ORDERKEY]\n" + " │ └─ filters: [{[NULL, ∞)}]\n" + @@ -674,7 +732,7 @@ order by " │ │ ├─ LookupJoin (estimated cost=2834.358 rows=850)\n" + " │ │ │ ├─ LookupJoin (estimated cost=2834.358 rows=850)\n" + " │ │ │ │ ├─ Filter\n" + - " │ │ │ │ │ ├─ ((orders.o_orderdate >= '1994-01-01') AND (orders.o_orderdate < 1995-01-01 00:00:00 +0000 UTC))\n" + + " │ │ │ │ │ ├─ ((orders.o_orderdate >= '1994-01-01') AND (orders.o_orderdate < '1995-01-01 00:00:00'))\n" + " │ │ │ │ │ └─ Table\n" + " │ │ │ │ │ ├─ name: orders\n" + " │ │ │ │ │ └─ columns: [o_orderkey o_custkey o_orderdate]\n" + @@ -716,7 +774,7 @@ order by " │ │ ├─ LookupJoin (estimated cost=2834.358 rows=850) (actual rows=0 loops=1)\n" + " │ │ │ ├─ LookupJoin (estimated cost=2834.358 rows=850) (actual rows=0 loops=1)\n" + " │ │ │ │ ├─ Filter\n" + - " │ │ │ │ │ ├─ ((orders.o_orderdate >= '1994-01-01') AND (orders.o_orderdate < 1995-01-01 00:00:00 +0000 UTC))\n" + + " │ │ │ │ │ ├─ ((orders.o_orderdate >= '1994-01-01') AND (orders.o_orderdate < '1995-01-01 00:00:00'))\n" + " │ │ │ │ │ └─ Table\n" + " │ │ │ │ │ ├─ name: orders\n" + " │ │ │ │ │ └─ columns: [o_orderkey o_custkey o_orderdate]\n" + @@ -792,7 +850,7 @@ where " ├─ select: SUM((lineitem.l_extendedprice * lineitem.l_discount))\n" + " ├─ group: \n" + " └─ Filter\n" + - " ├─ (((((lineitem.l_shipdate >= '1994-01-01') AND (lineitem.l_shipdate < 1995-01-01 00:00:00 +0000 UTC)) AND (lineitem.l_discount >= 0.05)) AND (lineitem.l_discount <= 0.07)) AND (lineitem.l_quantity < 24))\n" + + " ├─ (((((lineitem.l_shipdate >= '1994-01-01') AND (lineitem.l_shipdate < '1995-01-01 00:00:00')) AND (lineitem.l_discount >= 0.05)) AND (lineitem.l_discount <= 0.07)) AND (lineitem.l_quantity < 24))\n" + " └─ Table\n" + " ├─ name: lineitem\n" + " └─ columns: [l_quantity l_extendedprice l_discount l_shipdate]\n" + @@ -803,7 +861,7 @@ where " ├─ select: SUM((lineitem.l_extendedprice * lineitem.l_discount))\n" + " ├─ group: \n" + " └─ Filter\n" + - " ├─ (((((lineitem.l_shipdate >= '1994-01-01') AND (lineitem.l_shipdate < 1995-01-01 00:00:00 +0000 UTC)) AND (lineitem.l_discount >= 0.05)) AND (lineitem.l_discount <= 0.07)) AND (lineitem.l_quantity < 24))\n" + + " ├─ (((((lineitem.l_shipdate >= '1994-01-01') AND (lineitem.l_shipdate < '1995-01-01 00:00:00')) AND (lineitem.l_discount >= 0.05)) AND (lineitem.l_discount <= 0.07)) AND (lineitem.l_quantity < 24))\n" + " └─ Table\n" + " ├─ name: lineitem\n" + " └─ columns: [l_quantity l_extendedprice l_discount l_shipdate]\n" + @@ -1667,7 +1725,7 @@ order by " │ ├─ MergeJoin (estimated cost=1725.500 rows=850)\n" + " │ │ ├─ cmp: (orders.o_orderkey = lineitem.l_orderkey)\n" + " │ │ ├─ Filter\n" + - " │ │ │ ├─ ((orders.o_orderdate >= '1993-10-01') AND (orders.o_orderdate < 1994-01-01 00:00:00 +0000 UTC))\n" + + " │ │ │ ├─ ((orders.o_orderdate >= '1993-10-01') AND (orders.o_orderdate < '1994-01-01 00:00:00'))\n" + " │ │ │ └─ IndexedTableAccess(orders)\n" + " │ │ │ ├─ index: [orders.O_ORDERKEY]\n" + " │ │ │ ├─ filters: [{[NULL, ∞)}]\n" + @@ -1700,7 +1758,7 @@ order by " │ ├─ MergeJoin (estimated cost=1725.500 rows=850) (actual rows=0 loops=1)\n" + " │ │ ├─ cmp: (orders.o_orderkey = lineitem.l_orderkey)\n" + " │ │ ├─ Filter\n" + - " │ │ │ ├─ ((orders.o_orderdate >= '1993-10-01') AND (orders.o_orderdate < 1994-01-01 00:00:00 +0000 UTC))\n" + + " │ │ │ ├─ ((orders.o_orderdate >= '1993-10-01') AND (orders.o_orderdate < '1994-01-01 00:00:00'))\n" + " │ │ │ └─ IndexedTableAccess(orders)\n" + " │ │ │ ├─ index: [orders.O_ORDERKEY]\n" + " │ │ │ ├─ filters: [{[NULL, ∞)}]\n" + @@ -1825,11 +1883,11 @@ order by " └─ columns: [n_nationkey n_name n_regionkey n_comment]\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [partsupp.ps_partkey, sum((partsupp.ps_supplycost * partsupp.ps_availqty)) as value]\n" + + " ├─ columns: [partsupp.ps_partkey, sum((partsupp.ps_supplycost * partsupp.ps_availqty)) as `value`]\n" + " └─ Sort(value DESC)\n" + - " └─ Having((sum((partsupp.ps_supplycost * partsupp.ps_availqty)) > Subquery(select sum(ps_supplycost * ps_availqty) * 0.0001000000 from partsupp, supplier, nation where ps_suppkey = s_suppkey and s_nationkey = n_nationkey and n_name = 'GERMANY')))\n" + + " └─ Having((sum((partsupp.ps_supplycost * partsupp.ps_availqty)) > (select sum(ps_supplycost * ps_availqty) * 0.0001000000 from partsupp, supplier, nation where ps_suppkey = s_suppkey and s_nationkey = n_nationkey and n_name = 'GERMANY')))\n" + " └─ Project\n" + - " ├─ columns: [sum((partsupp.ps_supplycost * partsupp.ps_availqty)), partsupp.ps_partkey, partsupp.PS_SUPPLYCOST, partsupp.PS_AVAILQTY, sum((partsupp.ps_supplycost * partsupp.ps_availqty)) as value]\n" + + " ├─ columns: [sum((partsupp.ps_supplycost * partsupp.ps_availqty)), partsupp.ps_partkey, partsupp.PS_SUPPLYCOST, partsupp.PS_AVAILQTY, sum((partsupp.ps_supplycost * partsupp.ps_availqty)) as `value`]\n" + " └─ GroupBy\n" + " ├─ select: SUM((partsupp.ps_supplycost * partsupp.ps_availqty)), partsupp.ps_partkey, partsupp.PS_SUPPLYCOST, partsupp.PS_AVAILQTY\n" + " ├─ group: partsupp.ps_partkey\n" + @@ -1847,11 +1905,11 @@ order by " └─ keys: supplier.s_nationkey\n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [partsupp.ps_partkey, sum((partsupp.ps_supplycost * partsupp.ps_availqty)) as value]\n" + + " ├─ columns: [partsupp.ps_partkey, sum((partsupp.ps_supplycost * partsupp.ps_availqty)) as `value`]\n" + " └─ Sort(value DESC)\n" + - " └─ Having((sum((partsupp.ps_supplycost * partsupp.ps_availqty)) > Subquery(select sum(ps_supplycost * ps_availqty) * 0.0001000000 from partsupp, supplier, nation where ps_suppkey = s_suppkey and s_nationkey = n_nationkey and n_name = 'GERMANY')))\n" + + " └─ Having((sum((partsupp.ps_supplycost * partsupp.ps_availqty)) > (select sum(ps_supplycost * ps_availqty) * 0.0001000000 from partsupp, supplier, nation where ps_suppkey = s_suppkey and s_nationkey = n_nationkey and n_name = 'GERMANY')))\n" + " └─ Project\n" + - " ├─ columns: [sum((partsupp.ps_supplycost * partsupp.ps_availqty)), partsupp.ps_partkey, partsupp.PS_SUPPLYCOST, partsupp.PS_AVAILQTY, sum((partsupp.ps_supplycost * partsupp.ps_availqty)) as value]\n" + + " ├─ columns: [sum((partsupp.ps_supplycost * partsupp.ps_availqty)), partsupp.ps_partkey, partsupp.PS_SUPPLYCOST, partsupp.PS_AVAILQTY, sum((partsupp.ps_supplycost * partsupp.ps_availqty)) as `value`]\n" + " └─ GroupBy\n" + " ├─ select: SUM((partsupp.ps_supplycost * partsupp.ps_availqty)), partsupp.ps_partkey, partsupp.PS_SUPPLYCOST, partsupp.PS_AVAILQTY\n" + " ├─ group: partsupp.ps_partkey\n" + @@ -1976,7 +2034,7 @@ order by " │ ├─ filters: [{[NULL, ∞)}]\n" + " │ └─ columns: [o_orderkey o_orderpriority]\n" + " └─ Filter\n" + - " ├─ (((((lineitem.l_shipmode HASH IN ('MAIL', 'SHIP')) AND (lineitem.l_commitdate < lineitem.l_receiptdate)) AND (lineitem.l_shipdate < lineitem.l_commitdate)) AND (lineitem.l_receiptdate >= '1994-01-01')) AND (lineitem.l_receiptdate < 1995-01-01 00:00:00 +0000 UTC))\n" + + " ├─ (((((lineitem.l_shipmode HASH IN ('MAIL', 'SHIP')) AND (lineitem.l_commitdate < lineitem.l_receiptdate)) AND (lineitem.l_shipdate < lineitem.l_commitdate)) AND (lineitem.l_receiptdate >= '1994-01-01')) AND (lineitem.l_receiptdate < '1995-01-01 00:00:00'))\n" + " └─ IndexedTableAccess(lineitem)\n" + " ├─ index: [lineitem.L_ORDERKEY,lineitem.L_LINENUMBER]\n" + " ├─ filters: [{[NULL, ∞), [NULL, ∞)}]\n" + @@ -1995,7 +2053,7 @@ order by " │ ├─ filters: [{[NULL, ∞)}]\n" + " │ └─ columns: [o_orderkey o_orderpriority]\n" + " └─ Filter\n" + - " ├─ (((((lineitem.l_shipmode HASH IN ('MAIL', 'SHIP')) AND (lineitem.l_commitdate < lineitem.l_receiptdate)) AND (lineitem.l_shipdate < lineitem.l_commitdate)) AND (lineitem.l_receiptdate >= '1994-01-01')) AND (lineitem.l_receiptdate < 1995-01-01 00:00:00 +0000 UTC))\n" + + " ├─ (((((lineitem.l_shipmode HASH IN ('MAIL', 'SHIP')) AND (lineitem.l_commitdate < lineitem.l_receiptdate)) AND (lineitem.l_shipdate < lineitem.l_commitdate)) AND (lineitem.l_receiptdate >= '1994-01-01')) AND (lineitem.l_receiptdate < '1995-01-01 00:00:00'))\n" + " └─ IndexedTableAccess(lineitem)\n" + " ├─ index: [lineitem.L_ORDERKEY,lineitem.L_LINENUMBER]\n" + " ├─ filters: [{[NULL, ∞), [NULL, ∞)}]\n" + @@ -2079,7 +2137,7 @@ order by " ├─ colSet: (19,20)\n" + " ├─ tableId: 3\n" + " └─ Project\n" + - " ├─ columns: [customer.c_custkey, count(orders.o_orderkey) as count(o_orderkey)]\n" + + " ├─ columns: [customer.c_custkey, count(orders.o_orderkey) as `count(o_orderkey)`]\n" + " └─ GroupBy\n" + " ├─ select: COUNT(orders.o_orderkey), customer.c_custkey\n" + " ├─ group: customer.c_custkey\n" + @@ -2108,7 +2166,7 @@ order by " ├─ colSet: (19,20)\n" + " ├─ tableId: 3\n" + " └─ Project\n" + - " ├─ columns: [customer.c_custkey, count(orders.o_orderkey) as count(o_orderkey)]\n" + + " ├─ columns: [customer.c_custkey, count(orders.o_orderkey) as `count(o_orderkey)`]\n" + " └─ GroupBy\n" + " ├─ select: COUNT(orders.o_orderkey), customer.c_custkey\n" + " ├─ group: customer.c_custkey\n" + @@ -2172,7 +2230,7 @@ where " ├─ group: \n" + " └─ LookupJoin (estimated cost=2834.358 rows=850)\n" + " ├─ Filter\n" + - " │ ├─ ((lineitem.l_shipdate >= '1995-09-01') AND (lineitem.l_shipdate < 1995-10-01 00:00:00 +0000 UTC))\n" + + " │ ├─ ((lineitem.l_shipdate >= '1995-09-01') AND (lineitem.l_shipdate < '1995-10-01 00:00:00'))\n" + " │ └─ Table\n" + " │ ├─ name: lineitem\n" + " │ └─ columns: [l_partkey l_extendedprice l_discount l_shipdate]\n" + @@ -2188,7 +2246,7 @@ where " ├─ group: \n" + " └─ LookupJoin (estimated cost=2834.358 rows=850) (actual rows=0 loops=1)\n" + " ├─ Filter\n" + - " │ ├─ ((lineitem.l_shipdate >= '1995-09-01') AND (lineitem.l_shipdate < 1995-10-01 00:00:00 +0000 UTC))\n" + + " │ ├─ ((lineitem.l_shipdate >= '1995-09-01') AND (lineitem.l_shipdate < '1995-10-01 00:00:00'))\n" + " │ └─ Table\n" + " │ ├─ name: lineitem\n" + " │ └─ columns: [l_partkey l_extendedprice l_discount l_shipdate]\n" + @@ -2309,7 +2367,31 @@ order by " ├─ columns: [supplier.s_suppkey, supplier.s_name, supplier.s_address, supplier.s_phone, revenue0.total_revenue]\n" + " └─ Sort(supplier.s_suppkey ASC)\n" + " └─ Filter\n" + - " ├─ (revenue0.total_revenue = Subquery(select max(total_revenue) from revenue0))\n" + + " ├─ (revenue0.total_revenue = Subquery\n" + + " │ ├─ cacheable: true\n" + + " │ └─ Project\n" + + " │ ├─ columns: [max(revenue0.total_revenue) as `max(total_revenue)`]\n" + + " │ └─ GroupBy\n" + + " │ ├─ select: MAX(revenue0.total_revenue)\n" + + " │ ├─ group: \n" + + " │ └─ SubqueryAlias\n" + + " │ ├─ name: revenue0\n" + + " │ ├─ outerVisibility: true\n" + + " │ ├─ isLateral: false\n" + + " │ ├─ cacheable: true\n" + + " │ ├─ colSet: (29,30)\n" + + " │ ├─ tableId: 4\n" + + " │ └─ Project\n" + + " │ ├─ columns: [lineitem.l_suppkey, sum((lineitem.l_extendedprice * (1 - lineitem.l_discount))) as `sum(l_extendedprice * (1 - l_discount))`]\n" + + " │ └─ GroupBy\n" + + " │ ├─ select: SUM((lineitem.l_extendedprice * (1 - lineitem.l_discount))), lineitem.l_suppkey\n" + + " │ ├─ group: lineitem.l_suppkey\n" + + " │ └─ Filter\n" + + " │ ├─ ((lineitem.l_shipdate >= '1996-01-01') AND (lineitem.l_shipdate < '1996-04-01 00:00:00'))\n" + + " │ └─ Table\n" + + " │ ├─ name: lineitem\n" + + " │ └─ columns: [l_suppkey l_extendedprice l_discount l_shipdate]\n" + + " │ )\n" + " └─ LookupJoin (estimated cost=333.454 rows=100)\n" + " ├─ SubqueryAlias\n" + " │ ├─ name: revenue0\n" + @@ -2319,12 +2401,12 @@ order by " │ ├─ colSet: (27,28)\n" + " │ ├─ tableId: 3\n" + " │ └─ Project\n" + - " │ ├─ columns: [lineitem.l_suppkey, sum((lineitem.l_extendedprice * (1 - lineitem.l_discount))) as sum(l_extendedprice * (1 - l_discount))]\n" + + " │ ├─ columns: [lineitem.l_suppkey, sum((lineitem.l_extendedprice * (1 - lineitem.l_discount))) as `sum(l_extendedprice * (1 - l_discount))`]\n" + " │ └─ GroupBy\n" + " │ ├─ select: SUM((lineitem.l_extendedprice * (1 - lineitem.l_discount))), lineitem.l_suppkey\n" + " │ ├─ group: lineitem.l_suppkey\n" + " │ └─ Filter\n" + - " │ ├─ ((lineitem.l_shipdate >= '1996-01-01') AND (lineitem.l_shipdate < 1996-04-01 00:00:00 +0000 UTC))\n" + + " │ ├─ ((lineitem.l_shipdate >= '1996-01-01') AND (lineitem.l_shipdate < '1996-04-01 00:00:00'))\n" + " │ └─ Table\n" + " │ ├─ name: lineitem\n" + " │ └─ columns: [l_suppkey l_extendedprice l_discount l_shipdate]\n" + @@ -2336,7 +2418,31 @@ order by " ├─ columns: [supplier.s_suppkey, supplier.s_name, supplier.s_address, supplier.s_phone, revenue0.total_revenue]\n" + " └─ Sort(supplier.s_suppkey ASC)\n" + " └─ Filter\n" + - " ├─ (revenue0.total_revenue = Subquery(select max(total_revenue) from revenue0))\n" + + " ├─ (revenue0.total_revenue = Subquery\n" + + " │ ├─ cacheable: true\n" + + " │ └─ Project\n" + + " │ ├─ columns: [max(revenue0.total_revenue) as `max(total_revenue)`]\n" + + " │ └─ GroupBy\n" + + " │ ├─ select: MAX(revenue0.total_revenue)\n" + + " │ ├─ group: \n" + + " │ └─ SubqueryAlias\n" + + " │ ├─ name: revenue0\n" + + " │ ├─ outerVisibility: true\n" + + " │ ├─ isLateral: false\n" + + " │ ├─ cacheable: true\n" + + " │ ├─ colSet: (29,30)\n" + + " │ ├─ tableId: 4\n" + + " │ └─ Project\n" + + " │ ├─ columns: [lineitem.l_suppkey, sum((lineitem.l_extendedprice * (1 - lineitem.l_discount))) as `sum(l_extendedprice * (1 - l_discount))`]\n" + + " │ └─ GroupBy\n" + + " │ ├─ select: SUM((lineitem.l_extendedprice * (1 - lineitem.l_discount))), lineitem.l_suppkey\n" + + " │ ├─ group: lineitem.l_suppkey\n" + + " │ └─ Filter\n" + + " │ ├─ ((lineitem.l_shipdate >= '1996-01-01') AND (lineitem.l_shipdate < '1996-04-01 00:00:00'))\n" + + " │ └─ Table\n" + + " │ ├─ name: lineitem\n" + + " │ └─ columns: [l_suppkey l_extendedprice l_discount l_shipdate]\n" + + " │ )\n" + " └─ LookupJoin (estimated cost=333.454 rows=100) (actual rows=0 loops=1)\n" + " ├─ SubqueryAlias\n" + " │ ├─ name: revenue0\n" + @@ -2346,12 +2452,12 @@ order by " │ ├─ colSet: (27,28)\n" + " │ ├─ tableId: 3\n" + " │ └─ Project\n" + - " │ ├─ columns: [lineitem.l_suppkey, sum((lineitem.l_extendedprice * (1 - lineitem.l_discount))) as sum(l_extendedprice * (1 - l_discount))]\n" + + " │ ├─ columns: [lineitem.l_suppkey, sum((lineitem.l_extendedprice * (1 - lineitem.l_discount))) as `sum(l_extendedprice * (1 - l_discount))`]\n" + " │ └─ GroupBy\n" + " │ ├─ select: SUM((lineitem.l_extendedprice * (1 - lineitem.l_discount))), lineitem.l_suppkey\n" + " │ ├─ group: lineitem.l_suppkey\n" + " │ └─ Filter\n" + - " │ ├─ ((lineitem.l_shipdate >= '1996-01-01') AND (lineitem.l_shipdate < 1996-04-01 00:00:00 +0000 UTC))\n" + + " │ ├─ ((lineitem.l_shipdate >= '1996-01-01') AND (lineitem.l_shipdate < '1996-04-01 00:00:00'))\n" + " │ └─ Table\n" + " │ ├─ name: lineitem\n" + " │ └─ columns: [l_suppkey l_extendedprice l_discount l_shipdate]\n" + @@ -2394,12 +2500,12 @@ order by p_type, p_size;`, ExpectedPlan: "Project\n" + - " ├─ columns: [part.p_brand:1!null, part.p_type:2!null, part.p_size:3!null, countdistinct([partsupp.ps_suppkey]):0!null->supplier_cnt:0]\n" + + " ├─ columns: [part.p_brand:1!null, part.p_type:2!null, part.p_size:3!null, count(distinct partsupp.ps_suppkey):0!null->supplier_cnt:0]\n" + " └─ Sort(supplier_cnt:4!null DESC nullsFirst, part.p_brand:1!null ASC nullsFirst, part.p_type:2!null ASC nullsFirst, part.p_size:3!null ASC nullsFirst)\n" + " └─ Project\n" + - " ├─ columns: [countdistinct([partsupp.ps_suppkey]):0!null, part.p_brand:1!null, part.p_type:2!null, part.p_size:3!null, countdistinct([partsupp.ps_suppkey]):0!null->supplier_cnt:0]\n" + + " ├─ columns: [count(distinct partsupp.ps_suppkey):0!null, part.p_brand:1!null, part.p_type:2!null, part.p_size:3!null, count(distinct partsupp.ps_suppkey):0!null->supplier_cnt:0]\n" + " └─ GroupBy\n" + - " ├─ select: COUNTDISTINCT([partsupp.ps_suppkey]), part.p_brand:8!null, part.p_type:9!null, part.p_size:10!null\n" + + " ├─ select: COUNT(DISTINCT partsupp.ps_suppkey), part.p_brand:8!null, part.p_type:9!null, part.p_size:10!null\n" + " ├─ group: part.p_brand:8!null, part.p_type:9!null, part.p_size:10!null\n" + " └─ Project\n" + " ├─ columns: [partsupp.PS_PARTKEY:0!null, partsupp.PS_SUPPKEY:1!null, partsupp.PS_AVAILQTY:2!null, partsupp.PS_SUPPLYCOST:3!null, partsupp.PS_COMMENT:4!null, part.P_PARTKEY:5!null, part.P_NAME:6!null, part.P_MFGR:7!null, part.P_BRAND:8!null, part.P_TYPE:9!null, part.P_SIZE:10!null, part.P_CONTAINER:11!null, part.P_RETAILPRICE:12!null, part.P_COMMENT:13!null]\n" + @@ -2457,12 +2563,12 @@ order by " └─ columns: [s_suppkey s_name s_address s_nationkey s_phone s_acctbal s_comment]\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [part.p_brand, part.p_type, part.p_size, countdistinct([partsupp.ps_suppkey]) as supplier_cnt]\n" + + " ├─ columns: [part.p_brand, part.p_type, part.p_size, count(distinct partsupp.ps_suppkey) as supplier_cnt]\n" + " └─ Sort(supplier_cnt DESC, part.p_brand ASC, part.p_type ASC, part.p_size ASC)\n" + " └─ Project\n" + - " ├─ columns: [countdistinct([partsupp.ps_suppkey]), part.p_brand, part.p_type, part.p_size, countdistinct([partsupp.ps_suppkey]) as supplier_cnt]\n" + + " ├─ columns: [count(distinct partsupp.ps_suppkey), part.p_brand, part.p_type, part.p_size, count(distinct partsupp.ps_suppkey) as supplier_cnt]\n" + " └─ GroupBy\n" + - " ├─ select: COUNTDISTINCT([partsupp.ps_suppkey]), part.p_brand, part.p_type, part.p_size\n" + + " ├─ select: COUNT(DISTINCT partsupp.ps_suppkey), part.p_brand, part.p_type, part.p_size\n" + " ├─ group: part.p_brand, part.p_type, part.p_size\n" + " └─ Project\n" + " ├─ columns: [partsupp.PS_PARTKEY, partsupp.PS_SUPPKEY, partsupp.PS_AVAILQTY, partsupp.PS_SUPPLYCOST, partsupp.PS_COMMENT, part.P_PARTKEY, part.P_NAME, part.P_MFGR, part.P_BRAND, part.P_TYPE, part.P_SIZE, part.P_CONTAINER, part.P_RETAILPRICE, part.P_COMMENT]\n" + @@ -2488,12 +2594,12 @@ order by " └─ keys: partsupp.ps_suppkey\n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [part.p_brand, part.p_type, part.p_size, countdistinct([partsupp.ps_suppkey]) as supplier_cnt]\n" + + " ├─ columns: [part.p_brand, part.p_type, part.p_size, count(distinct partsupp.ps_suppkey) as supplier_cnt]\n" + " └─ Sort(supplier_cnt DESC, part.p_brand ASC, part.p_type ASC, part.p_size ASC)\n" + " └─ Project\n" + - " ├─ columns: [countdistinct([partsupp.ps_suppkey]), part.p_brand, part.p_type, part.p_size, countdistinct([partsupp.ps_suppkey]) as supplier_cnt]\n" + + " ├─ columns: [count(distinct partsupp.ps_suppkey), part.p_brand, part.p_type, part.p_size, count(distinct partsupp.ps_suppkey) as supplier_cnt]\n" + " └─ GroupBy\n" + - " ├─ select: COUNTDISTINCT([partsupp.ps_suppkey]), part.p_brand, part.p_type, part.p_size\n" + + " ├─ select: COUNT(DISTINCT partsupp.ps_suppkey), part.p_brand, part.p_type, part.p_size\n" + " ├─ group: part.p_brand, part.p_type, part.p_size\n" + " └─ Project\n" + " ├─ columns: [partsupp.PS_PARTKEY, partsupp.PS_SUPPKEY, partsupp.PS_AVAILQTY, partsupp.PS_SUPPLYCOST, partsupp.PS_COMMENT, part.P_PARTKEY, part.P_NAME, part.P_MFGR, part.P_BRAND, part.P_TYPE, part.P_SIZE, part.P_CONTAINER, part.P_RETAILPRICE, part.P_COMMENT]\n" + @@ -2592,7 +2698,19 @@ where " ├─ select: SUM(lineitem.l_extendedprice)\n" + " ├─ group: \n" + " └─ Filter\n" + - " ├─ (lineitem.l_quantity < Subquery(select 0.2 * avg(l_quantity) from lineitem where l_partkey = p_partkey))\n" + + " ├─ (lineitem.l_quantity < Subquery\n" + + " │ ├─ cacheable: false\n" + + " │ └─ Project\n" + + " │ ├─ columns: [(0.2 * avg(lineitem.l_quantity)) as `0.2 * avg(l_quantity)`]\n" + + " │ └─ GroupBy\n" + + " │ ├─ select: AVG(lineitem.l_quantity)\n" + + " │ ├─ group: \n" + + " │ └─ Filter\n" + + " │ ├─ (lineitem.l_partkey = part.p_partkey)\n" + + " │ └─ Table\n" + + " │ ├─ name: lineitem\n" + + " │ └─ columns: [l_partkey l_quantity]\n" + + " │ )\n" + " └─ LookupJoin (estimated cost=3333.726 rows=1000)\n" + " ├─ Table\n" + " │ └─ name: lineitem\n" + @@ -2608,7 +2726,19 @@ where " ├─ select: SUM(lineitem.l_extendedprice)\n" + " ├─ group: \n" + " └─ Filter\n" + - " ├─ (lineitem.l_quantity < Subquery(select 0.2 * avg(l_quantity) from lineitem where l_partkey = p_partkey))\n" + + " ├─ (lineitem.l_quantity < Subquery\n" + + " │ ├─ cacheable: false\n" + + " │ └─ Project\n" + + " │ ├─ columns: [(0.2 * avg(lineitem.l_quantity)) as `0.2 * avg(l_quantity)`]\n" + + " │ └─ GroupBy\n" + + " │ ├─ select: AVG(lineitem.l_quantity)\n" + + " │ ├─ group: \n" + + " │ └─ Filter\n" + + " │ ├─ (lineitem.l_partkey = part.p_partkey)\n" + + " │ └─ Table\n" + + " │ ├─ name: lineitem\n" + + " │ └─ columns: [l_partkey l_quantity]\n" + + " │ )\n" + " └─ LookupJoin (estimated cost=3333.726 rows=1000) (actual rows=0 loops=1)\n" + " ├─ Table\n" + " │ └─ name: lineitem\n" + @@ -2701,7 +2831,7 @@ order by " └─ tableId: 4\n" + "", ExpectedEstimates: "Project\n" + - " ├─ columns: [customer.c_name, customer.c_custkey, orders.o_orderkey, orders.o_orderdate, orders.o_totalprice, sum(lineitem.l_quantity) as sum(l_quantity)]\n" + + " ├─ columns: [customer.c_name, customer.c_custkey, orders.o_orderkey, orders.o_orderdate, orders.o_totalprice, sum(lineitem.l_quantity) as `sum(l_quantity)`]\n" + " └─ Sort(orders.o_totalprice DESC, orders.o_orderdate ASC)\n" + " └─ GroupBy\n" + " ├─ select: SUM(lineitem.l_quantity), customer.c_name, customer.c_custkey, orders.o_orderkey, orders.o_orderdate, orders.o_totalprice\n" + @@ -2729,7 +2859,7 @@ order by " └─ name: lineitem\n" + "", ExpectedAnalysis: "Project\n" + - " ├─ columns: [customer.c_name, customer.c_custkey, orders.o_orderkey, orders.o_orderdate, orders.o_totalprice, sum(lineitem.l_quantity) as sum(l_quantity)]\n" + + " ├─ columns: [customer.c_name, customer.c_custkey, orders.o_orderkey, orders.o_orderdate, orders.o_totalprice, sum(lineitem.l_quantity) as `sum(l_quantity)`]\n" + " └─ Sort(orders.o_totalprice DESC, orders.o_orderdate ASC)\n" + " └─ GroupBy\n" + " ├─ select: SUM(lineitem.l_quantity), customer.c_name, customer.c_custkey, orders.o_orderkey, orders.o_orderdate, orders.o_totalprice\n" + @@ -3137,7 +3267,19 @@ order by " │ │ └─ Project\n" + " │ │ ├─ columns: [partsupp.ps_suppkey]\n" + " │ │ └─ Filter\n" + - " │ │ ├─ (partsupp.ps_availqty > Subquery(select 0.5 * sum(l_quantity) from lineitem where l_partkey = ps_partkey and l_suppkey = ps_suppkey and l_shipdate >= '1994-01-01' and l_shipdate < '1994-01-01' + interval '1' year))\n" + + " │ │ ├─ (partsupp.ps_availqty > Subquery\n" + + " │ │ │ ├─ cacheable: false\n" + + " │ │ │ └─ Project\n" + + " │ │ │ ├─ columns: [(0.5 * sum(lineitem.l_quantity)) as `0.5 * sum(l_quantity)`]\n" + + " │ │ │ └─ GroupBy\n" + + " │ │ │ ├─ select: SUM(lineitem.l_quantity)\n" + + " │ │ │ ├─ group: \n" + + " │ │ │ └─ Filter\n" + + " │ │ │ ├─ ((((lineitem.l_partkey = partsupp.ps_partkey) AND (lineitem.l_suppkey = partsupp.ps_suppkey)) AND (lineitem.l_shipdate >= '1994-01-01')) AND (lineitem.l_shipdate < '1995-01-01 00:00:00'))\n" + + " │ │ │ └─ Table\n" + + " │ │ │ ├─ name: lineitem\n" + + " │ │ │ └─ columns: [l_partkey l_suppkey l_quantity l_shipdate]\n" + + " │ │ │ )\n" + " │ │ └─ Project\n" + " │ │ ├─ columns: [partsupp.PS_PARTKEY, partsupp.PS_SUPPKEY, partsupp.PS_AVAILQTY, partsupp.PS_SUPPLYCOST, partsupp.PS_COMMENT]\n" + " │ │ └─ MergeJoin (estimated cost=1878.500 rows=1000)\n" + @@ -3172,7 +3314,19 @@ order by " │ │ └─ Project\n" + " │ │ ├─ columns: [partsupp.ps_suppkey]\n" + " │ │ └─ Filter\n" + - " │ │ ├─ (partsupp.ps_availqty > Subquery(select 0.5 * sum(l_quantity) from lineitem where l_partkey = ps_partkey and l_suppkey = ps_suppkey and l_shipdate >= '1994-01-01' and l_shipdate < '1994-01-01' + interval '1' year))\n" + + " │ │ ├─ (partsupp.ps_availqty > Subquery\n" + + " │ │ │ ├─ cacheable: false\n" + + " │ │ │ └─ Project\n" + + " │ │ │ ├─ columns: [(0.5 * sum(lineitem.l_quantity)) as `0.5 * sum(l_quantity)`]\n" + + " │ │ │ └─ GroupBy\n" + + " │ │ │ ├─ select: SUM(lineitem.l_quantity)\n" + + " │ │ │ ├─ group: \n" + + " │ │ │ └─ Filter\n" + + " │ │ │ ├─ ((((lineitem.l_partkey = partsupp.ps_partkey) AND (lineitem.l_suppkey = partsupp.ps_suppkey)) AND (lineitem.l_shipdate >= '1994-01-01')) AND (lineitem.l_shipdate < '1995-01-01 00:00:00'))\n" + + " │ │ │ └─ Table\n" + + " │ │ │ ├─ name: lineitem\n" + + " │ │ │ └─ columns: [l_partkey l_suppkey l_quantity l_shipdate]\n" + + " │ │ │ )\n" + " │ │ └─ Project\n" + " │ │ ├─ columns: [partsupp.PS_PARTKEY, partsupp.PS_SUPPKEY, partsupp.PS_AVAILQTY, partsupp.PS_SUPPLYCOST, partsupp.PS_COMMENT]\n" + " │ │ └─ MergeJoin (estimated cost=1878.500 rows=1000) (actual rows=0 loops=1)\n" + @@ -3550,7 +3704,19 @@ order by " └─ Project\n" + " ├─ columns: [SUBSTRING(customer.c_phone, 1, 2) as cntrycode, customer.c_acctbal]\n" + " └─ Filter\n" + - " ├─ (customer.c_acctbal > Subquery(select avg(c_acctbal) from customer where c_acctbal > 0.00 and substr(c_phone, 1, 2) in ('13', '31', '23', '29', '30', '18', '17')))\n" + + " ├─ (customer.c_acctbal > Subquery\n" + + " │ ├─ cacheable: true\n" + + " │ └─ Project\n" + + " │ ├─ columns: [avg(customer.c_acctbal) as `avg(c_acctbal)`]\n" + + " │ └─ GroupBy\n" + + " │ ├─ select: AVG(customer.c_acctbal)\n" + + " │ ├─ group: \n" + + " │ └─ Filter\n" + + " │ ├─ ((customer.c_acctbal > 0.00) AND (SUBSTRING(customer.c_phone, 1, 2) HASH IN ('13', '31', '23', '29', '30', '18', '17')))\n" + + " │ └─ Table\n" + + " │ ├─ name: customer\n" + + " │ └─ columns: [c_phone c_acctbal]\n" + + " │ )\n" + " └─ Project\n" + " ├─ columns: [customer.C_CUSTKEY, customer.C_NAME, customer.C_ADDRESS, customer.C_NATIONKEY, customer.C_PHONE, customer.C_ACCTBAL, customer.C_MKTSEGMENT, customer.C_COMMENT]\n" + " └─ Filter\n" + @@ -3586,7 +3752,19 @@ order by " └─ Project\n" + " ├─ columns: [SUBSTRING(customer.c_phone, 1, 2) as cntrycode, customer.c_acctbal]\n" + " └─ Filter\n" + - " ├─ (customer.c_acctbal > Subquery(select avg(c_acctbal) from customer where c_acctbal > 0.00 and substr(c_phone, 1, 2) in ('13', '31', '23', '29', '30', '18', '17')))\n" + + " ├─ (customer.c_acctbal > Subquery\n" + + " │ ├─ cacheable: true\n" + + " │ └─ Project\n" + + " │ ├─ columns: [avg(customer.c_acctbal) as `avg(c_acctbal)`]\n" + + " │ └─ GroupBy\n" + + " │ ├─ select: AVG(customer.c_acctbal)\n" + + " │ ├─ group: \n" + + " │ └─ Filter\n" + + " │ ├─ ((customer.c_acctbal > 0.00) AND (SUBSTRING(customer.c_phone, 1, 2) HASH IN ('13', '31', '23', '29', '30', '18', '17')))\n" + + " │ └─ Table\n" + + " │ ├─ name: customer\n" + + " │ └─ columns: [c_phone c_acctbal]\n" + + " │ )\n" + " └─ Project\n" + " ├─ columns: [customer.C_CUSTKEY, customer.C_NAME, customer.C_ADDRESS, customer.C_NATIONKEY, customer.C_PHONE, customer.C_ACCTBAL, customer.C_MKTSEGMENT, customer.C_COMMENT]\n" + " └─ Filter\n" + diff --git a/enginetest/queries/window_functions_queries.go b/enginetest/queries/window_functions_queries.go index 185c66eee3..bc50cd776b 100644 --- a/enginetest/queries/window_functions_queries.go +++ b/enginetest/queries/window_functions_queries.go @@ -24,6 +24,52 @@ import ( // WindowFunctionsScriptTests tests window function queries such as rank, dense_rank, percent_rank, // first_value, last_value, lead, lag, and the bitwise aggregate functions. var WindowFunctionsScriptTests = []ScriptTest{ + { + Name: "window aggregate over grouped aggregate", + Dialect: "mysql", + SetUpScript: []string{ + "SET @@sql_mode = ''", + "CREATE TABLE grouped_window_values (grp INT, ord INT, val INT)", + "INSERT INTO grouped_window_values VALUES (1, 1, 10), (1, 1, 5), (1, 2, 7), (2, 1, 3), (2, 2, 4)", + }, + Assertions: []ScriptTestAssertion{ + { + Query: "SELECT grp, ord, SUM(SUM(val)) OVER (PARTITION BY grp ORDER BY ord ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_sum FROM grouped_window_values GROUP BY grp, ord ORDER BY grp, ord", + Expected: []sql.Row{ + {1, 1, float64(15)}, + {1, 2, float64(22)}, + {2, 1, float64(3)}, + {2, 2, float64(7)}, + }, + }, + { + Query: "SELECT grp, ord, SUM(SUM(val)) OVER (PARTITION BY grp ORDER BY ord ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_sum, ROW_NUMBER() OVER (PARTITION BY grp ORDER BY ord) AS row_num FROM grouped_window_values GROUP BY grp, ord ORDER BY grp, ord", + Expected: []sql.Row{ + {1, 1, float64(15), int64(1)}, + {1, 2, float64(22), int64(2)}, + {2, 1, float64(3), int64(1)}, + {2, 2, float64(7), int64(2)}, + }, + }, + { + Query: "SELECT grp, ord, ROW_NUMBER() OVER (PARTITION BY grp ORDER BY ord) AS row_num, SUM(SUM(val)) OVER (PARTITION BY grp ORDER BY ord ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_sum FROM grouped_window_values GROUP BY grp, ord ORDER BY grp, ord", + Expected: []sql.Row{ + {1, 1, int64(1), float64(15)}, + {1, 2, int64(2), float64(22)}, + {2, 1, int64(1), float64(3)}, + {2, 2, int64(2), float64(7)}, + }, + }, + { + Query: "SELECT grp, ord, SUM(SUM(val)) OVER (PARTITION BY grp ORDER BY ord ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_sum FROM grouped_window_values GROUP BY grp, ord HAVING SUM(val) <> 3 ORDER BY grp, ord", + Expected: []sql.Row{ + {1, 1, float64(15)}, + {1, 2, float64(22)}, + {2, 2, float64(4)}, + }, + }, + }, + }, { Name: "nondeterministic window expressions are evaluated independently", SetUpScript: []string{ @@ -946,6 +992,27 @@ var WindowFunctionsScriptTests = []ScriptTest{ FIRST_VALUE('a%' LIKE 'a!%' ESCAPE '#') OVER ()`, Expected: []sql.Row{{true, false}}, }, + { + Name: "JSON_LENGTH paths in window expressions", + Query: `SELECT + FIRST_VALUE(JSON_LENGTH('{"a":[1,2]}', '$.a')) OVER (), + FIRST_VALUE(JSON_LENGTH('{"a":[1,2]}', '$')) OVER ()`, + Expected: []sql.Row{{2, 1}}, + }, + { + Name: "JSON_SEARCH paths in window expressions", + Query: `SELECT + JSON_UNQUOTE(FIRST_VALUE(JSON_SEARCH('["abc"]', 'one', 'abc')) OVER ()), + JSON_UNQUOTE(FIRST_VALUE(JSON_SEARCH('["abc"]', 'one', 'abc', NULL, NULL)) OVER ())`, + Expected: []sql.Row{{"$[0]", nil}}, + }, + { + Name: "JSON_VALUE return types in window expressions", + Query: `SELECT + FIRST_VALUE(JSON_VALUE('{"a":"12"}', '$.a', 'signed')) OVER (), + FIRST_VALUE(JSON_VALUE('{"a":"12"}', '$.a', 'char')) OVER ()`, + Expected: []sql.Row{{int64(12), `"12"`}}, + }, { // https://github.com/dolthub/dolt/issues/11498 Name: "customer reproduction: LIKE escape characters in window expressions", @@ -1062,6 +1129,14 @@ var WindowFunctionsScriptTests = []ScriptTest{ "insert into t values (1, 1), (2, 1), (3, 1), (4, 1), (5, 1), (6, 2), (7, 2), (8, 2), (9, 2), (10, 2);", }, Assertions: []ScriptTestAssertion{ + { + Query: "select i, ntile(2) over(order by i), ntile(3) over(order by i) from t where i <= 3;", + Expected: []sql.Row{ + {1, uint64(1), uint64(1)}, + {2, uint64(1), uint64(2)}, + {3, uint64(2), uint64(3)}, + }, + }, { Query: "select i, ntile(0) over() from t;", ExpectedErr: sql.ErrInvalidArgument, diff --git a/internal/exprtest/roundtrip.go b/internal/exprtest/roundtrip.go new file mode 100644 index 0000000000..4e751b082c --- /dev/null +++ b/internal/exprtest/roundtrip.go @@ -0,0 +1,305 @@ +// Copyright 2026 Dolthub, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package exprtest + +import ( + "context" + "encoding/hex" + "fmt" + "reflect" + "strings" + "testing" + "time" + + "github.com/dolthub/vitess/go/vt/sqlparser" + "github.com/stretchr/testify/require" + + "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/types" +) + +// ParseExpression parses the String representation of original as exactly one SELECT expression. Callers must inspect +// the returned AST fields and compare them to original; successfully parsing alone is not a round-trip assertion. +func ParseExpression(t testing.TB, original fmt.Stringer) sqlparser.SelectExpr { + t.Helper() + statement, err := sqlparser.Parse("SELECT " + original.String()) + require.NoError(t, err) + + selectStatement, ok := statement.(*sqlparser.Select) + require.Truef(t, ok, "expected SELECT statement, found %T", statement) + require.Len(t, selectStatement.SelectExprs, 1) + return selectStatement.SelectExprs[0] +} + +// RequireExpression returns the expression contained by a parsed SELECT expression. +func RequireExpression(t testing.TB, parsed sqlparser.SelectExpr) sqlparser.Expr { + t.Helper() + aliased, ok := parsed.(*sqlparser.AliasedExpr) + require.Truef(t, ok, "expected aliased expression, found %T", parsed) + return aliased.Expr +} + +// RequireFunction returns a parsed function expression. +func RequireFunction(t testing.TB, parsed sqlparser.SelectExpr) *sqlparser.FuncExpr { + t.Helper() + expression := RequireExpression(t, parsed) + function, ok := expression.(*sqlparser.FuncExpr) + require.Truef(t, ok, "expected function expression, found %T", expression) + return function +} + +// RequireColumn returns a parsed column expression. +func RequireColumn(t testing.TB, parsed sqlparser.SelectExpr) *sqlparser.ColName { + t.Helper() + expression := RequireExpression(t, parsed) + column, ok := expression.(*sqlparser.ColName) + require.Truef(t, ok, "expected column expression, found %T", expression) + return column +} + +// RequireFunctionArgument returns a parsed function argument. +func RequireFunctionArgument(t testing.TB, function *sqlparser.FuncExpr, index int) sqlparser.Expr { + t.Helper() + require.Greater(t, len(function.Exprs), index) + argument, ok := function.Exprs[index].(*sqlparser.AliasedExpr) + require.Truef(t, ok, "expected aliased function argument, found %T", function.Exprs[index]) + return argument.Expr +} + +// AssertFunctionRoundTrip compares the semantic fields parsed from original.String() to the fields of original. +func AssertFunctionRoundTrip(t testing.TB, original sql.FunctionExpression) { + t.Helper() + AssertFunctionRoundTripAs(t, original, original.FunctionName()) +} + +// AssertFunctionRoundTripAs is AssertFunctionRoundTrip for expressions whose parser-visible name differs from their +// registry name. +func AssertFunctionRoundTripAs(t testing.TB, original sql.FunctionExpression, parsedName string) { + t.Helper() + parsed := RequireFunction(t, ParseExpression(t, original)) + require.False(t, parsed.Distinct) + assertFunction(t, parsed, original, parsedName) +} + +// AssertDistinctFunctionRoundTrip additionally verifies the DISTINCT modifier on a function expression. +func AssertDistinctFunctionRoundTripAs(t testing.TB, original sql.FunctionExpression, parsedName string) { + t.Helper() + parsed := RequireFunction(t, ParseExpression(t, original)) + require.True(t, parsed.Distinct) + assertFunction(t, parsed, original, parsedName) +} + +// AssertColumnRoundTrip compares a parsed column name and qualifier to the original expression. +func AssertColumnRoundTrip(t testing.TB, original sql.Expression) { + t.Helper() + AssertExpressionValue(t, RequireExpression(t, ParseExpression(t, original)), original) +} + +// AssertAliasRoundTrip compares both the parsed alias and its value to the original expression. +func AssertAliasRoundTrip(t testing.TB, original interface { + sql.Expression + sql.Nameable +}) { + t.Helper() + parsed, ok := ParseExpression(t, original).(*sqlparser.AliasedExpr) + require.Truef(t, ok, "expected aliased expression, found %T", parsed) + require.Equal(t, original.Name(), parsed.As.String()) + require.Len(t, original.Children(), 1) + AssertExpressionValue(t, parsed.Expr, original.Children()[0]) +} + +// AssertLiteralRoundTrip compares the parsed literal token and value to the original literal value. +func AssertLiteralRoundTrip(t testing.TB, original sql.Expression) { + t.Helper() + parsed := RequireExpression(t, ParseExpression(t, original)) + valueExpression, ok := original.(interface{ Value() interface{} }) + require.Truef(t, ok, "expected literal expression, found %T", original) + value := valueExpression.Value() + typ := original.Type(sql.NewEmptyContext()) + + if stringValue, ok := value.(string); ok && types.IsBinaryType(typ) { + actual := requireSQLValue(t, parsed, sqlparser.HexNum) + decoded, err := hex.DecodeString(strings.TrimPrefix(string(actual.Val), "0x")) + require.NoError(t, err) + require.Equal(t, []byte(stringValue), decoded) + return + } + switch value := value.(type) { + case time.Time, types.Timespan: + actual := requireSQLValue(t, parsed, sqlparser.StrVal) + converted, _, err := typ.Convert(context.Background(), string(actual.Val)) + require.NoError(t, err) + comparison, err := typ.Compare(context.Background(), value, converted) + require.NoError(t, err) + require.Zero(t, comparison) + case types.JSONDocument: + actual, ok := parsed.(*sqlparser.ConvertExpr) + require.Truef(t, ok, "expected JSON cast, found %T", parsed) + require.Equal(t, "cast", strings.ToLower(actual.Name)) + require.Equal(t, "json", strings.ToLower(actual.Type.Type)) + jsonText := requireSQLValue(t, actual.Expr, sqlparser.StrVal) + converted, _, err := types.JSON.Convert(context.Background(), string(jsonText.Val)) + require.NoError(t, err) + comparison, err := value.Compare(context.Background(), converted.(sql.JSONWrapper)) + require.NoError(t, err) + require.Zero(t, comparison) + default: + assertLiteral(t, parsed, value) + } +} + +// AssertBindVariableRoundTrip compares the parser bind-variable token to the original bind variable name. +func AssertBindVariableRoundTrip(t testing.TB, original fmt.Stringer, name string) { + t.Helper() + parsed := requireSQLValue(t, RequireExpression(t, ParseExpression(t, original)), sqlparser.ValArg) + require.Equal(t, ":"+name, string(parsed.Val)) +} + +// AssertStarRoundTrip compares a parsed star qualifier to the original table qualifier. +func AssertStarRoundTrip(t testing.TB, original sql.Expression, table string) { + t.Helper() + parsed, ok := ParseExpression(t, original).(*sqlparser.StarExpr) + require.Truef(t, ok, "expected star expression, found %T", parsed) + require.Equal(t, table, parsed.TableName.Name.String()) +} + +func assertFunction(t testing.TB, parsed *sqlparser.FuncExpr, original sql.FunctionExpression, parsedName string) { + t.Helper() + require.Equal(t, strings.ToLower(parsedName), parsed.Name.Lowered()) + require.True(t, parsed.Qualifier.IsEmpty()) + children := original.Children() + if windowed, ok := original.(sql.WindowAdaptableExpression); ok && windowed.Window() != nil && len(children) != len(parsed.Exprs) { + require.Equal(t, len(parsed.Exprs)+windowed.Window().ExpressionsLen(), len(children)) + children = children[:len(parsed.Exprs)] + } + require.Equal(t, len(children), len(parsed.Exprs)) + for i, child := range children { + if table, ok := starTable(child); ok { + star, ok := parsed.Exprs[i].(*sqlparser.StarExpr) + require.Truef(t, ok, "expected star function argument, found %T", parsed.Exprs[i]) + require.Equal(t, table, star.TableName.Name.String()) + continue + } + AssertExpressionValue(t, RequireFunctionArgument(t, parsed, i), child) + } + + windowed, ok := original.(sql.WindowAdaptableExpression) + if !ok || windowed.Window() == nil { + require.Nil(t, parsed.Over) + return + } + assertWindow(t, parsed.Over, windowed.Window()) +} + +func assertWindow(t testing.TB, parsed *sqlparser.Over, original *sql.WindowDefinition) { + t.Helper() + require.NotNil(t, parsed) + require.Equal(t, original.Ref, parsed.NameRef.String()) + require.Len(t, parsed.PartitionBy, len(original.PartitionBy)) + for i, partition := range original.PartitionBy { + AssertExpressionValue(t, parsed.PartitionBy[i], partition) + } + require.Len(t, parsed.OrderBy, len(original.OrderBy)) + for i, order := range original.OrderBy { + AssertExpressionValue(t, parsed.OrderBy[i].Expr, order.Expr) + require.Equal(t, strings.ToLower(order.Order.String()), parsed.OrderBy[i].Direction) + } + // WindowFrame does not expose whether it represents ROWS or RANGE. Framed-window tests must inspect Frame directly. + require.Nil(t, original.Frame) + require.Nil(t, parsed.Frame) +} + +// AssertExpressionValue compares a parsed AST expression to the corresponding original GMS expression. +func AssertExpressionValue(t testing.TB, parsed sqlparser.Expr, original sql.Expression) { + t.Helper() + + if valueExpression, ok := original.(interface{ Value() interface{} }); ok { + assertLiteral(t, parsed, valueExpression.Value()) + return + } + if function, ok := original.(sql.FunctionExpression); ok { + parsedFunction, ok := parsed.(*sqlparser.FuncExpr) + require.Truef(t, ok, "expected function expression, found %T", parsed) + assertFunction(t, parsedFunction, function, function.FunctionName()) + return + } + if nameable, ok := original.(sql.Nameable); ok { + column, ok := parsed.(*sqlparser.ColName) + require.Truef(t, ok, "expected column name, found %T", parsed) + require.Equal(t, nameable.Name(), column.Name.String()) + if tableable, ok := original.(sql.Tableable); ok { + require.Equal(t, tableable.Table(), column.Qualifier.Name.String()) + } + return + } + + require.Failf(t, "unsupported original expression", "%T", original) +} + +func starTable(original sql.Expression) (string, bool) { + value := reflect.ValueOf(original) + if value.Kind() != reflect.Ptr || !value.Elem().IsValid() || value.Elem().Type().Name() != "Star" { + return "", false + } + field := value.Elem().FieldByName("Table") + return field.String(), true +} + +func assertLiteral(t testing.TB, parsed sqlparser.Expr, original interface{}) { + t.Helper() + switch value := original.(type) { + case nil: + require.IsType(t, &sqlparser.NullVal{}, parsed) + case bool: + actual, ok := parsed.(sqlparser.BoolVal) + require.Truef(t, ok, "expected boolean literal, found %T", parsed) + require.Equal(t, value, bool(actual)) + case string: + actual := requireSQLValue(t, parsed, sqlparser.StrVal) + require.Equal(t, value, string(actual.Val)) + case []byte: + actual := requireSQLValue(t, parsed, sqlparser.HexNum) + decoded, err := hex.DecodeString(strings.TrimPrefix(string(actual.Val), "0x")) + require.NoError(t, err) + require.Equal(t, value, decoded) + case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: + actual := requireSQLValue(t, parsed, sqlparser.IntVal) + require.Equal(t, fmt.Sprint(value), string(actual.Val)) + case float32, float64: + actual := requireSQLValue(t, parsed, sqlparser.FloatVal) + require.Equal(t, fmt.Sprint(value), string(actual.Val)) + case types.GeometryValue: + actual, ok := parsed.(*sqlparser.FuncExpr) + require.Truef(t, ok, "expected geometry constructor, found %T", parsed) + require.Equal(t, "st_geomfromwkb", actual.Name.Lowered()) + require.Len(t, actual.Exprs, 2) + binary := requireSQLValue(t, RequireFunctionArgument(t, actual, 0), sqlparser.HexNum) + decoded, err := hex.DecodeString(strings.TrimPrefix(string(binary.Val), "0x")) + require.NoError(t, err) + require.Equal(t, value.Serialize()[types.SRIDSize:], decoded) + srid := requireSQLValue(t, RequireFunctionArgument(t, actual, 1), sqlparser.IntVal) + require.Equal(t, fmt.Sprint(value.GetSRID()), string(srid.Val)) + default: + require.Failf(t, "unsupported literal value", "%T", original) + } +} + +func requireSQLValue(t testing.TB, parsed sqlparser.Expr, valueType sqlparser.ValType) *sqlparser.SQLVal { + t.Helper() + value, ok := parsed.(*sqlparser.SQLVal) + require.Truef(t, ok, "expected SQL value, found %T", parsed) + require.Equal(t, valueType, value.Type) + return value +} diff --git a/internal/exprtest/roundtrip_test.go b/internal/exprtest/roundtrip_test.go new file mode 100644 index 0000000000..8d115632a9 --- /dev/null +++ b/internal/exprtest/roundtrip_test.go @@ -0,0 +1,32 @@ +// Copyright 2026 Dolthub, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package exprtest + +import ( + "testing" + + "github.com/dolthub/vitess/go/vt/sqlparser" + "github.com/stretchr/testify/require" + + "github.com/dolthub/go-mysql-server/sql/expression" + "github.com/dolthub/go-mysql-server/sql/types" +) + +func TestParseExpressionReturnsTypedValue(t *testing.T) { + original := expression.NewLiteral("value", types.Text) + parsed := RequireExpression(t, ParseExpression(t, original)) + value := requireSQLValue(t, parsed, sqlparser.StrVal) + require.Equal(t, original.Value(), string(value.Val)) +} diff --git a/optgen/cmd/source/unary_aggs.yaml b/optgen/cmd/source/unary_aggs.yaml index 5290efc057..5652ffba6b 100644 --- a/optgen/cmd/source/unary_aggs.yaml +++ b/optgen/cmd/source/unary_aggs.yaml @@ -1,44 +1,70 @@ unaryAggs: - name: "AnyValue" + sqlName: "any_value" + sqlString: true desc: "returns any single value in the grouped rows" nullable: true - name: "Avg" + sqlString: true desc: "returns the average value of expr in all rows." nullable: true retType: "types.Float64" - name: "BitAnd" + sqlName: "bit_and" + sqlString: true desc: "returns the bitwise AND of all bits in expr." retType: "types.Uint64" - name: "BitOr" + sqlName: "bit_or" + sqlString: true desc: "returns the bitwise OR of all bits in expr." retType: "types.Uint64" - name: "BitXor" + sqlName: "bit_xor" + sqlString: true desc: "returns the bitwise XOR of all bits in expr." retType: "types.Uint64" - name: "Count" + sqlString: true desc: "returns a count of the number of non-NULL values of expr in the rows retrieved by a SELECT statement." retType: "types.Int64" - name: "First" + sqlString: true + windowSqlName: "first_value" desc: "returns the first value in a sequence of elements of an aggregation." - name: "JsonArray" sqlName: "json_arrayagg" + sqlString: true desc: "returns result set as a single JSON array." retType: "types.JSON" - name: "Last" + sqlString: true + windowSqlName: "last_value" desc: "returns the last value in a sequence of elements of an aggregation." - name: "Max" + sqlString: true desc: "returns the maximum value of expr in all rows." - name: "Min" + sqlString: true desc: "returns the minimum value of expr in all rows." - name: "Sum" + sqlString: true desc: "returns the sum of expr in all rows" nullable: false retType: "types.Float64" - name: "StdDevPop" + sqlName: "stddev_pop" + sqlString: true desc: "returns the population standard deviation of expr" - name: "StdDevSamp" + sqlName: "stddev_samp" + sqlString: true desc: "returns the sample standard deviation of expr" - name: "VarPop" + sqlName: "var_pop" + sqlString: true desc: "returns the population variance of expr" - name: "VarSamp" - desc: "returns the sample variance of expr" \ No newline at end of file + sqlName: "var_samp" + sqlString: true + desc: "returns the sample variance of expr" diff --git a/optgen/cmd/support/agg_gen.go b/optgen/cmd/support/agg_gen.go index 497ac0b6c9..4ebda481a8 100644 --- a/optgen/cmd/support/agg_gen.go +++ b/optgen/cmd/support/agg_gen.go @@ -15,11 +15,13 @@ type AggDefs struct { } type AggDef struct { - Name string `yaml:"name"` - SqlName string `yaml:"sqlName"` - Desc string `yaml:"desc"` - RetType string `yaml:"retType"` // must be valid sql.Type - Nullable bool `yaml:"nullable"` + Name string `yaml:"name"` + SqlName string `yaml:"sqlName"` + WindowSqlName string `yaml:"windowSqlName"` + Desc string `yaml:"desc"` + RetType string `yaml:"retType"` // must be valid sql.Type + Nullable bool `yaml:"nullable"` + SqlString bool `yaml:"sqlString"` } var _ GenDefs = ([]AggDef)(nil) @@ -59,6 +61,7 @@ func (g *AggGen) Generate(defines GenDefs, w io.Writer) { g.genAggConstructor(define) g.genAggPropAccessors(define) g.genAggStringer(define) + g.genAggDescriber(define) g.genAggWithWindow(define) g.genAggWithChildren(define) g.genAggWithId(define) @@ -76,6 +79,7 @@ func (g *AggGen) genAggType(define AggDef) { func (g *AggGen) genAggInterfaces(define AggDef) { fmt.Fprintf(g.w, "var _ sql.FunctionExpression = (*%s)(nil)\n", define.Name) fmt.Fprintf(g.w, "var _ sql.Aggregation = (*%s)(nil)\n", define.Name) + fmt.Fprintf(g.w, "var _ sql.Describable = (*%s)(nil)\n", define.Name) fmt.Fprintf(g.w, "var _ sql.WindowAdaptableExpression = (*%s)(nil)\n", define.Name) fmt.Fprintf(g.w, "\n") @@ -113,25 +117,78 @@ func (g *AggGen) genAggStringer(define AggDef) { sqlName = define.SqlName } fmt.Fprintf(g.w, "func (a *%s) String() string {\n", define.Name) - fmt.Fprintf(g.w, " if a.window != nil {\n") - fmt.Fprintf(g.w, " pr := sql.NewTreePrinter()\n") - fmt.Fprintf(g.w, " _ = pr.WriteNode(\"%s\")\n ", strings.ToUpper(sqlName)) - fmt.Fprintf(g.w, " children := []string{a.window.String(), a.Child.String()}\n") - fmt.Fprintf(g.w, " pr.WriteChildren(children...)\n") - fmt.Fprintf(g.w, " return pr.String()\n") + if define.SqlString { + if define.WindowSqlName != "" { + fmt.Fprintf(g.w, " if a.window != nil {\n") + fmt.Fprintf(g.w, " return \"%s(\" + a.Child.String() + \") \" + a.window.String()\n", strings.ToUpper(define.WindowSqlName)) + fmt.Fprintf(g.w, " }\n") + fmt.Fprintf(g.w, " return \"%s(\" + a.Child.String() + \")\"\n", strings.ToUpper(sqlName)) + fmt.Fprintf(g.w, "}\n\n") + } else { + fmt.Fprintf(g.w, " ret := \"%s(\" + a.Child.String() + \")\"\n", strings.ToUpper(sqlName)) + fmt.Fprintf(g.w, " if a.window != nil {\n") + fmt.Fprintf(g.w, " ret += \" \" + a.window.String()\n") + fmt.Fprintf(g.w, " }\n") + fmt.Fprintf(g.w, " return ret\n") + fmt.Fprintf(g.w, "}\n\n") + } + } else { + fmt.Fprintf(g.w, " if a.window != nil {\n") + fmt.Fprintf(g.w, " pr := sql.NewTreePrinter()\n") + fmt.Fprintf(g.w, " _ = pr.WriteNode(\"%s\")\n ", strings.ToUpper(sqlName)) + fmt.Fprintf(g.w, " children := []string{a.window.String(), a.Child.String()}\n") + fmt.Fprintf(g.w, " pr.WriteChildren(children...)\n") + fmt.Fprintf(g.w, " return pr.String()\n") + fmt.Fprintf(g.w, " }\n") + fmt.Fprintf(g.w, " return \"%s(\" + a.Child.String() + \")\"\n", strings.ToUpper(sqlName)) + fmt.Fprintf(g.w, "}\n\n") + } + +} + +func (g *AggGen) genAggDescriber(define AggDef) { + sqlName := define.Name + if define.SqlName != "" { + sqlName = define.SqlName + } + fmt.Fprintf(g.w, "func (a *%s) Describe(ctx *sql.Context, options sql.DescribeOptions) string {\n", define.Name) + fmt.Fprintf(g.w, " if options.Debug {\n") + fmt.Fprintf(g.w, " if a.window != nil {\n") + fmt.Fprintf(g.w, " pr := sql.NewTreePrinter()\n") + fmt.Fprintf(g.w, " _ = pr.WriteNode(\"%s\")\n ", strings.ToUpper(sqlName)) + fmt.Fprintf(g.w, " children := []string{sql.Describe(ctx, a.window, options), sql.Describe(ctx, a.Child, options)}\n") + fmt.Fprintf(g.w, " pr.WriteChildren(children...)\n") + fmt.Fprintf(g.w, " return pr.String()\n") + fmt.Fprintf(g.w, " }\n") + fmt.Fprintf(g.w, " return fmt.Sprintf(\"%s(%%s)\", sql.Describe(ctx, a.Child, options))\n", strings.ToUpper(sqlName)) fmt.Fprintf(g.w, " }\n") - fmt.Fprintf(g.w, " return \"%s(\" + a.Child.String() + \")\"\n", strings.ToUpper(sqlName)) + if define.SqlString { + if define.WindowSqlName != "" { + fmt.Fprintf(g.w, " if a.window != nil {\n") + fmt.Fprintf(g.w, " return \"%s(\" + sql.Describe(ctx, a.Child, options) + \") \" + sql.Describe(ctx, a.window, options)\n", strings.ToUpper(define.WindowSqlName)) + fmt.Fprintf(g.w, " }\n") + fmt.Fprintf(g.w, " return \"%s(\" + sql.Describe(ctx, a.Child, options) + \")\"\n", strings.ToUpper(sqlName)) + } else { + fmt.Fprintf(g.w, " ret := \"%s(\" + sql.Describe(ctx, a.Child, options) + \")\"\n", strings.ToUpper(sqlName)) + fmt.Fprintf(g.w, " if a.window != nil {\n") + fmt.Fprintf(g.w, " ret += \" \" + sql.Describe(ctx, a.window, options)\n") + fmt.Fprintf(g.w, " }\n") + fmt.Fprintf(g.w, " return ret\n") + } + } else { + fmt.Fprintf(g.w, " if a.window != nil {\n") + fmt.Fprintf(g.w, " pr := sql.NewTreePrinter()\n") + fmt.Fprintf(g.w, " _ = pr.WriteNode(\"%s\")\n ", strings.ToUpper(sqlName)) + fmt.Fprintf(g.w, " children := []string{sql.Describe(ctx, a.window, options), sql.Describe(ctx, a.Child, options)}\n") + fmt.Fprintf(g.w, " pr.WriteChildren(children...)\n") + fmt.Fprintf(g.w, " return pr.String()\n") + fmt.Fprintf(g.w, " }\n") + fmt.Fprintf(g.w, " return \"%s(\" + sql.Describe(ctx, a.Child, options) + \")\"\n", strings.ToUpper(sqlName)) + } fmt.Fprintf(g.w, "}\n\n") fmt.Fprintf(g.w, "func (a *%s) DebugString(ctx *sql.Context) string {\n", define.Name) - fmt.Fprintf(g.w, " if a.window != nil {\n") - fmt.Fprintf(g.w, " pr := sql.NewTreePrinter()\n") - fmt.Fprintf(g.w, " _ = pr.WriteNode(\"%s\")\n ", strings.ToUpper(sqlName)) - fmt.Fprintf(g.w, " children := []string{sql.DebugString(ctx, a.window), sql.DebugString(ctx, a.Child)}\n") - fmt.Fprintf(g.w, " pr.WriteChildren(children...)\n") - fmt.Fprintf(g.w, " return pr.String()\n") - fmt.Fprintf(g.w, " }\n") - fmt.Fprintf(g.w, " return fmt.Sprintf(\"%s(%%s)\", sql.DebugString(ctx, a.Child))\n", strings.ToUpper(sqlName)) + fmt.Fprintf(g.w, " return a.Describe(ctx, sql.DescribeOptions{Debug: true})\n") fmt.Fprintf(g.w, "}\n\n") } diff --git a/optgen/cmd/support/agg_gen_test.go b/optgen/cmd/support/agg_gen_test.go index a8eaf0b80c..f00ea82f84 100644 --- a/optgen/cmd/support/agg_gen_test.go +++ b/optgen/cmd/support/agg_gen_test.go @@ -15,9 +15,11 @@ func TestAggGen(t *testing.T) { defines: AggDefs{ []AggDef{ { - Name: "Test", - Desc: "Test description", - RetType: "sql.Float64", + Name: "Test", + Desc: "Test description", + RetType: "sql.Float64", + SqlString: true, + WindowSqlName: "test_window", }, }, }, @@ -36,6 +38,7 @@ func TestAggGen(t *testing.T) { var _ sql.FunctionExpression = (*Test)(nil) var _ sql.Aggregation = (*Test)(nil) + var _ sql.Describable = (*Test)(nil) var _ sql.WindowAdaptableExpression = (*Test)(nil) func NewTest(e sql.Expression) *Test { @@ -58,24 +61,30 @@ func TestAggGen(t *testing.T) { func (a *Test) String() string { if a.window != nil { - pr := sql.NewTreePrinter() - _ = pr.WriteNode("TEST") - children := []string{a.window.String(), a.Child.String()} - pr.WriteChildren(children...) - return pr.String() + return "TEST_WINDOW(" + a.Child.String() + ") " + a.window.String() } - return "TEST(" + a.Child.String() + ")" + return "TEST(" + a.Child.String() + ")" } - func (a *Test) DebugString(ctx *sql.Context) string { + func (a *Test) Describe(ctx *sql.Context, options sql.DescribeOptions) string { + if options.Debug { + if a.window != nil { + pr := sql.NewTreePrinter() + _ = pr.WriteNode("TEST") + children := []string{sql.Describe(ctx, a.window, options), sql.Describe(ctx, a.Child, options)} + pr.WriteChildren(children...) + return pr.String() + } + return fmt.Sprintf("TEST(%s)", sql.Describe(ctx, a.Child, options)) + } if a.window != nil { - pr := sql.NewTreePrinter() - _ = pr.WriteNode("TEST") - children := []string{sql.DebugString(ctx, a.window), sql.DebugString(ctx, a.Child)} - pr.WriteChildren(children...) - return pr.String() + return "TEST_WINDOW(" + sql.Describe(ctx, a.Child, options) + ") " + sql.Describe(ctx, a.window, options) } - return fmt.Sprintf("TEST(%s)", sql.DebugString(ctx, a.Child)) + return "TEST(" + sql.Describe(ctx, a.Child, options) + ")" + } + + func (a *Test) DebugString(ctx *sql.Context) string { + return a.Describe(ctx, sql.DescribeOptions{Debug: true}) } func (a *Test) WithWindow(ctx *sql.Context, window *sql.WindowDefinition) sql.WindowAdaptableExpression { diff --git a/sql/analyzer/vector_index_test.go b/sql/analyzer/vector_index_test.go index 7646c8177a..8471785e42 100644 --- a/sql/analyzer/vector_index_test.go +++ b/sql/analyzer/vector_index_test.go @@ -73,7 +73,7 @@ func vectorIndexTestCases(t *testing.T, db *memory.Database, table sql.IndexedTa expectedPlan: ` IndexedTableAccess(test) ├─ index: [test-table.v] - └─ order: VEC_DISTANCE_L2_SQUARED([0, 0], test-table.v) LIMIT 1 (bigint) + └─ order: VEC_DISTANCE_L2_SQUARED(CAST('[0, 0]' AS JSON), test-table.v) LIMIT 1 (bigint) `, expectedRows: []sql.Row{ sql.NewRow(int64(3), jsontests.ConvertToJson(t, "[1.0, 1.0]")), @@ -118,7 +118,7 @@ Limit(1) └─ Offset(1) └─ IndexedTableAccess(test) ├─ index: [test-table.v] - └─ order: VEC_DISTANCE_L2_SQUARED([0, 0], test-table.v) LIMIT (1 (bigint) + 1 (bigint)) + └─ order: VEC_DISTANCE_L2_SQUARED(CAST('[0, 0]' AS JSON), test-table.v) LIMIT (1 (bigint) + 1 (bigint)) `, expectedRows: []sql.Row{ sql.NewRow(int64(2), jsontests.ConvertToJson(t, "[2.0, 2.0]")), @@ -142,7 +142,7 @@ Limit(1) expectedPlan: ` IndexedTableAccess(test) ├─ index: [test-table.v] - └─ order: VEC_DISTANCE_EUCLIDEAN([0, 0], test-table.v) LIMIT 1 (bigint) + └─ order: VEC_DISTANCE_EUCLIDEAN(CAST('[0, 0]' AS JSON), test-table.v) LIMIT 1 (bigint) `, expectedRows: []sql.Row{ sql.NewRow(int64(3), jsontests.ConvertToJson(t, "[1.0, 1.0]")), @@ -169,7 +169,7 @@ IndexedTableAccess(test) expectedPlan: ` IndexedTableAccess(test) ├─ index: [test-table.v] - └─ order: VEC_DISTANCE_COSINE([3, 4], test-table.v) LIMIT 1 (bigint) + └─ order: VEC_DISTANCE_COSINE(CAST('[3, 4]' AS JSON), test-table.v) LIMIT 1 (bigint) `, expectedRows: []sql.Row{ sql.NewRow(int64(1), jsontests.ConvertToJson(t, "[3.0, 4.0]")), diff --git a/sql/columndefault_test.go b/sql/columndefault_test.go new file mode 100644 index 0000000000..7933c481cb --- /dev/null +++ b/sql/columndefault_test.go @@ -0,0 +1,36 @@ +// Copyright 2026 Dolthub, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sql + +import ( + "testing" + + "github.com/dolthub/vitess/go/vt/sqlparser" + "github.com/stretchr/testify/require" +) + +func TestColumnDefaultValueString(t *testing.T) { + var implicitDefault *ColumnDefaultValue + require.Equal(t, "", implicitDefault.String()) + + explicitNull, err := NewColumnDefaultValue(UnresolvedColumnDefault{ExprString: "NULL"}, nil, true, false, true) + require.NoError(t, err) + require.Equal(t, "NULL", explicitNull.String()) + statement, err := sqlparser.Parse("SELECT " + explicitNull.String()) + require.NoError(t, err) + parsed := statement.(*sqlparser.Select).SelectExprs[0].(*sqlparser.AliasedExpr).Expr + require.IsType(t, &sqlparser.NullVal{}, parsed) + require.Equal(t, "NULL", explicitNull.Expr.(UnresolvedColumnDefault).ExprString) +} diff --git a/sql/describe.go b/sql/describe.go index 63bac89f5f..0c5b3bdcdc 100644 --- a/sql/describe.go +++ b/sql/describe.go @@ -93,7 +93,8 @@ type Describable interface { } // Describe produces a human-readable string for |n|, based on the values set in |options|. -// For |n| to benefit from |options|, it must implement `sql.Describable`. +// Describable values control their own output. Non-debug expression descriptions recursively pass the options to +// their children so nested Describable expressions retain their detailed output. func Describe(ctx *Context, n fmt.Stringer, options DescribeOptions) string { if d, ok := n.(Describable); ok { return d.Describe(ctx, options) @@ -101,9 +102,54 @@ func Describe(ctx *Context, n fmt.Stringer, options DescribeOptions) string { if d, ok := n.(DebugStringer); ok && options.Debug { return d.DebugString(ctx) } + if e, ok := n.(Expression); ok && !options.Debug { + return describeExpression(ctx, e, options) + } return n.String() } +// describedExpression delegates all expression behavior except String to the wrapped expression. It allows +// describeExpression to preserve an expression's own formatting while substituting descriptions for its children. +type describedExpression struct { + Expression + description string +} + +func (e describedExpression) String() string { + return e.description +} + +// describeExpression recursively propagates describe options through expressions that do not implement Describable. +// This is necessary for plan expressions such as Subquery to include their plan details when nested in ordinary +// scalar expressions such as AND, OR, and comparisons. +func describeExpression(ctx *Context, e Expression, options DescribeOptions) string { + children := e.Children() + if len(children) == 0 { + return e.String() + } + + describedChildren := make([]Expression, len(children)) + changed := false + for i, child := range children { + description := Describe(ctx, child, options) + if description != child.String() { + describedChildren[i] = describedExpression{Expression: child, description: description} + changed = true + } else { + describedChildren[i] = child + } + } + if !changed { + return e.String() + } + + described, err := e.WithChildren(ctx, describedChildren...) + if err != nil { + return e.String() + } + return described.String() +} + type DescribeOptions struct { Analyze bool Estimates bool diff --git a/sql/describe_test.go b/sql/describe_test.go new file mode 100644 index 0000000000..a94d42b061 --- /dev/null +++ b/sql/describe_test.go @@ -0,0 +1,59 @@ +// Copyright 2026 Dolthub, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sql_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/expression" + "github.com/dolthub/go-mysql-server/sql/types" +) + +type optionDescribingExpression struct{} + +var _ sql.Expression = optionDescribingExpression{} +var _ sql.Describable = optionDescribingExpression{} + +func (optionDescribingExpression) Resolved() bool { return true } +func (optionDescribingExpression) String() string { return "string" } +func (optionDescribingExpression) Type(*sql.Context) sql.Type { + return types.Boolean +} +func (optionDescribingExpression) IsNullable(*sql.Context) bool { return false } +func (optionDescribingExpression) Eval(*sql.Context, sql.Row) (interface{}, error) { + return true, nil +} +func (optionDescribingExpression) Children() []sql.Expression { return nil } +func (e optionDescribingExpression) WithChildren(*sql.Context, ...sql.Expression) (sql.Expression, error) { + return e, nil +} +func (optionDescribingExpression) Describe(_ *sql.Context, options sql.DescribeOptions) string { + return fmt.Sprintf("description(estimates=%t, analyze=%t)", options.Estimates, options.Analyze) +} + +func TestDescribeOptionsPropagateThroughExpressions(t *testing.T) { + ctx := sql.NewEmptyContext() + expr := expression.NewAnd( + expression.NewLiteral(true, types.Boolean), + optionDescribingExpression{}, + ) + + description := sql.Describe(ctx, expr, sql.DescribeOptions{Estimates: true, Analyze: true}) + require.Equal(t, "(true AND description(estimates=true, analyze=true))", description) +} diff --git a/sql/expression/alias.go b/sql/expression/alias.go index 7490b220b1..aaca8ef398 100644 --- a/sql/expression/alias.go +++ b/sql/expression/alias.go @@ -17,6 +17,8 @@ package expression import ( "fmt" + "github.com/dolthub/vitess/go/vt/sqlparser" + "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/types" ) @@ -40,7 +42,7 @@ func (a AliasReference) Table() string { } func (a AliasReference) String() string { - return fmt.Sprintf("(alias reference)%s", a.name) + return a.name } func (a AliasReference) Resolved() bool { @@ -141,7 +143,8 @@ func (e *Alias) Describe(ctx *sql.Context, options sql.DescribeOptions) string { return fmt.Sprintf("%s->%s:%d", sql.Describe(ctx, e.Child, options), e.name, e.id) } } - return fmt.Sprintf("%s as %s", sql.Describe(ctx, e.Child, options), e.name) + name := sqlparser.String(sqlparser.NewColIdent(e.name)) + return fmt.Sprintf("%s as %s", sql.Describe(ctx, e.Child, options), name) } func (e *Alias) String() string { diff --git a/sql/expression/alias_test.go b/sql/expression/alias_test.go new file mode 100644 index 0000000000..7a915fcfdb --- /dev/null +++ b/sql/expression/alias_test.go @@ -0,0 +1,37 @@ +// Copyright 2026 Dolthub, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package expression + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/dolthub/go-mysql-server/internal/exprtest" + "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/types" +) + +func TestAliasReferenceString(t *testing.T) { + expr := NewAliasReference("alias_name") + require.Equal(t, "alias_name", expr.String()) + exprtest.AssertColumnRoundTrip(t, expr) +} + +func TestAliasString(t *testing.T) { + expr := NewAlias(sql.NewEmptyContext(), "alias name", NewLiteral(42, types.Int64)) + require.Equal(t, "42 as `alias name`", expr.String()) + exprtest.AssertAliasRoundTrip(t, expr) +} diff --git a/sql/expression/bindvar.go b/sql/expression/bindvar.go index 776ffa6c11..3bdc515037 100644 --- a/sql/expression/bindvar.go +++ b/sql/expression/bindvar.go @@ -36,7 +36,7 @@ func (bv *BindVar) Resolved() bool { } func (bv *BindVar) String() string { - return "BindVar(" + bv.Name + ")" + return ":" + bv.Name } func (bv *BindVar) Type(ctx *sql.Context) sql.Type { diff --git a/sql/expression/bindvar_test.go b/sql/expression/bindvar_test.go new file mode 100644 index 0000000000..4f90ccb539 --- /dev/null +++ b/sql/expression/bindvar_test.go @@ -0,0 +1,29 @@ +// Copyright 2026 Dolthub, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package expression + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/dolthub/go-mysql-server/internal/exprtest" +) + +func TestBindVarString(t *testing.T) { + expr := NewBindVar("arg1") + require.Equal(t, ":arg1", expr.String()) + exprtest.AssertBindVariableRoundTrip(t, expr, expr.Name) +} diff --git a/sql/expression/function/aggregation/any_value_test.go b/sql/expression/function/aggregation/any_value_test.go new file mode 100644 index 0000000000..06f4024fc5 --- /dev/null +++ b/sql/expression/function/aggregation/any_value_test.go @@ -0,0 +1,31 @@ +// Copyright 2026 Dolthub, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package aggregation + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/dolthub/go-mysql-server/internal/exprtest" + "github.com/dolthub/go-mysql-server/sql/expression" + "github.com/dolthub/go-mysql-server/sql/types" +) + +func TestAnyValueString(t *testing.T) { + expr := NewAnyValue(expression.NewGetField(0, types.Int64, "value", false)) + require.Equal(t, "ANY_VALUE(value)", expr.String()) + exprtest.AssertFunctionRoundTripAs(t, expr, "ANY_VALUE") +} diff --git a/sql/expression/function/aggregation/avg_test.go b/sql/expression/function/aggregation/avg_test.go index 43b2f72a20..5e700059a9 100644 --- a/sql/expression/function/aggregation/avg_test.go +++ b/sql/expression/function/aggregation/avg_test.go @@ -19,6 +19,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/dolthub/go-mysql-server/internal/exprtest" "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/expression" "github.com/dolthub/go-mysql-server/sql/types" @@ -30,6 +31,12 @@ func TestAvg_String(t *testing.T) { avg := NewAvg(expression.NewGetField(0, types.Int32, "col1", true)) require.Equal("AVG(col1)", avg.String()) + + windowed := avg.WithWindow(sql.NewEmptyContext(), &sql.WindowDefinition{ + PartitionBy: []sql.Expression{expression.NewGetField(1, types.Int32, "col2", true)}, + }) + require.Equal("AVG(col1) over ( partition by col2)", windowed.String()) + exprtest.AssertFunctionRoundTrip(t, windowed.(sql.FunctionExpression)) } func TestAvg_Float64(t *testing.T) { diff --git a/sql/expression/function/aggregation/bitwise_test.go b/sql/expression/function/aggregation/bitwise_test.go index 342aa99f36..b7741c0375 100644 --- a/sql/expression/function/aggregation/bitwise_test.go +++ b/sql/expression/function/aggregation/bitwise_test.go @@ -19,6 +19,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/dolthub/go-mysql-server/internal/exprtest" "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/expression" "github.com/dolthub/go-mysql-server/sql/types" @@ -27,7 +28,11 @@ import ( func TestBitAnd_String(t *testing.T) { assert := require.New(t) m := NewBitAnd(expression.NewGetField(0, types.Int32, "field", true)) - assert.Equal("BITAND(field)", m.String()) + assert.Equal("BIT_AND(field)", m.String()) + + windowed := m.WithWindow(sql.NewEmptyContext(), &sql.WindowDefinition{}) + assert.Equal("BIT_AND(field) over ()", windowed.String()) + exprtest.AssertFunctionRoundTripAs(t, windowed.(sql.FunctionExpression), "BIT_AND") } func TestBitAnd_Eval_Int(t *testing.T) { @@ -109,7 +114,11 @@ func TestBitAnd_Eval_Empty(t *testing.T) { func TestBitOr_String(t *testing.T) { assert := require.New(t) m := NewBitOr(expression.NewGetField(0, types.Int32, "field", true)) - assert.Equal("BITOR(field)", m.String()) + assert.Equal("BIT_OR(field)", m.String()) + + windowed := m.WithWindow(sql.NewEmptyContext(), &sql.WindowDefinition{}) + assert.Equal("BIT_OR(field) over ()", windowed.String()) + exprtest.AssertFunctionRoundTripAs(t, windowed.(sql.FunctionExpression), "BIT_OR") } func TestBitOr_Eval_Int(t *testing.T) { @@ -191,7 +200,11 @@ func TestBitOr_Eval_Empty(t *testing.T) { func TestBitXor_String(t *testing.T) { assert := require.New(t) m := NewBitXor(expression.NewGetField(0, types.Int32, "field", true)) - assert.Equal("BITXOR(field)", m.String()) + assert.Equal("BIT_XOR(field)", m.String()) + + windowed := m.WithWindow(sql.NewEmptyContext(), &sql.WindowDefinition{}) + assert.Equal("BIT_XOR(field) over ()", windowed.String()) + exprtest.AssertFunctionRoundTripAs(t, windowed.(sql.FunctionExpression), "BIT_XOR") } func TestBitXor_Eval_Int(t *testing.T) { diff --git a/sql/expression/function/aggregation/common_test.go b/sql/expression/function/aggregation/common_test.go index a431637e67..a7ba4b630d 100644 --- a/sql/expression/function/aggregation/common_test.go +++ b/sql/expression/function/aggregation/common_test.go @@ -20,6 +20,8 @@ import ( "github.com/stretchr/testify/require" "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/expression" + "github.com/dolthub/go-mysql-server/sql/types" ) func eval(t *testing.T, e sql.Expression, row sql.Row) interface{} { @@ -50,3 +52,44 @@ func aggregate(t *testing.T, agg sql.Aggregation, rows ...sql.Row) interface{} { } return evalBuffer(t, buf) } + +func TestGeneratedUnaryAggregateDescribe(t *testing.T) { + ctx := sql.NewEmptyContext() + hashIn, err := expression.NewHashInTuple( + ctx, + expression.NewGetField(0, types.Int64, "x", false), + expression.NewTuple(expression.NewLiteral(1, types.Int64)), + ) + require.NoError(t, err) + + aggregates := []sql.Expression{ + NewAnyValue(hashIn), + NewAvg(hashIn), + NewBitAnd(hashIn), + NewBitOr(hashIn), + NewBitXor(hashIn), + NewCount(hashIn), + NewFirst(hashIn), + NewJsonArray(hashIn), + NewLast(hashIn), + NewMax(hashIn), + NewMin(hashIn), + NewSum(hashIn), + NewStdDevPop(hashIn), + NewStdDevSamp(hashIn), + NewVarPop(hashIn), + NewVarSamp(hashIn), + } + for _, aggregate := range aggregates { + require.Contains(t, sql.Describe(ctx, aggregate, sql.DescribeOptions{Estimates: true}), "(x HASH IN (1))") + require.Equal(t, sql.DebugString(ctx, aggregate), sql.Describe(ctx, aggregate, sql.DescribeOptions{Debug: true})) + } + + windowed := NewAvg(expression.NewLiteral(1, types.Int64)).WithWindow(ctx, &sql.WindowDefinition{ + PartitionBy: []sql.Expression{hashIn}, + }) + require.Equal(t, + "AVG(1) over ( partition by (x HASH IN (1)))", + sql.Describe(ctx, windowed, sql.DescribeOptions{Estimates: true}), + ) +} diff --git a/sql/expression/function/aggregation/count_distinct.go b/sql/expression/function/aggregation/count_distinct.go index 0daa0b61c3..ae3e06469f 100644 --- a/sql/expression/function/aggregation/count_distinct.go +++ b/sql/expression/function/aggregation/count_distinct.go @@ -16,6 +16,7 @@ package aggregation import ( "fmt" + "strings" "github.com/dolthub/go-mysql-server/sql/expression" "github.com/dolthub/go-mysql-server/sql/transform" @@ -121,7 +122,15 @@ func (a *CountDistinct) Window() *sql.WindowDefinition { // String implements the ValueStats interface. func (a *CountDistinct) String() string { - return fmt.Sprintf("COUNTDISTINCT(%s)", a.ChildExpressions) + children := make([]string, len(a.ChildExpressions)) + for i, child := range a.ChildExpressions { + children[i] = child.String() + } + ret := fmt.Sprintf("COUNT(DISTINCT %s)", strings.Join(children, ", ")) + if a.window != nil { + ret += " " + a.window.String() + } + return ret } // NewWindowFunction implements the WindowAdaptableExpression interface. diff --git a/sql/expression/function/aggregation/count_test.go b/sql/expression/function/aggregation/count_test.go index 10c769853b..3d1c056405 100644 --- a/sql/expression/function/aggregation/count_test.go +++ b/sql/expression/function/aggregation/count_test.go @@ -20,6 +20,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/dolthub/go-mysql-server/internal/exprtest" "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/expression" "github.com/dolthub/go-mysql-server/sql/types" @@ -73,6 +74,12 @@ func TestCountEvalStar(t *testing.T) { require.Equal(int64(5), evalBuffer(t, b)) } +func TestCountString(t *testing.T) { + count := NewCount(expression.NewStar()).WithWindow(sql.NewEmptyContext(), &sql.WindowDefinition{}) + require.Equal(t, "COUNT(*) over ()", count.String()) + exprtest.AssertFunctionRoundTrip(t, count.(sql.FunctionExpression)) +} + func TestCountEvalString(t *testing.T) { require := require.New(t) ctx := sql.NewEmptyContext() @@ -104,6 +111,24 @@ func TestCountDistinctEval1(t *testing.T) { require.Equal(int64(1), evalBuffer(t, b)) } +func TestCountDistinctString(t *testing.T) { + ctx := sql.NewEmptyContext() + count := NewCountDistinct( + expression.NewGetField(0, types.Int64, "foo", false), + expression.NewGetField(1, types.Int64, "bar", false), + ) + require.Equal(t, "COUNT(DISTINCT foo, bar)", count.String()) + exprtest.AssertDistinctFunctionRoundTripAs(t, count, "COUNT") + + window := sql.NewWindowDefinition( + []sql.Expression{expression.NewGetField(2, types.Int64, "baz", false)}, + nil, nil, "", "", + ) + windowed := count.WithWindow(ctx, window) + require.Equal(t, "COUNT(DISTINCT foo, bar) over ( partition by baz)", windowed.String()) + exprtest.AssertDistinctFunctionRoundTripAs(t, windowed.(sql.FunctionExpression), "COUNT") +} + func TestCountDistinctEvalStar(t *testing.T) { require := require.New(t) ctx := sql.NewEmptyContext() diff --git a/sql/expression/function/aggregation/first_test.go b/sql/expression/function/aggregation/first_test.go index acf6bf1042..873883a1e6 100644 --- a/sql/expression/function/aggregation/first_test.go +++ b/sql/expression/function/aggregation/first_test.go @@ -19,6 +19,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/dolthub/go-mysql-server/internal/exprtest" "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/expression" "github.com/dolthub/go-mysql-server/sql/types" @@ -43,3 +44,10 @@ func TestFirst(t *testing.T) { }) } } + +func TestFirstString(t *testing.T) { + expr := NewFirst(expression.NewGetField(0, types.Text, "value", false)). + WithWindow(sql.NewEmptyContext(), &sql.WindowDefinition{}) + require.Equal(t, "FIRST_VALUE(value) over ()", expr.String()) + exprtest.AssertFunctionRoundTripAs(t, expr.(sql.FunctionExpression), "FIRST_VALUE") +} diff --git a/sql/expression/function/aggregation/group_concat.go b/sql/expression/function/aggregation/group_concat.go index a791341538..3fa0bf56bf 100644 --- a/sql/expression/function/aggregation/group_concat.go +++ b/sql/expression/function/aggregation/group_concat.go @@ -22,6 +22,7 @@ import ( "github.com/dolthub/vitess/go/vt/proto/query" "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/expression" "github.com/dolthub/go-mysql-server/sql/sorters" "github.com/dolthub/go-mysql-server/sql/types" ) @@ -133,7 +134,7 @@ func (g *GroupConcat) String() string { sb := strings.Builder{} sb.WriteString("group_concat(") if g.distinct != "" { - sb.WriteString(fmt.Sprintf("distinct %s", g.distinct)) + sb.WriteString(g.distinct) } if g.selectExprs != nil { @@ -156,9 +157,13 @@ func (g *GroupConcat) String() string { } sb.WriteString(" separator ") - sb.WriteString(fmt.Sprintf("'%s'", g.separator)) + sb.WriteString(expression.NewLiteral(g.separator, types.Text).String()) sb.WriteString(")") + if g.window != nil { + sb.WriteString(" ") + sb.WriteString(g.window.String()) + } return sb.String() } diff --git a/sql/expression/function/aggregation/group_concat_test.go b/sql/expression/function/aggregation/group_concat_test.go index ba11ebeced..64743cfcea 100644 --- a/sql/expression/function/aggregation/group_concat_test.go +++ b/sql/expression/function/aggregation/group_concat_test.go @@ -18,8 +18,10 @@ import ( "testing" "github.com/dolthub/vitess/go/vt/proto/query" + "github.com/dolthub/vitess/go/vt/sqlparser" "github.com/stretchr/testify/require" + "github.com/dolthub/go-mysql-server/internal/exprtest" "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/expression" "github.com/dolthub/go-mysql-server/sql/types" @@ -28,11 +30,11 @@ import ( func TestGroupConcat_FunctionName(t *testing.T) { assert := require.New(t) - m := NewGroupConcat("field", nil, ",", nil, 1024) + m := NewGroupConcat("distinct ", nil, ",", []sql.Expression{expression.NewUnresolvedColumn("field")}, 1024) assert.Equal("group_concat(distinct field separator ',')", m.String()) - m = NewGroupConcat("field", nil, "-", nil, 1024) + m = NewGroupConcat("distinct ", nil, "-", []sql.Expression{expression.NewUnresolvedColumn("field")}, 1024) assert.Equal("group_concat(distinct field separator '-')", m.String()) @@ -41,9 +43,30 @@ func TestGroupConcat_FunctionName(t *testing.T) { {Expr: expression.NewUnresolvedColumn("field2"), Order: sql.Descending}, } - m = NewGroupConcat("field", sc, "-", nil, 1024) + m = NewGroupConcat("distinct ", sc, "a'b\\c", []sql.Expression{expression.NewUnresolvedColumn("field")}, 1024) - assert.Equal("group_concat(distinct field order by field ASC, field2 DESC separator '-')", m.String()) + assert.Equal("group_concat(distinct field order by field ASC, field2 DESC separator 'a''b\\\\c')", m.String()) + parsed, ok := exprtest.RequireExpression(t, exprtest.ParseExpression(t, m)).(*sqlparser.GroupConcatExpr) + require.True(t, ok) + require.Equal(t, m.distinct, parsed.Distinct) + require.Len(t, parsed.Exprs, len(m.selectExprs)) + for i, output := range m.selectExprs { + exprtest.AssertExpressionValue(t, parsed.Exprs[i].(*sqlparser.AliasedExpr).Expr, output) + } + require.Len(t, parsed.OrderBy, len(m.sortConditions)) + for i, order := range m.sortConditions { + exprtest.AssertExpressionValue(t, parsed.OrderBy[i].Expr, order.Expr) + expectedDirection := sqlparser.AscScr + if order.Order == sql.Descending { + expectedDirection = sqlparser.DescScr + } + require.Equal(t, expectedDirection, parsed.OrderBy[i].Direction) + } + require.Equal(t, m.separator, parsed.Separator.SeparatorString) + require.False(t, parsed.Separator.DefaultSeparator) + + windowed := m.WithWindow(sql.NewEmptyContext(), &sql.WindowDefinition{}) + assert.Equal("group_concat(distinct field order by field ASC, field2 DESC separator 'a''b\\\\c') over ()", windowed.String()) } // Validates that the return length of GROUP_CONCAT is bounded by group_concat_max_len (default 1024) diff --git a/sql/expression/function/aggregation/json_agg.go b/sql/expression/function/aggregation/json_agg.go index d71be85b46..ee08e36c3a 100644 --- a/sql/expression/function/aggregation/json_agg.go +++ b/sql/expression/function/aggregation/json_agg.go @@ -75,7 +75,11 @@ func (j *JSONObjectAgg) Resolved() bool { } func (j *JSONObjectAgg) String() string { - return fmt.Sprintf("JSON_OBJECTAGG(%s, %s)", j.key, j.value) + ret := fmt.Sprintf("JSON_OBJECTAGG(%s, %s)", j.key, j.value) + if j.window != nil { + ret += " " + j.window.String() + } + return ret } // Type implements the Expression interface. diff --git a/sql/expression/function/aggregation/json_agg_test.go b/sql/expression/function/aggregation/json_agg_test.go index 239a36421f..f1b4403d1d 100644 --- a/sql/expression/function/aggregation/json_agg_test.go +++ b/sql/expression/function/aggregation/json_agg_test.go @@ -19,6 +19,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/dolthub/go-mysql-server/internal/exprtest" "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/expression" "github.com/dolthub/go-mysql-server/sql/types" @@ -29,6 +30,27 @@ func TestJsonArrayAgg_Name(t *testing.T) { m := NewJsonArray(expression.NewGetField(0, types.Int32, "field", true)) assert.Equal("JSON_ARRAYAGG(field)", m.String()) + + windowed := m.WithWindow(sql.NewEmptyContext(), &sql.WindowDefinition{}) + assert.Equal("JSON_ARRAYAGG(field) over ()", windowed.String()) + exprtest.AssertFunctionRoundTripAs(t, windowed.(sql.FunctionExpression), "JSON_ARRAYAGG") +} + +func TestJSONObjectAggString(t *testing.T) { + ctx := sql.NewEmptyContext() + expr := NewJSONObjectAgg( + ctx, + expression.NewGetField(0, types.Text, "k", false), + expression.NewGetField(1, types.Int64, "v", false), + ).(sql.WindowAdaptableExpression).WithWindow( + ctx, + sql.NewWindowDefinition( + []sql.Expression{expression.NewGetField(2, types.Int64, "g", false)}, + nil, nil, "", "", + ), + ) + require.Equal(t, "JSON_OBJECTAGG(k, v) over ( partition by g)", expr.String()) + exprtest.AssertFunctionRoundTrip(t, expr.(sql.FunctionExpression)) } func TestJsonArrayAgg_SimpleIntField(t *testing.T) { diff --git a/sql/expression/function/aggregation/last_test.go b/sql/expression/function/aggregation/last_test.go index cb41de28c2..0b04cd4424 100644 --- a/sql/expression/function/aggregation/last_test.go +++ b/sql/expression/function/aggregation/last_test.go @@ -19,6 +19,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/dolthub/go-mysql-server/internal/exprtest" "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/expression" "github.com/dolthub/go-mysql-server/sql/types" @@ -43,3 +44,10 @@ func TestLast(t *testing.T) { }) } } + +func TestLastString(t *testing.T) { + expr := NewLast(expression.NewGetField(0, types.Text, "value", false)). + WithWindow(sql.NewEmptyContext(), &sql.WindowDefinition{}) + require.Equal(t, "LAST_VALUE(value) over ()", expr.String()) + exprtest.AssertFunctionRoundTripAs(t, expr.(sql.FunctionExpression), "LAST_VALUE") +} diff --git a/sql/expression/function/aggregation/max_test.go b/sql/expression/function/aggregation/max_test.go index 51e8dd764a..c4307f41b7 100644 --- a/sql/expression/function/aggregation/max_test.go +++ b/sql/expression/function/aggregation/max_test.go @@ -20,6 +20,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/dolthub/go-mysql-server/internal/exprtest" "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/expression" "github.com/dolthub/go-mysql-server/sql/types" @@ -29,6 +30,10 @@ func TestMax_String(t *testing.T) { assert := require.New(t) m := NewMax(expression.NewGetField(0, types.Int32, "field", true)) assert.Equal("MAX(field)", m.String()) + + windowed := m.WithWindow(sql.NewEmptyContext(), &sql.WindowDefinition{}) + assert.Equal("MAX(field) over ()", windowed.String()) + exprtest.AssertFunctionRoundTrip(t, windowed.(sql.FunctionExpression)) } func TestMax_Eval_Int32(t *testing.T) { diff --git a/sql/expression/function/aggregation/min_test.go b/sql/expression/function/aggregation/min_test.go index ae0e2aac97..ca1fddf7e0 100644 --- a/sql/expression/function/aggregation/min_test.go +++ b/sql/expression/function/aggregation/min_test.go @@ -20,6 +20,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/dolthub/go-mysql-server/internal/exprtest" "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/expression" "github.com/dolthub/go-mysql-server/sql/types" @@ -30,6 +31,10 @@ func TestMin_Name(t *testing.T) { m := NewMin(expression.NewGetField(0, types.Int32, "field", true)) assert.Equal("MIN(field)", m.String()) + + windowed := m.WithWindow(sql.NewEmptyContext(), &sql.WindowDefinition{}) + assert.Equal("MIN(field) over ()", windowed.String()) + exprtest.AssertFunctionRoundTrip(t, windowed.(sql.FunctionExpression)) } func TestMin_Eval_Int32(t *testing.T) { diff --git a/sql/expression/function/aggregation/std_test.go b/sql/expression/function/aggregation/std_test.go index f49418256b..962b0c8f30 100644 --- a/sql/expression/function/aggregation/std_test.go +++ b/sql/expression/function/aggregation/std_test.go @@ -20,6 +20,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/dolthub/go-mysql-server/internal/exprtest" "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/expression" ) @@ -28,6 +29,13 @@ func isFloatEqual(a, b float64) bool { return math.Abs(a-b) < 1e-9 } +func TestStdDevPopString(t *testing.T) { + expr := NewStdDevPop(expression.NewGetField(0, nil, "value", false)). + WithWindow(sql.NewEmptyContext(), &sql.WindowDefinition{}) + require.Equal(t, "STDDEV_POP(value) over ()", expr.String()) + exprtest.AssertFunctionRoundTripAs(t, expr.(sql.FunctionExpression), "STDDEV_POP") +} + func TestStd(t *testing.T) { sum := NewStdDevPop(expression.NewGetField(0, nil, "", false)) @@ -180,6 +188,20 @@ func TestStdSamp(t *testing.T) { } } +func TestStdDevSampString(t *testing.T) { + expr := NewStdDevSamp(expression.NewGetField(0, nil, "value", false)). + WithWindow(sql.NewEmptyContext(), &sql.WindowDefinition{}) + require.Equal(t, "STDDEV_SAMP(value) over ()", expr.String()) + exprtest.AssertFunctionRoundTripAs(t, expr.(sql.FunctionExpression), "STDDEV_SAMP") +} + +func TestVarPopString(t *testing.T) { + expr := NewVarPop(expression.NewGetField(0, nil, "value", false)). + WithWindow(sql.NewEmptyContext(), &sql.WindowDefinition{}) + require.Equal(t, "VAR_POP(value) over ()", expr.String()) + exprtest.AssertFunctionRoundTripAs(t, expr.(sql.FunctionExpression), "VAR_POP") +} + func TestVariance(t *testing.T) { sum := NewVarPop(expression.NewGetField(0, nil, "", false)) @@ -256,6 +278,13 @@ func TestVariance(t *testing.T) { } } +func TestVarSampString(t *testing.T) { + expr := NewVarSamp(expression.NewGetField(0, nil, "value", false)). + WithWindow(sql.NewEmptyContext(), &sql.WindowDefinition{}) + require.Equal(t, "VAR_SAMP(value) over ()", expr.String()) + exprtest.AssertFunctionRoundTripAs(t, expr.(sql.FunctionExpression), "VAR_SAMP") +} + func TestVarSamp(t *testing.T) { sum := NewVarSamp(expression.NewGetField(0, nil, "", false)) diff --git a/sql/expression/function/aggregation/sum_test.go b/sql/expression/function/aggregation/sum_test.go index 2c12700fae..e15b44cfa8 100644 --- a/sql/expression/function/aggregation/sum_test.go +++ b/sql/expression/function/aggregation/sum_test.go @@ -19,10 +19,18 @@ import ( "github.com/stretchr/testify/require" + "github.com/dolthub/go-mysql-server/internal/exprtest" "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/expression" ) +func TestSumString(t *testing.T) { + expr := NewSum(expression.NewGetField(0, nil, "value", false)). + WithWindow(sql.NewEmptyContext(), &sql.WindowDefinition{}) + require.Equal(t, "SUM(value) over ()", expr.String()) + exprtest.AssertFunctionRoundTrip(t, expr.(sql.FunctionExpression)) +} + func TestSum(t *testing.T) { sum := NewSum(expression.NewGetField(0, nil, "", false)) diff --git a/sql/expression/function/aggregation/unary_aggs.og.go b/sql/expression/function/aggregation/unary_aggs.og.go index 5d418c2ea3..17b4bf596b 100644 --- a/sql/expression/function/aggregation/unary_aggs.og.go +++ b/sql/expression/function/aggregation/unary_aggs.og.go @@ -16,6 +16,7 @@ type AnyValue struct { var _ sql.FunctionExpression = (*AnyValue)(nil) var _ sql.Aggregation = (*AnyValue)(nil) +var _ sql.Describable = (*AnyValue)(nil) var _ sql.WindowAdaptableExpression = (*AnyValue)(nil) func NewAnyValue(e sql.Expression) *AnyValue { @@ -37,25 +38,33 @@ func (a *AnyValue) IsNullable(ctx *sql.Context) bool { } func (a *AnyValue) String() string { + ret := "ANY_VALUE(" + a.Child.String() + ")" if a.window != nil { - pr := sql.NewTreePrinter() - _ = pr.WriteNode("ANYVALUE") - children := []string{a.window.String(), a.Child.String()} - pr.WriteChildren(children...) - return pr.String() + ret += " " + a.window.String() } - return "ANYVALUE(" + a.Child.String() + ")" + return ret } -func (a *AnyValue) DebugString(ctx *sql.Context) string { +func (a *AnyValue) Describe(ctx *sql.Context, options sql.DescribeOptions) string { + if options.Debug { + if a.window != nil { + pr := sql.NewTreePrinter() + _ = pr.WriteNode("ANY_VALUE") + children := []string{sql.Describe(ctx, a.window, options), sql.Describe(ctx, a.Child, options)} + pr.WriteChildren(children...) + return pr.String() + } + return fmt.Sprintf("ANY_VALUE(%s)", sql.Describe(ctx, a.Child, options)) + } + ret := "ANY_VALUE(" + sql.Describe(ctx, a.Child, options) + ")" if a.window != nil { - pr := sql.NewTreePrinter() - _ = pr.WriteNode("ANYVALUE") - children := []string{sql.DebugString(ctx, a.window), sql.DebugString(ctx, a.Child)} - pr.WriteChildren(children...) - return pr.String() + ret += " " + sql.Describe(ctx, a.window, options) } - return fmt.Sprintf("ANYVALUE(%s)", sql.DebugString(ctx, a.Child)) + return ret +} + +func (a *AnyValue) DebugString(ctx *sql.Context) string { + return a.Describe(ctx, sql.DescribeOptions{Debug: true}) } func (a *AnyValue) WithWindow(ctx *sql.Context, window *sql.WindowDefinition) sql.WindowAdaptableExpression { @@ -95,6 +104,7 @@ type Avg struct { var _ sql.FunctionExpression = (*Avg)(nil) var _ sql.Aggregation = (*Avg)(nil) +var _ sql.Describable = (*Avg)(nil) var _ sql.WindowAdaptableExpression = (*Avg)(nil) func NewAvg(e sql.Expression) *Avg { @@ -116,25 +126,33 @@ func (a *Avg) IsNullable(ctx *sql.Context) bool { } func (a *Avg) String() string { + ret := "AVG(" + a.Child.String() + ")" if a.window != nil { - pr := sql.NewTreePrinter() - _ = pr.WriteNode("AVG") - children := []string{a.window.String(), a.Child.String()} - pr.WriteChildren(children...) - return pr.String() + ret += " " + a.window.String() } - return "AVG(" + a.Child.String() + ")" + return ret } -func (a *Avg) DebugString(ctx *sql.Context) string { +func (a *Avg) Describe(ctx *sql.Context, options sql.DescribeOptions) string { + if options.Debug { + if a.window != nil { + pr := sql.NewTreePrinter() + _ = pr.WriteNode("AVG") + children := []string{sql.Describe(ctx, a.window, options), sql.Describe(ctx, a.Child, options)} + pr.WriteChildren(children...) + return pr.String() + } + return fmt.Sprintf("AVG(%s)", sql.Describe(ctx, a.Child, options)) + } + ret := "AVG(" + sql.Describe(ctx, a.Child, options) + ")" if a.window != nil { - pr := sql.NewTreePrinter() - _ = pr.WriteNode("AVG") - children := []string{sql.DebugString(ctx, a.window), sql.DebugString(ctx, a.Child)} - pr.WriteChildren(children...) - return pr.String() + ret += " " + sql.Describe(ctx, a.window, options) } - return fmt.Sprintf("AVG(%s)", sql.DebugString(ctx, a.Child)) + return ret +} + +func (a *Avg) DebugString(ctx *sql.Context) string { + return a.Describe(ctx, sql.DescribeOptions{Debug: true}) } func (a *Avg) WithWindow(ctx *sql.Context, window *sql.WindowDefinition) sql.WindowAdaptableExpression { @@ -174,6 +192,7 @@ type BitAnd struct { var _ sql.FunctionExpression = (*BitAnd)(nil) var _ sql.Aggregation = (*BitAnd)(nil) +var _ sql.Describable = (*BitAnd)(nil) var _ sql.WindowAdaptableExpression = (*BitAnd)(nil) func NewBitAnd(e sql.Expression) *BitAnd { @@ -195,25 +214,33 @@ func (a *BitAnd) IsNullable(ctx *sql.Context) bool { } func (a *BitAnd) String() string { + ret := "BIT_AND(" + a.Child.String() + ")" if a.window != nil { - pr := sql.NewTreePrinter() - _ = pr.WriteNode("BITAND") - children := []string{a.window.String(), a.Child.String()} - pr.WriteChildren(children...) - return pr.String() + ret += " " + a.window.String() } - return "BITAND(" + a.Child.String() + ")" + return ret } -func (a *BitAnd) DebugString(ctx *sql.Context) string { +func (a *BitAnd) Describe(ctx *sql.Context, options sql.DescribeOptions) string { + if options.Debug { + if a.window != nil { + pr := sql.NewTreePrinter() + _ = pr.WriteNode("BIT_AND") + children := []string{sql.Describe(ctx, a.window, options), sql.Describe(ctx, a.Child, options)} + pr.WriteChildren(children...) + return pr.String() + } + return fmt.Sprintf("BIT_AND(%s)", sql.Describe(ctx, a.Child, options)) + } + ret := "BIT_AND(" + sql.Describe(ctx, a.Child, options) + ")" if a.window != nil { - pr := sql.NewTreePrinter() - _ = pr.WriteNode("BITAND") - children := []string{sql.DebugString(ctx, a.window), sql.DebugString(ctx, a.Child)} - pr.WriteChildren(children...) - return pr.String() + ret += " " + sql.Describe(ctx, a.window, options) } - return fmt.Sprintf("BITAND(%s)", sql.DebugString(ctx, a.Child)) + return ret +} + +func (a *BitAnd) DebugString(ctx *sql.Context) string { + return a.Describe(ctx, sql.DescribeOptions{Debug: true}) } func (a *BitAnd) WithWindow(ctx *sql.Context, window *sql.WindowDefinition) sql.WindowAdaptableExpression { @@ -253,6 +280,7 @@ type BitOr struct { var _ sql.FunctionExpression = (*BitOr)(nil) var _ sql.Aggregation = (*BitOr)(nil) +var _ sql.Describable = (*BitOr)(nil) var _ sql.WindowAdaptableExpression = (*BitOr)(nil) func NewBitOr(e sql.Expression) *BitOr { @@ -274,25 +302,33 @@ func (a *BitOr) IsNullable(ctx *sql.Context) bool { } func (a *BitOr) String() string { + ret := "BIT_OR(" + a.Child.String() + ")" if a.window != nil { - pr := sql.NewTreePrinter() - _ = pr.WriteNode("BITOR") - children := []string{a.window.String(), a.Child.String()} - pr.WriteChildren(children...) - return pr.String() + ret += " " + a.window.String() } - return "BITOR(" + a.Child.String() + ")" + return ret } -func (a *BitOr) DebugString(ctx *sql.Context) string { +func (a *BitOr) Describe(ctx *sql.Context, options sql.DescribeOptions) string { + if options.Debug { + if a.window != nil { + pr := sql.NewTreePrinter() + _ = pr.WriteNode("BIT_OR") + children := []string{sql.Describe(ctx, a.window, options), sql.Describe(ctx, a.Child, options)} + pr.WriteChildren(children...) + return pr.String() + } + return fmt.Sprintf("BIT_OR(%s)", sql.Describe(ctx, a.Child, options)) + } + ret := "BIT_OR(" + sql.Describe(ctx, a.Child, options) + ")" if a.window != nil { - pr := sql.NewTreePrinter() - _ = pr.WriteNode("BITOR") - children := []string{sql.DebugString(ctx, a.window), sql.DebugString(ctx, a.Child)} - pr.WriteChildren(children...) - return pr.String() + ret += " " + sql.Describe(ctx, a.window, options) } - return fmt.Sprintf("BITOR(%s)", sql.DebugString(ctx, a.Child)) + return ret +} + +func (a *BitOr) DebugString(ctx *sql.Context) string { + return a.Describe(ctx, sql.DescribeOptions{Debug: true}) } func (a *BitOr) WithWindow(ctx *sql.Context, window *sql.WindowDefinition) sql.WindowAdaptableExpression { @@ -332,6 +368,7 @@ type BitXor struct { var _ sql.FunctionExpression = (*BitXor)(nil) var _ sql.Aggregation = (*BitXor)(nil) +var _ sql.Describable = (*BitXor)(nil) var _ sql.WindowAdaptableExpression = (*BitXor)(nil) func NewBitXor(e sql.Expression) *BitXor { @@ -353,25 +390,33 @@ func (a *BitXor) IsNullable(ctx *sql.Context) bool { } func (a *BitXor) String() string { + ret := "BIT_XOR(" + a.Child.String() + ")" if a.window != nil { - pr := sql.NewTreePrinter() - _ = pr.WriteNode("BITXOR") - children := []string{a.window.String(), a.Child.String()} - pr.WriteChildren(children...) - return pr.String() + ret += " " + a.window.String() } - return "BITXOR(" + a.Child.String() + ")" + return ret } -func (a *BitXor) DebugString(ctx *sql.Context) string { +func (a *BitXor) Describe(ctx *sql.Context, options sql.DescribeOptions) string { + if options.Debug { + if a.window != nil { + pr := sql.NewTreePrinter() + _ = pr.WriteNode("BIT_XOR") + children := []string{sql.Describe(ctx, a.window, options), sql.Describe(ctx, a.Child, options)} + pr.WriteChildren(children...) + return pr.String() + } + return fmt.Sprintf("BIT_XOR(%s)", sql.Describe(ctx, a.Child, options)) + } + ret := "BIT_XOR(" + sql.Describe(ctx, a.Child, options) + ")" if a.window != nil { - pr := sql.NewTreePrinter() - _ = pr.WriteNode("BITXOR") - children := []string{sql.DebugString(ctx, a.window), sql.DebugString(ctx, a.Child)} - pr.WriteChildren(children...) - return pr.String() + ret += " " + sql.Describe(ctx, a.window, options) } - return fmt.Sprintf("BITXOR(%s)", sql.DebugString(ctx, a.Child)) + return ret +} + +func (a *BitXor) DebugString(ctx *sql.Context) string { + return a.Describe(ctx, sql.DescribeOptions{Debug: true}) } func (a *BitXor) WithWindow(ctx *sql.Context, window *sql.WindowDefinition) sql.WindowAdaptableExpression { @@ -411,6 +456,7 @@ type Count struct { var _ sql.FunctionExpression = (*Count)(nil) var _ sql.Aggregation = (*Count)(nil) +var _ sql.Describable = (*Count)(nil) var _ sql.WindowAdaptableExpression = (*Count)(nil) func NewCount(e sql.Expression) *Count { @@ -432,25 +478,33 @@ func (a *Count) IsNullable(ctx *sql.Context) bool { } func (a *Count) String() string { + ret := "COUNT(" + a.Child.String() + ")" if a.window != nil { - pr := sql.NewTreePrinter() - _ = pr.WriteNode("COUNT") - children := []string{a.window.String(), a.Child.String()} - pr.WriteChildren(children...) - return pr.String() + ret += " " + a.window.String() } - return "COUNT(" + a.Child.String() + ")" + return ret } -func (a *Count) DebugString(ctx *sql.Context) string { +func (a *Count) Describe(ctx *sql.Context, options sql.DescribeOptions) string { + if options.Debug { + if a.window != nil { + pr := sql.NewTreePrinter() + _ = pr.WriteNode("COUNT") + children := []string{sql.Describe(ctx, a.window, options), sql.Describe(ctx, a.Child, options)} + pr.WriteChildren(children...) + return pr.String() + } + return fmt.Sprintf("COUNT(%s)", sql.Describe(ctx, a.Child, options)) + } + ret := "COUNT(" + sql.Describe(ctx, a.Child, options) + ")" if a.window != nil { - pr := sql.NewTreePrinter() - _ = pr.WriteNode("COUNT") - children := []string{sql.DebugString(ctx, a.window), sql.DebugString(ctx, a.Child)} - pr.WriteChildren(children...) - return pr.String() + ret += " " + sql.Describe(ctx, a.window, options) } - return fmt.Sprintf("COUNT(%s)", sql.DebugString(ctx, a.Child)) + return ret +} + +func (a *Count) DebugString(ctx *sql.Context) string { + return a.Describe(ctx, sql.DescribeOptions{Debug: true}) } func (a *Count) WithWindow(ctx *sql.Context, window *sql.WindowDefinition) sql.WindowAdaptableExpression { @@ -490,6 +544,7 @@ type First struct { var _ sql.FunctionExpression = (*First)(nil) var _ sql.Aggregation = (*First)(nil) +var _ sql.Describable = (*First)(nil) var _ sql.WindowAdaptableExpression = (*First)(nil) func NewFirst(e sql.Expression) *First { @@ -512,24 +567,30 @@ func (a *First) IsNullable(ctx *sql.Context) bool { func (a *First) String() string { if a.window != nil { - pr := sql.NewTreePrinter() - _ = pr.WriteNode("FIRST") - children := []string{a.window.String(), a.Child.String()} - pr.WriteChildren(children...) - return pr.String() + return "FIRST_VALUE(" + a.Child.String() + ") " + a.window.String() } return "FIRST(" + a.Child.String() + ")" } -func (a *First) DebugString(ctx *sql.Context) string { +func (a *First) Describe(ctx *sql.Context, options sql.DescribeOptions) string { + if options.Debug { + if a.window != nil { + pr := sql.NewTreePrinter() + _ = pr.WriteNode("FIRST") + children := []string{sql.Describe(ctx, a.window, options), sql.Describe(ctx, a.Child, options)} + pr.WriteChildren(children...) + return pr.String() + } + return fmt.Sprintf("FIRST(%s)", sql.Describe(ctx, a.Child, options)) + } if a.window != nil { - pr := sql.NewTreePrinter() - _ = pr.WriteNode("FIRST") - children := []string{sql.DebugString(ctx, a.window), sql.DebugString(ctx, a.Child)} - pr.WriteChildren(children...) - return pr.String() + return "FIRST_VALUE(" + sql.Describe(ctx, a.Child, options) + ") " + sql.Describe(ctx, a.window, options) } - return fmt.Sprintf("FIRST(%s)", sql.DebugString(ctx, a.Child)) + return "FIRST(" + sql.Describe(ctx, a.Child, options) + ")" +} + +func (a *First) DebugString(ctx *sql.Context) string { + return a.Describe(ctx, sql.DescribeOptions{Debug: true}) } func (a *First) WithWindow(ctx *sql.Context, window *sql.WindowDefinition) sql.WindowAdaptableExpression { @@ -569,6 +630,7 @@ type JsonArray struct { var _ sql.FunctionExpression = (*JsonArray)(nil) var _ sql.Aggregation = (*JsonArray)(nil) +var _ sql.Describable = (*JsonArray)(nil) var _ sql.WindowAdaptableExpression = (*JsonArray)(nil) func NewJsonArray(e sql.Expression) *JsonArray { @@ -590,25 +652,33 @@ func (a *JsonArray) IsNullable(ctx *sql.Context) bool { } func (a *JsonArray) String() string { + ret := "JSON_ARRAYAGG(" + a.Child.String() + ")" if a.window != nil { - pr := sql.NewTreePrinter() - _ = pr.WriteNode("JSON_ARRAYAGG") - children := []string{a.window.String(), a.Child.String()} - pr.WriteChildren(children...) - return pr.String() + ret += " " + a.window.String() } - return "JSON_ARRAYAGG(" + a.Child.String() + ")" + return ret } -func (a *JsonArray) DebugString(ctx *sql.Context) string { +func (a *JsonArray) Describe(ctx *sql.Context, options sql.DescribeOptions) string { + if options.Debug { + if a.window != nil { + pr := sql.NewTreePrinter() + _ = pr.WriteNode("JSON_ARRAYAGG") + children := []string{sql.Describe(ctx, a.window, options), sql.Describe(ctx, a.Child, options)} + pr.WriteChildren(children...) + return pr.String() + } + return fmt.Sprintf("JSON_ARRAYAGG(%s)", sql.Describe(ctx, a.Child, options)) + } + ret := "JSON_ARRAYAGG(" + sql.Describe(ctx, a.Child, options) + ")" if a.window != nil { - pr := sql.NewTreePrinter() - _ = pr.WriteNode("JSON_ARRAYAGG") - children := []string{sql.DebugString(ctx, a.window), sql.DebugString(ctx, a.Child)} - pr.WriteChildren(children...) - return pr.String() + ret += " " + sql.Describe(ctx, a.window, options) } - return fmt.Sprintf("JSON_ARRAYAGG(%s)", sql.DebugString(ctx, a.Child)) + return ret +} + +func (a *JsonArray) DebugString(ctx *sql.Context) string { + return a.Describe(ctx, sql.DescribeOptions{Debug: true}) } func (a *JsonArray) WithWindow(ctx *sql.Context, window *sql.WindowDefinition) sql.WindowAdaptableExpression { @@ -648,6 +718,7 @@ type Last struct { var _ sql.FunctionExpression = (*Last)(nil) var _ sql.Aggregation = (*Last)(nil) +var _ sql.Describable = (*Last)(nil) var _ sql.WindowAdaptableExpression = (*Last)(nil) func NewLast(e sql.Expression) *Last { @@ -670,24 +741,30 @@ func (a *Last) IsNullable(ctx *sql.Context) bool { func (a *Last) String() string { if a.window != nil { - pr := sql.NewTreePrinter() - _ = pr.WriteNode("LAST") - children := []string{a.window.String(), a.Child.String()} - pr.WriteChildren(children...) - return pr.String() + return "LAST_VALUE(" + a.Child.String() + ") " + a.window.String() } return "LAST(" + a.Child.String() + ")" } -func (a *Last) DebugString(ctx *sql.Context) string { +func (a *Last) Describe(ctx *sql.Context, options sql.DescribeOptions) string { + if options.Debug { + if a.window != nil { + pr := sql.NewTreePrinter() + _ = pr.WriteNode("LAST") + children := []string{sql.Describe(ctx, a.window, options), sql.Describe(ctx, a.Child, options)} + pr.WriteChildren(children...) + return pr.String() + } + return fmt.Sprintf("LAST(%s)", sql.Describe(ctx, a.Child, options)) + } if a.window != nil { - pr := sql.NewTreePrinter() - _ = pr.WriteNode("LAST") - children := []string{sql.DebugString(ctx, a.window), sql.DebugString(ctx, a.Child)} - pr.WriteChildren(children...) - return pr.String() + return "LAST_VALUE(" + sql.Describe(ctx, a.Child, options) + ") " + sql.Describe(ctx, a.window, options) } - return fmt.Sprintf("LAST(%s)", sql.DebugString(ctx, a.Child)) + return "LAST(" + sql.Describe(ctx, a.Child, options) + ")" +} + +func (a *Last) DebugString(ctx *sql.Context) string { + return a.Describe(ctx, sql.DescribeOptions{Debug: true}) } func (a *Last) WithWindow(ctx *sql.Context, window *sql.WindowDefinition) sql.WindowAdaptableExpression { @@ -727,6 +804,7 @@ type Max struct { var _ sql.FunctionExpression = (*Max)(nil) var _ sql.Aggregation = (*Max)(nil) +var _ sql.Describable = (*Max)(nil) var _ sql.WindowAdaptableExpression = (*Max)(nil) func NewMax(e sql.Expression) *Max { @@ -748,25 +826,33 @@ func (a *Max) IsNullable(ctx *sql.Context) bool { } func (a *Max) String() string { + ret := "MAX(" + a.Child.String() + ")" if a.window != nil { - pr := sql.NewTreePrinter() - _ = pr.WriteNode("MAX") - children := []string{a.window.String(), a.Child.String()} - pr.WriteChildren(children...) - return pr.String() + ret += " " + a.window.String() } - return "MAX(" + a.Child.String() + ")" + return ret } -func (a *Max) DebugString(ctx *sql.Context) string { +func (a *Max) Describe(ctx *sql.Context, options sql.DescribeOptions) string { + if options.Debug { + if a.window != nil { + pr := sql.NewTreePrinter() + _ = pr.WriteNode("MAX") + children := []string{sql.Describe(ctx, a.window, options), sql.Describe(ctx, a.Child, options)} + pr.WriteChildren(children...) + return pr.String() + } + return fmt.Sprintf("MAX(%s)", sql.Describe(ctx, a.Child, options)) + } + ret := "MAX(" + sql.Describe(ctx, a.Child, options) + ")" if a.window != nil { - pr := sql.NewTreePrinter() - _ = pr.WriteNode("MAX") - children := []string{sql.DebugString(ctx, a.window), sql.DebugString(ctx, a.Child)} - pr.WriteChildren(children...) - return pr.String() + ret += " " + sql.Describe(ctx, a.window, options) } - return fmt.Sprintf("MAX(%s)", sql.DebugString(ctx, a.Child)) + return ret +} + +func (a *Max) DebugString(ctx *sql.Context) string { + return a.Describe(ctx, sql.DescribeOptions{Debug: true}) } func (a *Max) WithWindow(ctx *sql.Context, window *sql.WindowDefinition) sql.WindowAdaptableExpression { @@ -806,6 +892,7 @@ type Min struct { var _ sql.FunctionExpression = (*Min)(nil) var _ sql.Aggregation = (*Min)(nil) +var _ sql.Describable = (*Min)(nil) var _ sql.WindowAdaptableExpression = (*Min)(nil) func NewMin(e sql.Expression) *Min { @@ -827,25 +914,33 @@ func (a *Min) IsNullable(ctx *sql.Context) bool { } func (a *Min) String() string { + ret := "MIN(" + a.Child.String() + ")" if a.window != nil { - pr := sql.NewTreePrinter() - _ = pr.WriteNode("MIN") - children := []string{a.window.String(), a.Child.String()} - pr.WriteChildren(children...) - return pr.String() + ret += " " + a.window.String() } - return "MIN(" + a.Child.String() + ")" + return ret } -func (a *Min) DebugString(ctx *sql.Context) string { +func (a *Min) Describe(ctx *sql.Context, options sql.DescribeOptions) string { + if options.Debug { + if a.window != nil { + pr := sql.NewTreePrinter() + _ = pr.WriteNode("MIN") + children := []string{sql.Describe(ctx, a.window, options), sql.Describe(ctx, a.Child, options)} + pr.WriteChildren(children...) + return pr.String() + } + return fmt.Sprintf("MIN(%s)", sql.Describe(ctx, a.Child, options)) + } + ret := "MIN(" + sql.Describe(ctx, a.Child, options) + ")" if a.window != nil { - pr := sql.NewTreePrinter() - _ = pr.WriteNode("MIN") - children := []string{sql.DebugString(ctx, a.window), sql.DebugString(ctx, a.Child)} - pr.WriteChildren(children...) - return pr.String() + ret += " " + sql.Describe(ctx, a.window, options) } - return fmt.Sprintf("MIN(%s)", sql.DebugString(ctx, a.Child)) + return ret +} + +func (a *Min) DebugString(ctx *sql.Context) string { + return a.Describe(ctx, sql.DescribeOptions{Debug: true}) } func (a *Min) WithWindow(ctx *sql.Context, window *sql.WindowDefinition) sql.WindowAdaptableExpression { @@ -885,6 +980,7 @@ type Sum struct { var _ sql.FunctionExpression = (*Sum)(nil) var _ sql.Aggregation = (*Sum)(nil) +var _ sql.Describable = (*Sum)(nil) var _ sql.WindowAdaptableExpression = (*Sum)(nil) func NewSum(e sql.Expression) *Sum { @@ -906,25 +1002,33 @@ func (a *Sum) IsNullable(ctx *sql.Context) bool { } func (a *Sum) String() string { + ret := "SUM(" + a.Child.String() + ")" if a.window != nil { - pr := sql.NewTreePrinter() - _ = pr.WriteNode("SUM") - children := []string{a.window.String(), a.Child.String()} - pr.WriteChildren(children...) - return pr.String() + ret += " " + a.window.String() } - return "SUM(" + a.Child.String() + ")" + return ret } -func (a *Sum) DebugString(ctx *sql.Context) string { +func (a *Sum) Describe(ctx *sql.Context, options sql.DescribeOptions) string { + if options.Debug { + if a.window != nil { + pr := sql.NewTreePrinter() + _ = pr.WriteNode("SUM") + children := []string{sql.Describe(ctx, a.window, options), sql.Describe(ctx, a.Child, options)} + pr.WriteChildren(children...) + return pr.String() + } + return fmt.Sprintf("SUM(%s)", sql.Describe(ctx, a.Child, options)) + } + ret := "SUM(" + sql.Describe(ctx, a.Child, options) + ")" if a.window != nil { - pr := sql.NewTreePrinter() - _ = pr.WriteNode("SUM") - children := []string{sql.DebugString(ctx, a.window), sql.DebugString(ctx, a.Child)} - pr.WriteChildren(children...) - return pr.String() + ret += " " + sql.Describe(ctx, a.window, options) } - return fmt.Sprintf("SUM(%s)", sql.DebugString(ctx, a.Child)) + return ret +} + +func (a *Sum) DebugString(ctx *sql.Context) string { + return a.Describe(ctx, sql.DescribeOptions{Debug: true}) } func (a *Sum) WithWindow(ctx *sql.Context, window *sql.WindowDefinition) sql.WindowAdaptableExpression { @@ -964,6 +1068,7 @@ type StdDevPop struct { var _ sql.FunctionExpression = (*StdDevPop)(nil) var _ sql.Aggregation = (*StdDevPop)(nil) +var _ sql.Describable = (*StdDevPop)(nil) var _ sql.WindowAdaptableExpression = (*StdDevPop)(nil) func NewStdDevPop(e sql.Expression) *StdDevPop { @@ -985,25 +1090,33 @@ func (a *StdDevPop) IsNullable(ctx *sql.Context) bool { } func (a *StdDevPop) String() string { + ret := "STDDEV_POP(" + a.Child.String() + ")" if a.window != nil { - pr := sql.NewTreePrinter() - _ = pr.WriteNode("STDDEVPOP") - children := []string{a.window.String(), a.Child.String()} - pr.WriteChildren(children...) - return pr.String() + ret += " " + a.window.String() } - return "STDDEVPOP(" + a.Child.String() + ")" + return ret } -func (a *StdDevPop) DebugString(ctx *sql.Context) string { +func (a *StdDevPop) Describe(ctx *sql.Context, options sql.DescribeOptions) string { + if options.Debug { + if a.window != nil { + pr := sql.NewTreePrinter() + _ = pr.WriteNode("STDDEV_POP") + children := []string{sql.Describe(ctx, a.window, options), sql.Describe(ctx, a.Child, options)} + pr.WriteChildren(children...) + return pr.String() + } + return fmt.Sprintf("STDDEV_POP(%s)", sql.Describe(ctx, a.Child, options)) + } + ret := "STDDEV_POP(" + sql.Describe(ctx, a.Child, options) + ")" if a.window != nil { - pr := sql.NewTreePrinter() - _ = pr.WriteNode("STDDEVPOP") - children := []string{sql.DebugString(ctx, a.window), sql.DebugString(ctx, a.Child)} - pr.WriteChildren(children...) - return pr.String() + ret += " " + sql.Describe(ctx, a.window, options) } - return fmt.Sprintf("STDDEVPOP(%s)", sql.DebugString(ctx, a.Child)) + return ret +} + +func (a *StdDevPop) DebugString(ctx *sql.Context) string { + return a.Describe(ctx, sql.DescribeOptions{Debug: true}) } func (a *StdDevPop) WithWindow(ctx *sql.Context, window *sql.WindowDefinition) sql.WindowAdaptableExpression { @@ -1043,6 +1156,7 @@ type StdDevSamp struct { var _ sql.FunctionExpression = (*StdDevSamp)(nil) var _ sql.Aggregation = (*StdDevSamp)(nil) +var _ sql.Describable = (*StdDevSamp)(nil) var _ sql.WindowAdaptableExpression = (*StdDevSamp)(nil) func NewStdDevSamp(e sql.Expression) *StdDevSamp { @@ -1064,25 +1178,33 @@ func (a *StdDevSamp) IsNullable(ctx *sql.Context) bool { } func (a *StdDevSamp) String() string { + ret := "STDDEV_SAMP(" + a.Child.String() + ")" if a.window != nil { - pr := sql.NewTreePrinter() - _ = pr.WriteNode("STDDEVSAMP") - children := []string{a.window.String(), a.Child.String()} - pr.WriteChildren(children...) - return pr.String() + ret += " " + a.window.String() } - return "STDDEVSAMP(" + a.Child.String() + ")" + return ret } -func (a *StdDevSamp) DebugString(ctx *sql.Context) string { +func (a *StdDevSamp) Describe(ctx *sql.Context, options sql.DescribeOptions) string { + if options.Debug { + if a.window != nil { + pr := sql.NewTreePrinter() + _ = pr.WriteNode("STDDEV_SAMP") + children := []string{sql.Describe(ctx, a.window, options), sql.Describe(ctx, a.Child, options)} + pr.WriteChildren(children...) + return pr.String() + } + return fmt.Sprintf("STDDEV_SAMP(%s)", sql.Describe(ctx, a.Child, options)) + } + ret := "STDDEV_SAMP(" + sql.Describe(ctx, a.Child, options) + ")" if a.window != nil { - pr := sql.NewTreePrinter() - _ = pr.WriteNode("STDDEVSAMP") - children := []string{sql.DebugString(ctx, a.window), sql.DebugString(ctx, a.Child)} - pr.WriteChildren(children...) - return pr.String() + ret += " " + sql.Describe(ctx, a.window, options) } - return fmt.Sprintf("STDDEVSAMP(%s)", sql.DebugString(ctx, a.Child)) + return ret +} + +func (a *StdDevSamp) DebugString(ctx *sql.Context) string { + return a.Describe(ctx, sql.DescribeOptions{Debug: true}) } func (a *StdDevSamp) WithWindow(ctx *sql.Context, window *sql.WindowDefinition) sql.WindowAdaptableExpression { @@ -1122,6 +1244,7 @@ type VarPop struct { var _ sql.FunctionExpression = (*VarPop)(nil) var _ sql.Aggregation = (*VarPop)(nil) +var _ sql.Describable = (*VarPop)(nil) var _ sql.WindowAdaptableExpression = (*VarPop)(nil) func NewVarPop(e sql.Expression) *VarPop { @@ -1143,25 +1266,33 @@ func (a *VarPop) IsNullable(ctx *sql.Context) bool { } func (a *VarPop) String() string { + ret := "VAR_POP(" + a.Child.String() + ")" if a.window != nil { - pr := sql.NewTreePrinter() - _ = pr.WriteNode("VARPOP") - children := []string{a.window.String(), a.Child.String()} - pr.WriteChildren(children...) - return pr.String() + ret += " " + a.window.String() } - return "VARPOP(" + a.Child.String() + ")" + return ret } -func (a *VarPop) DebugString(ctx *sql.Context) string { +func (a *VarPop) Describe(ctx *sql.Context, options sql.DescribeOptions) string { + if options.Debug { + if a.window != nil { + pr := sql.NewTreePrinter() + _ = pr.WriteNode("VAR_POP") + children := []string{sql.Describe(ctx, a.window, options), sql.Describe(ctx, a.Child, options)} + pr.WriteChildren(children...) + return pr.String() + } + return fmt.Sprintf("VAR_POP(%s)", sql.Describe(ctx, a.Child, options)) + } + ret := "VAR_POP(" + sql.Describe(ctx, a.Child, options) + ")" if a.window != nil { - pr := sql.NewTreePrinter() - _ = pr.WriteNode("VARPOP") - children := []string{sql.DebugString(ctx, a.window), sql.DebugString(ctx, a.Child)} - pr.WriteChildren(children...) - return pr.String() + ret += " " + sql.Describe(ctx, a.window, options) } - return fmt.Sprintf("VARPOP(%s)", sql.DebugString(ctx, a.Child)) + return ret +} + +func (a *VarPop) DebugString(ctx *sql.Context) string { + return a.Describe(ctx, sql.DescribeOptions{Debug: true}) } func (a *VarPop) WithWindow(ctx *sql.Context, window *sql.WindowDefinition) sql.WindowAdaptableExpression { @@ -1201,6 +1332,7 @@ type VarSamp struct { var _ sql.FunctionExpression = (*VarSamp)(nil) var _ sql.Aggregation = (*VarSamp)(nil) +var _ sql.Describable = (*VarSamp)(nil) var _ sql.WindowAdaptableExpression = (*VarSamp)(nil) func NewVarSamp(e sql.Expression) *VarSamp { @@ -1222,25 +1354,33 @@ func (a *VarSamp) IsNullable(ctx *sql.Context) bool { } func (a *VarSamp) String() string { + ret := "VAR_SAMP(" + a.Child.String() + ")" if a.window != nil { - pr := sql.NewTreePrinter() - _ = pr.WriteNode("VARSAMP") - children := []string{a.window.String(), a.Child.String()} - pr.WriteChildren(children...) - return pr.String() + ret += " " + a.window.String() } - return "VARSAMP(" + a.Child.String() + ")" + return ret } -func (a *VarSamp) DebugString(ctx *sql.Context) string { +func (a *VarSamp) Describe(ctx *sql.Context, options sql.DescribeOptions) string { + if options.Debug { + if a.window != nil { + pr := sql.NewTreePrinter() + _ = pr.WriteNode("VAR_SAMP") + children := []string{sql.Describe(ctx, a.window, options), sql.Describe(ctx, a.Child, options)} + pr.WriteChildren(children...) + return pr.String() + } + return fmt.Sprintf("VAR_SAMP(%s)", sql.Describe(ctx, a.Child, options)) + } + ret := "VAR_SAMP(" + sql.Describe(ctx, a.Child, options) + ")" if a.window != nil { - pr := sql.NewTreePrinter() - _ = pr.WriteNode("VARSAMP") - children := []string{sql.DebugString(ctx, a.window), sql.DebugString(ctx, a.Child)} - pr.WriteChildren(children...) - return pr.String() + ret += " " + sql.Describe(ctx, a.window, options) } - return fmt.Sprintf("VARSAMP(%s)", sql.DebugString(ctx, a.Child)) + return ret +} + +func (a *VarSamp) DebugString(ctx *sql.Context) string { + return a.Describe(ctx, sql.DescribeOptions{Debug: true}) } func (a *VarSamp) WithWindow(ctx *sql.Context, window *sql.WindowDefinition) sql.WindowAdaptableExpression { diff --git a/sql/expression/function/aggregation/window/ntile_test.go b/sql/expression/function/aggregation/window/ntile_test.go new file mode 100644 index 0000000000..8a956ad3bf --- /dev/null +++ b/sql/expression/function/aggregation/window/ntile_test.go @@ -0,0 +1,43 @@ +// Copyright 2026 Dolthub, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package window + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/dolthub/go-mysql-server/internal/exprtest" + "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/expression" + "github.com/dolthub/go-mysql-server/sql/types" +) + +func TestNTileString(t *testing.T) { + ctx := sql.NewEmptyContext() + buckets := expression.NewLiteral(2, types.Int64) + expr := NewNTile(ctx, buckets) + expr = expr.(sql.WindowAdaptableExpression).WithWindow(ctx, sql.NewWindowDefinition(nil, nil, nil, "", "")) + require.Equal(t, "ntile(2) over ()", expr.String()) + parsed := exprtest.RequireFunction(t, exprtest.ParseExpression(t, expr)) + require.Equal(t, strings.ToLower(expr.(sql.FunctionExpression).FunctionName()), parsed.Name.Lowered()) + require.Len(t, parsed.Exprs, 1) + exprtest.AssertExpressionValue(t, exprtest.RequireFunctionArgument(t, parsed, 0), buckets) + require.NotNil(t, parsed.Over) + require.Empty(t, parsed.Over.PartitionBy) + require.Empty(t, parsed.Over.OrderBy) + require.Nil(t, parsed.Over.Frame) +} diff --git a/sql/expression/function/aggregation/window_iter.go b/sql/expression/function/aggregation/window_iter.go index b7590bd605..1ffe35968f 100644 --- a/sql/expression/function/aggregation/window_iter.go +++ b/sql/expression/function/aggregation/window_iter.go @@ -50,7 +50,7 @@ var _ sql.Disposable = (*WindowIter)(nil) // Close implements sql.RowIter func (i *WindowIter) Close(ctx *sql.Context) error { i.Dispose(ctx) - var err error + err := i.iter.Close(ctx) for _, p := range i.partitionIters { e := p.Close(ctx) if err == nil && e != nil { diff --git a/sql/expression/function/char.go b/sql/expression/function/char.go index 98ce1dd542..0779605750 100644 --- a/sql/expression/function/char.go +++ b/sql/expression/function/char.go @@ -59,7 +59,10 @@ func (c *Char) String() string { args[i] = arg.String() } str := strings.Join(args, ", ") - return fmt.Sprintf("%s(%s)", c.FunctionName(), str) + if c.Collation == sql.Collation_Unspecified { + return fmt.Sprintf("%s(%s)", c.FunctionName(), str) + } + return fmt.Sprintf("%s(%s USING %s)", c.FunctionName(), str, c.Collation.CharacterSet().Name()) } // Type implements sql.Expression diff --git a/sql/expression/function/char_test.go b/sql/expression/function/char_test.go index 6ae1f723a1..c56a7ee409 100644 --- a/sql/expression/function/char_test.go +++ b/sql/expression/function/char_test.go @@ -17,8 +17,10 @@ package function import ( "testing" + "github.com/dolthub/vitess/go/vt/sqlparser" "github.com/stretchr/testify/require" + "github.com/dolthub/go-mysql-server/internal/exprtest" "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/expression" "github.com/dolthub/go-mysql-server/sql/types" @@ -173,3 +175,19 @@ func TestChar(t *testing.T) { }) } } + +func TestCharString(t *testing.T) { + expr, err := NewChar(sql.NewEmptyContext(), expression.NewLiteral(65, types.Int64)) + require.NoError(t, err) + charExpr := expr.(*Char) + charExpr.Collation = sql.Collation_utf8mb4_0900_ai_ci + + require.Equal(t, "char(65 USING utf8mb4)", charExpr.String()) + parsed, ok := exprtest.RequireExpression(t, exprtest.ParseExpression(t, charExpr)).(*sqlparser.CharExpr) + require.True(t, ok) + require.Equal(t, charExpr.Collation.CharacterSet().Name(), parsed.Type) + require.Len(t, parsed.Exprs, len(charExpr.Children())) + for i, child := range charExpr.Children() { + exprtest.AssertExpressionValue(t, parsed.Exprs[i].(*sqlparser.AliasedExpr).Expr, child) + } +} diff --git a/sql/expression/function/find_in_set.go b/sql/expression/function/find_in_set.go index da8efe272e..269d57719a 100644 --- a/sql/expression/function/find_in_set.go +++ b/sql/expression/function/find_in_set.go @@ -60,7 +60,7 @@ func (*FindInSet) CollationCoercibility(ctx *sql.Context) (collation sql.Collati } func (f *FindInSet) String() string { - return fmt.Sprintf("%s(%s from %s)", f.FunctionName(), f.LeftChild, f.RightChild) + return fmt.Sprintf("%s(%s, %s)", f.FunctionName(), f.LeftChild, f.RightChild) } // WithChildren implements the Expression interface. diff --git a/sql/expression/function/find_in_set_test.go b/sql/expression/function/find_in_set_test.go index dca4db5254..570b860fab 100644 --- a/sql/expression/function/find_in_set_test.go +++ b/sql/expression/function/find_in_set_test.go @@ -19,6 +19,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/dolthub/go-mysql-server/internal/exprtest" "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/expression" "github.com/dolthub/go-mysql-server/sql/types" @@ -125,3 +126,14 @@ func TestFindInSet(t *testing.T) { require.Equal(4, v) }) } + +func TestFindInSetString(t *testing.T) { + ctx := sql.NewEmptyContext() + expr := NewFindInSet( + ctx, + expression.NewLiteral("needle", types.Text), + expression.NewLiteral("haystack,needle", types.Text), + ) + require.Equal(t, "find_in_set('needle', 'haystack,needle')", expr.String()) + exprtest.AssertFunctionRoundTrip(t, expr.(sql.FunctionExpression)) +} diff --git a/sql/expression/function/format.go b/sql/expression/function/format.go index abffb6b608..18ba53ebe3 100644 --- a/sql/expression/function/format.go +++ b/sql/expression/function/format.go @@ -79,6 +79,9 @@ func (f *Format) IsNullable(ctx *sql.Context) bool { } func (f *Format) String() string { + if f.Locale == nil { + return fmt.Sprintf("%s(%s,%s)", f.FunctionName(), f.NumValue, f.NumDecimalPlaces) + } return fmt.Sprintf("%s(%s,%s,%s)", f.FunctionName(), f.NumValue, f.NumDecimalPlaces, f.Locale) } diff --git a/sql/expression/function/format_test.go b/sql/expression/function/format_test.go index 5050c1edc1..4df4361663 100644 --- a/sql/expression/function/format_test.go +++ b/sql/expression/function/format_test.go @@ -20,11 +20,25 @@ import ( "github.com/stretchr/testify/require" "gopkg.in/src-d/go-errors.v1" + "github.com/dolthub/go-mysql-server/internal/exprtest" "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/expression" "github.com/dolthub/go-mysql-server/sql/types" ) +func TestFormatString(t *testing.T) { + ctx := sql.NewEmptyContext() + twoArgs, err := NewFormat(ctx, expression.NewLiteral(1234, types.Int64), expression.NewLiteral(2, types.Int64)) + require.NoError(t, err) + require.Equal(t, "format(1234,2)", twoArgs.String()) + exprtest.AssertFunctionRoundTrip(t, twoArgs.(sql.FunctionExpression)) + + threeArgs, err := NewFormat(ctx, expression.NewLiteral(1234, types.Int64), expression.NewLiteral(2, types.Int64), expression.NewLiteral("de_DE", types.Text)) + require.NoError(t, err) + require.Equal(t, "format(1234,2,'de_DE')", threeArgs.String()) + exprtest.AssertFunctionRoundTrip(t, threeArgs.(sql.FunctionExpression)) +} + func TestFormat(t *testing.T) { testCases := []struct { name string diff --git a/sql/expression/function/functionregistry_test.go b/sql/expression/function/functionregistry_test.go index dbad6123c5..dd9be1afca 100644 --- a/sql/expression/function/functionregistry_test.go +++ b/sql/expression/function/functionregistry_test.go @@ -19,9 +19,12 @@ import ( "github.com/stretchr/testify/require" + "github.com/dolthub/go-mysql-server/internal/exprtest" "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/expression" "github.com/dolthub/go-mysql-server/sql/expression/function" + "github.com/dolthub/go-mysql-server/sql/expression/function/spatial" + "github.com/dolthub/go-mysql-server/sql/types" ) func TestFunctionRegistry(t *testing.T) { @@ -60,3 +63,32 @@ func TestFunctionRegistryMissingFunction(t *testing.T) { require.False(ok) require.Nil(f) } + +func TestMultiLineStringFromTextRegistryString(t *testing.T) { + ctx := sql.NewEmptyContext() + reg := function.NewRegistry() + reg.Register(function.BuiltIns...) + fn, ok := reg.Function(ctx, "", "st_multilinestringfromtext") + require.True(t, ok) + expr, err := fn.NewInstance(ctx, []sql.Expression{ + expression.NewLiteral("MULTILINESTRING((1 2, 3 4))", types.Text), + }) + require.NoError(t, err) + require.IsType(t, &spatial.MLineFromText{}, expr) + require.Equal(t, "st_mlinefromtext('MULTILINESTRING((1 2, 3 4))')", expr.String()) + exprtest.AssertFunctionRoundTrip(t, expr.(sql.FunctionExpression)) +} + +func TestSTEqualsRegistryString(t *testing.T) { + ctx := sql.NewEmptyContext() + reg := function.NewRegistry() + reg.Register(function.BuiltIns...) + fn, ok := reg.Function(ctx, "", "st_equals") + require.True(t, ok) + point := expression.NewLiteral(types.Point{X: 1, Y: 2}, types.PointType{}) + expr, err := fn.NewInstance(ctx, []sql.Expression{point, point}) + require.NoError(t, err) + require.IsType(t, &spatial.STEquals{}, expr) + require.Equal(t, "ST_EQUALS(ST_GeomFromWKB(0x0101000000000000000000F03F0000000000000040, 0), ST_GeomFromWKB(0x0101000000000000000000F03F0000000000000040, 0))", expr.String()) + exprtest.AssertFunctionRoundTrip(t, expr.(sql.FunctionExpression)) +} diff --git a/sql/expression/function/hash.go b/sql/expression/function/hash.go index db19253ad2..fc16ec77f6 100644 --- a/sql/expression/function/hash.go +++ b/sql/expression/function/hash.go @@ -434,7 +434,7 @@ var _ sql.CollationCoercible = (*UncompressedLength)(nil) // NewUncompressedLength returns a new UncompressedLength function expression func NewUncompressedLength(ctx *sql.Context, arg sql.Expression) sql.Expression { - return &UncompressedLength{NewUnaryFunc(arg, "UncompressedLength", types.Uint32)} + return &UncompressedLength{NewUnaryFunc(arg, "uncompressed_length", types.Uint32)} } // Description implements sql.FunctionExpression @@ -505,7 +505,7 @@ var _ sql.CollationCoercible = (*ValidatePasswordStrength)(nil) // NewValidatePasswordStrength returns a new ValidatePasswordStrength function expression func NewValidatePasswordStrength(ctx *sql.Context, arg sql.Expression) sql.Expression { - return &ValidatePasswordStrength{NewUnaryFunc(arg, "ValidatePasswordStrength", types.Uint32)} + return &ValidatePasswordStrength{NewUnaryFunc(arg, "validate_password_strength", types.Uint32)} } // Description implements sql.FunctionExpression diff --git a/sql/expression/function/hash_test.go b/sql/expression/function/hash_test.go index b9c82e0a72..10ea5821b9 100644 --- a/sql/expression/function/hash_test.go +++ b/sql/expression/function/hash_test.go @@ -20,6 +20,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/dolthub/go-mysql-server/internal/exprtest" "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/expression" "github.com/dolthub/go-mysql-server/sql/types" @@ -392,6 +393,10 @@ func TestUncompress(t *testing.T) { } func TestUncompressedLength(t *testing.T) { + stringExpr := NewUncompressedLength(sql.NewEmptyContext(), expression.NewLiteral("value", types.Text)) + require.Equal(t, "uncompressed_length('value')", stringExpr.String()) + exprtest.AssertFunctionRoundTrip(t, stringExpr.(sql.FunctionExpression)) + tests := []struct { val sql.Expression exp interface{} @@ -447,6 +452,10 @@ func TestUncompressedLength(t *testing.T) { } func TestValidatePasswordStrength(t *testing.T) { + stringExpr := NewValidatePasswordStrength(sql.NewEmptyContext(), expression.NewLiteral("value", types.Text)) + require.Equal(t, "validate_password_strength('value')", stringExpr.String()) + exprtest.AssertFunctionRoundTrip(t, stringExpr.(sql.FunctionExpression)) + tests := []struct { val sql.Expression exp interface{} diff --git a/sql/expression/function/json/json_length.go b/sql/expression/function/json/json_length.go index edd3d9fcb0..b93d01dc88 100644 --- a/sql/expression/function/json/json_length.go +++ b/sql/expression/function/json/json_length.go @@ -138,5 +138,5 @@ func (j *JsonLength) WithChildren(ctx *sql.Context, children ...sql.Expression) } func (j *JsonLength) String() string { - return fmt.Sprintf("json_length(%s)", j.JSON.String()) + return fmt.Sprintf("json_length(%s, %s)", j.JSON.String(), j.Path.String()) } diff --git a/sql/expression/function/json/json_length_test.go b/sql/expression/function/json/json_length_test.go new file mode 100644 index 0000000000..0c4f485357 --- /dev/null +++ b/sql/expression/function/json/json_length_test.go @@ -0,0 +1,38 @@ +// Copyright 2026 Dolthub, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package json + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/dolthub/go-mysql-server/internal/exprtest" + "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/expression" + "github.com/dolthub/go-mysql-server/sql/types" +) + +func TestJsonLengthString(t *testing.T) { + ctx := sql.NewEmptyContext() + expr, err := NewJsonLength( + ctx, + expression.NewGetField(0, types.JSON, "doc", false), + expression.NewLiteral("$.items", types.Text), + ) + require.NoError(t, err) + require.Equal(t, "json_length(doc, '$.items')", expr.String()) + exprtest.AssertFunctionRoundTrip(t, expr.(sql.FunctionExpression)) +} diff --git a/sql/expression/function/json/json_search.go b/sql/expression/function/json/json_search.go index 140e2f5c2a..a52fb5ce0c 100644 --- a/sql/expression/function/json/json_search.go +++ b/sql/expression/function/json/json_search.go @@ -119,30 +119,14 @@ func (j *JSONSearch) Resolved() bool { // String implements sql.Expression func (j *JSONSearch) String() string { - // TODO: maybe just don't print if escape/path are nil? - var escapeStr, pathStr string - if j.Escape == nil { - escapeStr = "NULL" - } else { - escapeStr = j.Escape.String() - } - if len(j.Paths) == 0 { - pathStr = "NULL" - } else { - var paths []string - for _, p := range j.Paths { - paths = append(paths, p.String()) + args := []string{j.JSON.String(), j.OneOrAll.String(), j.Search.String()} + if j.Escape != nil { + args = append(args, j.Escape.String()) + for _, path := range j.Paths { + args = append(args, path.String()) } - pathStr = strings.Join(paths, ", ") } - return fmt.Sprintf("%s(%s, %s, %s, %s, %s)", - j.FunctionName(), - j.JSON.String(), - j.OneOrAll.String(), - j.Search.String(), - escapeStr, - pathStr, - ) + return fmt.Sprintf("%s(%s)", j.FunctionName(), strings.Join(args, ", ")) } // Type implements sql.Expression diff --git a/sql/expression/function/json/json_value.go b/sql/expression/function/json/json_value.go index 44cac3e110..de842e5a62 100644 --- a/sql/expression/function/json/json_value.go +++ b/sql/expression/function/json/json_value.go @@ -164,9 +164,39 @@ func (j *JsonValue) String() string { for i, c := range children { parts[i] = c.String() } + if !j.Typ.Equals(jsonValueDefaultType) { + parts = append(parts, fmt.Sprintf("'%s'", jsonValueTypeName(j.Typ))) + } return fmt.Sprintf("json_value(%s)", strings.Join(parts, ", ")) } +func jsonValueTypeName(typ sql.Type) string { + switch { + case typ.Type() == sqltypes.Year: + return "year" + case typ.Type() == sqltypes.Time: + return "time" + case typ.Type() == sqltypes.Date: + return "date" + case typ.Type() == sqltypes.Datetime || typ.Type() == sqltypes.Timestamp: + return "datetime" + case types.IsJSON(typ): + return "json" + case typ == types.Float32: + return "float" + case typ == types.Float64: + return "double" + case types.IsSigned(typ): + return "signed" + case types.IsUnsigned(typ): + return "unsigned" + case types.IsText(typ): + return "char" + default: + return typ.String() + } +} + // GetJSONFromWrapperOrCoercibleString takes a valid argument for JSON functions (either a JSON wrapper type or a string) // and unwraps the JSON, or coerces the string into JSON. The return value can return any type that can be stored in // a JSON column, not just maps. For a complete list, see diff --git a/sql/expression/function/json/jsontests/json_function_test.go b/sql/expression/function/json/jsontests/json_function_test.go index 974c12de0e..548caced63 100644 --- a/sql/expression/function/json/jsontests/json_function_test.go +++ b/sql/expression/function/json/jsontests/json_function_test.go @@ -17,9 +17,11 @@ package jsontests import ( "testing" + "github.com/dolthub/vitess/go/vt/sqlparser" "github.com/stretchr/testify/require" "gopkg.in/src-d/go-errors.v1" + "github.com/dolthub/go-mysql-server/internal/exprtest" "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/expression" "github.com/dolthub/go-mysql-server/sql/expression/function/json" @@ -112,6 +114,36 @@ func TestJsonValue(t *testing.T) { } } +func TestJsonValueString(t *testing.T) { + ctx := sql.NewEmptyContext() + doc := expression.NewGetField(0, types.JSON, "doc", false) + path := expression.NewLiteral("$.a", types.Text) + + signed, err := json.NewJsonValue(ctx, doc, path, expression.NewLiteral(int64(0), types.Int64)) + require.NoError(t, err) + require.Equal(t, "json_value(doc, '$.a', 'signed')", signed.String()) + parsedSigned := exprtest.RequireFunction(t, exprtest.ParseExpression(t, signed)) + signedValue := signed.(*json.JsonValue) + require.Equal(t, signedValue.FunctionName(), parsedSigned.Name.Lowered()) + require.Len(t, parsedSigned.Exprs, 3) + exprtest.AssertExpressionValue(t, exprtest.RequireFunctionArgument(t, parsedSigned, 0), signedValue.JSON) + exprtest.AssertExpressionValue(t, exprtest.RequireFunctionArgument(t, parsedSigned, 1), signedValue.Path) + parsedType := exprtest.RequireFunctionArgument(t, parsedSigned, 2).(*sqlparser.SQLVal) + require.Equal(t, sqlparser.StrVal, parsedType.Type) + require.True(t, types.IsSigned(signedValue.Typ)) + require.Equal(t, "signed", string(parsedType.Val)) + + defaultType, err := json.NewJsonValue(ctx, doc, path) + require.NoError(t, err) + require.Equal(t, "json_value(doc, '$.a')", defaultType.String()) + parsedDefault := exprtest.RequireFunction(t, exprtest.ParseExpression(t, defaultType)) + defaultValue := defaultType.(*json.JsonValue) + require.Equal(t, defaultValue.FunctionName(), parsedDefault.Name.Lowered()) + require.Len(t, parsedDefault.Exprs, 2) + exprtest.AssertExpressionValue(t, exprtest.RequireFunctionArgument(t, parsedDefault, 0), defaultValue.JSON) + exprtest.AssertExpressionValue(t, exprtest.RequireFunctionArgument(t, parsedDefault, 1), defaultValue.Path) +} + func TestJsonContainsPath(t *testing.T) { ctx := sql.NewEmptyContext() // Verify arg count 3 or more. diff --git a/sql/expression/function/json/jsontests/json_search_test.go b/sql/expression/function/json/jsontests/json_search_test.go index feae42c1c2..715b3cd021 100644 --- a/sql/expression/function/json/jsontests/json_search_test.go +++ b/sql/expression/function/json/jsontests/json_search_test.go @@ -22,6 +22,7 @@ import ( "github.com/stretchr/testify/require" "gopkg.in/src-d/go-errors.v1" + "github.com/dolthub/go-mysql-server/internal/exprtest" "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/expression" json "github.com/dolthub/go-mysql-server/sql/expression/function/json" @@ -50,6 +51,12 @@ func TestJSONSearch(t *testing.T) { f4 := buildGetFieldExpressions(t, json.NewJSONSearch, 4) f5 := buildGetFieldExpressions(t, json.NewJSONSearch, 5) f6 := buildGetFieldExpressions(t, json.NewJSONSearch, 6) + require.Equal(t, "json_search(arg0, arg1, arg2)", f3.String()) + require.Equal(t, "json_search(arg0, arg1, arg2, arg3)", f4.String()) + require.Equal(t, "json_search(arg0, arg1, arg2, arg3, arg4)", f5.String()) + for _, expr := range []sql.Expression{f3, f4, f5, f6} { + exprtest.AssertFunctionRoundTrip(t, expr.(sql.FunctionExpression)) + } jsonInput := `["abc", [{"k": "10"}, "def"], {"x":"abc"}, {"y":"bcd"}]` diff --git a/sql/expression/function/math.go b/sql/expression/function/math.go index 8b4697ccef..73f73efd77 100644 --- a/sql/expression/function/math.go +++ b/sql/expression/function/math.go @@ -460,7 +460,7 @@ func (a *Atan) Resolved() bool { // String implements sql.Expression func (a *Atan) String() string { if a.x != nil { - return fmt.Sprintf("%s(%s, %s)", a.FunctionName(), a.x, a.y) + return fmt.Sprintf("%s(%s, %s)", a.FunctionName(), a.y, a.x) } return fmt.Sprintf("%s(%s)", a.FunctionName(), a.y) } diff --git a/sql/expression/function/math_test.go b/sql/expression/function/math_test.go index 402fc1426b..dee66893a1 100644 --- a/sql/expression/function/math_test.go +++ b/sql/expression/function/math_test.go @@ -23,6 +23,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/dolthub/go-mysql-server/internal/exprtest" "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/expression" "github.com/dolthub/go-mysql-server/sql/types" @@ -252,6 +253,18 @@ func TestTrigFunctions(t *testing.T) { } } +func TestAtanString(t *testing.T) { + ctx := sql.NewEmptyContext() + expr, err := NewAtan( + ctx, + expression.NewLiteral(1, types.Int64), + expression.NewLiteral(2, types.Int64), + ) + require.NoError(t, err) + require.Equal(t, "atan(1, 2)", expr.String()) + exprtest.AssertFunctionRoundTrip(t, expr.(sql.FunctionExpression)) +} + func withinRoundingErr(v1, v2 float64) bool { const roundingErr = 0.00001 diff := v1 - v2 diff --git a/sql/expression/function/queryinfo.go b/sql/expression/function/queryinfo.go index 557893249b..4026458051 100644 --- a/sql/expression/function/queryinfo.go +++ b/sql/expression/function/queryinfo.go @@ -180,6 +180,9 @@ func (r *LastInsertId) Resolved() bool { // String implements sql.Expression func (r *LastInsertId) String() string { + if r.Child == nil { + return fmt.Sprintf("%s()", r.FunctionName()) + } return fmt.Sprintf("%s(%s)", r.FunctionName(), r.Child) } diff --git a/sql/expression/function/queryinfo_test.go b/sql/expression/function/queryinfo_test.go new file mode 100644 index 0000000000..c9f0d5d8ef --- /dev/null +++ b/sql/expression/function/queryinfo_test.go @@ -0,0 +1,39 @@ +// Copyright 2026 Dolthub, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package function + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/dolthub/go-mysql-server/internal/exprtest" + "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/expression" + "github.com/dolthub/go-mysql-server/sql/types" +) + +func TestLastInsertIdString(t *testing.T) { + ctx := sql.NewEmptyContext() + withoutArg, err := NewLastInsertId(ctx) + require.NoError(t, err) + require.Equal(t, "last_insert_id()", withoutArg.String()) + exprtest.AssertFunctionRoundTrip(t, withoutArg.(sql.FunctionExpression)) + + withArg, err := NewLastInsertId(ctx, expression.NewLiteral(42, types.Int64)) + require.NoError(t, err) + require.Equal(t, "last_insert_id(42)", withArg.String()) + exprtest.AssertFunctionRoundTrip(t, withArg.(sql.FunctionExpression)) +} diff --git a/sql/expression/function/registry.go b/sql/expression/function/registry.go index ae487860c8..6b9e7ffcd9 100644 --- a/sql/expression/function/registry.go +++ b/sql/expression/function/registry.go @@ -250,6 +250,7 @@ var BuiltIns = []sql.Function{ sql.Function1{Name: "st_centroid", Fn: spatial.NewCentroid}, sql.Function2{Name: "st_contains", Fn: spatial.NewContains}, sql.Function2{Name: "st_equal", Fn: spatial.NewSTEquals}, + sql.Function2{Name: "st_equals", Fn: spatial.NewSTEquals}, sql.Function1{Name: "st_endpoint", Fn: spatial.NewEndPoint}, sql.Function1{Name: "st_convexhull", Fn: spatial.NewConvexHull}, sql.Function1{Name: "st_envelope", Fn: spatial.NewEnvelope}, @@ -283,7 +284,7 @@ var BuiltIns = []sql.Function{ sql.FunctionN{Name: "st_linestringfromwkb", Fn: spatial.NewLineFromWKB}, sql.FunctionN{Name: "st_mlinefromtext", Fn: spatial.NewMLineFromText}, sql.FunctionN{Name: "st_mlinefromwkb", Fn: spatial.NewMLineFromWKB}, - sql.FunctionN{Name: "st_multilinestringfromtext", Fn: spatial.NewLineFromText}, + sql.FunctionN{Name: "st_multilinestringfromtext", Fn: spatial.NewMLineFromText}, sql.FunctionN{Name: "st_multilinestringfromwkb", Fn: spatial.NewMLineFromWKB}, sql.FunctionN{Name: "st_mpointfromtext", Fn: spatial.NewMPointFromText}, sql.FunctionN{Name: "st_mpointfromwkb", Fn: spatial.NewMPointFromWKB}, diff --git a/sql/expression/function/spatial/wkb.go b/sql/expression/function/spatial/wkb.go index 4dc5daad56..50e50d9c2d 100644 --- a/sql/expression/function/spatial/wkb.go +++ b/sql/expression/function/spatial/wkb.go @@ -633,7 +633,7 @@ func NewGeomCollFromWKB(ctx *sql.Context, args ...sql.Expression) (sql.Expressio if len(args) < 1 || len(args) > 3 { return nil, sql.ErrInvalidArgumentNumber.New("ST_GEOMCOLLFROMWKB", "1, 2, or 3", len(args)) } - return &MPolyFromWKB{expression.NaryExpression{ChildExpressions: args}}, nil + return &GeomCollFromWKB{expression.NaryExpression{ChildExpressions: args}}, nil } // FunctionName implements sql.FunctionExpression diff --git a/sql/expression/function/spatial/wkb_test.go b/sql/expression/function/spatial/wkb_test.go index 39b18404fb..543a4fa8ad 100644 --- a/sql/expression/function/spatial/wkb_test.go +++ b/sql/expression/function/spatial/wkb_test.go @@ -20,6 +20,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/dolthub/go-mysql-server/internal/exprtest" "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/expression" "github.com/dolthub/go-mysql-server/sql/types" @@ -163,6 +164,20 @@ func TestAsWKB(t *testing.T) { }) } +func TestGeomCollFromWKBString(t *testing.T) { + ctx := sql.NewEmptyContext() + arg := expression.NewLiteral([]byte{1, 7, 0, 0, 0, 0, 0, 0, 0}, types.Blob) + expr, err := NewGeomCollFromWKB(ctx, arg) + require.NoError(t, err) + require.IsType(t, &GeomCollFromWKB{}, expr) + require.Equal(t, "st_geomcollfromwkb(0x010700000000000000)", expr.String()) + exprtest.AssertFunctionRoundTrip(t, expr.(sql.FunctionExpression)) + + cloned, err := expr.WithChildren(ctx, arg) + require.NoError(t, err) + require.IsType(t, &GeomCollFromWKB{}, cloned) +} + func TestGeomFromWKB(t *testing.T) { t.Run("convert point in little endian", func(t *testing.T) { require := require.New(t) diff --git a/sql/expression/function/spatial/wkt.go b/sql/expression/function/spatial/wkt.go index 4fc29fbe68..c20970f170 100644 --- a/sql/expression/function/spatial/wkt.go +++ b/sql/expression/function/spatial/wkt.go @@ -39,12 +39,12 @@ func NewAsWKT(ctx *sql.Context, e sql.Expression) sql.Expression { // FunctionName implements sql.FunctionExpression func (p *AsWKT) FunctionName() string { - return "st_aswkb" + return "st_aswkt" } // Description implements sql.FunctionExpression func (p *AsWKT) Description() string { - return "returns binary representation of given spatial type." + return "returns text representation of given spatial type." } // IsNullable implements the sql.Expression interface. @@ -1058,7 +1058,7 @@ func (p *MPolyFromText) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) return mpoly, err } -// GeomCollFromText is a function that returns a MultiPolygon type from a WKT string +// GeomCollFromText is a function that returns a GeometryCollection type from a WKT string type GeomCollFromText struct { expression.NaryExpression } @@ -1066,12 +1066,12 @@ type GeomCollFromText struct { var _ sql.FunctionExpression = (*GeomCollFromText)(nil) var _ sql.CollationCoercible = (*GeomCollFromText)(nil) -// NewGeomCollFromText creates a new multilinestring expression. +// NewGeomCollFromText creates a new geometry collection expression. func NewGeomCollFromText(ctx *sql.Context, args ...sql.Expression) (sql.Expression, error) { if len(args) < 1 || len(args) > 3 { return nil, sql.ErrInvalidArgumentNumber.New("ST_GeomCollFromText", "1 or 2", len(args)) } - return &MPolyFromText{expression.NaryExpression{ChildExpressions: args}}, nil + return &GeomCollFromText{expression.NaryExpression{ChildExpressions: args}}, nil } // FunctionName implements sql.FunctionExpression @@ -1104,7 +1104,7 @@ func (p *GeomCollFromText) String() string { // WithChildren implements the Expression interface. func (p *GeomCollFromText) WithChildren(ctx *sql.Context, children ...sql.Expression) (sql.Expression, error) { - return NewGeomFromText(ctx, children...) + return NewGeomCollFromText(ctx, children...) } // Eval implements the sql.Expression interface. diff --git a/sql/expression/function/spatial/wkt_test.go b/sql/expression/function/spatial/wkt_test.go index 0f37aefccd..6dc4241fb1 100644 --- a/sql/expression/function/spatial/wkt_test.go +++ b/sql/expression/function/spatial/wkt_test.go @@ -19,6 +19,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/dolthub/go-mysql-server/internal/exprtest" "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/expression" "github.com/dolthub/go-mysql-server/sql/types" @@ -26,6 +27,12 @@ import ( func TestAsWKT(t *testing.T) { ctx := sql.NewEmptyContext() + t.Run("string round trip", func(t *testing.T) { + f := NewAsWKT(ctx, expression.NewLiteral(types.Point{X: 1, Y: 2}, types.PointType{})) + require.Equal(t, "st_aswkt(ST_GeomFromWKB(0x0101000000000000000000F03F0000000000000040, 0))", f.String()) + exprtest.AssertFunctionRoundTrip(t, f.(sql.FunctionExpression)) + }) + t.Run("convert point", func(t *testing.T) { require := require.New(t) f := NewAsWKT(sql.NewEmptyContext(), expression.NewLiteral(types.Point{X: 1, Y: 2}, types.PointType{})) @@ -153,6 +160,20 @@ func TestAsWKT(t *testing.T) { }) } +func TestGeomCollFromTextString(t *testing.T) { + ctx := sql.NewEmptyContext() + arg := expression.NewLiteral("GEOMETRYCOLLECTION(POINT(1 2))", types.Text) + expr, err := NewGeomCollFromText(ctx, arg) + require.NoError(t, err) + require.IsType(t, &GeomCollFromText{}, expr) + require.Equal(t, "st_geomcollfromtext('GEOMETRYCOLLECTION(POINT(1 2))')", expr.String()) + exprtest.AssertFunctionRoundTrip(t, expr.(sql.FunctionExpression)) + + cloned, err := expr.WithChildren(ctx, arg) + require.NoError(t, err) + require.IsType(t, &GeomCollFromText{}, cloned) +} + func TestGeomFromText(t *testing.T) { t.Run("create valid point with well formatted string", func(t *testing.T) { require := require.New(t) diff --git a/sql/expression/function/time.go b/sql/expression/function/time.go index 5f52461aa6..f4ca49cb53 100644 --- a/sql/expression/function/time.go +++ b/sql/expression/function/time.go @@ -418,7 +418,7 @@ func (m *Minute) Description() string { return "returns the minutes of the given date." } -func (m *Minute) String() string { return fmt.Sprintf("%s(%d)", m.FunctionName(), m.Child) } +func (m *Minute) String() string { return fmt.Sprintf("%s(%s)", m.FunctionName(), m.Child) } // Type implements the Expression interface. func (m *Minute) Type(ctx *sql.Context) sql.Type { return types.Int32 } @@ -639,7 +639,7 @@ func (d *YearWeek) Description() string { return "returns year and week for a date. The year in the result may be different from the year in the date argument for the first and the last week of the year." } -func (d *YearWeek) String() string { return fmt.Sprintf("YEARWEEK(%s, %d)", d.date, d.mode) } +func (d *YearWeek) String() string { return fmt.Sprintf("YEARWEEK(%s, %s)", d.date, d.mode) } // Type implements the Expression interface. func (d *YearWeek) Type(ctx *sql.Context) sql.Type { return types.Int32 } @@ -1080,11 +1080,15 @@ func (*Now) CollationCoercibility(ctx *sql.Context) (collation sql.CollationID, // String implements the sql.Expression interface. func (n *Now) String() string { + name := "NOW" + if n.alwaysUseExactTime { + name = "SYSDATE" + } if n.prec == nil { - return "NOW()" + return name + "()" } - return fmt.Sprintf("NOW(%s)", n.prec.String()) + return fmt.Sprintf("%s(%s)", name, n.prec.String()) } // IsNullable implements the sql.Expression interface. @@ -1191,7 +1195,12 @@ func (n *Now) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { // WithChildren implements the Expression interface. func (n *Now) WithChildren(ctx *sql.Context, children ...sql.Expression) (sql.Expression, error) { - return NewNow(ctx, children...) + ret, err := NewNow(ctx, children...) + if err != nil { + return nil, err + } + ret.(*Now).alwaysUseExactTime = n.alwaysUseExactTime + return ret, nil } // NewSysdate returns a new SYSDATE() function, using the supplied |args| for an diff --git a/sql/expression/function/time_test.go b/sql/expression/function/time_test.go index ea12276edf..1a374f29d6 100644 --- a/sql/expression/function/time_test.go +++ b/sql/expression/function/time_test.go @@ -22,6 +22,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/dolthub/go-mysql-server/internal/exprtest" "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/expression" "github.com/dolthub/go-mysql-server/sql/types" @@ -308,6 +309,8 @@ func TestTime_Hour(t *testing.T) { func TestTime_Minute(t *testing.T) { ctx := sql.NewEmptyContext() f := NewMinute(ctx, expression.NewGetField(0, types.LongText, "foo", false)) + require.Equal(t, "minute(foo)", f.String()) + exprtest.AssertFunctionRoundTrip(t, f.(sql.FunctionExpression)) testCases := []struct { name string @@ -492,6 +495,13 @@ func TestYearWeek(t *testing.T) { ctx := sql.NewEmptyContext() f, err := NewYearWeek(ctx, expression.NewGetField(0, types.LongText, "foo", false)) require.NoError(t, err) + require.Equal(t, "YEARWEEK(foo, 0)", f.String()) + exprtest.AssertFunctionRoundTrip(t, f.(sql.FunctionExpression)) + + explicitMode, err := NewYearWeek(ctx, expression.NewGetField(0, types.LongText, "foo", false), expression.NewLiteral(1, types.Int64)) + require.NoError(t, err) + require.Equal(t, "YEARWEEK(foo, 1)", explicitMode.String()) + exprtest.AssertFunctionRoundTrip(t, explicitMode.(sql.FunctionExpression)) testCases := []struct { name string @@ -779,6 +789,25 @@ func TestSysdate(t *testing.T) { } } +func TestNowString(t *testing.T) { + ctx := sql.NewEmptyContext() + precision := expression.NewLiteral(3, types.Int64) + + now, err := NewNow(ctx, precision) + require.NoError(t, err) + require.Equal(t, "NOW(3)", now.String()) + exprtest.AssertFunctionRoundTrip(t, now.(sql.FunctionExpression)) + + sysdate, err := NewSysdate(ctx, precision) + require.NoError(t, err) + require.Equal(t, "SYSDATE(3)", sysdate.String()) + exprtest.AssertFunctionRoundTripAs(t, sysdate.(sql.FunctionExpression), "SYSDATE") + + cloned, err := sysdate.WithChildren(ctx, sysdate.Children()...) + require.NoError(t, err) + require.Equal(t, "SYSDATE(3)", cloned.String()) +} + func TestTime(t *testing.T) { ctx := sql.NewEmptyContext() f := NewTime(ctx, expression.NewGetField(0, types.LongText, "foo", false)) diff --git a/sql/expression/get_field.go b/sql/expression/get_field.go index f5585c0b10..6d2cec7057 100644 --- a/sql/expression/get_field.go +++ b/sql/expression/get_field.go @@ -32,6 +32,7 @@ type GetField struct { db string table string name string + debugName string fieldIndex int // exprId lets the lifecycle of getFields be idempotent. We can re-index @@ -48,6 +49,7 @@ type GetField struct { var _ sql.Expression = (*GetField)(nil) var _ sql.ValueExpression = (*GetField)(nil) var _ sql.CollationCoercible = (*GetField)(nil) +var _ sql.Describable = (*GetField)(nil) var _ sql.IdExpression = (*GetField)(nil) // NewGetField creates a GetField expression. @@ -110,6 +112,13 @@ func (p *GetField) WithName(name string) *GetField { return &p2 } +// WithDebugName returns a copy of this expression with a separate name for debug plan output. +func (p *GetField) WithDebugName(name string) *GetField { + p2 := *p + p2.debugName = name + return &p2 +} + // Resolved implements the Expression interface. func (p *GetField) Resolved() bool { return true @@ -176,15 +185,28 @@ func (p *GetField) String() string { return p.table + "." + p.name } -func (p *GetField) DebugString(ctx *sql.Context) string { +// Describe implements the sql.Describable interface. +func (p *GetField) Describe(ctx *sql.Context, options sql.DescribeOptions) string { + if !options.Debug { + return p.String() + } var notNull string if !p.nullable { notNull = "!null" } + name := p.name + if p.debugName != "" { + name = p.debugName + } if p.table == "" { - return fmt.Sprintf("%s:%d%s", p.name, p.fieldIndex, notNull) + return fmt.Sprintf("%s:%d%s", name, p.fieldIndex, notNull) } - return fmt.Sprintf("%s.%s:%d%s", p.table, p.name, p.fieldIndex, notNull) + return fmt.Sprintf("%s.%s:%d%s", p.table, name, p.fieldIndex, notNull) + +} + +func (p *GetField) DebugString(ctx *sql.Context) string { + return p.Describe(ctx, sql.DescribeOptions{Debug: true}) } // WithIndex returns this same GetField with a new index. diff --git a/sql/expression/get_field_test.go b/sql/expression/get_field_test.go new file mode 100644 index 0000000000..9d24093f5d --- /dev/null +++ b/sql/expression/get_field_test.go @@ -0,0 +1,48 @@ +// Copyright 2026 Dolthub, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package expression + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/dolthub/go-mysql-server/internal/exprtest" + "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/types" +) + +func TestGetFieldString(t *testing.T) { + tests := []struct { + expr *GetField + expected string + }{ + {NewGetField(0, types.Int64, "normal_name", false), "normal_name"}, + {NewGetFieldWithTable(0, 0, types.Int64, "", "table_name", "column_name", false), "table_name.column_name"}, + } + + for _, test := range tests { + require.Equal(t, test.expected, test.expr.String()) + exprtest.AssertColumnRoundTrip(t, test.expr) + } +} + +func TestGetFieldDescribeUsesSeparateDebugName(t *testing.T) { + expr := NewGetField(2, types.Int64, "sum(x) over ()", false).WithDebugName("sum\n └─ x\n") + + require.Equal(t, "sum(x) over ()", expr.String()) + require.Equal(t, "sum(x) over ()", sql.Describe(nil, expr, sql.DescribeOptions{Estimates: true})) + require.Equal(t, "sum\n └─ x\n:2!null", sql.Describe(nil, expr, sql.DescribeOptions{Debug: true})) +} diff --git a/sql/expression/in.go b/sql/expression/in.go index c154757130..6f5e53de3a 100644 --- a/sql/expression/in.go +++ b/sql/expression/in.go @@ -173,6 +173,7 @@ type HashInTuple struct { var _ Comparer = (*HashInTuple)(nil) var _ sql.CollationCoercible = (*HashInTuple)(nil) +var _ sql.Describable = (*HashInTuple)(nil) var _ sql.Expression = (*HashInTuple)(nil) // NewHashInTuple creates an InTuple expression. @@ -363,7 +364,17 @@ func (hit *HashInTuple) Right() sql.Expression { } func (hit *HashInTuple) String() string { - return fmt.Sprintf("(%s HASH IN %s)", hit.in.Left(), hit.in.Right()) + return fmt.Sprintf("(%s IN %s)", hit.in.Left(), hit.in.Right()) +} + +// Describe preserves the physical hash lookup in plan output while String emits parseable SQL. +func (hit *HashInTuple) Describe(ctx *sql.Context, options sql.DescribeOptions) string { + if options.Debug { + return hit.DebugString(ctx) + } + return fmt.Sprintf("(%s HASH IN %s)", + sql.Describe(ctx, hit.in.Left(), options), + sql.Describe(ctx, hit.in.Right(), options)) } func (hit *HashInTuple) DebugString(ctx *sql.Context) string { diff --git a/sql/expression/in_test.go b/sql/expression/in_test.go index e9ba1d3655..c56551ca61 100644 --- a/sql/expression/in_test.go +++ b/sql/expression/in_test.go @@ -19,10 +19,12 @@ import ( "time" "github.com/dolthub/vitess/go/sqltypes" + "github.com/dolthub/vitess/go/vt/sqlparser" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gopkg.in/src-d/go-errors.v1" + "github.com/dolthub/go-mysql-server/internal/exprtest" "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/expression" "github.com/dolthub/go-mysql-server/sql/expression/function" @@ -44,7 +46,20 @@ func TestRoundTripNames(t *testing.T) { expression.NewLiteral(int64(2), types.Int64), )) assert.NoError(t, err) - assert.Equal(t, "(foo HASH IN (2))", hit.String()) + assert.Equal(t, "(foo IN (2))", hit.String()) + parsedParen := exprtest.RequireExpression(t, exprtest.ParseExpression(t, hit)).(*sqlparser.ParenExpr) + parsedIn := parsedParen.Expr.(*sqlparser.ComparisonExpr) + assert.Equal(t, sqlparser.InStr, parsedIn.Operator) + exprtest.AssertExpressionValue(t, parsedIn.Left, hit.Left()) + parsedTuple := parsedIn.Right.(sqlparser.ValTuple) + originalTuple := hit.Right().(expression.Tuple) + assert.Len(t, parsedTuple, len(originalTuple)) + for i, value := range originalTuple { + exprtest.AssertExpressionValue(t, parsedTuple[i], value) + } + assert.Equal(t, "(foo HASH IN (2))", sql.Describe(nil, hit, sql.DescribeOptions{Estimates: true})) + nested := expression.NewAnd(expression.NewLiteral(true, types.Boolean), hit) + assert.Equal(t, "(true AND (foo HASH IN (2)))", sql.Describe(nil, nested, sql.DescribeOptions{Estimates: true})) } func TestInTuple(t *testing.T) { diff --git a/sql/expression/literal.go b/sql/expression/literal.go index ee4fb2891b..cc1c7a823d 100644 --- a/sql/expression/literal.go +++ b/sql/expression/literal.go @@ -18,6 +18,7 @@ import ( "context" "fmt" "strings" + "time" "github.com/cockroachdb/apd/v3" "github.com/dolthub/vitess/go/vt/proto/query" @@ -98,20 +99,34 @@ func (lit *Literal) String() string { case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: return fmt.Sprintf("%d", litVal) case string: - switch lit.Typ.Type() { - // utf8 charset cannot encode binary string - case query.Type_VARBINARY, query.Type_BINARY: - return fmt.Sprintf("'0x%X'", litVal) + if lit.Typ != nil { + switch lit.Typ.Type() { + // utf8 charset cannot encode binary string + case query.Type_VARBINARY, query.Type_BINARY: + return fmt.Sprintf("0x%X", litVal) + } } - // Conversion of \' to \'\' required as this string will be interpreted by the sql engine. - // Backslash chars also need to be replaced. - escaped := strings.ReplaceAll(litVal, "'", "''") - escaped = strings.ReplaceAll(escaped, "\\", "\\\\") - return fmt.Sprintf("'%s'", escaped) + return quoteSQLString(litVal) case *apd.Decimal: return litVal.Text('f') case []byte: return fmt.Sprintf("0x%X", litVal) + case time.Time: + if lit.Typ != nil && lit.Typ.Type() == query.Type_DATE { + return quoteSQLString(litVal.Format("2006-01-02")) + } + return quoteSQLString(litVal.Format(sql.TimestampDatetimeLayout)) + case types.Timespan: + return quoteSQLString(litVal.String()) + case sql.JSONWrapper: + jsonString, err := types.JsonToMySqlString(context.Background(), litVal) + if err == nil { + return fmt.Sprintf("CAST(%s AS JSON)", quoteSQLString(jsonString)) + } + return quoteSQLString(fmt.Sprint(litVal)) + case types.GeometryValue: + serialized := litVal.Serialize() + return fmt.Sprintf("ST_GeomFromWKB(0x%X, %d)", serialized[types.SRIDSize:], litVal.GetSRID()) case nil: return "NULL" default: @@ -119,6 +134,13 @@ func (lit *Literal) String() string { } } +func quoteSQLString(value string) string { + // Conversion of \' to \'\' is required because this string will be interpreted by the SQL engine. + escaped := strings.ReplaceAll(value, "'", "''") + escaped = strings.ReplaceAll(escaped, "\\", "\\\\") + return fmt.Sprintf("'%s'", escaped) +} + func (lit *Literal) DebugString(ctx *sql.Context) string { typeStr := lit.Typ.String() switch v := lit.Val.(type) { diff --git a/sql/expression/literal_test.go b/sql/expression/literal_test.go new file mode 100644 index 0000000000..40f62b1920 --- /dev/null +++ b/sql/expression/literal_test.go @@ -0,0 +1,48 @@ +// Copyright 2026 Dolthub, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package expression + +import ( + "testing" + "time" + + "github.com/dolthub/vitess/go/vt/proto/query" + "github.com/stretchr/testify/require" + + "github.com/dolthub/go-mysql-server/internal/exprtest" + "github.com/dolthub/go-mysql-server/sql/types" +) + +func TestLiteralStringRoundTrips(t *testing.T) { + tests := []struct { + name string + literal *Literal + expected string + }{ + {"binary string", NewLiteral("a\x00", types.MustCreateBinary(query.Type_VARBINARY, 2)), "0x6100"}, + {"date", NewLiteral(time.Date(2026, time.September, 4, 0, 0, 0, 0, time.UTC), types.Date), "'2026-09-04'"}, + {"datetime", NewLiteral(time.Date(2026, time.September, 4, 1, 2, 3, 456000000, time.UTC), types.DatetimeMaxPrecision), "'2026-09-04 01:02:03.456'"}, + {"time", NewLiteral(types.Timespan(45_296_123_456), types.Time), "'12:34:56.123456'"}, + {"json", NewLiteral(types.MustJSON(`{"a": 1}`), types.JSON), `CAST('{"a": 1}' AS JSON)`}, + {"geometry", NewLiteral(types.Point{SRID: 4326, X: 1, Y: 2}, types.PointType{}), "ST_GeomFromWKB(0x0101000000000000000000F03F0000000000000040, 4326)"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.expected, tt.literal.String()) + exprtest.AssertLiteralRoundTrip(t, tt.literal) + }) + } +} diff --git a/sql/expression/procedurereference.go b/sql/expression/procedurereference.go index dae44cecb5..5f4c012f37 100644 --- a/sql/expression/procedurereference.go +++ b/sql/expression/procedurereference.go @@ -19,6 +19,8 @@ import ( "fmt" "strings" + "github.com/dolthub/vitess/go/vt/sqlparser" + "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/types" ) @@ -352,7 +354,7 @@ func (pp *ProcedureParam) Name() string { // String implements the sql.Expression interface. func (pp *ProcedureParam) String() string { - return pp.name + return sqlparser.String(sqlparser.NewColIdent(pp.name)) } // Eval implements the sql.Expression interface. @@ -425,7 +427,7 @@ func (upp *UnresolvedProcedureParam) Name() string { // String implements the sql.Expression interface. func (upp *UnresolvedProcedureParam) String() string { - return upp.name + return sqlparser.String(sqlparser.NewColIdent(upp.name)) } // Eval implements the sql.Expression interface. diff --git a/sql/expression/procedurereference_test.go b/sql/expression/procedurereference_test.go new file mode 100644 index 0000000000..de9c17d8c7 --- /dev/null +++ b/sql/expression/procedurereference_test.go @@ -0,0 +1,36 @@ +// Copyright 2026 Dolthub, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package expression + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/dolthub/go-mysql-server/internal/exprtest" + "github.com/dolthub/go-mysql-server/sql/types" +) + +func TestProcedureParamString(t *testing.T) { + expr := NewProcedureParam("param name", types.Int64) + require.Equal(t, "`param name`", expr.String()) + exprtest.AssertColumnRoundTrip(t, expr) +} + +func TestUnresolvedProcedureParamString(t *testing.T) { + expr := NewUnresolvedProcedureParam("param name") + require.Equal(t, "`param name`", expr.String()) + exprtest.AssertColumnRoundTrip(t, expr) +} diff --git a/sql/expression/star.go b/sql/expression/star.go index e75b3e3f72..80af44682b 100644 --- a/sql/expression/star.go +++ b/sql/expression/star.go @@ -17,6 +17,8 @@ package expression import ( "fmt" + "github.com/dolthub/vitess/go/vt/sqlparser" + "github.com/dolthub/go-mysql-server/sql" ) @@ -67,7 +69,7 @@ func (*Star) CollationCoercibility(ctx *sql.Context) (collation sql.CollationID, func (s *Star) String() string { if s.Table != "" { - return fmt.Sprintf("%s.*", s.Table) + return fmt.Sprintf("%s.*", sqlparser.String(sqlparser.NewTableIdent(s.Table))) } return "*" } diff --git a/sql/expression/star_test.go b/sql/expression/star_test.go new file mode 100644 index 0000000000..93248b200b --- /dev/null +++ b/sql/expression/star_test.go @@ -0,0 +1,39 @@ +// Copyright 2026 Dolthub, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package expression + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/dolthub/go-mysql-server/internal/exprtest" +) + +func TestStarString(t *testing.T) { + tests := []struct { + expr *Star + expected string + }{ + {NewStar(), "*"}, + {NewQualifiedStar("normal_name"), "normal_name.*"}, + {NewQualifiedStar("table name"), "`table name`.*"}, + } + + for _, test := range tests { + require.Equal(t, test.expected, test.expr.String()) + exprtest.AssertStarRoundTrip(t, test.expr, test.expr.Table) + } +} diff --git a/sql/expression/unresolved.go b/sql/expression/unresolved.go index 4be40d5bff..c74e23bd37 100644 --- a/sql/expression/unresolved.go +++ b/sql/expression/unresolved.go @@ -18,6 +18,7 @@ import ( "fmt" "strings" + "github.com/dolthub/vitess/go/vt/sqlparser" "gopkg.in/src-d/go-errors.v1" "github.com/dolthub/go-mysql-server/sql" @@ -299,7 +300,8 @@ func (uf *UnresolvedFunction) String() string { over = fmt.Sprintf(" %s", uf.Window) } - return fmt.Sprintf("%s(%s)%s", uf.name, strings.Join(exprs, ", "), over) + name := sqlparser.String(sqlparser.NewColIdent(uf.name)) + return fmt.Sprintf("%s(%s)%s", name, strings.Join(exprs, ", "), over) } func (uf *UnresolvedFunction) DebugString(ctx *sql.Context) string { diff --git a/sql/expression/unresolved_test.go b/sql/expression/unresolved_test.go index f8ecb1be40..dcc194e9e2 100644 --- a/sql/expression/unresolved_test.go +++ b/sql/expression/unresolved_test.go @@ -19,6 +19,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/dolthub/go-mysql-server/internal/exprtest" "github.com/dolthub/go-mysql-server/sql" ) @@ -31,3 +32,27 @@ func TestUnresolvedExpression(t *testing.T) { o = NewNot(e) require.NotNil(o) } + +func TestUnresolvedColumnString(t *testing.T) { + tests := []struct { + expr *UnresolvedColumn + expected string + }{ + {NewUnresolvedColumn("normal_name"), "normal_name"}, + {NewUnresolvedQualifiedColumn("table_name", "column_name"), "table_name.column_name"}, + } + + for _, test := range tests { + require.Equal(t, test.expected, test.expr.String()) + exprtest.AssertColumnRoundTrip(t, test.expr) + } +} + +func TestUnresolvedFunctionString(t *testing.T) { + expr := NewUnresolvedFunction("function name", false, nil) + require.Equal(t, "`function name`()", expr.String()) + parsed := exprtest.RequireFunction(t, exprtest.ParseExpression(t, expr)) + require.Equal(t, expr.Name(), parsed.Name.String()) + require.Equal(t, len(expr.Arguments), len(parsed.Exprs)) + require.Nil(t, parsed.Over) +} diff --git a/sql/expression/variables.go b/sql/expression/variables.go index d6c2eca500..dbeac091f1 100644 --- a/sql/expression/variables.go +++ b/sql/expression/variables.go @@ -15,6 +15,10 @@ package expression import ( + "fmt" + + "github.com/dolthub/vitess/go/vt/sqlparser" + "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/types" ) @@ -80,7 +84,10 @@ func (v *SystemVar) String() string { if sysVar, _, ok := sql.SystemVariables.GetGlobal(v.Name); ok { return sysVar.DisplayString(v.SpecifiedScope) } - return "" + if v.SpecifiedScope == "" { + return "@@" + v.Name + } + return fmt.Sprintf("@@%s.%s", v.SpecifiedScope, v.Name) } // WithChildren implements the Expression interface. @@ -145,7 +152,9 @@ func (v *UserVar) IsNullable(ctx *sql.Context) bool { return true } func (v *UserVar) Resolved() bool { return true } // String implements the sql.Expression interface. -func (v *UserVar) String() string { return "@" + v.Name } +func (v *UserVar) String() string { + return "@" + sqlparser.String(sqlparser.NewColIdent(v.Name)) +} // WithChildren implements the Expression interface. func (v *UserVar) WithChildren(ctx *sql.Context, children ...sql.Expression) (sql.Expression, error) { diff --git a/sql/expression/variables_test.go b/sql/expression/variables_test.go new file mode 100644 index 0000000000..7162a17647 --- /dev/null +++ b/sql/expression/variables_test.go @@ -0,0 +1,68 @@ +// Copyright 2026 Dolthub, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package expression + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/dolthub/go-mysql-server/internal/exprtest" + "github.com/dolthub/go-mysql-server/sql" +) + +func TestSystemVarString(t *testing.T) { + tests := []struct { + scope string + expected string + }{ + {"", "@@unregistered_system_variable"}, + {"session", "@@session.unregistered_system_variable"}, + } + for _, tt := range tests { + expr := NewSystemVar("unregistered_system_variable", sql.GetMysqlScope(sql.SystemVariableScope_Session), tt.scope) + require.Equal(t, tt.expected, expr.String()) + parsed := exprtest.RequireColumn(t, exprtest.ParseExpression(t, expr)) + parts := strings.Split(strings.TrimPrefix(parsed.Name.String(), "@@"), ".") + if expr.SpecifiedScope == "" { + require.Equal(t, []string{expr.Name}, parts) + } else { + require.Equal(t, []string{expr.SpecifiedScope, expr.Name}, parts) + } + } +} + +func TestUserVarString(t *testing.T) { + tests := []struct { + name string + expected string + }{ + {"normal_name", "@normal_name"}, + {"var name", "@`var name`"}, + {"select", "@`select`"}, + } + + for _, test := range tests { + expr := NewUserVar(test.name) + require.Equal(t, test.expected, expr.String()) + parsed := exprtest.RequireColumn(t, exprtest.ParseExpression(t, expr)) + parsedName := strings.TrimPrefix(parsed.Name.String(), "@") + if strings.HasPrefix(parsedName, "`") { + parsedName = strings.ReplaceAll(strings.Trim(parsedName, "`"), "``", "`") + } + require.Equal(t, expr.Name, parsedName) + } +} diff --git a/sql/expression/wrapper.go b/sql/expression/wrapper.go index 70d2a0b022..1ca49cf4d7 100644 --- a/sql/expression/wrapper.go +++ b/sql/expression/wrapper.go @@ -81,7 +81,7 @@ func (w *Wrapper) Resolved() bool { // String implements sql.Expression func (w *Wrapper) String() string { if w.inner == nil { - return "" + return "NULL" } return fmt.Sprintf("(%s)", w.inner.String()) } diff --git a/sql/expression/wrapper_test.go b/sql/expression/wrapper_test.go new file mode 100644 index 0000000000..c134a30c91 --- /dev/null +++ b/sql/expression/wrapper_test.go @@ -0,0 +1,47 @@ +// Copyright 2026 Dolthub, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package expression + +import ( + "testing" + + "github.com/dolthub/vitess/go/vt/sqlparser" + "github.com/stretchr/testify/require" + + "github.com/dolthub/go-mysql-server/internal/exprtest" + "github.com/dolthub/go-mysql-server/sql/types" +) + +func TestWrapperString(t *testing.T) { + tests := []struct { + expr *Wrapper + expected string + }{ + {WrapExpression(nil), "NULL"}, + {WrapExpression(NewLiteral(42, types.Int64)), "(42)"}, + } + + for _, test := range tests { + require.Equal(t, test.expected, test.expr.String()) + parsed := exprtest.RequireExpression(t, exprtest.ParseExpression(t, test.expr)) + if test.expr.Unwrap() == nil { + require.IsType(t, &sqlparser.NullVal{}, parsed) + } else { + parenthesized, ok := parsed.(*sqlparser.ParenExpr) + require.True(t, ok) + exprtest.AssertExpressionValue(t, parenthesized.Expr, test.expr.Unwrap()) + } + } +} diff --git a/sql/plan/alter_table.go b/sql/plan/alter_table.go index bd52ff3e1b..fa8ab4bd78 100644 --- a/sql/plan/alter_table.go +++ b/sql/plan/alter_table.go @@ -371,8 +371,10 @@ type ColDefaultExpression struct { var _ sql.Expression = ColDefaultExpression{} var _ sql.CollationCoercible = ColDefaultExpression{} -func (c ColDefaultExpression) Resolved() bool { return true } -func (c ColDefaultExpression) String() string { return "" } +func (c ColDefaultExpression) Resolved() bool { return true } +func (c ColDefaultExpression) String() string { + return fmt.Sprintf("DEFAULT(%s)", sql.DefaultMySQLSchemaFormatter.QuoteIdentifier(c.Column.Name)) +} func (c ColDefaultExpression) Type(*sql.Context) sql.Type { return c.Column.Type } func (c ColDefaultExpression) IsNullable(*sql.Context) bool { return c.Column.Default == nil } func (c ColDefaultExpression) CollationCoercibility(ctx *sql.Context) (collation sql.CollationID, coercibility byte) { diff --git a/sql/plan/alter_table_test.go b/sql/plan/alter_table_test.go new file mode 100644 index 0000000000..a8bc415d4b --- /dev/null +++ b/sql/plan/alter_table_test.go @@ -0,0 +1,34 @@ +// Copyright 2026 Dolthub, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package plan + +import ( + "testing" + + "github.com/dolthub/vitess/go/vt/sqlparser" + "github.com/stretchr/testify/require" + + "github.com/dolthub/go-mysql-server/internal/exprtest" + "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/types" +) + +func TestColDefaultExpressionString(t *testing.T) { + expr := ColDefaultExpression{Column: &sql.Column{Name: "odd` name", Type: types.Int64}} + require.Equal(t, "DEFAULT(`odd`` name`)", expr.String()) + parsed, ok := exprtest.RequireExpression(t, exprtest.ParseExpression(t, expr)).(*sqlparser.Default) + require.True(t, ok) + require.Equal(t, expr.Column.Name, parsed.ColName) +} diff --git a/sql/plan/insubquery.go b/sql/plan/insubquery.go index 57073b25ff..3441c0dc95 100644 --- a/sql/plan/insubquery.go +++ b/sql/plan/insubquery.go @@ -163,11 +163,7 @@ func (in *InSubquery) Describe(ctx *sql.Context, options sql.DescribeOptions) st // String implements the fmt.Stringer interface func (in *InSubquery) String() string { - return in.Describe(in.ctx, sql.DescribeOptions{ - Analyze: false, - Estimates: false, - Debug: false, - }) + return fmt.Sprintf("(%s IN %s)", in.Left(), in.Right()) } // DebugString implements the sql.DebugStringer interface diff --git a/sql/plan/subquery.go b/sql/plan/subquery.go index e7f5e7fec2..23518e87d8 100644 --- a/sql/plan/subquery.go +++ b/sql/plan/subquery.go @@ -485,7 +485,7 @@ func (s *Subquery) Describe(ctx *sql.Context, options sql.DescribeOptions) strin } func (s *Subquery) String() string { - return fmt.Sprintf("Subquery(%s)", s.QueryString) + return fmt.Sprintf("(%s)", s.QueryString) } func (s *Subquery) DebugString(ctx *sql.Context) string { diff --git a/sql/plan/subquery_test.go b/sql/plan/subquery_test.go new file mode 100644 index 0000000000..a8220dafcd --- /dev/null +++ b/sql/plan/subquery_test.go @@ -0,0 +1,102 @@ +// Copyright 2026 Dolthub, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package plan + +import ( + "reflect" + "testing" + + "github.com/dolthub/vitess/go/vt/sqlparser" + "github.com/stretchr/testify/require" + + "github.com/dolthub/go-mysql-server/internal/exprtest" + "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/expression" + "github.com/dolthub/go-mysql-server/sql/types" +) + +func TestSubqueryString(t *testing.T) { + expr := NewSubquery(nil, "select 1") + require.Equal(t, "(select 1)", expr.String()) + parsed := exprtest.RequireExpression(t, exprtest.ParseExpression(t, expr)).(*sqlparser.Subquery) + assertParsedSubquery(t, parsed, expr) +} + +func TestInSubqueryString(t *testing.T) { + ctx := sql.NewEmptyContext() + expr := NewInSubquery(ctx, expression.NewUnresolvedColumn("i"), NewSubquery(nil, "select j from t")) + require.Equal(t, "(i IN (select j from t))", expr.String()) + assertParsedInSubquery(t, exprtest.RequireExpression(t, exprtest.ParseExpression(t, expr)), expr) +} + +func TestNestedInSubqueryDescription(t *testing.T) { + ctx := sql.NewEmptyContext() + subquery := NewSubquery(NewUnresolvedTable("t", ""), "select j from t") + inSubquery := NewInSubquery(ctx, expression.NewUnresolvedColumn("i"), subquery) + expr := expression.NewAnd(expression.NewLiteral(true, types.Boolean), inSubquery) + + description := sql.Describe(ctx, expr, sql.DescribeOptions{Estimates: true}) + require.Contains(t, description, "InSubquery") + require.Contains(t, description, "right: Subquery") + require.Contains(t, description, "UnresolvedTable(t)") + + require.Equal(t, "(true AND (i IN (select j from t)))", expr.String()) + parsed := exprtest.RequireExpression(t, exprtest.ParseExpression(t, expr)).(*sqlparser.ParenExpr) + parsedAnd := parsed.Expr.(*sqlparser.AndExpr) + exprtest.AssertExpressionValue(t, parsedAnd.Left, expr.Children()[0]) + assertParsedInSubquery(t, parsedAnd.Right, inSubquery) +} + +func assertParsedInSubquery(t *testing.T, parsed sqlparser.Expr, original *InSubquery) { + t.Helper() + parenthesized := parsed.(*sqlparser.ParenExpr) + comparison := parenthesized.Expr.(*sqlparser.ComparisonExpr) + require.Equal(t, sqlparser.InStr, comparison.Operator) + exprtest.AssertExpressionValue(t, comparison.Left, original.Left()) + assertParsedSubquery(t, comparison.Right.(*sqlparser.Subquery), original.Right().(*Subquery)) +} + +func assertParsedSubquery(t *testing.T, parsed *sqlparser.Subquery, original *Subquery) { + t.Helper() + expectedStatement, err := sqlparser.Parse(original.QueryString) + require.NoError(t, err) + expected := expectedStatement.(*sqlparser.Select) + actual := parsed.Select.(*sqlparser.Select) + require.Len(t, actual.SelectExprs, len(expected.SelectExprs)) + for i := range expected.SelectExprs { + expectedExpr := expected.SelectExprs[i].(*sqlparser.AliasedExpr).Expr + actualExpr := actual.SelectExprs[i].(*sqlparser.AliasedExpr).Expr + require.Equal(t, reflect.TypeOf(expectedExpr), reflect.TypeOf(actualExpr)) + switch expectedExpr := expectedExpr.(type) { + case *sqlparser.SQLVal: + actualValue := actualExpr.(*sqlparser.SQLVal) + require.Equal(t, expectedExpr.Type, actualValue.Type) + require.Equal(t, expectedExpr.Val, actualValue.Val) + case *sqlparser.ColName: + actualColumn := actualExpr.(*sqlparser.ColName) + require.Equal(t, expectedExpr.Name.String(), actualColumn.Name.String()) + require.Equal(t, expectedExpr.Qualifier.Name.String(), actualColumn.Qualifier.Name.String()) + default: + require.Failf(t, "unsupported subquery select expression", "%T", expectedExpr) + } + } + require.Len(t, actual.From, len(expected.From)) + for i := range expected.From { + expectedTable := expected.From[i].(*sqlparser.AliasedTableExpr).Expr.(sqlparser.TableName) + actualTable := actual.From[i].(*sqlparser.AliasedTableExpr).Expr.(sqlparser.TableName) + require.Equal(t, expectedTable.Name.String(), actualTable.Name.String()) + require.Equal(t, expectedTable.DbQualifier.String(), actualTable.DbQualifier.String()) + } +} diff --git a/sql/planbuilder/aggregates.go b/sql/planbuilder/aggregates.go index d99a203a81..e76d2239cb 100644 --- a/sql/planbuilder/aggregates.go +++ b/sql/planbuilder/aggregates.go @@ -202,6 +202,10 @@ func (b *Builder) buildAggregation(fromScope, projScope *scope, groupingCols []s var selectGfs []sql.Expression selectStr := make(map[string]bool) aliasDeps := make(map[string]bool) + windowIds := make(map[sql.ColumnId]struct{}, len(fromScope.windowFuncs)) + for _, col := range fromScope.windowFuncs { + windowIds[sql.ColumnId(col.id)] = struct{}{} + } for _, e := range group.aggregations() { if !selectStr[strings.ToLower(e.String())] { selectDeps = append(selectDeps, e.scalar) @@ -215,7 +219,11 @@ func (b *Builder) buildAggregation(fromScope, projScope *scope, groupingCols []s // eval aliases in project scope switch e := col.scalar.(type) { case *expression.Alias: - if !e.Unreferencable() { + var aliasesWindowFunc bool + if gf, ok := e.Child.(*expression.GetField); ok { + _, aliasesWindowFunc = windowIds[gf.Id()] + } + if !e.Unreferencable() && !aliasesWindowFunc { aliases = append(aliases, e.WithId(sql.ColumnId(col.id)).(*expression.Alias)) inAlias = true } @@ -226,6 +234,9 @@ func (b *Builder) buildAggregation(fromScope, projScope *scope, groupingCols []s findSelectDeps = func(ctx *sql.Context, e sql.Expression) bool { switch e := e.(type) { case *expression.GetField: + if _, ok := windowIds[e.Id()]; ok { + return false + } colName := strings.ToLower(e.String()) if !selectStr[colName] { selectDeps = append(selectDeps, e) @@ -289,7 +300,7 @@ func IsMySQLAggregateFuncName(ctx *sql.Context, name string) (bool, error) { // buildAggregateFunc tags aggregate functions in the correct scope // and makes the aggregate available for reference by other clauses. func (b *Builder) buildAggregateFunc(inScope *scope, name string, e *ast.FuncExpr) sql.Expression { - if len(inScope.windowFuncs) > 0 { + if len(inScope.windowFuncs) > 0 && !inScope.explicitGrouping { err := sql.ErrNonAggregatedColumnWithoutGroupBy.New() b.handleErr(err) } @@ -529,7 +540,7 @@ func IsMySQLWindowFuncName(ctx *sql.Context, name string) (bool, error) { } func (b *Builder) buildWindowFunc(inScope *scope, name string, e *ast.FuncExpr, over *ast.WindowDef) sql.Expression { - if inScope.groupBy != nil { + if inScope.groupBy != nil && !inScope.explicitGrouping { err := sql.ErrNonAggregatedColumnWithoutGroupBy.New() b.handleErr(err) } @@ -540,7 +551,6 @@ func (b *Builder) buildWindowFunc(inScope *scope, name string, e *ast.FuncExpr, e := b.selectExprToExpression(inScope, arg) args = append(args, e) } - var win sql.WindowAdaptableExpression if name == "count" { if _, ok := e.Exprs[0].(*ast.StarExpr); ok { @@ -576,6 +586,9 @@ func (b *Builder) buildWindowFunc(inScope *scope, name string, e *ast.FuncExpr, } col := scopeColumn{col: strings.ToLower(win.String()), scalar: win, typ: win.Type(b.ctx), nullable: win.IsNullable(b.ctx)} + if _, ok := win.(sql.Describable); ok { + col.debugCol = strings.ToLower(sql.Describe(b.ctx, win, sql.DescribeOptions{Debug: true})) + } id := inScope.newColumn(col) col.id = id win = win.WithId(sql.ColumnId(id)).(sql.WindowAdaptableExpression) diff --git a/sql/planbuilder/parse_test.go b/sql/planbuilder/parse_test.go index 9662d41576..365478288c 100644 --- a/sql/planbuilder/parse_test.go +++ b/sql/planbuilder/parse_test.go @@ -1371,7 +1371,7 @@ Project Project ├─ columns: [xy.x:1!null, (xy.x:1!null * xy.y:2!null)->x*y, row_number() over ( partition by xy.x rows between unbounded preceding and unbounded following):4!null->row_num1:5, sum │ ├─ over ( partition by xy.y order by xy.x asc) - │ └─ xy.x + │ └─ xy.x:1!null │ :6!null->sum:7] └─ Window ├─ row_number() over ( partition by xy.x ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) @@ -1398,7 +1398,7 @@ Project Project ├─ columns: [(xy.x:1!null + 1 (tinyint))->x:4, sum │ ├─ over ( partition by xy.y order by xy.x asc) - │ └─ xy.x + │ └─ xy.x:1!null │ :5!null->sum:6] └─ Having ├─ GreaterThan @@ -1407,10 +1407,10 @@ Project └─ Project ├─ columns: [sum │ ├─ over ( partition by xy.y order by xy.x asc) - │ └─ xy.x + │ └─ xy.x:1!null │ :5!null, xy.x:1!null, (xy.x:1!null + 1 (tinyint))->x:4, sum │ ├─ over ( partition by xy.y order by xy.x asc) - │ └─ xy.x + │ └─ xy.x:1!null │ :5!null->sum:6] └─ Window ├─ SUM @@ -1604,7 +1604,7 @@ Project Project ├─ columns: [xy.x:1!null, count │ ├─ over ( order by xy.y asc) - │ └─ 1 + │ └─ 1 (bigint) │ :4!null->count(*) over (order by y)] └─ Sort(xy.x:1!null ASC nullsFirst) └─ Window @@ -1973,7 +1973,7 @@ Union distinct Project ├─ columns: [sum │ ├─ over ( partition by xy.z order by xy.x asc rows between unbounded preceding and current row) - │ └─ xy.y + │ └─ xy.y:2!null │ :4!null->sum(y) over w] └─ Sort(xy.x:1!null ASC nullsFirst) └─ Window diff --git a/sql/planbuilder/project.go b/sql/planbuilder/project.go index ce3c397445..233d5087d8 100644 --- a/sql/planbuilder/project.go +++ b/sql/planbuilder/project.go @@ -211,6 +211,11 @@ func (b *Builder) selectExprToExpression(inScope *scope, se ast.SelectExpr) sql. return expression.NewAlias(b.ctx, e.As.String(), expr) } if selectExprNeedsAlias(b.ctx, e, expr) { + // Scalar subqueries must remain unreferencable by their full text. Their String method is valid SQL, but the + // expression text is not an implicit alias that can be resolved from an outer query. + if _, ok := expr.(*plan.Subquery); ok { + return expression.NewAlias(b.ctx, e.InputExpression, expr).AsUnreferencable() + } // if the input expression is the same as expression string, then it's referencable. // E.g. "SLEEP(1)" is the same as "sleep(1)" if strings.EqualFold(e.InputExpression, expr.String()) { diff --git a/sql/planbuilder/scope.go b/sql/planbuilder/scope.go index 1f0521b792..653b1a67c6 100644 --- a/sql/planbuilder/scope.go +++ b/sql/planbuilder/scope.go @@ -57,7 +57,8 @@ type scope struct { activeSubquery *subquery // groupBy collects aggregation functions and inputs - groupBy *groupBy + groupBy *groupBy + explicitGrouping bool insertTableAlias string @@ -659,6 +660,7 @@ type scopeColumn struct { db string table string col string + debugCol string originalCol string id columnId tableId sql.TableId @@ -713,10 +715,15 @@ func (c scopeColumn) scalarGf() sql.Expression { return e } } + name := c.col if c.originalCol != "" { - return expression.NewGetFieldWithTable(int(c.id), int(c.tableId), c.typ, c.db, c.table, c.originalCol, c.nullable) + name = c.originalCol } - return expression.NewGetFieldWithTable(int(c.id), int(c.tableId), c.typ, c.db, c.table, c.col, c.nullable) + field := expression.NewGetFieldWithTable(int(c.id), int(c.tableId), c.typ, c.db, c.table, name, c.nullable) + if c.debugCol != "" { + field = field.WithDebugName(c.debugCol) + } + return field } func (c scopeColumn) String() string { diff --git a/sql/planbuilder/select.go b/sql/planbuilder/select.go index 772f035704..8b78ed666e 100644 --- a/sql/planbuilder/select.go +++ b/sql/planbuilder/select.go @@ -67,6 +67,7 @@ func (b *Builder) buildSelect(inScope *scope, s *ast.Select) (outScope *scope) { // projections from (4). // 6) Finish with final target projections. fromScope := b.buildFrom(inScope, s.From) + fromScope.explicitGrouping = len(s.GroupBy) > 0 if cn, ok := fromScope.node.(sql.CommentedNode); ok && len(s.Comments) > 0 { fromScope.node = cn.WithComment(string(s.Comments[0])) } @@ -92,9 +93,16 @@ func (b *Builder) buildSelect(inScope *scope, s *ast.Select) (outScope *scope) { // At this point we've recorded dependencies for higher-level scopes, // so we can build the FROM clause - if b.needsAggregation(fromScope, s) { + hasAggregation := b.needsAggregation(fromScope, s) + if hasAggregation { groupingCols := b.buildGroupingCols(fromScope, projScope, s.GroupBy, s.SelectExprs) outScope = b.buildAggregation(fromScope, projScope, groupingCols) + // Window functions operate on the grouped rows that remain after HAVING. + b.buildHaving(fromScope, projScope, outScope, s.Having) + if len(fromScope.windowFuncs) > 0 { + outScope.windowFuncs = fromScope.windowFuncs + outScope = b.buildWindow(outScope, projScope) + } } else if fromScope.windowFuncs != nil { outScope = b.buildWindow(fromScope, projScope) } else { @@ -106,7 +114,9 @@ func (b *Builder) buildSelect(inScope *scope, s *ast.Select) (outScope *scope) { // expressions in higher level scopes will be replaced with GetField // references. - b.buildHaving(fromScope, projScope, outScope, s.Having) + if !hasAggregation { + b.buildHaving(fromScope, projScope, outScope, s.Having) + } b.buildOrderBy(outScope, orderByScope) diff --git a/sql/window.go b/sql/window.go index 737950c6a1..6f6c29127b 100644 --- a/sql/window.go +++ b/sql/window.go @@ -102,6 +102,42 @@ func (w *WindowDefinition) String() string { return sb.String() } +// Describe implements the Describable interface. Window definitions must pass describe options to their expressions +// so that physical expression details are not lost when they are nested in an aggregate. +func (w *WindowDefinition) Describe(ctx *Context, options DescribeOptions) string { + if w == nil { + return "" + } + if options.Debug { + return w.DebugString(ctx) + } + sb := strings.Builder{} + sb.WriteString("over (") + if len(w.PartitionBy) > 0 { + sb.WriteString(" partition by ") + for i, expression := range w.PartitionBy { + if i > 0 { + sb.WriteString(", ") + } + sb.WriteString(Describe(ctx, expression, options)) + } + } + if len(w.OrderBy) > 0 { + sb.WriteString(" order by ") + for i, condition := range w.OrderBy { + if i > 0 { + sb.WriteString(", ") + } + sb.WriteString(fmt.Sprintf("%s %s", Describe(ctx, condition.Expr, options), condition.Order)) + } + } + if w.Frame != nil { + sb.WriteString(fmt.Sprintf(" %s", w.Frame.String())) + } + sb.WriteString(")") + return sb.String() +} + func (w *WindowDefinition) PartitionId() (uint64, error) { if w == nil { return 0, nil