-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/3, 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, PK) -> {SQL, Params} = kura_dialect_pg:update(SchemaOrTable, Fields, Changes, PK), {swap_placeholders(SQL), Params}. -spec delete(atom() | module(), atom(), term()) -> {iodata(), [term()]}. delete(SchemaOrTable, PKField, PKValue) -> {SQL, Params} = kura_dialect_pg:delete(SchemaOrTable, PKField, PKValue), {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({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, <>); swap(<>, Acc) -> swap(Rest, <>).