Erlang with SQL & JSON

Joe Gasewicz
3 min readJul 28, 2019

--

How to return formatted JSON responses from SQL in Erlang

Recently, I have been playing around with Erlang’s Cowboy & Yaws. Great fun but I really got stuck when I tried to build a JSON response from the erlang:odbc engine’s output. The Erlang odbc package sends back SQL in the form of Columns as strings, & Rows as tuples.

So after a few days of searching for a complete library that abstracts this out entirely under the hood (think Python’s SqlAlchemy), I realised I just have to get cracking & find a solution myself.

This example uses Erlang’s Cowboy but will work Yaws etc.

First, let’s create the boiley plate to handle a request in Cowboy:

init(Req0, State) ->
ResData = get_data(),
Req = cowboy_req:reply(200,
#{<<"content-type">> => <<"application/json">>},
ResData,
Req0),
{ok, Req, State}.

Above, we are setting the response headers to <<”content-type”>> => <<”application/json”>> and also return ResData from the get_data/0 function to make the initial SQL query, below:

get_data() ->
Sql = "SELECT * FROM alloys;",
Ref = connect(),
case odbc:sql_query(Ref, Sql) of
{selected, Columns, Results} ->
set_json_from_sql(Columns, Results, []);
{error, Reason} ->
{error, Reason}
end.

Pretty standard stuff if you’re familiar with Cowboy.

Now let’s look at a solution to returning formatted JSON from SQL in Erlang. Lets create a few functions, the first of these will use recursion to pattern-match against a list of “objects”, set_json_from_sql is the main entry function, it has an arity of 3 which accepts Columns, Rows and an empty list. For each recursion we are appending the new object to the main list with lists:append([TupleRow], Data) . Also note, I’m using the jsone library: https://github.com/sile/jsone but you could use jerry or jsx.

set_json_from_sql([], [], Data) -> jsone:encode([{<<"data">>, lists:reverse(Data)}]);

set_json_from_sql(Columns, [H], Data) ->
NewData = set_json_merge(H, Columns, Data),
set_json_from_sql([], [], NewData);

set_json_from_sql(Columns, [H|T], Data) ->
NewData = set_json_merge(H, Columns, Data),
set_json_from_sql(Columns, T, NewData).

The set_json_from_sql/3 function calls set_json_merge/3 to return each row in the table, which eventually, in turn, gets serialised to a JSON object.

set_json_merge(Row, Columns, Data) ->
TupleRow = json_by_type(tuple_to_list(Row), Columns, []),
lists:append([TupleRow], Data).

json_by_type/3 is a function for creating the actual list of tuple pairs, which eventually get formatted to key values:

json_by_type([H], [Hc], Data) ->
NewH = format_by_type(H),
set_json_flatten(Data, Hc, NewH);

json_by_type([H|T], [Hc|Tc], Data) ->
NewH = format_by_type(H),
NewData = set_json_flatten(Data, Hc, NewH),
json_by_type(T, Tc, NewData).

set_json_flatten/3 is a helper called from json_by_type to merge the new list of tuple pairs (the SQL row) into the main list.

set_json_flatten(Data, Column, Row) ->
ResTuple = {list_to_binary(Column), Row},
lists:flatten(Data, [ResTuple]).

Also, note that, because I’m using format specifiers, I need to make sure which type gets formatted. Although, atoms & numbers do not need converting:

format_by_type(Item) ->
if
is_list(Item) -> list_to_binary(io_lib:format("~s", [Item]));
is_integer(Item) -> Item;
is_boolean(Item) -> io_lib:format("~a", [Item]);
is_atom(Item) -> Item
end.

Here is the complete code:

%%%-------------------------------------------------------------------
%%% @author joegasewicz
%%% @copyright (C) 2019, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 13. Jul 2019 13:31
%%%-------------------------------------------------------------------
-module(index_handler).
-author("joegasewicz").
%% API
-export([init/2, get_data/0]).
-import(account_manager_api_app, [connect/0]).

get_data() ->
Sql = "SELECT * FROM alloys;",
Ref = connect(),
case odbc:sql_query(Ref, Sql) of
{selected, Columns, Results} ->
set_json_from_sql(Columns, Results, []);
{error, Reason} ->
{error, Reason}
end.

%%% Check the type Item -> format specifier OR its int/atom
format_by_type(Item) ->
if
is_list(Item) -> list_to_binary(io_lib:format("~s", [Item]));
is_integer(Item) -> Item;
is_boolean(Item) -> io_lib:format("~a", [Item]);
is_atom(Item) -> Item
end.

json_by_type([H], [Hc], Data) ->
NewH = format_by_type(H),
set_json_flatten(Data, Hc, NewH);

json_by_type([H|T], [Hc|Tc], Data) ->
NewH = format_by_type(H),
NewData = set_json_flatten(Data, Hc, NewH),
json_by_type(T, Tc, NewData).

set_json_flatten(Data, Column, Row) ->
ResTuple = {list_to_binary(Column), Row},
lists:flatten(Data, [ResTuple]).

set_json_from_sql([], [], Data) -> jsone:encode([{<<"data">>, lists:reverse(Data)}]);

set_json_from_sql(Columns, [H], Data) ->
NewData = set_json_merge(H, Columns, Data),
set_json_from_sql([], [], NewData);

set_json_from_sql(Columns, [H|T], Data) ->
NewData = set_json_merge(H, Columns, Data),
set_json_from_sql(Columns, T, NewData).

set_json_merge(Row, Columns, Data) ->
TupleRow = json_by_type(tuple_to_list(Row), Columns, []),
lists:append([TupleRow], Data).

init(Req0, State) ->
ResData = get_data(),
Req = cowboy_req:reply(200,
#{<<"content-type">> => <<"application/json">>},
ResData,
Req0),
{ok, Req, State}.

Well, that’s all for now, if anyone is stuck with this problem, I hope this solution is useful. I plan to embark on a lot more Erlang adventures, so please follow to hear more about Erlang!

--

--

Joe Gasewicz

London based Javascript / Python / C developer and Jazz bassist 🎸