-module(squirrel@internal@database@postgres). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]). -export([main/1]). -export_type([pg_type/0, context/0, nullability/0, plan/0, join_type/0, parent_relation/0]). -type pg_type() :: {p_base, binary()} | {p_array, pg_type()} | {p_option, pg_type()}. -type context() :: {context, squirrel@internal@database@postgres_protocol:connection(), gleam@dict:dict(integer(), squirrel@internal@gleam:type()), gleam@dict:dict({integer(), integer()}, nullability())}. -type nullability() :: nullable | not_nullable. -type plan() :: {plan, gleam@option:option(join_type()), gleam@option:option(parent_relation()), gleam@option:option(list(binary())), gleam@option:option(list(plan()))}. -type join_type() :: full | left | right | other. -type parent_relation() :: inner | not_inner. -spec pg_to_gleam_type(pg_type()) -> squirrel@internal@gleam:type(). pg_to_gleam_type(Type_) -> case Type_ of {p_array, Inner} -> {list, pg_to_gleam_type(Inner)}; {p_option, Inner@1} -> {option, pg_to_gleam_type(Inner@1)}; {p_base, Name} -> case Name of <<"bool"/utf8>> -> bool; <<"text"/utf8>> -> string; <<"char"/utf8>> -> string; <<"float4"/utf8>> -> float; <<"float8"/utf8>> -> float; <<"int2"/utf8>> -> int; <<"int4"/utf8>> -> int; <<"int8"/utf8>> -> int; _ -> erlang:error(#{gleam_error => panic, message => <<"TODO)) unsupported type"/utf8>>, module => <<"squirrel/internal/database/postgres"/utf8>>, function => <<"pg_to_gleam_type"/utf8>>, line => 123}) end end. -spec error_fields_to_parse_error( squirrel@internal@query:'query'(), gleam@set:set(squirrel@internal@database@postgres_protocol:error_or_notice_field()) ) -> squirrel@internal@error:error(). error_fields_to_parse_error(Query, Errors) -> {Code@2, Message@2, Position@3, Hint@2} = (gleam@set:fold( Errors, {none, none, none, none}, fun(Acc, Error_field) -> {Code, Message, Position, Hint} = Acc, case Error_field of {code, Code@1} -> {{some, Code@1}, Message, Position, Hint}; {message, Message@1} -> {Code, {some, Message@1}, Position, Hint}; {hint, Hint@1} -> {Code, Message, Position, {some, Hint@1}}; {position, Position@1} -> case gleam@int:parse(Position@1) of {ok, Position@2} -> {Code, Message, {some, Position@2}, Hint}; {error, _} -> Acc end; _ -> Acc end end )), {cannot_parse_query, Query, Code@2, Message@2, Hint@2, Position@3}. -spec do_nullables_from_plan( plan(), gleam@dict:dict(binary(), integer()), gleam@set:set(integer()) ) -> gleam@set:set(integer()). do_nullables_from_plan(Plan, Query_outputs, Nullables) -> Nullables@2 = case {erlang:element(4, Plan), erlang:element(2, Plan), erlang:element(3, Plan)} of {{some, Outputs}, {some, full}, _} -> gleam@list:fold( Outputs, Nullables, fun(Nullables@1, Output) -> case gleam@dict:get(Query_outputs, Output) of {ok, I} -> gleam@set:insert(Nullables@1, I); {error, _} -> Nullables@1 end end ); {{some, Outputs}, _, {some, inner}} -> gleam@list:fold( Outputs, Nullables, fun(Nullables@1, Output) -> case gleam@dict:get(Query_outputs, Output) of {ok, I} -> gleam@set:insert(Nullables@1, I); {error, _} -> Nullables@1 end end ); {_, _, _} -> Nullables end, case {erlang:element(5, Plan), erlang:element(2, Plan)} of {{some, Plans}, {some, left}} -> gleam@list:fold( Plans, Nullables@2, fun(Nullables@3, Plan@1) -> do_nullables_from_plan(Plan@1, Query_outputs, Nullables@3) end ); {{some, Plans}, {some, right}} -> gleam@list:fold( Plans, Nullables@2, fun(Nullables@3, Plan@1) -> do_nullables_from_plan(Plan@1, Query_outputs, Nullables@3) end ); {_, _} -> Nullables@2 end. -spec nullables_from_plan(plan()) -> gleam@set:set(integer()). nullables_from_plan(Plan) -> Outputs@1 = case erlang:element(4, Plan) of {some, Outputs} -> gleam@list:index_fold( Outputs, gleam@dict:new(), fun gleam@dict:insert/3 ); none -> gleam@dict:new() end, do_nullables_from_plan(Plan, Outputs@1, gleam@set:new()). -spec 'receive'() -> eval:eval(squirrel@internal@database@postgres_protocol:backend_message(), squirrel@internal@error:error(), context()). 'receive'() -> eval:from( fun(_use0) -> {context, Db, _, _} = Context = _use0, case squirrel@internal@database@postgres_protocol:'receive'(Db) of {ok, {Db@1, Msg}} -> {erlang:setelement(2, Context, Db@1), {ok, Msg}}; {error, {read_decode_error, Error}} -> {Context, {error, {pg_cannot_decode_received_message, gleam@string:inspect(Error)}}}; {error, {socket_error, Error@1}} -> {Context, {error, {pg_cannot_receive_message, gleam@string:inspect(Error@1)}}} end end ). -spec send(squirrel@internal@database@postgres_protocol:frontend_message()) -> eval:eval(nil, squirrel@internal@error:error(), context()). send(Message) -> eval:from( fun(_use0) -> {context, Db, _, _} = Context = _use0, Result = begin _pipe = Message, _pipe@1 = squirrel@internal@database@postgres_protocol:encode_frontend_message( _pipe ), squirrel@internal@database@postgres_protocol:send(Db, _pipe@1) end, {Db@2, Result@1} = case Result of {ok, Db@1} -> {Db@1, {ok, nil}}; {error, Error} -> {Db, {error, {pg_cannot_send_message, gleam@string:inspect(Error)}}} end, {erlang:setelement(2, Context, Db@2), Result@1} end ). -spec send_all( list(squirrel@internal@database@postgres_protocol:frontend_message()) ) -> eval:eval(nil, squirrel@internal@error:error(), context()). send_all(Messages) -> squirrel@internal@eval_extra:try_fold( Messages, nil, fun(Acc, Msg) -> eval:'try'(send(Msg), fun(_) -> eval:return(Acc) end) end ). -spec parameters_and_returns(squirrel@internal@query:'query'()) -> eval:eval({list(integer()), list(squirrel@internal@database@postgres_protocol:row_description_field())}, squirrel@internal@error:error(), context()). parameters_and_returns(Query) -> eval:'try'( send_all( [{fe_parse, <<""/utf8>>, erlang:element(4, Query), []}, {fe_describe, prepared_statement, <<""/utf8>>}, fe_sync] ), fun(_) -> eval:'try'('receive'(), fun(Msg) -> case Msg of {be_error_response, Errors} -> eval:throw( error_fields_to_parse_error(Query, Errors) ); be_parse_complete -> eval:'try'( 'receive'(), fun(Msg@1) -> {be_parameter_description, Parameters} = case Msg@1 of {be_parameter_description, _} -> Msg@1; _assert_fail -> erlang:error( #{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail, module => <<"squirrel/internal/database/postgres"/utf8>>, function => <<"parameters_and_returns"/utf8>>, line => 216} ) end, eval:'try'( 'receive'(), fun(Msg@2) -> {be_row_descriptions, Rows} = case Msg@2 of {be_row_descriptions, _} -> Msg@2; _assert_fail@1 -> erlang:error( #{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail@1, module => <<"squirrel/internal/database/postgres"/utf8>>, function => <<"parameters_and_returns"/utf8>>, line => 218} ) end, eval:'try'( 'receive'(), fun(Msg@3) -> {be_ready_for_query, _} = case Msg@3 of {be_ready_for_query, _} -> Msg@3; _assert_fail@2 -> erlang:error( #{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail@2, module => <<"squirrel/internal/database/postgres"/utf8>>, function => <<"parameters_and_returns"/utf8>>, line => 220} ) end, eval:return( {Parameters, Rows} ) end ) end ) end ); _ -> erlang:error(#{gleam_error => panic, message => <<"parse complete: unexpected message sequence"/utf8>>, module => <<"squirrel/internal/database/postgres"/utf8>>, function => <<"parameters_and_returns"/utf8>>, line => 223}) end end) end ). -spec run_query( binary(), list(squirrel@internal@database@postgres_protocol:parameter_value()), list(integer()) ) -> eval:eval(list(bitstring()), squirrel@internal@error:error(), context()). run_query(Query, Parameters, Parameters_object_ids) -> eval:'try'( send_all( [{fe_parse, <<""/utf8>>, Query, Parameters_object_ids}, {fe_bind, <<""/utf8>>, <<""/utf8>>, {format_all, binary}, Parameters, {format_all, binary}}, {fe_execute, <<""/utf8>>, 0}, {fe_close, prepared_statement, <<""/utf8>>}, {fe_close, portal, <<""/utf8>>}, fe_sync] ), fun(_) -> eval:'try'( 'receive'(), fun(Msg) -> be_parse_complete = case Msg of be_parse_complete -> Msg; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail, module => <<"squirrel/internal/database/postgres"/utf8>>, function => <<"run_query"/utf8>>, line => 489}) end, eval:'try'( 'receive'(), fun(Msg@1) -> be_bind_complete = case Msg@1 of be_bind_complete -> Msg@1; _assert_fail@1 -> erlang:error(#{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail@1, module => <<"squirrel/internal/database/postgres"/utf8>>, function => <<"run_query"/utf8>>, line => 491}) end, eval:'try'( 'receive'(), fun(Msg@2) -> {be_message_data_row, Res} = case Msg@2 of {be_message_data_row, _} -> Msg@2; _assert_fail@2 -> erlang:error( #{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail@2, module => <<"squirrel/internal/database/postgres"/utf8>>, function => <<"run_query"/utf8>>, line => 493} ) end, eval:'try'( 'receive'(), fun(Msg@3) -> {be_command_complete, _, _} = case Msg@3 of {be_command_complete, _, _} -> Msg@3; _assert_fail@3 -> erlang:error( #{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail@3, module => <<"squirrel/internal/database/postgres"/utf8>>, function => <<"run_query"/utf8>>, line => 495} ) end, eval:'try'( 'receive'(), fun(Msg@4) -> be_close_complete = case Msg@4 of be_close_complete -> Msg@4; _assert_fail@4 -> erlang:error( #{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail@4, module => <<"squirrel/internal/database/postgres"/utf8>>, function => <<"run_query"/utf8>>, line => 497} ) end, eval:'try'( 'receive'(), fun(Msg@5) -> be_close_complete = case Msg@5 of be_close_complete -> Msg@5; _assert_fail@5 -> erlang:error( #{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail@5, module => <<"squirrel/internal/database/postgres"/utf8>>, function => <<"run_query"/utf8>>, line => 499} ) end, eval:'try'( 'receive'(), fun(Msg@6) -> {be_ready_for_query, _} = case Msg@6 of {be_ready_for_query, _} -> Msg@6; _assert_fail@6 -> erlang:error( #{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail@6, module => <<"squirrel/internal/database/postgres"/utf8>>, function => <<"run_query"/utf8>>, line => 501} ) end, eval:return( Res ) end ) end ) end ) end ) end ) end ) end ) end ). -spec do_wait_until_ready() -> eval:eval(nil, squirrel@internal@error:error(), context()). do_wait_until_ready() -> eval:'try'('receive'(), fun(Msg) -> case Msg of {be_ready_for_query, _} -> eval:return(nil); _ -> do_wait_until_ready() end end). -spec wait_until_ready() -> eval:eval(nil, squirrel@internal@error:error(), context()). wait_until_ready() -> eval:'try'(send(fe_flush), fun(_) -> do_wait_until_ready() end). -spec authenticate() -> eval:eval(nil, squirrel@internal@error:error(), context()). authenticate() -> Params = [{<<"user"/utf8>>, <<"giacomocavalieri"/utf8>>}, {<<"database"/utf8>>, <<"prova"/utf8>>}], eval:'try'( send({fe_startup_message, Params}), fun(_) -> eval:'try'( 'receive'(), fun(Msg) -> be_authentication_ok = case Msg of be_authentication_ok -> Msg; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail, module => <<"squirrel/internal/database/postgres"/utf8>>, function => <<"authenticate"/utf8>>, line => 157}) end, eval:'try'( wait_until_ready(), fun(_) -> eval:return(nil) end ) end ) end ). -spec with_cached_gleam_type( integer(), fun(() -> eval:eval(squirrel@internal@gleam:type(), squirrel@internal@error:error(), context())) ) -> eval:eval(squirrel@internal@gleam:type(), squirrel@internal@error:error(), context()). with_cached_gleam_type(Oid, Do) -> eval:from( fun(Context) -> case gleam@dict:get(erlang:element(3, Context), Oid) of {ok, Type_} -> {Context, {ok, Type_}}; {error, _} -> case eval:step(Do(), Context) of {_, {error, _}} = Result -> Result; {{context, _, Gleam_types, _} = Context@1, {ok, Type_@1}} -> Gleam_types@1 = gleam@dict:insert( Gleam_types, Oid, Type_@1 ), New_context = erlang:setelement( 3, Context@1, Gleam_types@1 ), {New_context, {ok, Type_@1}} end end end ). -spec with_cached_column( integer(), integer(), fun(() -> eval:eval(nullability(), squirrel@internal@error:error(), context())) ) -> eval:eval(nullability(), squirrel@internal@error:error(), context()). with_cached_column(Table_oid, Column, Do) -> eval:from( fun(Context) -> Key = {Table_oid, Column}, case gleam@dict:get(erlang:element(4, Context), Key) of {ok, Type_} -> {Context, {ok, Type_}}; {error, _} -> case eval:step(Do(), Context) of {_, {error, _}} = Result -> Result; {{context, _, _, Column_nullability} = Context@1, {ok, Type_@1}} -> Column_nullability@1 = gleam@dict:insert( Column_nullability, Key, Type_@1 ), New_context = erlang:setelement( 4, Context@1, Column_nullability@1 ), {New_context, {ok, Type_@1}} end end end ). -spec join_type_decoder(gleam@dynamic:dynamic_()) -> {ok, join_type()} | {error, list(gleam@dynamic:decode_error())}. join_type_decoder(Data) -> gleam@result:map(gleam@dynamic:string(Data), fun(Data@1) -> case Data@1 of <<"Full"/utf8>> -> full; <<"Left"/utf8>> -> left; <<"Right"/utf8>> -> right; _ -> other end end). -spec parent_relation_decoder(gleam@dynamic:dynamic_()) -> {ok, parent_relation()} | {error, list(gleam@dynamic:decode_error())}. parent_relation_decoder(Data) -> gleam@result:map(gleam@dynamic:string(Data), fun(Data@1) -> case Data@1 of <<"Inner"/utf8>> -> inner; _ -> not_inner end end). -spec plan_decoder(gleam@dynamic:dynamic_()) -> {ok, plan()} | {error, list(gleam@dynamic:decode_error())}. plan_decoder(Data) -> (gleam@dynamic:decode4( fun(Field@0, Field@1, Field@2, Field@3) -> {plan, Field@0, Field@1, Field@2, Field@3} end, gleam@dynamic:optional_field( <<"Join Type"/utf8>>, fun join_type_decoder/1 ), gleam@dynamic:optional_field( <<"Parent Relationship"/utf8>>, fun parent_relation_decoder/1 ), gleam@dynamic:optional_field( <<"Output"/utf8>>, gleam@dynamic:list(fun gleam@dynamic:string/1) ), gleam@dynamic:optional_field( <<"Plans"/utf8>>, gleam@dynamic:list(fun plan_decoder/1) ) ))(Data). -spec json_plans_decoder(gleam@dynamic:dynamic_()) -> {ok, list(plan())} | {error, list(gleam@dynamic:decode_error())}. json_plans_decoder(Data) -> (gleam@dynamic:list( gleam@dynamic:field(<<"Plan"/utf8>>, fun plan_decoder/1) ))(Data). -spec query_plan(squirrel@internal@query:'query'(), integer()) -> eval:eval(plan(), squirrel@internal@error:error(), context()). query_plan(Query, Parameters) -> Query@1 = <<"explain (format json, verbose) "/utf8, (erlang:element(4, Query))/binary>>, Params = gleam@list:repeat(null, Parameters), Run_query = run_query(Query@1, Params, []), eval:'try'( Run_query, fun(Res) -> [Plan] = case Res of [_] -> Res; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail, module => <<"squirrel/internal/database/postgres"/utf8>>, function => <<"query_plan"/utf8>>, line => 310}) end, _assert_subject = gleam@json:decode_bits( Plan, fun json_plans_decoder/1 ), {ok, [Plan@1 | _]} = case _assert_subject of {ok, [_ | _]} -> _assert_subject; _assert_fail@1 -> erlang:error(#{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail@1, module => <<"squirrel/internal/database/postgres"/utf8>>, function => <<"query_plan"/utf8>>, line => 311}) end, eval:return(Plan@1) end ). -spec bit_array_to_bool(bitstring()) -> boolean(). bit_array_to_bool(Bit_array) -> case Bit_array of <<0, Rest/bitstring>> -> bit_array_to_bool(Rest); <<>> -> false; _ -> true end. -spec find_gleam_type(integer()) -> eval:eval(squirrel@internal@gleam:type(), squirrel@internal@error:error(), context()). find_gleam_type(Oid) -> with_cached_gleam_type( Oid, fun() -> Params = [{parameter, <>}], Run_query = begin _pipe = <<" select -- The name of the type or, if the type is an array, the name of its -- elements' type. case when elem.typname is null then type.typname else elem.typname end as type, -- Tells us how to interpret the firs column: if this is true then the first -- column is the type of the elements of the array type. -- Otherwise it means we've found a base type. case when elem.typname is null then false else true end as is_array from pg_type as type left join pg_type as elem on type.typelem = elem.oid where type.oid = $1 "/utf8>>, run_query(_pipe, Params, [23]) end, eval:'try'( Run_query, fun(Res) -> [Name, Is_array] = case Res of [_, _] -> Res; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail, module => <<"squirrel/internal/database/postgres"/utf8>>, function => <<"find_gleam_type"/utf8>>, line => 282}) end, _assert_subject = gleam@bit_array:to_string(Name), {ok, Name@1} = case _assert_subject of {ok, _} -> _assert_subject; _assert_fail@1 -> erlang:error(#{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail@1, module => <<"squirrel/internal/database/postgres"/utf8>>, function => <<"find_gleam_type"/utf8>>, line => 287}) end, Type_ = case bit_array_to_bool(Is_array) of true -> {p_array, {p_base, Name@1}}; false -> {p_base, Name@1} end, eval:return(pg_to_gleam_type(Type_)) end ) end ). -spec resolve_parameters(list(integer())) -> eval:eval(list(squirrel@internal@gleam:type()), squirrel@internal@error:error(), context()). resolve_parameters(Parameters) -> squirrel@internal@eval_extra:try_map( Parameters, fun(Oid) -> find_gleam_type(Oid) end ). -spec column_nullability(integer(), integer()) -> eval:eval(nullability(), squirrel@internal@error:error(), context()). column_nullability(Table, Column) -> with_cached_column( Table, Column, fun() -> gleam@bool:guard( Table =:= 0, eval:return(not_nullable), fun() -> Params = [{parameter, <>}, {parameter, <>}], Run_query = begin _pipe = <<" select -- Whether the column has a not-null constraint. attnotnull from pg_attribute where -- The oid of the table the column comes from. attrelid = $1 -- The index of the column we're looking for. and attnum = $2 "/utf8>>, run_query(_pipe, Params, [23, 23]) end, eval:'try'( Run_query, fun(Res) -> [Has_non_null_constraint] = case Res of [_] -> Res; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail, module => <<"squirrel/internal/database/postgres"/utf8>>, function => <<"column_nullability"/utf8>>, line => 422}) end, case bit_array_to_bool(Has_non_null_constraint) of true -> eval:return(not_nullable); false -> eval:return(nullable) end end ) end ) end ). -spec resolve_returns( list(squirrel@internal@database@postgres_protocol:row_description_field()), gleam@set:set(integer()) ) -> eval:eval(list(squirrel@internal@gleam:field()), squirrel@internal@error:error(), context()). resolve_returns(Returns, Nullables) -> squirrel@internal@eval_extra:try_index_map( Returns, fun(Column, I) -> {row_description_field, Name, Table, Column@1, Type_oid, _, _, _} = Column, eval:'try'( find_gleam_type(Type_oid), fun(Type_) -> eval:'try'( case gleam@string:ends_with(Name, <<"!"/utf8>>) of true -> eval:return(not_nullable); false -> case gleam@string:ends_with(Name, <<"?"/utf8>>) of true -> eval:return(nullable); false -> case gleam@set:contains(Nullables, I) of true -> eval:return(nullable); false -> column_nullability( Table, Column@1 ) end end end, fun(Nullability) -> Type_@1 = case Nullability of nullable -> {option, Type_}; not_nullable -> Type_ end, Field = {field, squirrel@internal@gleam:to_function_name(Name), Type_@1}, eval:return(Field) end ) end ) end ). -spec infer_types(squirrel@internal@query:'query'()) -> eval:eval(squirrel@internal@query:typed_query(), squirrel@internal@error:error(), context()). infer_types(Query) -> Action = parameters_and_returns(Query), eval:'try'( Action, fun(_use0) -> {Parameters, Returns} = _use0, eval:'try'( resolve_parameters(Parameters), fun(Parameters@1) -> eval:'try'( query_plan(Query, erlang:length(Parameters@1)), fun(Plan) -> eval:'try'( resolve_returns( Returns, nullables_from_plan(Plan) ), fun(Returns@1) -> _pipe = Query, _pipe@1 = squirrel@internal@query:add_types( _pipe, Parameters@1, Returns@1 ), eval:return(_pipe@1) end ) end ) end ) end ). -spec main(list(squirrel@internal@query:'query'())) -> {ok, list(squirrel@internal@query:typed_query())} | {error, squirrel@internal@error:error()}. main(Queries) -> Script = (eval:'try'( authenticate(), fun(_) -> squirrel@internal@eval_extra:try_map( Queries, fun(Query) -> infer_types(Query) end ) end )), _pipe = Script, eval:run( _pipe, {context, squirrel@internal@database@postgres_protocol:connect( <<"localhost"/utf8>>, 5432, 1000 ), gleam@dict:new(), gleam@dict:new()} ).