Current section
Files
Jump to
Current section
Files
src/sqlode@schema_parser.erl
-module(sqlode@schema_parser).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/sqlode/schema_parser.gleam").
-export([parse_files_with_engine/2, parse_files/1, error_to_string/1, warning_to_string/1]).
-export_type([parse_error/0, schema_warning/0, parsed_schema/0, view_column/0, unknown_statement_kind/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 parse_error() :: {invalid_create_table, binary(), binary()} |
{invalid_column, binary(), binary(), binary()} |
{unsupported_statement, binary(), binary()}.
-type schema_warning() :: {unresolvable_view_column, binary()}.
-type parsed_schema() :: {parsed_schema,
list(sqlode@model:table()),
list(sqlode@model:enum_def()),
list(schema_warning())}.
-type view_column() :: {view_column, binary(), list(sqlode@lexer:token())}.
-type unknown_statement_kind() :: {unsupported, binary()} | silently_ignored.
-file("src/sqlode/schema_parser.gleam", 136).
?DOC(" Split token list on Semicolon tokens into a list of statements.\n").
-spec split_token_statements(
list(sqlode@lexer:token()),
list(sqlode@lexer:token()),
list(list(sqlode@lexer:token()))
) -> list(list(sqlode@lexer:token())).
split_token_statements(Tokens, Current, Acc) ->
case Tokens of
[] ->
case Current of
[] ->
lists:reverse(Acc);
_ ->
lists:reverse([lists:reverse(Current) | Acc])
end;
[semicolon | Rest] ->
case Current of
[] ->
split_token_statements(Rest, [], Acc);
_ ->
split_token_statements(
Rest,
[],
[lists:reverse(Current) | Acc]
)
end;
[Token | Rest@1] ->
split_token_statements(Rest@1, [Token | Current], Acc)
end.
-file("src/sqlode/schema_parser.gleam", 157).
?DOC(" Check if a token list represents a CREATE TYPE ... AS ENUM statement.\n").
-spec is_create_enum_tokens(list(sqlode@lexer:token())) -> boolean().
is_create_enum_tokens(Tokens) ->
case Tokens of
[{keyword, <<"create"/utf8>>},
{keyword, <<"type"/utf8>>},
_,
{keyword, <<"as"/utf8>>},
{keyword, <<"enum"/utf8>>} |
_] ->
true;
_ ->
false
end.
-file("src/sqlode/schema_parser.gleam", 197).
?DOC(" Extract string literal values from inside ENUM parentheses.\n").
-spec extract_enum_values(list(sqlode@lexer:token()), list(binary())) -> list(binary()).
extract_enum_values(Tokens, Acc) ->
case Tokens of
[] ->
lists:reverse(Acc);
[{string_lit, Value} | Rest] ->
extract_enum_values(Rest, [Value | Acc]);
[_ | Rest@1] ->
extract_enum_values(Rest@1, Acc)
end.
-file("src/sqlode/schema_parser.gleam", 172).
?DOC(" Parse a CREATE TYPE name AS ENUM (...) from tokens, handling escaped quotes correctly.\n").
-spec parse_create_enum_from_tokens(list(sqlode@lexer:token())) -> {ok,
sqlode@model:enum_def()} |
{error, nil}.
parse_create_enum_from_tokens(Tokens) ->
case Tokens of
[{keyword, <<"create"/utf8>>},
{keyword, <<"type"/utf8>>},
Name_token,
{keyword, <<"as"/utf8>>},
{keyword, <<"enum"/utf8>>} |
Rest] ->
Name = case Name_token of
{ident, N} ->
string:lowercase(N);
{quoted_ident, N@1} ->
string:lowercase(N@1);
_ ->
<<""/utf8>>
end,
Values = extract_enum_values(Rest, []),
{ok, {enum_def, Name, Values}};
_ ->
{error, nil}
end.
-file("src/sqlode/schema_parser.gleam", 209).
-spec is_create_view_tokens(list(sqlode@lexer:token())) -> boolean().
is_create_view_tokens(Tokens) ->
case Tokens of
[{keyword, <<"create"/utf8>>},
{keyword, <<"or"/utf8>>},
{keyword, <<"replace"/utf8>>},
{keyword, <<"view"/utf8>>} |
_] ->
true;
[{keyword, <<"create"/utf8>>}, {keyword, <<"view"/utf8>>} | _] ->
true;
_ ->
false
end.
-file("src/sqlode/schema_parser.gleam", 264).
-spec extract_view_name_result(list(sqlode@lexer:token())) -> gleam@option:option({binary(),
list(sqlode@lexer:token())}).
extract_view_name_result(Tokens) ->
Remaining = case Tokens of
[{keyword, <<"create"/utf8>>},
{keyword, <<"or"/utf8>>},
{keyword, <<"replace"/utf8>>},
{keyword, <<"view"/utf8>>} |
Rest] ->
Rest;
[{keyword, <<"create"/utf8>>}, {keyword, <<"view"/utf8>>} | Rest@1] ->
Rest@1;
_ ->
[]
end,
case Remaining of
[{ident, N} | Rest@2] ->
{some, {string:lowercase(N), Rest@2}};
[{quoted_ident, N@1} | Rest@3] ->
{some, {string:lowercase(N@1), Rest@3}};
_ ->
none
end.
-file("src/sqlode/schema_parser.gleam", 285).
-spec extract_after_select_result(list(sqlode@lexer:token())) -> gleam@option:option(list(sqlode@lexer:token())).
extract_after_select_result(Tokens) ->
case Tokens of
[{keyword, <<"select"/utf8>>} | Rest] ->
case Rest of
[] ->
none;
_ ->
{some, Rest}
end;
_ ->
none
end.
-file("src/sqlode/schema_parser.gleam", 333).
-spec skip_to_keyword(list(sqlode@lexer:token()), binary()) -> list(sqlode@lexer:token()).
skip_to_keyword(Tokens, Keyword) ->
case Tokens of
[] ->
[];
[{keyword, K} | Rest] when K =:= Keyword ->
Rest;
[_ | Rest@1] ->
skip_to_keyword(Rest@1, Keyword)
end.
-file("src/sqlode/schema_parser.gleam", 344).
-spec split_at_from(
list(sqlode@lexer:token()),
integer(),
list(sqlode@lexer:token())
) -> {list(sqlode@lexer:token()), list(sqlode@lexer:token())}.
split_at_from(Tokens, Depth, Acc) ->
case Tokens of
[] ->
{lists:reverse(Acc), []};
[l_paren | Rest] ->
split_at_from(Rest, Depth + 1, [l_paren | Acc]);
[r_paren | Rest@1] ->
split_at_from(Rest@1, Depth - 1, [r_paren | Acc]);
[{keyword, <<"from"/utf8>>} | Rest@2] when Depth =:= 0 ->
{lists:reverse(Acc), Rest@2};
[Token | Rest@3] ->
split_at_from(Rest@3, Depth, [Token | Acc])
end.
-file("src/sqlode/schema_parser.gleam", 479).
-spec infer_cast_type(list(sqlode@lexer:token())) -> gleam@option:option({sqlode@model:scalar_type(),
boolean()}).
infer_cast_type(Tokens) ->
case Tokens of
[] ->
none;
[{keyword, <<"as"/utf8>>}, {ident, Type_name} | _] ->
case sqlode@model:parse_sql_type(Type_name) of
{ok, Scalar_type} ->
{some, {Scalar_type, true}};
{error, _} ->
none
end;
[{keyword, <<"as"/utf8>>}, {keyword, Type_name} | _] ->
case sqlode@model:parse_sql_type(Type_name) of
{ok, Scalar_type} ->
{some, {Scalar_type, true}};
{error, _} ->
none
end;
[_ | Rest] ->
infer_cast_type(Rest)
end.
-file("src/sqlode/schema_parser.gleam", 494).
-spec tok_split_select_columns(
list(sqlode@lexer:token()),
integer(),
list(sqlode@lexer:token()),
list(list(sqlode@lexer:token()))
) -> list(list(sqlode@lexer:token())).
tok_split_select_columns(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 ->
tok_split_select_columns(
Rest,
0,
[],
[lists:reverse(Current) | Acc]
);
[l_paren | Rest@1] ->
tok_split_select_columns(
Rest@1,
Depth + 1,
[l_paren | Current],
Acc
);
[r_paren | Rest@2] ->
tok_split_select_columns(
Rest@2,
Depth - 1,
[r_paren | Current],
Acc
);
[Token | Rest@3] ->
tok_split_select_columns(Rest@3, Depth, [Token | Current], Acc)
end.
-file("src/sqlode/schema_parser.gleam", 360).
-spec extract_view_columns(list(sqlode@lexer:token())) -> list(view_column()).
extract_view_columns(Tokens) ->
Groups = tok_split_select_columns(Tokens, 0, [], []),
gleam@list:filter_map(
Groups,
fun(Col_tokens) ->
Reversed = lists:reverse(Col_tokens),
case Reversed of
[{ident, Alias}, {keyword, <<"as"/utf8>>} | Rest] ->
{ok, {view_column, Alias, lists:reverse(Rest)}};
[{quoted_ident, Alias@1}, {keyword, <<"as"/utf8>>} | Rest@1] ->
{ok, {view_column, Alias@1, lists:reverse(Rest@1)}};
_ ->
case lists:reverse(Reversed) of
[{ident, _}, dot, {ident, Col}] ->
{ok, {view_column, Col, Col_tokens}};
_ ->
case Reversed of
[{ident, Name} | _] ->
{ok, {view_column, Name, Col_tokens}};
[{quoted_ident, Name@1} | _] ->
{ok, {view_column, Name@1, Col_tokens}};
_ ->
{error, nil}
end
end
end
end
).
-file("src/sqlode/schema_parser.gleam", 518).
?DOC(" Detect ALTER TABLE ... ADD [COLUMN] pattern.\n").
-spec is_alter_table_add_column_tokens(list(sqlode@lexer:token())) -> boolean().
is_alter_table_add_column_tokens(Tokens) ->
case Tokens of
[{keyword, <<"alter"/utf8>>},
{keyword, <<"table"/utf8>>},
_,
{keyword, <<"add"/utf8>>},
{keyword, <<"column"/utf8>>} |
_] ->
true;
[{keyword, <<"alter"/utf8>>},
{keyword, <<"table"/utf8>>},
_,
{keyword, <<"add"/utf8>>} |
Rest] ->
case Rest of
[{keyword, K} | _] when (((((K =:= <<"constraint"/utf8>>) orelse (K =:= <<"primary"/utf8>>)) orelse (K =:= <<"unique"/utf8>>)) orelse (K =:= <<"foreign"/utf8>>)) orelse (K =:= <<"check"/utf8>>)) orelse (K =:= <<"index"/utf8>>) ->
false;
_ ->
true
end;
_ ->
false
end.
-file("src/sqlode/schema_parser.gleam", 607).
-spec extract_ident(sqlode@lexer:token()) -> binary().
extract_ident(Token) ->
case Token of
{ident, N} ->
sqlode@naming:normalize_identifier(N);
{quoted_ident, N@1} ->
sqlode@naming:normalize_identifier(N@1);
_ ->
<<""/utf8>>
end.
-file("src/sqlode/schema_parser.gleam", 584).
-spec extract_alter_table_parts(list(sqlode@lexer:token())) -> {binary(),
list(sqlode@lexer:token())}.
extract_alter_table_parts(Tokens) ->
case Tokens of
[{keyword, <<"alter"/utf8>>},
{keyword, <<"table"/utf8>>},
Name_tok,
{keyword, <<"add"/utf8>>},
{keyword, <<"column"/utf8>>} |
Rest] ->
{extract_ident(Name_tok), Rest};
[{keyword, <<"alter"/utf8>>},
{keyword, <<"table"/utf8>>},
Name_tok@1,
{keyword, <<"add"/utf8>>} |
Rest@1] ->
{extract_ident(Name_tok@1), Rest@1};
_ ->
{<<""/utf8>>, []}
end.
-file("src/sqlode/schema_parser.gleam", 682).
-spec classify_alter_table_action(list(binary())) -> unknown_statement_kind().
classify_alter_table_action(Words) ->
case Words of
[<<"drop"/utf8>>, <<"constraint"/utf8>> | _] ->
silently_ignored;
[<<"drop"/utf8>>, <<"column"/utf8>> | _] ->
{unsupported, <<"ALTER TABLE DROP COLUMN"/utf8>>};
[<<"drop"/utf8>> | _] ->
{unsupported, <<"ALTER TABLE DROP"/utf8>>};
[<<"rename"/utf8>>, <<"to"/utf8>> | _] ->
{unsupported, <<"ALTER TABLE RENAME TO"/utf8>>};
[<<"rename"/utf8>> | _] ->
{unsupported, <<"ALTER TABLE RENAME COLUMN"/utf8>>};
[<<"alter"/utf8>> | _] ->
{unsupported, <<"ALTER TABLE ALTER COLUMN"/utf8>>};
[<<"add"/utf8>>, <<"constraint"/utf8>> | _] ->
silently_ignored;
[<<"add"/utf8>>, <<"primary"/utf8>> | _] ->
silently_ignored;
[<<"add"/utf8>>, <<"unique"/utf8>> | _] ->
silently_ignored;
[<<"add"/utf8>>, <<"foreign"/utf8>> | _] ->
silently_ignored;
[<<"add"/utf8>>, <<"check"/utf8>> | _] ->
silently_ignored;
[<<"add"/utf8>>, <<"index"/utf8>> | _] ->
silently_ignored;
_ ->
silently_ignored
end.
-file("src/sqlode/schema_parser.gleam", 670).
-spec classify_alter_table(list(binary())) -> unknown_statement_kind().
classify_alter_table(Words) ->
After_modifiers = case Words of
[<<"if"/utf8>>, <<"exists"/utf8>> | R] ->
R;
[<<"only"/utf8>> | R@1] ->
R@1;
_ ->
Words
end,
case After_modifiers of
[_ | Rest] ->
classify_alter_table_action(Rest);
[] ->
silently_ignored
end.
-file("src/sqlode/schema_parser.gleam", 700).
-spec token_keyword_text(sqlode@lexer:token()) -> binary().
token_keyword_text(Token) ->
case Token of
{keyword, K} ->
K;
{ident, I} ->
string:lowercase(I);
_ ->
<<""/utf8>>
end.
-file("src/sqlode/schema_parser.gleam", 646).
?DOC(
" Classify a statement that is not a CREATE TABLE / VIEW / ENUM and not an\n"
" ALTER TABLE ... ADD COLUMN. The intent is to let informational or\n"
" out-of-scope DDL through silently (CREATE INDEX, COMMENT ON, transaction\n"
" control) while failing fast on DDL that materially changes the catalog\n"
" and would otherwise be missed (DROP TABLE, ALTER TABLE DROP/RENAME/ALTER).\n"
"\n"
" The lexer reserves only a subset of SQL words as `Keyword` tokens. Words\n"
" like `rename`, `savepoint`, and `comment` arrive as `Ident` tokens, so\n"
" we normalise the first few tokens of the statement to lowercase strings\n"
" before matching, treating Keyword and Ident uniformly.\n"
).
-spec classify_unknown_statement(list(sqlode@lexer:token())) -> unknown_statement_kind().
classify_unknown_statement(Tokens) ->
Words = begin
_pipe = Tokens,
_pipe@1 = gleam@list:take(_pipe, 8),
gleam@list:map(_pipe@1, fun token_keyword_text/1)
end,
case Words of
[<<"create"/utf8>>, <<"index"/utf8>> | _] ->
silently_ignored;
[<<"create"/utf8>>, <<"unique"/utf8>>, <<"index"/utf8>> | _] ->
silently_ignored;
[<<"drop"/utf8>>, <<"index"/utf8>> | _] ->
silently_ignored;
[<<"comment"/utf8>>, <<"on"/utf8>> | _] ->
silently_ignored;
[<<"begin"/utf8>> | _] ->
silently_ignored;
[<<"commit"/utf8>> | _] ->
silently_ignored;
[<<"rollback"/utf8>> | _] ->
silently_ignored;
[<<"savepoint"/utf8>> | _] ->
silently_ignored;
[<<"release"/utf8>> | _] ->
silently_ignored;
[<<"set"/utf8>> | _] ->
silently_ignored;
[<<"drop"/utf8>>, <<"table"/utf8>> | _] ->
{unsupported, <<"DROP TABLE"/utf8>>};
[<<"drop"/utf8>>, <<"view"/utf8>> | _] ->
{unsupported, <<"DROP VIEW"/utf8>>};
[<<"drop"/utf8>>, <<"type"/utf8>> | _] ->
{unsupported, <<"DROP TYPE"/utf8>>};
[<<"alter"/utf8>>, <<"table"/utf8>> | Rest] ->
classify_alter_table(Rest);
_ ->
silently_ignored
end.
-file("src/sqlode/schema_parser.gleam", 708).
-spec is_create_table_tokens(list(sqlode@lexer:token())) -> boolean().
is_create_table_tokens(Tokens) ->
case Tokens of
[{keyword, <<"create"/utf8>>}, {keyword, <<"table"/utf8>>} | _] ->
true;
[{keyword, <<"create"/utf8>>},
{keyword, <<"temporary"/utf8>>},
{keyword, <<"table"/utf8>>} |
_] ->
true;
[{keyword, <<"create"/utf8>>},
{keyword, <<"temp"/utf8>>},
{keyword, <<"table"/utf8>>} |
_] ->
true;
[{keyword, <<"create"/utf8>>},
{keyword, <<"unlogged"/utf8>>},
{keyword, <<"table"/utf8>>} |
_] ->
true;
_ ->
false
end.
-file("src/sqlode/schema_parser.gleam", 729).
-spec find_column_in_tables(list(sqlode@model:table()), binary()) -> gleam@option:option(sqlode@model:column()).
find_column_in_tables(Tables, Column_name) ->
_pipe@1 = gleam@list:find_map(
Tables,
fun(Table) ->
_pipe = gleam@list:find(
erlang:element(3, Table),
fun(Col) ->
string:lowercase(erlang:element(2, Col)) =:= string:lowercase(
Column_name
)
end
),
gleam@result:map_error(_pipe, fun(_) -> nil end)
end
),
gleam@option:from_result(_pipe@1).
-file("src/sqlode/schema_parser.gleam", 390).
-spec resolve_column_from_expr_tokens(
list(sqlode@lexer:token()),
list(sqlode@model:table())
) -> gleam@option:option(sqlode@model:column()).
resolve_column_from_expr_tokens(Expr_tokens, Tables) ->
case Expr_tokens of
[{ident, Name}] ->
find_column_in_tables(Tables, string:lowercase(Name));
[{quoted_ident, Name@1}] ->
find_column_in_tables(Tables, string:lowercase(Name@1));
[{ident, _}, dot, {ident, Col}] ->
find_column_in_tables(Tables, string:lowercase(Col));
[{quoted_ident, _}, dot, {ident, Col@1}] ->
find_column_in_tables(Tables, string:lowercase(Col@1));
_ ->
none
end.
-file("src/sqlode/schema_parser.gleam", 462).
-spec extract_aggregate_inner_type(
list(sqlode@lexer:token()),
list(sqlode@model:table())
) -> gleam@option:option(sqlode@model:column()).
extract_aggregate_inner_type(Tokens, Tables) ->
case Tokens of
[{ident, Name}, r_paren | _] ->
find_column_in_tables(Tables, string:lowercase(Name));
[{ident, Name}, comma | _] ->
find_column_in_tables(Tables, string:lowercase(Name));
[{ident, _}, dot, {ident, Col}, r_paren | _] ->
find_column_in_tables(Tables, string:lowercase(Col));
[{ident, _}, dot, {ident, Col}, comma | _] ->
find_column_in_tables(Tables, string:lowercase(Col));
[{keyword, <<"distinct"/utf8>>}, {ident, Name@1}, r_paren | _] ->
find_column_in_tables(Tables, string:lowercase(Name@1));
[{keyword, <<"distinct"/utf8>>}, {ident, Name@1}, comma | _] ->
find_column_in_tables(Tables, string:lowercase(Name@1));
_ ->
none
end.
-file("src/sqlode/schema_parser.gleam", 406).
-spec infer_view_expression_type(
list(sqlode@lexer:token()),
list(sqlode@model:table())
) -> gleam@option:option({sqlode@model:scalar_type(), boolean()}).
infer_view_expression_type(Expr_tokens, Tables) ->
case Expr_tokens of
[{keyword, <<"cast"/utf8>>}, l_paren | Rest] ->
infer_cast_type(Rest);
[{ident, Fn_name}, l_paren | Rest@1] ->
Lowered = string:lowercase(Fn_name),
case Lowered of
<<"count"/utf8>> ->
{some, {int_type, false}};
<<"avg"/utf8>> ->
{some, {case extract_aggregate_inner_type(Rest@1, Tables) of
{some, Col} ->
case erlang:element(3, Col) of
int_type ->
float_type;
float_type ->
float_type;
Other ->
Other
end;
none ->
float_type
end, true}};
<<"sum"/utf8>> ->
case extract_aggregate_inner_type(Rest@1, Tables) of
{some, Col@1} ->
{some, {erlang:element(3, Col@1), true}};
none ->
{some, {float_type, true}}
end;
<<"min"/utf8>> ->
case extract_aggregate_inner_type(Rest@1, Tables) of
{some, Col@2} ->
{some, {erlang:element(3, Col@2), true}};
none ->
none
end;
<<"max"/utf8>> ->
case extract_aggregate_inner_type(Rest@1, Tables) of
{some, Col@2} ->
{some, {erlang:element(3, Col@2), true}};
none ->
none
end;
<<"coalesce"/utf8>> ->
case extract_aggregate_inner_type(Rest@1, Tables) of
{some, Col@3} ->
{some, {erlang:element(3, Col@3), false}};
none ->
none
end;
<<"row_number"/utf8>> ->
{some, {int_type, false}};
<<"rank"/utf8>> ->
{some, {int_type, false}};
<<"dense_rank"/utf8>> ->
{some, {int_type, false}};
_ ->
none
end;
[{string_lit, _} | _] ->
{some, {string_type, false}};
[{number_lit, N} | _] ->
case gleam_stdlib:contains_string(N, <<"."/utf8>>) of
true ->
{some, {float_type, false}};
false ->
{some, {int_type, false}}
end;
_ ->
none
end.
-file("src/sqlode/schema_parser.gleam", 313).
-spec resolve_single_view_column(
binary(),
list(sqlode@lexer:token()),
list(sqlode@model:table())
) -> {ok, sqlode@model:column()} | {error, schema_warning()}.
resolve_single_view_column(Name, Expr_tokens, Tables) ->
case find_column_in_tables(Tables, Name) of
{some, Col} ->
{ok, {column, Name, erlang:element(3, Col), erlang:element(4, Col)}};
none ->
case resolve_column_from_expr_tokens(Expr_tokens, Tables) of
{some, Col@1} ->
{ok,
{column,
Name,
erlang:element(3, Col@1),
erlang:element(4, Col@1)}};
none ->
case infer_view_expression_type(Expr_tokens, Tables) of
{some, {Scalar_type, Nullable}} ->
{ok, {column, Name, Scalar_type, Nullable}};
none ->
{error, {unresolvable_view_column, Name}}
end
end
end.
-file("src/sqlode/schema_parser.gleam", 298).
-spec resolve_view_columns(
list(sqlode@lexer:token()),
list(sqlode@model:table())
) -> {list(sqlode@model:column()), list(schema_warning())}.
resolve_view_columns(Select_tokens, Tables) ->
View_cols = extract_view_columns(Select_tokens),
gleam@list:fold(
View_cols,
{[], []},
fun(Acc, View_col) ->
{Columns, Warnings} = Acc,
Normalized = sqlode@naming:normalize_identifier(
erlang:element(2, View_col)
),
case resolve_single_view_column(
Normalized,
erlang:element(3, View_col),
Tables
) of
{ok, Col} ->
{lists:append(Columns, [Col]), Warnings};
{error, Warning} ->
{Columns, lists:append(Warnings, [Warning])}
end
end
).
-file("src/sqlode/schema_parser.gleam", 223).
-spec parse_create_view_from_tokens(
list(sqlode@lexer:token()),
list(sqlode@model:table())
) -> {gleam@option:option(sqlode@model:table()), list(schema_warning())}.
parse_create_view_from_tokens(Tokens, Tables) ->
case extract_view_name_result(Tokens) of
none ->
{none, []};
{some, {View_name, After_name}} ->
After_as = skip_to_keyword(After_name, <<"as"/utf8>>),
case extract_after_select_result(After_as) of
none ->
{none, []};
{some, After_select} ->
{Select_tokens, From_tokens} = split_at_from(
After_select,
0,
[]
),
Source_tables = sqlode@query_analyzer@token_utils:extract_table_names(
[{keyword, <<"from"/utf8>>} | From_tokens]
),
{Columns, Warnings} = case Select_tokens of
[star] ->
{gleam@list:flat_map(
Source_tables,
fun(Table_name) ->
case gleam@list:find(
Tables,
fun(T) ->
erlang:element(2, T) =:= Table_name
end
) of
{ok, Table} ->
erlang:element(3, Table);
{error, _} ->
[]
end
end
),
[]};
_ ->
resolve_view_columns(Select_tokens, Tables)
end,
case Columns of
[] ->
{none, Warnings};
_ ->
{{some, {table, View_name, Columns}}, Warnings}
end
end
end.
-file("src/sqlode/schema_parser.gleam", 779).
?DOC(" Split tokens at the first top-level LParen. Returns (header, body_after_lparen).\n").
-spec split_at_lparen(list(sqlode@lexer:token()), list(sqlode@lexer:token())) -> {list(sqlode@lexer:token()),
list(sqlode@lexer:token())}.
split_at_lparen(Tokens, Header) ->
case Tokens of
[] ->
{lists:reverse(Header), []};
[l_paren | Rest] ->
{lists:reverse(Header), Rest};
[Tok | Rest@1] ->
split_at_lparen(Rest@1, [Tok | Header])
end.
-file("src/sqlode/schema_parser.gleam", 791).
?DOC(" Find the last Ident or QuotedIdent token in a list and return its name.\n").
-spec find_last_ident(list(sqlode@lexer:token())) -> {ok, binary()} |
{error, nil}.
find_last_ident(Tokens) ->
_pipe = Tokens,
_pipe@1 = gleam@list:filter_map(_pipe, fun(Tok) -> case Tok of
{ident, N} ->
{ok, sqlode@naming:normalize_identifier(N)};
{quoted_ident, N} ->
{ok, sqlode@naming:normalize_identifier(N)};
_ ->
{error, nil}
end end),
gleam@list:last(_pipe@1).
-file("src/sqlode/schema_parser.gleam", 811).
-spec drop_trailing_rparens(list(sqlode@lexer:token())) -> list(sqlode@lexer:token()).
drop_trailing_rparens(Rev_tokens) ->
case Rev_tokens of
[r_paren | Rest] ->
Rest;
_ ->
Rev_tokens
end.
-file("src/sqlode/schema_parser.gleam", 804).
?DOC(" Strip trailing RParen from token list.\n").
-spec strip_trailing_rparen(list(sqlode@lexer:token())) -> list(sqlode@lexer:token()).
strip_trailing_rparen(Tokens) ->
_pipe = Tokens,
_pipe@1 = lists:reverse(_pipe),
_pipe@2 = drop_trailing_rparens(_pipe@1),
lists:reverse(_pipe@2).
-file("src/sqlode/schema_parser.gleam", 845).
-spec split_tokens_by_comma_loop(
list(sqlode@lexer:token()),
integer(),
list(sqlode@lexer:token()),
list(list(sqlode@lexer:token()))
) -> list(list(sqlode@lexer:token())).
split_tokens_by_comma_loop(Tokens, Depth, Current, Acc) ->
case Tokens of
[] ->
case Current of
[] ->
lists:reverse(Acc);
_ ->
lists:reverse([lists:reverse(Current) | Acc])
end;
[l_paren | Rest] ->
split_tokens_by_comma_loop(
Rest,
Depth + 1,
[l_paren | Current],
Acc
);
[r_paren | Rest@1] ->
split_tokens_by_comma_loop(Rest@1, case Depth > 0 of
true ->
Depth - 1;
false ->
0
end, [r_paren | Current], Acc);
[comma | Rest@2] when Depth =:= 0 ->
split_tokens_by_comma_loop(
Rest@2,
Depth,
[],
[lists:reverse(Current) | Acc]
);
[Tok | Rest@3] ->
split_tokens_by_comma_loop(Rest@3, Depth, [Tok | Current], Acc)
end.
-file("src/sqlode/schema_parser.gleam", 841).
?DOC(" Split a token list by top-level commas (depth-0 only).\n").
-spec split_tokens_by_comma(list(sqlode@lexer:token())) -> list(list(sqlode@lexer:token())).
split_tokens_by_comma(Tokens) ->
split_tokens_by_comma_loop(Tokens, 0, [], []).
-file("src/sqlode/schema_parser.gleam", 959).
-spec render_type_tokens_loop(list(sqlode@lexer:token()), list(binary())) -> list(binary()).
render_type_tokens_loop(Tokens, Acc) ->
case Tokens of
[] ->
Acc;
[{operator, <<"["/utf8>>}, {operator, <<"]"/utf8>>} | Rest] ->
case Acc of
[Prev | Prev_rest] ->
render_type_tokens_loop(
Rest,
[<<Prev/binary, "[]"/utf8>> | Prev_rest]
);
[] ->
render_type_tokens_loop(Rest, [<<"[]"/utf8>> | Acc])
end;
[Tok | Rest@1] ->
S = case Tok of
{keyword, K} ->
K;
{ident, N} ->
N;
{quoted_ident, N@1} ->
N@1;
{number_lit, N@2} ->
N@2;
{operator, Op} ->
Op;
l_paren ->
<<"("/utf8>>;
r_paren ->
<<")"/utf8>>;
comma ->
<<","/utf8>>;
star ->
<<"*"/utf8>>;
_ ->
<<""/utf8>>
end,
case S of
<<""/utf8>> ->
render_type_tokens_loop(Rest@1, Acc);
_ ->
render_type_tokens_loop(Rest@1, [S | Acc])
end
end.
-file("src/sqlode/schema_parser.gleam", 953).
?DOC(
" Render type tokens back to a type string for parse_sql_type lookup.\n"
" Handles array syntax ([] operators) by joining without spaces.\n"
).
-spec render_type_tokens(list(sqlode@lexer:token())) -> binary().
render_type_tokens(Tokens) ->
_pipe = render_type_tokens_loop(Tokens, []),
_pipe@1 = lists:reverse(_pipe),
gleam@string:join(_pipe@1, <<" "/utf8>>).
-file("src/sqlode/schema_parser.gleam", 993).
-spec tokens_contain_not_null(list(sqlode@lexer:token())) -> boolean().
tokens_contain_not_null(Tokens) ->
case Tokens of
[] ->
false;
[_] ->
false;
[{keyword, <<"not"/utf8>>}, {keyword, <<"null"/utf8>>} | _] ->
true;
[_ | Rest] ->
tokens_contain_not_null(Rest)
end.
-file("src/sqlode/schema_parser.gleam", 1001).
-spec tokens_contain_keyword(list(sqlode@lexer:token()), binary()) -> boolean().
tokens_contain_keyword(Tokens, Keyword) ->
gleam@list:any(Tokens, fun(Tok) -> case Tok of
{keyword, K} ->
K =:= Keyword;
_ ->
false
end end).
-file("src/sqlode/schema_parser.gleam", 1010).
-spec infer_scalar_type(binary()) -> {ok, sqlode@model:scalar_type()} |
{error, binary()}.
infer_scalar_type(Type_text) ->
_pipe = sqlode@model:parse_sql_type(Type_text),
gleam@result:replace_error(
_pipe,
<<<<<<<<"unrecognized SQL type \""/utf8, Type_text/binary>>/binary,
"\". Supported types: int, serial, float, numeric, bool, text, char, bytea,"/utf8>>/binary,
" uuid, json, jsonb, timestamp, datetime, date, time, interval."/utf8>>/binary,
" Hint: add a type override in sqlode.yaml under overrides.db_type"/utf8>>
).
-file("src/sqlode/schema_parser.gleam", 1021).
-spec is_column_constraint(binary()) -> boolean().
is_column_constraint(Keyword) ->
gleam@list:contains(
[<<"not"/utf8>>,
<<"null"/utf8>>,
<<"primary"/utf8>>,
<<"unique"/utf8>>,
<<"default"/utf8>>,
<<"references"/utf8>>,
<<"check"/utf8>>,
<<"constraint"/utf8>>,
<<"generated"/utf8>>,
<<"collate"/utf8>>,
<<"autoincrement"/utf8>>],
Keyword
).
-file("src/sqlode/schema_parser.gleam", 936).
-spec take_type_tokens_from_lexer(
list(sqlode@lexer:token()),
list(sqlode@lexer:token())
) -> list(sqlode@lexer:token()).
take_type_tokens_from_lexer(Tokens, Acc) ->
case Tokens of
[] ->
lists:reverse(Acc);
[{keyword, K} | Rest] ->
case is_column_constraint(K) of
true ->
lists:reverse(Acc);
false ->
take_type_tokens_from_lexer(Rest, [{keyword, K} | Acc])
end;
[Tok | Rest@1] ->
take_type_tokens_from_lexer(Rest@1, [Tok | Acc])
end.
-file("src/sqlode/schema_parser.gleam", 1040).
-spec find_enum(binary(), list(sqlode@model:enum_def())) -> gleam@option:option(binary()).
find_enum(Type_text, Enums) ->
Lowered = string:lowercase(gleam@string:trim(Type_text)),
case gleam@list:find(Enums, fun(E) -> erlang:element(2, E) =:= Lowered end) of
{ok, E@1} ->
{some, erlang:element(2, E@1)};
{error, _} ->
none
end.
-file("src/sqlode/schema_parser.gleam", 881).
-spec parse_column_tokens(
binary(),
binary(),
list(sqlode@lexer:token()),
list(sqlode@model:enum_def())
) -> {ok, gleam@option:option(sqlode@model:column())} | {error, parse_error()}.
parse_column_tokens(Path, Table_name, Tokens, Enums) ->
case Tokens of
[] ->
{ok, none};
[First | Rest] ->
case First of
{keyword, <<"primary"/utf8>>} ->
{ok, none};
{keyword, <<"foreign"/utf8>>} ->
{ok, none};
{keyword, <<"unique"/utf8>>} ->
{ok, none};
{keyword, <<"constraint"/utf8>>} ->
{ok, none};
{keyword, <<"check"/utf8>>} ->
{ok, none};
{ident, N} ->
Name = sqlode@naming:normalize_identifier(N),
Type_toks = take_type_tokens_from_lexer(Rest, []),
case Type_toks of
[] ->
{error,
{invalid_column,
Path,
Table_name,
<<"missing type for column "/utf8,
Name/binary>>}};
_ ->
Type_text = render_type_tokens(Type_toks),
Nullable = case (tokens_contain_not_null(Tokens)
orelse tokens_contain_keyword(
Tokens,
<<"primary"/utf8>>
))
orelse gleam_stdlib:contains_string(
Type_text,
<<"serial"/utf8>>
) of
true ->
false;
false ->
true
end,
gleam@result:'try'(
case find_enum(Type_text, Enums) of
{some, Enum_name} ->
{ok, {enum_type, Enum_name}};
none ->
_pipe = infer_scalar_type(Type_text),
gleam@result:map_error(
_pipe,
fun(Detail) ->
{invalid_column,
Path,
Table_name,
Detail}
end
)
end,
fun(Scalar_type) ->
{ok,
{some,
{column,
Name,
Scalar_type,
Nullable}}}
end
)
end;
{quoted_ident, N} ->
Name = sqlode@naming:normalize_identifier(N),
Type_toks = take_type_tokens_from_lexer(Rest, []),
case Type_toks of
[] ->
{error,
{invalid_column,
Path,
Table_name,
<<"missing type for column "/utf8,
Name/binary>>}};
_ ->
Type_text = render_type_tokens(Type_toks),
Nullable = case (tokens_contain_not_null(Tokens)
orelse tokens_contain_keyword(
Tokens,
<<"primary"/utf8>>
))
orelse gleam_stdlib:contains_string(
Type_text,
<<"serial"/utf8>>
) of
true ->
false;
false ->
true
end,
gleam@result:'try'(
case find_enum(Type_text, Enums) of
{some, Enum_name} ->
{ok, {enum_type, Enum_name}};
none ->
_pipe = infer_scalar_type(Type_text),
gleam@result:map_error(
_pipe,
fun(Detail) ->
{invalid_column,
Path,
Table_name,
Detail}
end
)
end,
fun(Scalar_type) ->
{ok,
{some,
{column,
Name,
Scalar_type,
Nullable}}}
end
)
end;
_ ->
{ok, none}
end
end.
-file("src/sqlode/schema_parser.gleam", 551).
?DOC(" Parse ALTER TABLE <name> ADD [COLUMN] <col_def> and apply to existing tables.\n").
-spec apply_alter_table_add_column(
binary(),
list(sqlode@lexer:token()),
list(sqlode@model:enum_def()),
list(sqlode@model:table())
) -> {ok, list(sqlode@model:table())} | {error, parse_error()}.
apply_alter_table_add_column(Path, Tokens, Enums, Tables) ->
{Table_name, Col_tokens} = extract_alter_table_parts(Tokens),
case Table_name of
<<""/utf8>> ->
{ok, Tables};
_ ->
gleam@result:'try'(
parse_column_tokens(Path, Table_name, Col_tokens, Enums),
fun(Maybe_col) -> case Maybe_col of
none ->
{ok, Tables};
{some, Col} ->
{ok,
gleam@list:map(
Tables,
fun(T) ->
case erlang:element(2, T) =:= Table_name of
true ->
{table,
erlang:element(2, T),
lists:append(
erlang:element(3, T),
[Col]
)};
false ->
T
end
end
)}
end end
)
end.
-file("src/sqlode/schema_parser.gleam", 818).
-spec parse_columns_tokens(
binary(),
binary(),
list(sqlode@lexer:token()),
list(sqlode@model:enum_def())
) -> {ok, list(sqlode@model:column())} | {error, parse_error()}.
parse_columns_tokens(Path, Table_name, Tokens, Enums) ->
_pipe = split_tokens_by_comma(Tokens),
_pipe@1 = gleam@list:try_fold(
_pipe,
[],
fun(Columns, Col_tokens) ->
gleam@result:'try'(
parse_column_tokens(Path, Table_name, Col_tokens, Enums),
fun(Maybe_column) -> {ok, case Maybe_column of
{some, Column} ->
[Column | Columns];
none ->
Columns
end} end
)
end
),
gleam@result:map(_pipe@1, fun lists:reverse/1).
-file("src/sqlode/schema_parser.gleam", 742).
-spec parse_create_table_tokens(
binary(),
list(sqlode@lexer:token()),
list(sqlode@model:enum_def())
) -> {ok, gleam@option:option(sqlode@model:table())} | {error, parse_error()}.
parse_create_table_tokens(Path, Tokens, Enums) ->
{Header, Body} = split_at_lparen(Tokens, []),
case Body of
[] ->
{error,
{invalid_create_table,
Path,
<<"missing opening parenthesis in CREATE TABLE statement"/utf8>>}};
_ ->
gleam@result:'try'(
begin
_pipe = find_last_ident(Header),
gleam@result:map_error(
_pipe,
fun(_) ->
{invalid_create_table,
Path,
<<"missing table name"/utf8>>}
end
)
end,
fun(Table_name) ->
Body_tokens = strip_trailing_rparen(Body),
gleam@result:'try'(
parse_columns_tokens(
Path,
Table_name,
Body_tokens,
Enums
),
fun(Columns) ->
{ok, {some, {table, Table_name, Columns}}}
end
)
end
)
end.
-file("src/sqlode/schema_parser.gleam", 615).
-spec parse_statement_tokens(
binary(),
list(sqlode@lexer:token()),
list(sqlode@model:enum_def())
) -> {ok, gleam@option:option(sqlode@model:table())} | {error, parse_error()}.
parse_statement_tokens(Path, Tokens, Enums) ->
case is_create_table_tokens(Tokens) of
true ->
parse_create_table_tokens(Path, Tokens, Enums);
false ->
case classify_unknown_statement(Tokens) of
{unsupported, Statement_type} ->
{error, {unsupported_statement, Path, Statement_type}};
silently_ignored ->
{ok, none}
end
end.
-file("src/sqlode/schema_parser.gleam", 68).
-spec parse_content(
binary(),
binary(),
list(sqlode@model:enum_def()),
sqlode@model:engine()
) -> {ok, parsed_schema()} | {error, parse_error()}.
parse_content(Path, Content, Known_enums, Engine) ->
Tokens = sqlode@lexer:tokenize(Content, Engine),
Statements = split_token_statements(Tokens, [], []),
Enums = begin
_pipe = Statements,
gleam@list:filter_map(
_pipe,
fun(Stmt_tokens) -> case is_create_enum_tokens(Stmt_tokens) of
true ->
parse_create_enum_from_tokens(Stmt_tokens);
false ->
{error, nil}
end end
)
end,
All_enums = lists:append(Known_enums, Enums),
gleam@result:'try'(
begin
_pipe@1 = Statements,
_pipe@2 = gleam@list:try_fold(
_pipe@1,
{[], []},
fun(Acc, Stmt_tokens@1) ->
{Tables, Warnings} = Acc,
case is_create_view_tokens(Stmt_tokens@1) of
true ->
{Maybe_table, View_warnings} = parse_create_view_from_tokens(
Stmt_tokens@1,
lists:reverse(Tables)
),
New_warnings = lists:append(Warnings, View_warnings),
{ok, case Maybe_table of
{some, Table} ->
{[Table | Tables], New_warnings};
none ->
{Tables, New_warnings}
end};
false ->
case is_alter_table_add_column_tokens(Stmt_tokens@1) of
true ->
gleam@result:'try'(
apply_alter_table_add_column(
Path,
Stmt_tokens@1,
All_enums,
Tables
),
fun(New_tables) ->
{ok, {New_tables, Warnings}}
end
);
false ->
gleam@result:'try'(
parse_statement_tokens(
Path,
Stmt_tokens@1,
All_enums
),
fun(Maybe_table@1) ->
{ok, case Maybe_table@1 of
{some, Table@1} ->
{[Table@1 | Tables],
Warnings};
none ->
{Tables, Warnings}
end}
end
)
end
end
end
),
gleam@result:map(
_pipe@2,
fun(Pair) ->
{lists:reverse(erlang:element(1, Pair)),
erlang:element(2, Pair)}
end
)
end,
fun(_use0) ->
{Tables@1, Warnings@1} = _use0,
{ok, {parsed_schema, Tables@1, Enums, Warnings@1}}
end
).
-file("src/sqlode/schema_parser.gleam", 43).
-spec parse_files_with_engine(list({binary(), binary()}), sqlode@model:engine()) -> {ok,
{sqlode@model:catalog(), list(schema_warning())}} |
{error, parse_error()}.
parse_files_with_engine(Entries, Engine) ->
_pipe = Entries,
_pipe@1 = gleam@list:try_fold(
_pipe,
{parsed_schema, [], [], []},
fun(Acc, Entry) ->
{Path, Content} = Entry,
gleam@result:'try'(
parse_content(Path, Content, erlang:element(3, Acc), Engine),
fun(Parsed) ->
{ok,
{parsed_schema,
lists:append(
erlang:element(2, Acc),
erlang:element(2, Parsed)
),
lists:append(
erlang:element(3, Acc),
erlang:element(3, Parsed)
),
lists:append(
erlang:element(4, Acc),
erlang:element(4, Parsed)
)}}
end
)
end
),
gleam@result:map(
_pipe@1,
fun(Schema) ->
{{catalog, erlang:element(2, Schema), erlang:element(3, Schema)},
erlang:element(4, Schema)}
end
).
-file("src/sqlode/schema_parser.gleam", 37).
-spec parse_files(list({binary(), binary()})) -> {ok,
{sqlode@model:catalog(), list(schema_warning())}} |
{error, parse_error()}.
parse_files(Entries) ->
parse_files_with_engine(Entries, postgre_s_q_l).
-file("src/sqlode/schema_parser.gleam", 1070).
-spec path_prefix(binary()) -> binary().
path_prefix(Path) ->
case Path of
<<""/utf8>> ->
<<""/utf8>>;
_ ->
<<Path/binary, ": "/utf8>>
end.
-file("src/sqlode/schema_parser.gleam", 1049).
-spec error_to_string(parse_error()) -> binary().
error_to_string(Error) ->
case Error of
{invalid_create_table, Path, Detail} ->
<<<<(path_prefix(Path))/binary,
"Invalid CREATE TABLE statement: "/utf8>>/binary,
Detail/binary>>;
{invalid_column, Path@1, Table, Detail@1} ->
<<<<<<<<(path_prefix(Path@1))/binary,
"Invalid column definition in table "/utf8>>/binary,
Table/binary>>/binary,
": "/utf8>>/binary,
Detail@1/binary>>;
{unsupported_statement, Path@2, Statement_type} ->
<<<<<<<<<<<<(path_prefix(Path@2))/binary,
"Unsupported schema DDL: "/utf8>>/binary,
Statement_type/binary>>/binary,
". sqlode only applies CREATE TABLE, CREATE VIEW, CREATE TYPE ... AS ENUM,"/utf8>>/binary,
" and ALTER TABLE ... ADD COLUMN to the catalog;"/utf8>>/binary,
" keep other schema-changing DDL out of the schema input"/utf8>>/binary,
" or consolidate migrations into a single CREATE TABLE snapshot."/utf8>>
end.
-file("src/sqlode/schema_parser.gleam", 1077).
-spec warning_to_string(schema_warning()) -> binary().
warning_to_string(Warning) ->
case Warning of
{unresolvable_view_column, Column} ->
<<<<"Warning: view column \""/utf8, Column/binary>>/binary,
"\" could not be resolved from source tables — skipping column."/utf8>>
end.