-module(telega_storage_postgres). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/telega_storage_postgres.gleam"). -export([new_with_table/2, new/1, migrate_table/2, migrate/1]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. ?MODULEDOC( " PostgreSQL storage adapter for Telega.\n" "\n" " Implements `telega/storage.KeyValueStorage` on top of a single Postgres\n" " table with a `text` value column, suitable for production bots. TTL is\n" " stored as an epoch-millisecond `expires_at` column and enforced lazily on\n" " access (`get`/`scan`), so no background job is required.\n" ). -file("src/telega_storage_postgres.gleam", 160). -spec now_ms() -> integer(). now_ms() -> os:system_time(erlang:binary_to_atom(<<"millisecond"/utf8>>)). -file("src/telega_storage_postgres.gleam", 131). -spec do_scan(pog:connection(), binary(), binary()) -> {ok, list(binary())} | {error, pog:query_error()}. do_scan(Conn, Table, Prefix) -> Sql = <<<<"SELECT key FROM "/utf8, Table/binary>>/binary, " WHERE key LIKE $1 AND (expires_at IS NULL OR expires_at > $2)"/utf8>>, Query = begin _pipe = Sql, _pipe@1 = pog:'query'(_pipe), _pipe@2 = pog:parameter( _pipe@1, pog_ffi:coerce(<>) ), _pipe@3 = pog:parameter(_pipe@2, pog_ffi:coerce(now_ms())), pog:returning( _pipe@3, gleam@dynamic@decode:at( [0], {decoder, fun gleam@dynamic@decode:decode_string/1} ) ) end, case pog:execute(Query, Conn) of {ok, {returned, _, Rows}} -> {ok, Rows}; {error, Err} -> {error, Err} end. -file("src/telega_storage_postgres.gleam", 116). -spec do_delete(pog:connection(), binary(), binary()) -> {ok, nil} | {error, pog:query_error()}. do_delete(Conn, Table, Key) -> Query = begin _pipe = (<<<<"DELETE FROM "/utf8, Table/binary>>/binary, " WHERE key = $1"/utf8>>), _pipe@1 = pog:'query'(_pipe), pog:parameter(_pipe@1, pog_ffi:coerce(Key)) end, case pog:execute(Query, Conn) of {ok, _} -> {ok, nil}; {error, Err} -> {error, Err} end. -file("src/telega_storage_postgres.gleam", 92). -spec do_set( pog:connection(), binary(), binary(), binary(), gleam@option:option(integer()) ) -> {ok, nil} | {error, pog:query_error()}. do_set(Conn, Table, Key, Value, Expires_at) -> Sql = <<<<<<"INSERT INTO "/utf8, Table/binary>>/binary, " (key, value, expires_at) VALUES ($1, $2, $3)"/utf8>>/binary, " ON CONFLICT (key) DO UPDATE SET value = $2, expires_at = $3"/utf8>>, Query = begin _pipe = Sql, _pipe@1 = pog:'query'(_pipe), _pipe@2 = pog:parameter(_pipe@1, pog_ffi:coerce(Key)), _pipe@3 = pog:parameter(_pipe@2, pog_ffi:coerce(Value)), pog:parameter(_pipe@3, pog:nullable(fun pog_ffi:coerce/1, Expires_at)) end, case pog:execute(Query, Conn) of {ok, _} -> {ok, nil}; {error, Err} -> {error, Err} end. -file("src/telega_storage_postgres.gleam", 153). ?DOC(" An absent (`None`) `expires_at` means \"never expires\".\n"). -spec is_live(gleam@option:option(integer())) -> boolean(). is_live(Expires_at) -> case Expires_at of none -> true; {some, At} -> now_ms() < At end. -file("src/telega_storage_postgres.gleam", 60). -spec do_get(pog:connection(), binary(), binary()) -> {ok, gleam@option:option(binary())} | {error, pog:query_error()}. do_get(Conn, Table, Key) -> Decoder = begin gleam@dynamic@decode:field( 0, {decoder, fun gleam@dynamic@decode:decode_string/1}, fun(Value) -> gleam@dynamic@decode:field( 1, gleam@dynamic@decode:optional( {decoder, fun gleam@dynamic@decode:decode_int/1} ), fun(Expires_at) -> gleam@dynamic@decode:success({Value, Expires_at}) end ) end ) end, Sql = <<<<"SELECT value, expires_at FROM "/utf8, Table/binary>>/binary, " WHERE key = $1 LIMIT 1"/utf8>>, Query = begin _pipe = Sql, _pipe@1 = pog:'query'(_pipe), _pipe@2 = pog:parameter(_pipe@1, pog_ffi:coerce(Key)), pog:returning(_pipe@2, Decoder) end, case pog:execute(Query, Conn) of {ok, {returned, _, [{Value@1, Expires_at@1} | _]}} -> case is_live(Expires_at@1) of true -> {ok, {some, Value@1}}; false -> case do_delete(Conn, Table, Key) of {ok, _} -> {ok, none}; {error, Err} -> {error, Err} end end; {ok, {returned, _, []}} -> {ok, none}; {error, Err@1} -> {error, Err@1} end. -file("src/telega_storage_postgres.gleam", 23). ?DOC(" Like `new`, but with a custom table name.\n"). -spec new_with_table(pog:connection(), binary()) -> telega@storage:key_value_storage(pog:query_error()). new_with_table(Conn, Table) -> {key_value_storage, fun(Key) -> do_get(Conn, Table, Key) end, fun(Key@1, Value) -> do_set(Conn, Table, Key@1, Value, none) end, fun(Key@2, Value@1, Ttl_ms) -> do_set(Conn, Table, Key@2, Value@1, {some, now_ms() + Ttl_ms}) end, fun(Key@3) -> do_delete(Conn, Table, Key@3) end, fun(Prefix) -> do_scan(Conn, Table, Prefix) end}. -file("src/telega_storage_postgres.gleam", 18). ?DOC( " Build a `KeyValueStorage` backed by the given pog connection.\n" "\n" " Uses the default table name `telega_storage`. Call `migrate` once at startup\n" " to create the table.\n" ). -spec new(pog:connection()) -> telega@storage:key_value_storage(pog:query_error()). new(Conn) -> new_with_table(Conn, <<"telega_storage"/utf8>>). -file("src/telega_storage_postgres.gleam", 46). ?DOC(" Create a custom-named storage table if it does not exist.\n"). -spec migrate_table(pog:connection(), binary()) -> {ok, nil} | {error, pog:query_error()}. migrate_table(Conn, Table) -> Sql = <<<<"CREATE TABLE IF NOT EXISTS "/utf8, Table/binary>>/binary, " (key TEXT PRIMARY KEY, value TEXT NOT NULL, expires_at BIGINT)"/utf8>>, case begin _pipe = Sql, _pipe@1 = pog:'query'(_pipe), pog:execute(_pipe@1, Conn) end of {ok, _} -> {ok, nil}; {error, Err} -> {error, Err} end. -file("src/telega_storage_postgres.gleam", 41). ?DOC(" Create the storage table if it does not exist. Run once at startup.\n"). -spec migrate(pog:connection()) -> {ok, nil} | {error, pog:query_error()}. migrate(Conn) -> migrate_table(Conn, <<"telega_storage"/utf8>>).