-module(telega_storage_sqlite).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/telega_storage_sqlite.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(
" SQLite storage adapter for Telega.\n"
"\n"
" Implements `telega/storage.KeyValueStorage` on top of a single SQLite\n"
" table, giving small bots single-file persistence that survives restarts.\n"
" TTL is stored as an epoch-millisecond `expires_at` column and enforced\n"
" lazily on access (`get`/`scan`), matching the core ETS backend.\n"
).
-file("src/telega_storage_sqlite.gleam", 158).
-spec now_ms() -> integer().
now_ms() ->
os:system_time(erlang:binary_to_atom(<<"millisecond"/utf8>>)).
-file("src/telega_storage_sqlite.gleam", 132).
-spec do_scan(sqlight:connection(), binary(), binary()) -> {ok, list(binary())} |
{error, sqlight:error()}.
do_scan(Conn, Table, Prefix) ->
Sql = <<<<"SELECT key FROM "/utf8, Table/binary>>/binary,
" WHERE key LIKE ? AND (expires_at IS NULL OR expires_at > ?)"/utf8>>,
Args = [sqlight:text(<<Prefix/binary, "%"/utf8>>), sqlight:int(now_ms())],
sqlight:'query'(
Sql,
Conn,
Args,
gleam@dynamic@decode:at(
[0],
{decoder, fun gleam@dynamic@decode:decode_string/1}
)
).
-file("src/telega_storage_sqlite.gleam", 113).
-spec do_delete(sqlight:connection(), binary(), binary()) -> {ok, nil} |
{error, sqlight:error()}.
do_delete(Conn, Table, Key) ->
Sql = <<<<"DELETE FROM "/utf8, Table/binary>>/binary,
" WHERE key = ?"/utf8>>,
case sqlight:'query'(
Sql,
Conn,
[sqlight:text(Key)],
{decoder, fun gleam@dynamic@decode:decode_dynamic/1}
) of
{ok, _} ->
{ok, nil};
{error, Err} ->
{error, Err}
end.
-file("src/telega_storage_sqlite.gleam", 89).
-spec do_set(
sqlight:connection(),
binary(),
binary(),
binary(),
gleam@option:option(integer())
) -> {ok, nil} | {error, sqlight:error()}.
do_set(Conn, Table, Key, Value, Expires_at) ->
Sql = <<<<<<<<"INSERT INTO "/utf8, Table/binary>>/binary,
" (key, value, expires_at) VALUES (?, ?, ?)"/utf8>>/binary,
" ON CONFLICT(key) DO UPDATE SET value = excluded.value,"/utf8>>/binary,
" expires_at = excluded.expires_at"/utf8>>,
Args = [sqlight:text(Key),
sqlight:text(Value),
sqlight:nullable(fun sqlight:int/1, Expires_at)],
case sqlight:'query'(
Sql,
Conn,
Args,
{decoder, fun gleam@dynamic@decode:decode_dynamic/1}
) of
{ok, _} ->
{ok, nil};
{error, Err} ->
{error, Err}
end.
-file("src/telega_storage_sqlite.gleam", 151).
?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_sqlite.gleam", 58).
-spec do_get(sqlight:connection(), binary(), binary()) -> {ok,
gleam@option:option(binary())} |
{error, sqlight: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 = ? LIMIT 1"/utf8>>,
case sqlight:'query'(Sql, Conn, [sqlight:text(Key)], Decoder) of
{ok, [{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, []} ->
{ok, none};
{error, Err@1} ->
{error, Err@1}
end.
-file("src/telega_storage_sqlite.gleam", 23).
?DOC(" Like `new`, but with a custom table name.\n").
-spec new_with_table(sqlight:connection(), binary()) -> telega@storage:key_value_storage(sqlight: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_sqlite.gleam", 18).
?DOC(
" Build a `KeyValueStorage` backed by the given SQLite connection.\n"
"\n"
" Uses the default table name `telega_storage`. Call `migrate` once at startup\n"
" to create the table.\n"
).
-spec new(sqlight:connection()) -> telega@storage:key_value_storage(sqlight:error()).
new(Conn) ->
new_with_table(Conn, <<"telega_storage"/utf8>>).
-file("src/telega_storage_sqlite.gleam", 46).
?DOC(" Create a custom-named storage table if it does not exist.\n").
-spec migrate_table(sqlight:connection(), binary()) -> {ok, nil} |
{error, sqlight:error()}.
migrate_table(Conn, Table) ->
sqlight:exec(
<<<<"CREATE TABLE IF NOT EXISTS "/utf8, Table/binary>>/binary,
" (key TEXT PRIMARY KEY, value TEXT NOT NULL, expires_at INTEGER)"/utf8>>,
Conn
).
-file("src/telega_storage_sqlite.gleam", 41).
?DOC(" Create the storage table if it does not exist. Run once at startup.\n").
-spec migrate(sqlight:connection()) -> {ok, nil} | {error, sqlight:error()}.
migrate(Conn) ->
migrate_table(Conn, <<"telega_storage"/utf8>>).