Current section

Files

Jump to
kura_sqlite src kura_dialect_sqlite.erl
Raw

src/kura_dialect_sqlite.erl

-module(kura_dialect_sqlite).
-moduledoc """
SQLite dialect. Translates `#kura_query{}` AST into SQLite-flavored
SQL.
## Strategy
SQLite 3.35+ supports nearly every SQL feature kura emits: `RETURNING`,
`ON CONFLICT (col) DO UPDATE`, `LIMIT/OFFSET`, double-quoted identifiers.
The main wire-protocol difference vs PG is the placeholder syntax: PG
uses `$N`, SQLite uses `?N`.
This dialect delegates the AST-to-SQL emission to `kura_dialect_pg` and
post-processes the SQL string to swap `$N` for `?N`. The number of
placeholders and their positions are unchanged. Edge cases where SQL
bytes between dialects truly diverge (`JSONB` operators, advisory
locks, partial-index expressions, etc.) are out of scope for this
phase 2 skeleton and will be addressed by capability flags in phase 3.
## Notes
- All callbacks are pure pass-throughs to `kura_dialect_pg` followed by
`swap_placeholders/1`.
- Identifier quoting: SQLite (with `SQLITE_DQS=0` as esqlite ships) treats
`"foo"` as an identifier. Same as PG.
- esqlite returns rows as tuples, not maps. Map-shaped result
conversion happens in `kura_driver_sqlite`.
""".
-behaviour(kura_dialect).
-include_lib("kura/include/kura.hrl").
-eqwalizer({nowarn_function, format_default/1}).
-export([
to_sql/1,
to_sql_from/2,
insert/3,
insert/4,
update/4,
delete/2,
update_all/2,
delete_all/1,
insert_all/3,
insert_all/4,
column_type/1,
format_default/1,
swap_placeholders/1
]).
%%----------------------------------------------------------------------
%% kura_dialect callbacks
%%----------------------------------------------------------------------
-spec to_sql(#kura_query{}) -> {iodata(), [term()]}.
to_sql(Query) ->
{SQL, Params} = kura_dialect_pg:to_sql(Query),
{swap_placeholders(SQL), Params}.
-spec to_sql_from(#kura_query{}, pos_integer()) -> {iodata(), [term()], pos_integer()}.
to_sql_from(Query, StartCounter) ->
{SQL, Params, NextCounter} = kura_dialect_pg:to_sql_from(Query, StartCounter),
{swap_placeholders(SQL), Params, NextCounter}.
-spec insert(atom() | module(), [atom()], map()) -> {iodata(), [term()]}.
insert(SchemaOrTable, Fields, Data) ->
{SQL, Params} = kura_dialect_pg:insert(SchemaOrTable, Fields, Data),
{swap_placeholders(SQL), Params}.
-spec insert(atom() | module(), [atom()], map(), map()) -> {iodata(), [term()]}.
insert(SchemaOrTable, Fields, Data, Opts) ->
{SQL, Params} = kura_dialect_pg:insert(SchemaOrTable, Fields, Data, Opts),
{swap_placeholders(SQL), Params}.
-spec update(atom() | module(), [atom()], map(), [{atom(), term()}]) -> {iodata(), [term()]}.
update(SchemaOrTable, Fields, Changes, KeyClauses) ->
{SQL, Params} = kura_dialect_pg:update(SchemaOrTable, Fields, Changes, KeyClauses),
{swap_placeholders(SQL), Params}.
-spec delete(atom() | module(), [{atom(), term()}]) -> {iodata(), [term()]}.
delete(SchemaOrTable, KeyClauses) ->
{SQL, Params} = kura_dialect_pg:delete(SchemaOrTable, KeyClauses),
{swap_placeholders(SQL), Params}.
-spec update_all(#kura_query{}, map()) -> {iodata(), [term()]}.
update_all(Query, SetMap) ->
{SQL, Params} = kura_dialect_pg:update_all(Query, SetMap),
{swap_placeholders(SQL), Params}.
-spec delete_all(#kura_query{}) -> {iodata(), [term()]}.
delete_all(Query) ->
{SQL, Params} = kura_dialect_pg:delete_all(Query),
{swap_placeholders(SQL), Params}.
-spec insert_all(atom() | module(), [atom()], [map()]) -> {iodata(), [term()]}.
insert_all(SchemaOrTable, Fields, Rows) ->
{SQL, Params} = kura_dialect_pg:insert_all(SchemaOrTable, Fields, Rows),
{swap_placeholders(SQL), Params}.
-spec insert_all(atom() | module(), [atom()], [map()], map()) -> {iodata(), [term()]}.
insert_all(SchemaOrTable, Fields, Rows, Opts) ->
{SQL, Params} = kura_dialect_pg:insert_all(SchemaOrTable, Fields, Rows, Opts),
{swap_placeholders(SQL), Params}.
%%----------------------------------------------------------------------
%% DDL: column type + default value emit
%%----------------------------------------------------------------------
-spec column_type(kura_types:kura_type()) -> binary().
column_type(id) ->
~"INTEGER";
column_type(integer) ->
~"INTEGER";
column_type(bigint) ->
~"INTEGER";
column_type(smallint) ->
~"INTEGER";
column_type(float) ->
~"REAL";
column_type(decimal) ->
~"NUMERIC";
column_type(string) ->
~"TEXT";
column_type(text) ->
~"TEXT";
column_type(binary) ->
~"BLOB";
column_type(boolean) ->
~"INTEGER";
column_type(date) ->
~"TEXT";
column_type(time) ->
~"TEXT";
column_type(utc_datetime) ->
~"TEXT";
column_type(naive_datetime) ->
~"TEXT";
column_type(uuid) ->
~"TEXT";
column_type(jsonb) ->
~"TEXT";
column_type({enum, _}) ->
~"TEXT";
column_type({array, _}) ->
~"TEXT";
column_type({embed, _, _}) ->
~"TEXT";
column_type({encrypted, _}) ->
~"BLOB";
column_type({custom, Mod}) ->
case erlang:function_exported(Mod, sqlite_type, 0) of
true -> Mod:sqlite_type();
false -> Mod:pg_type()
end.
-spec format_default(term()) -> binary().
format_default(Val) when is_integer(Val) -> integer_to_binary(Val);
format_default(Val) when is_float(Val) -> float_to_binary(Val);
format_default(Val) when is_binary(Val) -> <<"'", Val/binary, "'">>;
format_default(true) ->
~"1";
format_default(false) ->
~"0";
format_default(Val) when is_map(Val) ->
Json = iolist_to_binary(json:encode(Val)),
<<"'", Json/binary, "'">>;
format_default(Val) when is_list(Val) ->
Json = iolist_to_binary(json:encode(Val)),
<<"'", Json/binary, "'">>.
%%----------------------------------------------------------------------
%% Helpers
%%----------------------------------------------------------------------
-doc """
Rewrite PG's `$N` placeholders to SQLite's `?N` form.
Both backends accept positional and numbered placeholders. SQLite
specifically: `?NNN` where NNN is a 1-indexed integer. Position-for-
position swap; counts and ordering preserved.
Bytes inside string literals are not rewritten; SQLite uses single
quotes for literals (`SQLITE_DQS=0` excludes double-quoted strings)
and the kura emitter follows the same convention.
""".
-spec swap_placeholders(iodata()) -> binary().
swap_placeholders(SQL) ->
Bin = iolist_to_binary(SQL),
swap(Bin, <<>>).
-spec swap(binary(), binary()) -> binary().
swap(<<>>, Acc) ->
Acc;
swap(<<$$, Rest/binary>>, Acc) ->
swap(Rest, <<Acc/binary, $?>>);
swap(<<C, Rest/binary>>, Acc) ->
swap(Rest, <<Acc/binary, C>>).