From f0f1af58f02dd0554e7705f8f6519a110ab1b4d8 Mon Sep 17 00:00:00 2001 From: inoas Date: Tue, 30 Apr 2024 16:59:55 +0200 Subject: [PATCH] fix union order by limit offset --- src/cake.gleam | 8 +- src/cake/internal/query.gleam | 283 +++++++++++------- .../select_builder.gleam | 7 +- .../union_builder.gleam | 39 ++- 4 files changed, 221 insertions(+), 116 deletions(-) diff --git a/src/cake.gleam b/src/cake.gleam index 0f04491..2b86230 100644 --- a/src/cake.gleam +++ b/src/cake.gleam @@ -84,14 +84,12 @@ pub fn run_dummy_union_all() { "age", param.IntParam(7), )) - - // UNION must take ORDER BY at the outside - // it can also take an own LIMIT and OFFSET - // |> query.select_query_order_asc("name") + |> query.select_query_order_asc(by: "will_be_removed") let union_query = query.combined_union_all_query_new([select_query_a, select_query_b]) - // |> query.union_set_limit(1) + |> query.combined_query_set_limit(1) + |> query.combined_query_order_replace(by: "age", direction: query.Asc) |> query.query_combined_wrap |> iox.dbg diff --git a/src/cake/internal/query.gleam b/src/cake/internal/query.gleam index badfff5..2280238 100644 --- a/src/cake/internal/query.gleam +++ b/src/cake/internal/query.gleam @@ -1,4 +1,6 @@ // import cake/stdlib/iox +import cake/stdlib/listx +import gleam/int // —————————————————————————————————————————————————————————————————————————— // // ———— Query ——————————————————————————————————————————————————————————————— // @@ -18,7 +20,73 @@ pub fn query_combined_wrap(qry: CombinedQuery) -> Query { } // —————————————————————————————————————————————————————————————————————————— // -// ———— CombinedQuery ——————————————————————————————————————————————————————— // +// ———— OrderByDirectionPart ———————————————————————————————————————————————— // +// —————————————————————————————————————————————————————————————————————————— // + +pub type OrderByPart { + OrderByColumnPart(column: String, direction: OrderByDirectionPart) +} + +pub type OrderByDirectionPart { + Asc + Desc + AscNullsFirst + DescNullsFirst +} + +pub fn order_by_part_to_sql(order_by_part ordbpt: OrderByPart) { + case ordbpt.direction { + Asc -> "ASC NULLS LAST" + Desc -> "DESC NULLS LAST" + AscNullsFirst -> "ASC NULLS FIRST" + DescNullsFirst -> "DESC NULLS FIRST" + } +} + +// —————————————————————————————————————————————————————————————————————————— // +// ———— Limit & Offset Part ————————————————————————————————————————————————— // +// —————————————————————————————————————————————————————————————————————————— // + +pub opaque type LimitOffsetPart { + LimitOffset(limit: Int, offset: Int) + LimitNoOffset(limit: Int) + NoLimitOffset +} + +pub fn limit_offset_new(limit lmt: Int, offset offst: Int) -> LimitOffsetPart { + case lmt >= 0, offst >= 0 { + True, True -> LimitOffset(limit: lmt, offset: offst) + True, False -> LimitNoOffset(limit: lmt) + False, _ -> NoLimitOffset + } +} + +pub fn limit_new(limit lmt: Int) -> LimitOffsetPart { + case lmt >= 0 { + True -> LimitNoOffset(limit: lmt) + False -> NoLimitOffset + } +} + +pub fn limit_offset_apply( + prepared_statement prp_stm: PreparedStatement, + limit_part lmt_prt: LimitOffsetPart, +) -> PreparedStatement { + case lmt_prt { + LimitOffset(limit: lmt, offset: offst) -> + " LIMIT " <> int.to_string(lmt) <> " OFFSET " <> int.to_string(offst) + LimitNoOffset(limit: lmt) -> " LIMIT " <> int.to_string(lmt) + NoLimitOffset -> "" + } + |> prepared_statement.with_sql(prp_stm, _) +} + +pub fn limit_offset_get(select_query slct_qry: SelectQuery) -> LimitOffsetPart { + slct_qry.limit_offset +} + +// —————————————————————————————————————————————————————————————————————————— // +// ———— Combined Query —————————————————————————————————————————————————————— // // —————————————————————————————————————————————————————————————————————————— // pub type CombinedKind { @@ -40,53 +108,50 @@ pub type CombinedQuery { select_queries: List(SelectQuery), limit_offset: LimitOffsetPart, // TODO: before adding epilog to combined, fix it for selects with a custom type - // order_by: List(OrderByPart), + order_by: List(OrderByPart), // Epilog allows you to append raw SQL to the end of queries. // You should never put raw user data into epilog. epilog: EpilogPart, ) - // TODO: order_by of contained selects must be stripped - // ... or rather ignored during prep statement building? - // or still stipped? dev warning? } pub fn combined_union_query_new( select_queries slct_qrys: List(SelectQuery), ) -> CombinedQuery { - Union |> combined_query_query(slct_qrys) + Union |> combined_query_new(slct_qrys) } pub fn combined_union_all_query_new( select_queries slct_qrys: List(SelectQuery), ) -> CombinedQuery { - UnionAll |> combined_query_query(slct_qrys) + UnionAll |> combined_query_new(slct_qrys) } pub fn combined_except_query_new( select_queries slct_qrys: List(SelectQuery), ) -> CombinedQuery { - Except |> combined_query_query(slct_qrys) + Except |> combined_query_new(slct_qrys) } pub fn combined_except_all_query_new( select_queries slct_qrys: List(SelectQuery), ) -> CombinedQuery { - ExceptAll |> combined_query_query(slct_qrys) + ExceptAll |> combined_query_new(slct_qrys) } pub fn combined_intersect_query_new( select_queries slct_qrys: List(SelectQuery), ) -> CombinedQuery { - Intersect |> combined_query_query(slct_qrys) + Intersect |> combined_query_new(slct_qrys) } pub fn combined_intersect_all_query_new( select_queries slct_qrys: List(SelectQuery), ) -> CombinedQuery { - IntersectAll |> combined_query_query(slct_qrys) + IntersectAll |> combined_query_new(slct_qrys) } -fn combined_query_query( +fn combined_query_new( kind: CombinedKind, select_queries: List(SelectQuery), ) -> CombinedQuery { @@ -96,6 +161,7 @@ fn combined_query_query( kind: kind, select_queries: select_queries, limit_offset: NoLimitOffset, + order_by: [], epilog: NoEpilogPart, ) } @@ -113,6 +179,8 @@ pub fn combined_get_select_queries( cq.select_queries } +// ———— LIMIT & OFFSET ————————————————————————————————————————————————————————— // + pub fn combined_query_set_limit( query qry: CombinedQuery, limit lmt: Int, @@ -130,8 +198,94 @@ pub fn combined_query_set_limit_and_offset( CombinedQuery(..qry, limit_offset: limit_offset) } +// ———— ORDER BY —————————————————————————————————————————————————————————————————— // + +pub fn combined_query_order_asc( + query qry: CombinedQuery, + by ordb: String, +) -> CombinedQuery { + do_combined_order_by(qry, OrderByColumnPart(ordb, Asc), True) +} + +pub fn combined_query_order_asc_nulls_first( + query qry: CombinedQuery, + by ordb: String, +) -> CombinedQuery { + do_combined_order_by(qry, OrderByColumnPart(ordb, AscNullsFirst), True) +} + +pub fn combined_query_order_asc_replace( + query qry: CombinedQuery, + by ordb: String, +) -> CombinedQuery { + do_combined_order_by(qry, OrderByColumnPart(ordb, Asc), False) +} + +pub fn combined_query_order_asc_nulls_first_replace( + query qry: CombinedQuery, + by ordb: String, +) -> CombinedQuery { + do_combined_order_by(qry, OrderByColumnPart(ordb, AscNullsFirst), False) +} + +pub fn combined_query_order_desc( + query qry: CombinedQuery, + by ordb: String, +) -> CombinedQuery { + do_combined_order_by(qry, OrderByColumnPart(ordb, Desc), True) +} + +pub fn combined_query_order_desc_nulls_first( + query qry: CombinedQuery, + by ordb: String, +) -> CombinedQuery { + do_combined_order_by(qry, OrderByColumnPart(ordb, DescNullsFirst), True) +} + +pub fn combined_query_order_desc_replace( + query qry: CombinedQuery, + by ordb: String, +) -> CombinedQuery { + do_combined_order_by(qry, OrderByColumnPart(ordb, Desc), False) +} + +pub fn combined_query_order_desc_nulls_first_replace( + query qry: CombinedQuery, + by ordb: String, +) -> CombinedQuery { + do_combined_order_by(qry, OrderByColumnPart(ordb, DescNullsFirst), False) +} + +pub fn combined_query_order( + query qry: CombinedQuery, + by ordb: String, + direction dir: OrderByDirectionPart, +) -> CombinedQuery { + do_combined_order_by(qry, OrderByColumnPart(ordb, dir), True) +} + +pub fn combined_query_order_replace( + query qry: CombinedQuery, + by ordb: String, + direction dir: OrderByDirectionPart, +) -> CombinedQuery { + do_combined_order_by(qry, OrderByColumnPart(ordb, dir), False) +} + +fn do_combined_order_by( + query qry: CombinedQuery, + by ordb: OrderByPart, + append append: Bool, +) -> CombinedQuery { + case append { + True -> + CombinedQuery(..qry, order_by: qry.order_by |> listx.append_item(ordb)) + False -> CombinedQuery(..qry, order_by: listx.wrap(ordb)) + } +} + // —————————————————————————————————————————————————————————————————————————— // -// ———— SelectQuery ————————————————————————————————————————————————————————— // +// ———— Select Query ———————————————————————————————————————————————————————— // // —————————————————————————————————————————————————————————————————————————— // // List of SQL parts that will be used to build a select query. @@ -149,9 +303,6 @@ pub type SelectQuery { // having: String, // window: String, limit_offset: LimitOffsetPart, - // TODO: as not just SELECT but also UNION and maybe other causes - // use order_by, limit and offset, possibly make them real types, too - // at least alias them order_by: List(OrderByPart), epilog: EpilogPart, ) @@ -235,56 +386,56 @@ pub fn select_query_order_asc( query qry: SelectQuery, by ordb: String, ) -> SelectQuery { - do_order_by(qry, OrderByColumnPart(ordb, Asc), True) + do_select_order_by(qry, OrderByColumnPart(ordb, Asc), True) } pub fn select_query_order_asc_nulls_first( query qry: SelectQuery, by ordb: String, ) -> SelectQuery { - do_order_by(qry, OrderByColumnPart(ordb, AscNullsFirst), True) + do_select_order_by(qry, OrderByColumnPart(ordb, AscNullsFirst), True) } pub fn select_query_order_asc_replace( query qry: SelectQuery, by ordb: String, ) -> SelectQuery { - do_order_by(qry, OrderByColumnPart(ordb, Asc), False) + do_select_order_by(qry, OrderByColumnPart(ordb, Asc), False) } pub fn select_query_order_asc_nulls_first_replace( query qry: SelectQuery, by ordb: String, ) -> SelectQuery { - do_order_by(qry, OrderByColumnPart(ordb, AscNullsFirst), False) + do_select_order_by(qry, OrderByColumnPart(ordb, AscNullsFirst), False) } pub fn select_query_order_desc( query qry: SelectQuery, by ordb: String, ) -> SelectQuery { - do_order_by(qry, OrderByColumnPart(ordb, Desc), True) + do_select_order_by(qry, OrderByColumnPart(ordb, Desc), True) } pub fn select_query_order_desc_nulls_first( query qry: SelectQuery, by ordb: String, ) -> SelectQuery { - do_order_by(qry, OrderByColumnPart(ordb, DescNullsFirst), True) + do_select_order_by(qry, OrderByColumnPart(ordb, DescNullsFirst), True) } pub fn select_query_order_desc_replace( query qry: SelectQuery, by ordb: String, ) -> SelectQuery { - do_order_by(qry, OrderByColumnPart(ordb, Desc), False) + do_select_order_by(qry, OrderByColumnPart(ordb, Desc), False) } pub fn select_query_order_desc_nulls_first_replace( query qry: SelectQuery, by ordb: String, ) -> SelectQuery { - do_order_by(qry, OrderByColumnPart(ordb, DescNullsFirst), False) + do_select_order_by(qry, OrderByColumnPart(ordb, DescNullsFirst), False) } pub fn select_query_order( @@ -292,7 +443,7 @@ pub fn select_query_order( by ordb: String, direction dir: OrderByDirectionPart, ) -> SelectQuery { - do_order_by(qry, OrderByColumnPart(ordb, dir), True) + do_select_order_by(qry, OrderByColumnPart(ordb, dir), True) } pub fn select_query_order_replace( @@ -300,12 +451,10 @@ pub fn select_query_order_replace( by ordb: String, direction dir: OrderByDirectionPart, ) -> SelectQuery { - do_order_by(qry, OrderByColumnPart(ordb, dir), False) + do_select_order_by(qry, OrderByColumnPart(ordb, dir), False) } -import cake/stdlib/listx - -fn do_order_by( +fn do_select_order_by( query qry: SelectQuery, by ordb: OrderByPart, append append: Bool, @@ -317,7 +466,7 @@ fn do_order_by( } } -// // ———— LIMIT & OFFSET —————————————————————————————————————————————————————— // +// ———— LIMIT & OFFSET ————————————————————————————————————————————————————————— // pub fn select_query_set_limit( query qry: SelectQuery, @@ -337,7 +486,7 @@ pub fn select_query_set_limit_and_offset( } // —————————————————————————————————————————————————————————————————————————— // -// ———— FromPart ———————————————————————————————————————————————————————————— // +// ———— From Part ——————————————————————————————————————————————————————————— // // —————————————————————————————————————————————————————————————————————————— // pub type FromPart { @@ -360,31 +509,7 @@ pub fn from_part_to_sql(part prt: FromPart) { } // —————————————————————————————————————————————————————————————————————————— // -// ———— OrderByDirectionPart ———————————————————————————————————————————————— // -// —————————————————————————————————————————————————————————————————————————— // - -pub type OrderByPart { - OrderByColumnPart(column: String, direction: OrderByDirectionPart) -} - -pub type OrderByDirectionPart { - Asc - Desc - AscNullsFirst - DescNullsFirst -} - -pub fn order_by_part_to_sql(order_by_part ordbpt: OrderByPart) { - case ordbpt.direction { - Asc -> "ASC NULLS LAST" - Desc -> "DESC NULLS LAST" - AscNullsFirst -> "ASC NULLS FIRST" - DescNullsFirst -> "DESC NULLS FIRST" - } -} - -// —————————————————————————————————————————————————————————————————————————— // -// ———— SelectPart —————————————————————————————————————————————————————————— // +// ———— Select Part ————————————————————————————————————————————————————————— // // —————————————————————————————————————————————————————————————————————————— // pub type SelectPart { @@ -415,7 +540,7 @@ pub fn select_part_to_sql(part prt: SelectPart) { } // —————————————————————————————————————————————————————————————————————————— // -// ———— WherePart ——————————————————————————————————————————————————————————— // +// ———— Where Part —————————————————————————————————————————————————————————— // // —————————————————————————————————————————————————————————————————————————— // import cake/prepared_statement.{type PreparedStatement} @@ -608,50 +733,6 @@ fn where_part_apply_column_in_params( |> prepared_statement.with_sql(")") } -// —————————————————————————————————————————————————————————————————————————— // -// ———— Limit & Offset Part ————————————————————————————————————————————————— // -// —————————————————————————————————————————————————————————————————————————— // - -pub opaque type LimitOffsetPart { - LimitOffset(limit: Int, offset: Int) - LimitNoOffset(limit: Int) - NoLimitOffset -} - -pub fn limit_offset_new(limit lmt: Int, offset offst: Int) -> LimitOffsetPart { - case lmt >= 0, offst >= 0 { - True, True -> LimitOffset(limit: lmt, offset: offst) - True, False -> LimitNoOffset(limit: lmt) - False, _ -> NoLimitOffset - } -} - -pub fn limit_new(limit lmt: Int) -> LimitOffsetPart { - case lmt >= 0 { - True -> LimitNoOffset(limit: lmt) - False -> NoLimitOffset - } -} - -import gleam/int - -pub fn limit_offset_apply( - prepared_statement prp_stm: PreparedStatement, - limit_part lmt_prt: LimitOffsetPart, -) -> PreparedStatement { - case lmt_prt { - LimitOffset(limit: lmt, offset: offst) -> - " LIMIT " <> int.to_string(lmt) <> " OFFSET " <> int.to_string(offst) - LimitNoOffset(limit: lmt) -> " LIMIT " <> int.to_string(lmt) - NoLimitOffset -> "" - } - |> prepared_statement.with_sql(prp_stm, _) -} - -pub fn limit_offset_get(select_query slct_qry: SelectQuery) -> LimitOffsetPart { - slct_qry.limit_offset -} - // —————————————————————————————————————————————————————————————————————————— // // ———— Epilog Part ————————————————————————————————————————————————————————— // // —————————————————————————————————————————————————————————————————————————— // diff --git a/src/cake/prepared_statement_builder/select_builder.gleam b/src/cake/prepared_statement_builder/select_builder.gleam index f74f9a4..a2d9f3a 100644 --- a/src/cake/prepared_statement_builder/select_builder.gleam +++ b/src/cake/prepared_statement_builder/select_builder.gleam @@ -1,8 +1,7 @@ -import cake/internal/query.{ - type OrderByDirectionPart, type OrderByPart, type SelectQuery, -} +import cake/internal/query.{type OrderByPart, type SelectQuery} import cake/prepared_statement.{type PreparedStatement} -import cake/stdlib/iox + +// import cake/stdlib/iox import cake/stdlib/stringx import gleam/list import gleam/string diff --git a/src/cake/prepared_statement_builder/union_builder.gleam b/src/cake/prepared_statement_builder/union_builder.gleam index c9439c9..558a926 100644 --- a/src/cake/prepared_statement_builder/union_builder.gleam +++ b/src/cake/prepared_statement_builder/union_builder.gleam @@ -1,9 +1,10 @@ import cake/internal/query.{ - type CombinedQuery, type SelectQuery, Except, ExceptAll, Intersect, - IntersectAll, Union, UnionAll, + type CombinedQuery, type OrderByPart, type SelectQuery, Except, ExceptAll, + Intersect, IntersectAll, Union, UnionAll, } import cake/prepared_statement.{type PreparedStatement} import cake/prepared_statement_builder/select_builder +import gleam/string // import cake/stdlib/iox import gleam/list @@ -16,10 +17,15 @@ pub fn build( // -> In prepared statements we can already check this and return either an OK() or an Error() // The error would return that the column types missmatch // The user probably let assets this then? - prp_stm_prfx |> prepared_statement.new() |> apply_sql(cq) + prp_stm_prfx + |> prepared_statement.new() + |> apply_command_sql(cq) + |> apply_to_sql(maybe_add_order_sql, cq) + |> query.limit_offset_apply(cq.limit_offset) + |> query.epilog_apply(cq.epilog) } -pub fn apply_sql( +pub fn apply_command_sql( prepared_statement prp_stm: PreparedStatement, select cq: CombinedQuery, ) -> PreparedStatement { @@ -46,6 +52,27 @@ pub fn apply_sql( } }, ) - |> query.limit_offset_apply(cq.limit_offset) - |> query.epilog_apply(cq.epilog) +} + +fn apply_to_sql( + prp_stm: PreparedStatement, + maybe_add_fun: fn(CombinedQuery) -> String, + qry: CombinedQuery, +) -> PreparedStatement { + prepared_statement.with_sql(prp_stm, maybe_add_fun(qry)) +} + +fn maybe_add_order_sql(query qry: CombinedQuery) -> String { + case qry.order_by { + [] -> "" + _ -> { + let order_bys = + qry.order_by + |> list.map(fn(ordrb: OrderByPart) -> String { + ordrb.column <> " " <> query.order_by_part_to_sql(ordrb) + }) + + " ORDER BY " <> string.join(order_bys, ", ") + } + } }