Current section
Files
Jump to
Current section
Files
src/sqlode@query_analyzer@token_utils.erl
-module(sqlode@query_analyzer@token_utils).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/sqlode/query_analyzer/token_utils.gleam").
-export([skip_parens/2, read_table_name/1, extract_placeholders/1, collect_paren_contents/1, read_subquery_alias/1, extract_table_names/1, split_on_commas/1, find_insert_parts/1, find_equality_patterns/1, find_in_patterns/1, find_type_casts/1, parse_placeholder_index/1, find_set_patterns/1]).
-export_type([insert_parts/0, equality_match/0, type_cast/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
-type insert_parts() :: {insert_parts,
binary(),
list(binary()),
list(list(sqlode@lexer:token()))}.
-type equality_match() :: {equality_match,
binary(),
gleam@option:option(binary()),
binary()}.
-type type_cast() :: {type_cast, binary(), binary()}.
-file("src/sqlode/query_analyzer/token_utils.gleam", 121).
?DOC(" Skip tokens until all parentheses at the given depth are closed.\n").
-spec skip_parens(list(sqlode@lexer:token()), integer()) -> list(sqlode@lexer:token()).
skip_parens(Tokens, Depth) ->
case Depth =< 0 of
true ->
Tokens;
false ->
case Tokens of
[] ->
[];
[l_paren | Rest] ->
skip_parens(Rest, Depth + 1);
[r_paren | Rest@1] ->
skip_parens(Rest@1, Depth - 1);
[_ | Rest@2] ->
skip_parens(Rest@2, Depth)
end
end.
-file("src/sqlode/query_analyzer/token_utils.gleam", 102).
?DOC(
" Read a table name from the current token position, handling schema-qualified\n"
" names (schema.table) and subqueries in parentheses.\n"
).
-spec read_table_name(list(sqlode@lexer:token())) -> {gleam@option:option(binary()),
list(sqlode@lexer:token())}.
read_table_name(Tokens) ->
case Tokens of
[{ident, _}, dot, {ident, Name} | Rest] ->
{{some, string:lowercase(Name)}, Rest};
[{ident, Name@1} | Rest@1] ->
{{some, string:lowercase(Name@1)}, Rest@1};
[{quoted_ident, Name@2} | Rest@2] ->
{{some, string:lowercase(Name@2)}, Rest@2};
[l_paren | Rest@3] ->
Remaining = skip_parens(Rest@3, 1),
{none, Remaining};
_ ->
{none, Tokens}
end.
-file("src/sqlode/query_analyzer/token_utils.gleam", 139).
?DOC(" Extract all placeholder token strings from a token list.\n").
-spec extract_placeholders(list(sqlode@lexer:token())) -> list(binary()).
extract_placeholders(Tokens) ->
gleam@list:filter_map(Tokens, fun(Token) -> case Token of
{placeholder, P} ->
{ok, P};
_ ->
{error, nil}
end end).
-file("src/sqlode/query_analyzer/token_utils.gleam", 157).
-spec collect_paren_loop(
list(sqlode@lexer:token()),
integer(),
list(sqlode@lexer:token())
) -> {list(sqlode@lexer:token()), list(sqlode@lexer:token())}.
collect_paren_loop(Tokens, Depth, Acc) ->
case Depth =< 0 of
true ->
{lists:reverse(Acc), Tokens};
false ->
case Tokens of
[] ->
{lists:reverse(Acc), []};
[l_paren | Rest] ->
collect_paren_loop(Rest, Depth + 1, [l_paren | Acc]);
[r_paren | Rest@1] ->
case Depth =:= 1 of
true ->
{lists:reverse(Acc), Rest@1};
false ->
collect_paren_loop(
Rest@1,
Depth - 1,
[r_paren | Acc]
)
end;
[Token | Rest@2] ->
collect_paren_loop(Rest@2, Depth, [Token | Acc])
end
end.
-file("src/sqlode/query_analyzer/token_utils.gleam", 151).
?DOC(
" Collect tokens inside the next parenthesized group.\n"
" Expects tokens starting right after the opening LParen.\n"
" Returns #(inner_tokens, remaining_tokens_after_RParen).\n"
).
-spec collect_paren_contents(list(sqlode@lexer:token())) -> {list(sqlode@lexer:token()),
list(sqlode@lexer:token())}.
collect_paren_contents(Tokens) ->
collect_paren_loop(Tokens, 1, []).
-file("src/sqlode/query_analyzer/token_utils.gleam", 78).
?DOC(
" Read an optional AS followed by an identifier alias, optionally\n"
" followed by a parenthesised column list. Returns the alias and the\n"
" token stream positioned after the alias (and column list, if any).\n"
).
-spec read_subquery_alias(list(sqlode@lexer:token())) -> {gleam@option:option(binary()),
list(sqlode@lexer:token())}.
read_subquery_alias(Tokens) ->
After_as = case Tokens of
[{keyword, <<"as"/utf8>>} | Rest] ->
Rest;
_ ->
Tokens
end,
case After_as of
[{ident, Name}, l_paren | Rest_after_lp] ->
{_, After_cols} = collect_paren_contents(Rest_after_lp),
{{some, string:lowercase(Name)}, After_cols};
[{quoted_ident, Name@1}, l_paren | Rest_after_lp@1] ->
{_, After_cols@1} = collect_paren_contents(Rest_after_lp@1),
{{some, string:lowercase(Name@1)}, After_cols@1};
[{ident, Name@2} | Rest@1] ->
{{some, string:lowercase(Name@2)}, Rest@1};
[{quoted_ident, Name@3} | Rest@2] ->
{{some, string:lowercase(Name@3)}, Rest@2};
_ ->
{none, After_as}
end.
-file("src/sqlode/query_analyzer/token_utils.gleam", 14).
-spec table_names_loop(list(sqlode@lexer:token()), list(binary())) -> list(binary()).
table_names_loop(Tokens, Acc) ->
case Tokens of
[] ->
lists:reverse(Acc);
[{keyword, <<"from"/utf8>>}, l_paren | Rest] ->
Remaining = skip_parens(Rest, 1),
case read_subquery_alias(Remaining) of
{{some, N}, After_alias} ->
table_names_loop(After_alias, [N | Acc]);
{none, _} ->
table_names_loop(Remaining, Acc)
end;
[{keyword, Kw} | Rest@1] when ((Kw =:= <<"from"/utf8>>) orelse (Kw =:= <<"into"/utf8>>)) orelse (Kw =:= <<"update"/utf8>>) ->
{Name, Remaining@1} = read_table_name(Rest@1),
case Name of
{some, N@1} ->
table_names_loop(Remaining@1, [N@1 | Acc]);
none ->
table_names_loop(Rest@1, Acc)
end;
[{keyword, <<"join"/utf8>>},
{keyword, <<"lateral"/utf8>>},
l_paren |
Rest@2] ->
Remaining@2 = skip_parens(Rest@2, 1),
case read_subquery_alias(Remaining@2) of
{{some, N@2}, After_alias@1} ->
table_names_loop(After_alias@1, [N@2 | Acc]);
{none, _} ->
table_names_loop(Remaining@2, Acc)
end;
[{keyword, <<"join"/utf8>>}, l_paren | Rest@3] ->
Remaining@3 = skip_parens(Rest@3, 1),
case read_subquery_alias(Remaining@3) of
{{some, N@3}, After_alias@2} ->
table_names_loop(After_alias@2, [N@3 | Acc]);
{none, _} ->
table_names_loop(Remaining@3, Acc)
end;
[{keyword, <<"lateral"/utf8>>}, l_paren | Rest@4] ->
Remaining@4 = skip_parens(Rest@4, 1),
case read_subquery_alias(Remaining@4) of
{{some, N@4}, After_alias@3} ->
table_names_loop(After_alias@3, [N@4 | Acc]);
{none, _} ->
table_names_loop(Remaining@4, Acc)
end;
[{keyword, <<"join"/utf8>>} | Rest@5] ->
{Name@1, Remaining@5} = read_table_name(Rest@5),
case Name@1 of
{some, N@5} ->
table_names_loop(Remaining@5, [N@5 | Acc]);
none ->
table_names_loop(Rest@5, Acc)
end;
[_ | Rest@6] ->
table_names_loop(Rest@6, Acc)
end.
-file("src/sqlode/query_analyzer/token_utils.gleam", 9).
?DOC(" Extract all table names referenced in a token list (FROM, INTO, UPDATE, JOIN).\n").
-spec extract_table_names(list(sqlode@lexer:token())) -> list(binary()).
extract_table_names(Tokens) ->
_pipe = table_names_loop(Tokens, []),
gleam@list:unique(_pipe).
-file("src/sqlode/query_analyzer/token_utils.gleam", 184).
-spec split_commas_loop(
list(sqlode@lexer:token()),
integer(),
list(sqlode@lexer:token()),
list(list(sqlode@lexer:token()))
) -> list(list(sqlode@lexer:token())).
split_commas_loop(Tokens, Depth, Current, Acc) ->
case Tokens of
[] ->
case Current of
[] ->
lists:reverse(Acc);
_ ->
lists:reverse([lists:reverse(Current) | Acc])
end;
[comma | Rest] when Depth =:= 0 ->
case Current of
[] ->
split_commas_loop(Rest, 0, [], Acc);
_ ->
split_commas_loop(
Rest,
0,
[],
[lists:reverse(Current) | Acc]
)
end;
[l_paren | Rest@1] ->
split_commas_loop(Rest@1, Depth + 1, [l_paren | Current], Acc);
[r_paren | Rest@2] ->
split_commas_loop(Rest@2, Depth - 1, [r_paren | Current], Acc);
[Token | Rest@3] ->
split_commas_loop(Rest@3, Depth, [Token | Current], Acc)
end.
-file("src/sqlode/query_analyzer/token_utils.gleam", 180).
?DOC(" Split tokens on top-level commas (depth 0).\n").
-spec split_on_commas(list(sqlode@lexer:token())) -> list(list(sqlode@lexer:token())).
split_on_commas(Tokens) ->
split_commas_loop(Tokens, 0, [], []).
-file("src/sqlode/query_analyzer/token_utils.gleam", 275).
-spec skip_to_values(list(sqlode@lexer:token())) -> gleam@option:option(list(sqlode@lexer:token())).
skip_to_values(Tokens) ->
case Tokens of
[] ->
none;
[{keyword, <<"values"/utf8>>} | Rest] ->
{some, Rest};
[_ | Rest@1] ->
skip_to_values(Rest@1)
end.
-file("src/sqlode/query_analyzer/token_utils.gleam", 235).
-spec parse_insert_after_into(list(sqlode@lexer:token())) -> gleam@option:option(insert_parts()).
parse_insert_after_into(Tokens) ->
{Table_name_opt, Rest} = read_table_name(Tokens),
case Table_name_opt of
none ->
none;
{some, Table_name} ->
case Rest of
[l_paren | After_lparen] ->
{Col_tokens, After_cols} = collect_paren_contents(
After_lparen
),
Columns = begin
_pipe = split_on_commas(Col_tokens),
gleam@list:filter_map(_pipe, fun(Group) -> case Group of
[{ident, Name}] ->
{ok,
sqlode@naming:normalize_identifier(
Name
)};
[{quoted_ident, Name@1}] ->
{ok,
sqlode@naming:normalize_identifier(
Name@1
)};
_ ->
{error, nil}
end end)
end,
case skip_to_values(After_cols) of
none ->
none;
{some, After_values_kw} ->
case After_values_kw of
[l_paren | After_vlparen] ->
{Val_tokens, _} = collect_paren_contents(
After_vlparen
),
Values = split_on_commas(Val_tokens),
{some,
{insert_parts,
Table_name,
Columns,
Values}};
_ ->
none
end
end;
_ ->
none
end
end.
-file("src/sqlode/query_analyzer/token_utils.gleam", 226).
-spec find_insert_loop(list(sqlode@lexer:token())) -> gleam@option:option(insert_parts()).
find_insert_loop(Tokens) ->
case Tokens of
[] ->
none;
[{keyword, <<"insert"/utf8>>}, {keyword, <<"into"/utf8>>} | Rest] ->
parse_insert_after_into(Rest);
[_ | Rest@1] ->
find_insert_loop(Rest@1)
end.
-file("src/sqlode/query_analyzer/token_utils.gleam", 222).
?DOC(" Find INSERT INTO table (columns) VALUES (values) structure in tokens.\n").
-spec find_insert_parts(list(sqlode@lexer:token())) -> gleam@option:option(insert_parts()).
find_insert_parts(Tokens) ->
find_insert_loop(Tokens).
-file("src/sqlode/query_analyzer/token_utils.gleam", 301).
-spec find_equality_loop(list(sqlode@lexer:token()), list(equality_match())) -> list(equality_match()).
find_equality_loop(Tokens, Acc) ->
case Tokens of
[] ->
Acc;
[{ident, T}, dot, {ident, C}, {operator, Op}, {placeholder, P} | Rest] when ((((((Op =:= <<"="/utf8>>) orelse (Op =:= <<"!="/utf8>>)) orelse (Op =:= <<"<>"/utf8>>)) orelse (Op =:= <<"<"/utf8>>)) orelse (Op =:= <<">"/utf8>>)) orelse (Op =:= <<"<="/utf8>>)) orelse (Op =:= <<">="/utf8>>) ->
find_equality_loop(
Rest,
[{equality_match,
sqlode@naming:normalize_identifier(C),
{some, string:lowercase(T)},
P} |
Acc]
);
[{ident, T@1},
dot,
{quoted_ident, C@1},
{operator, Op@1},
{placeholder, P@1} |
Rest@1] when ((((((Op@1 =:= <<"="/utf8>>) orelse (Op@1 =:= <<"!="/utf8>>)) orelse (Op@1 =:= <<"<>"/utf8>>)) orelse (Op@1 =:= <<"<"/utf8>>)) orelse (Op@1 =:= <<">"/utf8>>)) orelse (Op@1 =:= <<"<="/utf8>>)) orelse (Op@1 =:= <<">="/utf8>>) ->
find_equality_loop(
Rest@1,
[{equality_match,
sqlode@naming:normalize_identifier(C@1),
{some, string:lowercase(T@1)},
P@1} |
Acc]
);
[{ident, C@2}, {operator, Op@2}, {placeholder, P@2} | Rest@2] when ((((((Op@2 =:= <<"="/utf8>>) orelse (Op@2 =:= <<"!="/utf8>>)) orelse (Op@2 =:= <<"<>"/utf8>>)) orelse (Op@2 =:= <<"<"/utf8>>)) orelse (Op@2 =:= <<">"/utf8>>)) orelse (Op@2 =:= <<"<="/utf8>>)) orelse (Op@2 =:= <<">="/utf8>>) ->
find_equality_loop(
Rest@2,
[{equality_match,
sqlode@naming:normalize_identifier(C@2),
none,
P@2} |
Acc]
);
[{quoted_ident, C@3}, {operator, Op@3}, {placeholder, P@3} | Rest@3] when ((((((Op@3 =:= <<"="/utf8>>) orelse (Op@3 =:= <<"!="/utf8>>)) orelse (Op@3 =:= <<"<>"/utf8>>)) orelse (Op@3 =:= <<"<"/utf8>>)) orelse (Op@3 =:= <<">"/utf8>>)) orelse (Op@3 =:= <<"<="/utf8>>)) orelse (Op@3 =:= <<">="/utf8>>) ->
find_equality_loop(
Rest@3,
[{equality_match,
sqlode@naming:normalize_identifier(C@3),
none,
P@3} |
Acc]
);
[{ident, T@2},
dot,
{ident, C@4},
{keyword, Kw},
{placeholder, P@4} |
Rest@4] when (Kw =:= <<"like"/utf8>>) orelse (Kw =:= <<"ilike"/utf8>>) ->
find_equality_loop(
Rest@4,
[{equality_match,
sqlode@naming:normalize_identifier(C@4),
{some, string:lowercase(T@2)},
P@4} |
Acc]
);
[{ident, C@5}, {keyword, Kw@1}, {placeholder, P@5} | Rest@5] when (Kw@1 =:= <<"like"/utf8>>) orelse (Kw@1 =:= <<"ilike"/utf8>>) ->
find_equality_loop(
Rest@5,
[{equality_match,
sqlode@naming:normalize_identifier(C@5),
none,
P@5} |
Acc]
);
[_ | Rest@6] ->
find_equality_loop(Rest@6, Acc)
end.
-file("src/sqlode/query_analyzer/token_utils.gleam", 296).
?DOC(" Find all column [op] placeholder patterns in tokens.\n").
-spec find_equality_patterns(list(sqlode@lexer:token())) -> list(equality_match()).
find_equality_patterns(Tokens) ->
_pipe = find_equality_loop(Tokens, []),
lists:reverse(_pipe).
-file("src/sqlode/query_analyzer/token_utils.gleam", 443).
-spec find_in_loop(list(sqlode@lexer:token()), list(equality_match())) -> list(equality_match()).
find_in_loop(Tokens, Acc) ->
case Tokens of
[] ->
Acc;
[{ident, T},
dot,
{ident, C},
{keyword, <<"in"/utf8>>},
l_paren,
{placeholder, P},
r_paren |
Rest] ->
find_in_loop(
Rest,
[{equality_match,
sqlode@naming:normalize_identifier(C),
{some, string:lowercase(T)},
P} |
Acc]
);
[{ident, C@1},
{keyword, <<"in"/utf8>>},
l_paren,
{placeholder, P@1},
r_paren |
Rest@1] ->
find_in_loop(
Rest@1,
[{equality_match,
sqlode@naming:normalize_identifier(C@1),
none,
P@1} |
Acc]
);
[{quoted_ident, C@2},
{keyword, <<"in"/utf8>>},
l_paren,
{placeholder, P@2},
r_paren |
Rest@2] ->
find_in_loop(
Rest@2,
[{equality_match,
sqlode@naming:normalize_identifier(C@2),
none,
P@2} |
Acc]
);
[_ | Rest@3] ->
find_in_loop(Rest@3, Acc)
end.
-file("src/sqlode/query_analyzer/token_utils.gleam", 438).
?DOC(" Find all column IN (placeholder) patterns in tokens.\n").
-spec find_in_patterns(list(sqlode@lexer:token())) -> list(equality_match()).
find_in_patterns(Tokens) ->
_pipe = find_in_loop(Tokens, []),
lists:reverse(_pipe).
-file("src/sqlode/query_analyzer/token_utils.gleam", 523).
-spec find_type_cast_loop(list(sqlode@lexer:token()), list(type_cast())) -> list(type_cast()).
find_type_cast_loop(Tokens, Acc) ->
case Tokens of
[] ->
Acc;
[{placeholder, P}, {operator, <<"::"/utf8>>}, {ident, T} | Rest] ->
find_type_cast_loop(
Rest,
[{type_cast, P, string:lowercase(T)} | Acc]
);
[{placeholder, P@1}, {operator, <<"::"/utf8>>}, {keyword, T@1} | Rest@1] ->
find_type_cast_loop(Rest@1, [{type_cast, P@1, T@1} | Acc]);
[_ | Rest@2] ->
find_type_cast_loop(Rest@2, Acc)
end.
-file("src/sqlode/query_analyzer/token_utils.gleam", 518).
?DOC(" Find all $N::type patterns in tokens (PostgreSQL only).\n").
-spec find_type_casts(list(sqlode@lexer:token())) -> list(type_cast()).
find_type_casts(Tokens) ->
_pipe = find_type_cast_loop(Tokens, []),
lists:reverse(_pipe).
-file("src/sqlode/query_analyzer/token_utils.gleam", 546).
?DOC(" Parse a placeholder string like \"$3\" into its integer index.\n").
-spec parse_placeholder_index(binary()) -> {ok, integer()} | {error, nil}.
parse_placeholder_index(Placeholder) ->
_pipe = Placeholder,
_pipe@1 = gleam@string:replace(_pipe, <<"$"/utf8>>, <<""/utf8>>),
_pipe@2 = gleam_stdlib:parse_int(_pipe@1),
_pipe@3 = gleam@option:from_result(_pipe@2),
gleam@option:to_result(_pipe@3, nil).
-file("src/sqlode/query_analyzer/token_utils.gleam", 575).
-spec scan_set_assignments(list(sqlode@lexer:token()), list(equality_match())) -> list(equality_match()).
scan_set_assignments(Tokens, Acc) ->
case Tokens of
[] ->
Acc;
[{keyword, Kw} | _] when ((Kw =:= <<"where"/utf8>>) orelse (Kw =:= <<"returning"/utf8>>)) orelse (Kw =:= <<"from"/utf8>>) ->
Acc;
[{ident, C}, {operator, <<"="/utf8>>}, {placeholder, P} | Rest] ->
scan_set_assignments(
Rest,
[{equality_match,
sqlode@naming:normalize_identifier(C),
none,
P} |
Acc]
);
[{quoted_ident, C@1},
{operator, <<"="/utf8>>},
{placeholder, P@1} |
Rest@1] ->
scan_set_assignments(
Rest@1,
[{equality_match,
sqlode@naming:normalize_identifier(C@1),
none,
P@1} |
Acc]
);
[_ | Rest@2] ->
scan_set_assignments(Rest@2, Acc)
end.
-file("src/sqlode/query_analyzer/token_utils.gleam", 564).
-spec find_set_clause(list(sqlode@lexer:token()), list(equality_match())) -> list(equality_match()).
find_set_clause(Tokens, Acc) ->
case Tokens of
[] ->
Acc;
[{keyword, <<"set"/utf8>>} | Rest] ->
scan_set_assignments(Rest, Acc);
[_ | Rest@1] ->
find_set_clause(Rest@1, Acc)
end.
-file("src/sqlode/query_analyzer/token_utils.gleam", 559).
?DOC(" Find all column = placeholder patterns in SET clauses.\n").
-spec find_set_patterns(list(sqlode@lexer:token())) -> list(equality_match()).
find_set_patterns(Tokens) ->
_pipe = find_set_clause(Tokens, []),
lists:reverse(_pipe).