Current section
Files
Jump to
Current section
Files
src/epsql.erl
%%#!/usr/bin/env escript
-module(epsql).
-export([main/1]).
-include_lib("epgsql/include/epgsql.hrl").
-type io_input() :: standard_io | file:io_device().
-type io_data() :: {ok, string()} | {error, any()} | eof.
-spec usage() -> no_return().
usage() ->
io:format("usage: epsql [-v][-h host][-p port][-t ms][-P pass][-U user] [database]~n~n"),
io:format("-h host\t\thost to connect to; default \"127.0.0.1\"~n"),
io:format("-p port\t\tport number to connect to; default \"5432\"~n"),
io:format("-t ms\t\tconnection timeout in milliseconds; default \"5000\"~n"),
io:format("-P pass\t\tuser password~n"),
io:format("-U user\t\tuser to connect as; default \"~s\"~n", [os:getenv("USER", "")]),
io:format("-v\t\tverbose output~n"),
halt(2).
-spec main([string()]) -> ok.
main(Args) ->
case opts:to_map(Args, [
{ $h, param, hostname },
{ $p, param, port },
{ $t, param, timeout },
{ $P, param, password },
{ $U, param, username },
{ $v, flag, verbose }
]) of
{error, Reason, Opt} ->
io:format("~s -~c~n", [Reason, Opt]),
usage();
{ok, _Options, ArgsN} ->
process(ArgsN)
end.
-spec process(egetopt:args() | [undefined]) -> ok.
process([]) ->
process([undefined]);
process([Database | _Args]) ->
PgOpts0 = #{
host => opts:get(hostname, "127.0.0.1"),
port => list_to_integer(opts:get(portnum, "5432")),
username => opts:get(username, os:getenv("USER")),
timeout => list_to_integer(opts:get(timeout, "5000"))
},
PgOpts1 = map_set(PgOpts0, password, opts:get(password)),
PgOpts2 = map_set(PgOpts1, database, Database),
case epgsql:connect(PgOpts2) of
{error, Reason} ->
io:format(standard_error, "epsql: ~p~n", [Reason]);
{ok, C} ->
psql(C, standard_io),
epgsql:close(C)
end.
-spec map_set(map(), atom(), egetopt:optarg()) -> map().
map_set(Map0, _Key, undefined) ->
Map0;
map_set(Map0, Key, Value) ->
maps:put(Key, Value, Map0).
-spec psql(epgsql:connection(), io_input()) -> ok.
psql(C, Fp) ->
% Read an SQL statement.
case read_sql(Fp) of
{ok, Sql} ->
case epgsql:equery(C, Sql) of
{error, Reason} ->
io:format(standard_error, "epsql: ~s~n", [Reason#error.message]);
Result ->
result(Result, opts:get(verbose, false)),
psql(C, Fp)
end;
{error, Reason} ->
io:format(standard_error, "epsql: ~p~n", [Reason]);
eof ->
ok
end.
-spec read_sql(io_input()) -> io_data().
read_sql(Fp) ->
case file:read(Fp, 1) of
{ok, "\\"} ->
cmd_to_sql(read_delim(Fp, $\n, "\\"));
{ok, Ch} ->
read_delim(Fp, $;, Ch);
eof ->
eof;
Other ->
Other
end.
-spec read_delim(io_input(), char(), string()) -> io_data().
read_delim(Fp, Delim, Acc) ->
case file:read(Fp, 1) of
{ok, [Delim]} ->
{ok, lists:reverse(Acc)};
{ok, [Ch]} ->
read_delim(Fp, Delim, [Ch | Acc]);
eof ->
case lists:reverse(Acc) of
[] ->
eof;
String ->
{ok, String}
end;
Other ->
Other
end.
-spec result(tuple(), boolean()) -> ok.
result({ok, _Count}, false) ->
% INSERT / UPDATE / DELETE
ok;
result({ok, Count}, true) ->
% INSERT / UPDATE / DELETE
io:format("rows ~p~n", [Count]);
result({ok, _Cols, Rows}, _) when length(Rows) == 0 ->
% SELECT
ok;
result({ok, Cols, Rows}, _) ->
% SELECT
headings(Cols),
ruler(Cols),
rows(Rows),
io:format("(~B rows)~n~n", [length(Rows)]);
result({ok, _Count, Cols, Rows}, _) ->
% INSERT...RETURNING...
headings(Cols),
ruler(Cols),
rows(Rows),
io:format("(~B rows)~n~n", [length(Rows)]).
-spec rows([tuple()]) -> ok.
rows([]) ->
ok;
rows([Row | Rows]) ->
cols(tuple_to_list(Row)),
rows(Rows).
-spec comma_nl(non_neg_integer()) -> ok.
comma_nl(0) ->
io:format("~n");
comma_nl(_) ->
io:format(", ").
-spec cols([any()]) -> ok.
cols([]) ->
ok;
cols([Col | Cols]) ->
case Col of
Bin when is_binary(Bin) ->
io:format("\"~s\"", [Bin]);
_ ->
io:format("~p", [Col])
end,
comma_nl(length(Cols)),
cols(Cols).
-spec headings([#column{}]) -> ok.
headings([]) ->
ok;
headings([Col | Cols]) ->
io:format("\"~s\"", [Col#column.name]),
comma_nl(length(Cols)),
headings(Cols).
-spec ruler([#column{}]) -> ok.
ruler([]) ->
ok;
ruler([Col | Cols]) ->
io:format("\"~*..-s\"", [byte_size(Col#column.name), ""]),
comma_nl(length(Cols)),
ruler(Cols).
-spec cmd_to_sql(Cmd :: {ok, string()} | string()) -> {ok, string()} | eof.
cmd_to_sql({ok, Cmd}) ->
cmd_to_sql(string:tokens(Cmd, " "));
cmd_to_sql(["\\q"]) ->
eof;
cmd_to_sql(["\\l"]) ->
{ok, "SELECT datname FROM pg_database;"};
cmd_to_sql(["\\dt"]) ->
{ok, "SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname != 'pg_catalog' AND schemaname != 'information_schema';"};
cmd_to_sql(["\\d", Table]) ->
{ok, "SELECT column_name, data_type, character_maximum_length FROM information_schema.columns WHERE table_name = '"++ Table ++"';"};
cmd_to_sql(Other) ->
Other.