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, statement_result/0, unknown_statement_kind/0, d_d_l_action/0, inline_enum_detection/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()}.
-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 statement_result() :: {create_table_result,
sqlode@model:table(),
list(sqlode@model:enum_def())} |
{d_d_l_applied, d_d_l_action()} |
ignored.
-type unknown_statement_kind() :: {destructive_d_d_l, d_d_l_action()} |
silently_ignored.
-type d_d_l_action() :: {drop_table, binary()} |
{drop_view, binary()} |
{drop_type, binary()} |
{alter_drop_column, binary(), binary()} |
{alter_rename_table, binary(), binary()} |
{alter_rename_column, binary(), binary(), binary()} |
{alter_column_type, binary(), binary(), list(sqlode@lexer:token())} |
{alter_column_set_not_null, binary(), binary()} |
{alter_column_drop_not_null, binary(), binary()}.
-type inline_enum_detection() :: {inline_enum,
sqlode@model:scalar_type(),
sqlode@model:enum_def()}.
-file("src/sqlode/schema_parser.gleam", 148).
?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", 169).
?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", 209).
?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", 184).
?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, postgres_enum}};
_ ->
{error, nil}
end.
-file("src/sqlode/schema_parser.gleam", 221).
-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", 276).
-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", 297).
-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", 345).
-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", 356).
-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", 491).
-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", 506).
-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", 372).
-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", 530).
?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", 626).
-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", 603).
-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", 723).
?DOC(" Parse DROP TABLE [IF EXISTS] <name> from the token list.\n").
-spec parse_drop_table(list(sqlode@lexer:token())) -> unknown_statement_kind().
parse_drop_table(Tokens) ->
case Tokens of
[_,
_,
{keyword, <<"if"/utf8>>},
{keyword, <<"exists"/utf8>>},
Name_tok |
_] ->
{destructive_d_d_l, {drop_table, extract_ident(Name_tok)}};
[_, _, Name_tok@1 | _] ->
{destructive_d_d_l, {drop_table, extract_ident(Name_tok@1)}};
_ ->
silently_ignored
end.
-file("src/sqlode/schema_parser.gleam", 734).
?DOC(" Parse DROP VIEW [IF EXISTS] <name> from the token list.\n").
-spec parse_drop_view(list(sqlode@lexer:token())) -> unknown_statement_kind().
parse_drop_view(Tokens) ->
case Tokens of
[_,
_,
{keyword, <<"if"/utf8>>},
{keyword, <<"exists"/utf8>>},
Name_tok |
_] ->
{destructive_d_d_l, {drop_view, extract_ident(Name_tok)}};
[_, _, Name_tok@1 | _] ->
{destructive_d_d_l, {drop_view, extract_ident(Name_tok@1)}};
_ ->
silently_ignored
end.
-file("src/sqlode/schema_parser.gleam", 745).
?DOC(" Parse DROP TYPE [IF EXISTS] <name> from the token list.\n").
-spec parse_drop_type(list(sqlode@lexer:token())) -> unknown_statement_kind().
parse_drop_type(Tokens) ->
case Tokens of
[_,
_,
{keyword, <<"if"/utf8>>},
{keyword, <<"exists"/utf8>>},
Name_tok |
_] ->
{destructive_d_d_l, {drop_type, extract_ident(Name_tok)}};
[_, _, Name_tok@1 | _] ->
{destructive_d_d_l, {drop_type, extract_ident(Name_tok@1)}};
_ ->
silently_ignored
end.
-file("src/sqlode/schema_parser.gleam", 774).
-spec skip_alter_modifiers(list(sqlode@lexer:token())) -> {list(sqlode@lexer:token()),
boolean()}.
skip_alter_modifiers(Tokens) ->
case Tokens of
[{keyword, <<"if"/utf8>>}, {keyword, <<"exists"/utf8>>} | Rest] ->
skip_alter_modifiers(Rest);
[{keyword, <<"only"/utf8>>} | Rest@1] ->
skip_alter_modifiers(Rest@1);
_ ->
{Tokens, false}
end.
-file("src/sqlode/schema_parser.gleam", 830).
?DOC(" Extract the column name from ALTER [COLUMN] <name> ... tokens.\n").
-spec extract_alter_column_name(list(sqlode@lexer:token())) -> binary().
extract_alter_column_name(Tokens) ->
case Tokens of
[_, {keyword, <<"column"/utf8>>}, Name_tok | _] ->
extract_ident(Name_tok);
[_, Name_tok@1 | _] ->
extract_ident(Name_tok@1);
_ ->
<<""/utf8>>
end.
-file("src/sqlode/schema_parser.gleam", 839).
?DOC(" Extract TYPE tokens from ALTER [COLUMN] <name> [SET DATA] TYPE <tokens>.\n").
-spec extract_alter_type_tokens(list(sqlode@lexer:token())) -> list(sqlode@lexer:token()).
extract_alter_type_tokens(Tokens) ->
case Tokens of
[] ->
[];
[{keyword, <<"type"/utf8>>} | Rest] ->
Rest;
[_ | Rest@1] ->
extract_alter_type_tokens(Rest@1)
end.
-file("src/sqlode/schema_parser.gleam", 983).
-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", 783).
-spec classify_alter_action_tokens(binary(), list(sqlode@lexer:token())) -> unknown_statement_kind().
classify_alter_action_tokens(Table_name, 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
[<<"drop"/utf8>>, <<"constraint"/utf8>> | _] ->
silently_ignored;
[<<"drop"/utf8>>,
<<"column"/utf8>>,
<<"if"/utf8>>,
<<"exists"/utf8>>,
Col_name |
_] ->
{destructive_d_d_l, {alter_drop_column, Table_name, Col_name}};
[<<"drop"/utf8>>, <<"column"/utf8>>, Col_name@1 | _] ->
{destructive_d_d_l, {alter_drop_column, Table_name, Col_name@1}};
[<<"drop"/utf8>>, Col_name@2 | _] ->
{destructive_d_d_l, {alter_drop_column, Table_name, Col_name@2}};
[<<"rename"/utf8>>, <<"to"/utf8>>, New_name | _] ->
{destructive_d_d_l, {alter_rename_table, Table_name, New_name}};
[<<"rename"/utf8>>,
<<"column"/utf8>>,
Old_name,
<<"to"/utf8>>,
New_name@1 |
_] ->
{destructive_d_d_l,
{alter_rename_column, Table_name, Old_name, New_name@1}};
[<<"rename"/utf8>>, Old_name@1, <<"to"/utf8>>, New_name@2 | _] ->
{destructive_d_d_l,
{alter_rename_column, Table_name, Old_name@1, New_name@2}};
[<<"alter"/utf8>>,
<<"column"/utf8>>,
_,
<<"set"/utf8>>,
<<"not"/utf8>>,
<<"null"/utf8>> |
_] ->
{destructive_d_d_l,
{alter_column_set_not_null,
Table_name,
extract_alter_column_name(Tokens)}};
[<<"alter"/utf8>>,
<<"column"/utf8>>,
_,
<<"drop"/utf8>>,
<<"not"/utf8>>,
<<"null"/utf8>> |
_] ->
{destructive_d_d_l,
{alter_column_drop_not_null,
Table_name,
extract_alter_column_name(Tokens)}};
[<<"alter"/utf8>>, <<"column"/utf8>>, _, <<"type"/utf8>> | _] ->
{destructive_d_d_l,
{alter_column_type,
Table_name,
extract_alter_column_name(Tokens),
extract_alter_type_tokens(Tokens)}};
[<<"alter"/utf8>>,
<<"column"/utf8>>,
_,
<<"set"/utf8>>,
<<"data"/utf8>>,
<<"type"/utf8>> |
_] ->
{destructive_d_d_l,
{alter_column_type,
Table_name,
extract_alter_column_name(Tokens),
extract_alter_type_tokens(Tokens)}};
[<<"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", 756).
?DOC(" Classify ALTER TABLE statements from the full token list.\n").
-spec classify_alter_table_from_tokens(list(sqlode@lexer:token())) -> unknown_statement_kind().
classify_alter_table_from_tokens(Tokens) ->
After_alter_table = case Tokens of
[_, _ | Rest] ->
Rest;
_ ->
[]
end,
{After_modifiers, _} = skip_alter_modifiers(After_alter_table),
case After_modifiers of
[Name_tok | Rest@1] ->
Table_name = extract_ident(Name_tok),
classify_alter_action_tokens(Table_name, Rest@1);
[] ->
silently_ignored
end.
-file("src/sqlode/schema_parser.gleam", 698).
?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>> | _] ->
parse_drop_table(Tokens);
[<<"drop"/utf8>>, <<"view"/utf8>> | _] ->
parse_drop_view(Tokens);
[<<"drop"/utf8>>, <<"type"/utf8>> | _] ->
parse_drop_type(Tokens);
[<<"alter"/utf8>>, <<"table"/utf8>> | _] ->
classify_alter_table_from_tokens(Tokens);
_ ->
silently_ignored
end.
-file("src/sqlode/schema_parser.gleam", 991).
-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", 1012).
-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", 402).
-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", 474).
-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", 418).
-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", 325).
-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", 310).
-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", 235).
-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", 1064).
?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", 1076).
?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", 1096).
-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", 1089).
?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", 1140).
-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", 1136).
?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", 1350).
-spec synthetic_enum_name(binary(), binary()) -> binary().
synthetic_enum_name(Table_name, Column_name) ->
<<<<(string:lowercase(Table_name))/binary, "_"/utf8>>/binary,
(string:lowercase(Column_name))/binary>>.
-file("src/sqlode/schema_parser.gleam", 1311).
?DOC(
" Detect MySQL inline `ENUM(...)` / `SET(...)` on a column's type\n"
" tokens. Gated by `engine == MySQL` so PostgreSQL/SQLite schemas —\n"
" where `enum` / `set` could conceivably appear as a non-type keyword\n"
" elsewhere — keep their existing behavior.\n"
"\n"
" The name synthesis rule is `<lowercased_table>_<column>`, which\n"
" lets two tables carry columns with overlapping value sets without\n"
" colliding. For SET the column resolves to `StringType` (the\n"
" documented fallback per Issue #407) but the values are still\n"
" preserved in the catalog for future native SET support.\n"
).
-spec detect_mysql_inline_enum_set(
list(sqlode@lexer:token()),
binary(),
binary(),
sqlode@model:engine()
) -> gleam@option:option(inline_enum_detection()).
detect_mysql_inline_enum_set(Type_toks, Table_name, Column_name, Engine) ->
case Engine of
my_s_q_l ->
case Type_toks of
[{keyword, <<"enum"/utf8>>}, l_paren | Rest] ->
Values = extract_enum_values(Rest, []),
Synthetic = synthetic_enum_name(Table_name, Column_name),
{some,
{inline_enum,
{enum_type, Synthetic},
{enum_def, Synthetic, Values, my_sql_enum}}};
[{keyword, <<"set"/utf8>>}, l_paren | Rest@1] ->
Values@1 = extract_enum_values(Rest@1, []),
Synthetic@1 = synthetic_enum_name(Table_name, Column_name),
{some,
{inline_enum,
string_type,
{enum_def, Synthetic@1, Values@1, my_sql_set}}};
_ ->
none
end;
_ ->
none
end.
-file("src/sqlode/schema_parser.gleam", 1377).
-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", 1371).
?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", 848).
?DOC(" Apply a destructive DDL action to the tables and enums lists.\n").
-spec apply_ddl_action(
d_d_l_action(),
list(sqlode@model:table()),
list(sqlode@model:enum_def())
) -> {list(sqlode@model:table()), list(sqlode@model:enum_def())}.
apply_ddl_action(Action, Tables, Enums) ->
case Action of
{drop_table, Table_name} ->
{gleam@list:filter(
Tables,
fun(T) ->
string:lowercase(erlang:element(2, T)) /= string:lowercase(
Table_name
)
end
),
Enums};
{drop_view, View_name} ->
{gleam@list:filter(
Tables,
fun(T@1) ->
string:lowercase(erlang:element(2, T@1)) /= string:lowercase(
View_name
)
end
),
Enums};
{drop_type, Type_name} ->
{Tables,
gleam@list:filter(
Enums,
fun(E) ->
string:lowercase(erlang:element(2, E)) /= string:lowercase(
Type_name
)
end
)};
{alter_drop_column, Table_name@1, Column_name} ->
{gleam@list:map(
Tables,
fun(T@2) ->
case string:lowercase(erlang:element(2, T@2)) =:= string:lowercase(
Table_name@1
) of
true ->
{table,
erlang:element(2, T@2),
gleam@list:filter(
erlang:element(3, T@2),
fun(C) ->
string:lowercase(
erlang:element(2, C)
)
/= string:lowercase(Column_name)
end
)};
false ->
T@2
end
end
),
Enums};
{alter_rename_table, Old_name, New_name} ->
{gleam@list:map(
Tables,
fun(T@3) ->
case string:lowercase(erlang:element(2, T@3)) =:= string:lowercase(
Old_name
) of
true ->
{table,
string:lowercase(New_name),
erlang:element(3, T@3)};
false ->
T@3
end
end
),
Enums};
{alter_rename_column, Table_name@2, Old_name@1, New_name@1} ->
{gleam@list:map(
Tables,
fun(T@4) ->
case string:lowercase(erlang:element(2, T@4)) =:= string:lowercase(
Table_name@2
) of
true ->
{table,
erlang:element(2, T@4),
gleam@list:map(
erlang:element(3, T@4),
fun(C@1) ->
case string:lowercase(
erlang:element(2, C@1)
)
=:= string:lowercase(Old_name@1) of
true ->
{column,
string:lowercase(
New_name@1
),
erlang:element(3, C@1),
erlang:element(4, C@1)};
false ->
C@1
end
end
)};
false ->
T@4
end
end
),
Enums};
{alter_column_type, Table_name@3, Column_name@1, Type_tokens} ->
Type_text = render_type_tokens(Type_tokens),
New_type = case sqlode@model:parse_sql_type(Type_text) of
{ok, T@5} ->
{some, T@5};
{error, _} ->
none
end,
case New_type of
{some, Scalar_type} ->
{gleam@list:map(
Tables,
fun(T@6) ->
case string:lowercase(erlang:element(2, T@6))
=:= string:lowercase(Table_name@3) of
true ->
{table,
erlang:element(2, T@6),
gleam@list:map(
erlang:element(3, T@6),
fun(C@2) ->
case string:lowercase(
erlang:element(2, C@2)
)
=:= string:lowercase(
Column_name@1
) of
true ->
{column,
erlang:element(
2,
C@2
),
Scalar_type,
erlang:element(
4,
C@2
)};
false ->
C@2
end
end
)};
false ->
T@6
end
end
),
Enums};
none ->
{Tables, Enums}
end;
{alter_column_set_not_null, Table_name@4, Column_name@2} ->
{gleam@list:map(
Tables,
fun(T@7) ->
case string:lowercase(erlang:element(2, T@7)) =:= string:lowercase(
Table_name@4
) of
true ->
{table,
erlang:element(2, T@7),
gleam@list:map(
erlang:element(3, T@7),
fun(C@3) ->
case string:lowercase(
erlang:element(2, C@3)
)
=:= string:lowercase(Column_name@2) of
true ->
{column,
erlang:element(2, C@3),
erlang:element(3, C@3),
false};
false ->
C@3
end
end
)};
false ->
T@7
end
end
),
Enums};
{alter_column_drop_not_null, Table_name@5, Column_name@3} ->
{gleam@list:map(
Tables,
fun(T@8) ->
case string:lowercase(erlang:element(2, T@8)) =:= string:lowercase(
Table_name@5
) of
true ->
{table,
erlang:element(2, T@8),
gleam@list:map(
erlang:element(3, T@8),
fun(C@4) ->
case string:lowercase(
erlang:element(2, C@4)
)
=:= string:lowercase(Column_name@3) of
true ->
{column,
erlang:element(2, C@4),
erlang:element(3, C@4),
true};
false ->
C@4
end
end
)};
false ->
T@8
end
end
),
Enums}
end.
-file("src/sqlode/schema_parser.gleam", 1411).
-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", 1419).
-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", 1428).
-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", 1439).
-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", 1354).
-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", 1458).
-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", 1261).
?DOC(
" Shared logic for `parse_named_column` and `parse_column_tokens`.\n"
" Inspects the column's type tokens, detects MySQL inline `ENUM(...)`\n"
" or `SET(...)` when `engine == MySQL`, synthesizes enum definitions,\n"
" and falls through to the shared lookup / `parse_sql_type` pipeline\n"
" otherwise.\n"
).
-spec build_column_from_type_tokens(
binary(),
binary(),
binary(),
list(sqlode@lexer:token()),
list(sqlode@lexer:token()),
list(sqlode@model:enum_def()),
sqlode@model:engine()
) -> {ok,
gleam@option:option({sqlode@model:column(),
list(sqlode@model:enum_def())})} |
{error, parse_error()}.
build_column_from_type_tokens(
Path,
Table_name,
Name,
All_tokens,
Type_toks,
Enums,
Engine
) ->
Type_text = render_type_tokens(Type_toks),
Nullable = case (tokens_contain_not_null(All_tokens) orelse tokens_contain_keyword(
All_tokens,
<<"primary"/utf8>>
))
orelse gleam_stdlib:contains_string(Type_text, <<"serial"/utf8>>) of
true ->
false;
false ->
true
end,
case detect_mysql_inline_enum_set(Type_toks, Table_name, Name, Engine) of
{some, {inline_enum, Scalar_type, New_enum}} ->
{ok, {some, {{column, Name, Scalar_type, Nullable}, [New_enum]}}};
none ->
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@1) ->
{ok, {some, {{column, Name, Scalar_type@1, Nullable}, []}}}
end)
end.
-file("src/sqlode/schema_parser.gleam", 1176).
-spec parse_named_column(
binary(),
binary(),
binary(),
list(sqlode@lexer:token()),
list(sqlode@lexer:token()),
list(sqlode@model:enum_def()),
sqlode@model:engine()
) -> {ok,
gleam@option:option({sqlode@model:column(),
list(sqlode@model:enum_def())})} |
{error, parse_error()}.
parse_named_column(Path, Table_name, Raw_name, All_tokens, Rest, Enums, Engine) ->
Name = sqlode@naming:normalize_identifier(Raw_name),
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>>}};
_ ->
build_column_from_type_tokens(
Path,
Table_name,
Name,
All_tokens,
Type_toks,
Enums,
Engine
)
end.
-file("src/sqlode/schema_parser.gleam", 1207).
-spec parse_column_tokens(
binary(),
binary(),
list(sqlode@lexer:token()),
list(sqlode@model:enum_def()),
sqlode@model:engine()
) -> {ok,
gleam@option:option({sqlode@model:column(),
list(sqlode@model:enum_def())})} |
{error, parse_error()}.
parse_column_tokens(Path, Table_name, Tokens, Enums, Engine) ->
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};
{keyword, N} ->
parse_named_column(
Path,
Table_name,
N,
Tokens,
Rest,
Enums,
Engine
);
{ident, N@1} ->
Name = sqlode@naming:normalize_identifier(N@1),
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>>}};
_ ->
build_column_from_type_tokens(
Path,
Table_name,
Name,
Tokens,
Type_toks,
Enums,
Engine
)
end;
{quoted_ident, N@1} ->
Name = sqlode@naming:normalize_identifier(N@1),
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>>}};
_ ->
build_column_from_type_tokens(
Path,
Table_name,
Name,
Tokens,
Type_toks,
Enums,
Engine
)
end;
_ ->
{ok, none}
end
end.
-file("src/sqlode/schema_parser.gleam", 567).
?DOC(
" Parse ALTER TABLE <name> ADD [COLUMN] <col_def> and apply to existing tables.\n"
"\n"
" Returns both the updated table list and any enum definitions synthesized\n"
" from inline MySQL `ENUM(...)` / `SET(...)` column types, so the caller can\n"
" thread them into the running catalog.\n"
).
-spec apply_alter_table_add_column(
binary(),
list(sqlode@lexer:token()),
list(sqlode@model:enum_def()),
list(sqlode@model:table()),
sqlode@model:engine()
) -> {ok, {list(sqlode@model:table()), list(sqlode@model:enum_def())}} |
{error, parse_error()}.
apply_alter_table_add_column(Path, Tokens, Enums, Tables, Engine) ->
{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, Engine),
fun(Maybe_col) -> case Maybe_col of
none ->
{ok, {Tables, []}};
{some, {Col, New_enums}} ->
{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
),
New_enums}}
end end
)
end.
-file("src/sqlode/schema_parser.gleam", 1103).
-spec parse_columns_tokens(
binary(),
binary(),
list(sqlode@lexer:token()),
list(sqlode@model:enum_def()),
sqlode@model:engine()
) -> {ok, {list(sqlode@model:column()), list(sqlode@model:enum_def())}} |
{error, parse_error()}.
parse_columns_tokens(Path, Table_name, Tokens, Enums, Engine) ->
_pipe = split_tokens_by_comma(Tokens),
_pipe@1 = gleam@list:try_fold(
_pipe,
{[], []},
fun(Acc, Col_tokens) ->
{Columns, Collected_enums} = Acc,
Visible_enums = lists:append(Enums, Collected_enums),
gleam@result:'try'(
parse_column_tokens(
Path,
Table_name,
Col_tokens,
Visible_enums,
Engine
),
fun(Maybe_column) -> case Maybe_column of
{some, {Column, New_enums}} ->
{ok,
{[Column | Columns],
lists:append(Collected_enums, New_enums)}};
none ->
{ok, Acc}
end end
)
end
),
gleam@result:map(
_pipe@1,
fun(Pair) ->
{lists:reverse(erlang:element(1, Pair)), erlang:element(2, Pair)}
end
).
-file("src/sqlode/schema_parser.gleam", 1025).
-spec parse_create_table_tokens(
binary(),
list(sqlode@lexer:token()),
list(sqlode@model:enum_def()),
sqlode@model:engine()
) -> {ok,
gleam@option:option({sqlode@model:table(),
list(sqlode@model:enum_def())})} |
{error, parse_error()}.
parse_create_table_tokens(Path, Tokens, Enums, Engine) ->
{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,
Engine
),
fun(_use0) ->
{Columns, New_enums} = _use0,
{ok,
{some,
{{table, Table_name, Columns}, New_enums}}}
end
)
end
)
end.
-file("src/sqlode/schema_parser.gleam", 640).
-spec parse_statement_tokens(
binary(),
list(sqlode@lexer:token()),
list(sqlode@model:enum_def()),
sqlode@model:engine()
) -> {ok, statement_result()} | {error, parse_error()}.
parse_statement_tokens(Path, Tokens, Enums, Engine) ->
case is_create_table_tokens(Tokens) of
true ->
gleam@result:'try'(
parse_create_table_tokens(Path, Tokens, Enums, Engine),
fun(Maybe_table) -> case Maybe_table of
{some, {Table, New_enums}} ->
{ok, {create_table_result, Table, New_enums}};
none ->
{ok, ignored}
end end
);
false ->
case classify_unknown_statement(Tokens) of
{destructive_d_d_l, Action} ->
{ok, {d_d_l_applied, Action}};
silently_ignored ->
{ok, ignored}
end
end.
-file("src/sqlode/schema_parser.gleam", 62).
-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,
{[], All_enums, []},
fun(Acc, Stmt_tokens@1) ->
{Tables, Current_enums, 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],
Current_enums,
New_warnings};
none ->
{Tables, Current_enums, 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,
Current_enums,
Tables,
Engine
),
fun(_use0) ->
{New_tables, Added_enums} = _use0,
{ok,
{New_tables,
lists:append(
Current_enums,
Added_enums
),
Warnings}}
end
);
false ->
gleam@result:'try'(
parse_statement_tokens(
Path,
Stmt_tokens@1,
Current_enums,
Engine
),
fun(Stmt_result) -> case Stmt_result of
{create_table_result,
Table@1,
New_enums} ->
{ok,
{[Table@1 | Tables],
lists:append(
Current_enums,
New_enums
),
Warnings}};
{d_d_l_applied, Action} ->
{New_tables@1, New_enums@1} = apply_ddl_action(
Action,
Tables,
Current_enums
),
{ok,
{New_tables@1,
New_enums@1,
Warnings}};
ignored ->
{ok,
{Tables,
Current_enums,
Warnings}}
end end
)
end
end
end
),
gleam@result:map(
_pipe@2,
fun(Triple) ->
{lists:reverse(erlang:element(1, Triple)),
erlang:element(2, Triple),
erlang:element(3, Triple)}
end
)
end,
fun(_use0@1) ->
{Tables@1, Final_enums, Warnings@1} = _use0@1,
{ok, {parsed_schema, Tables@1, Final_enums, Warnings@1}}
end
).
-file("src/sqlode/schema_parser.gleam", 37).
-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", 31).
-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", 1480).
-spec path_prefix(binary()) -> binary().
path_prefix(Path) ->
case Path of
<<""/utf8>> ->
<<""/utf8>>;
_ ->
<<Path/binary, ": "/utf8>>
end.
-file("src/sqlode/schema_parser.gleam", 1467).
-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>>
end.
-file("src/sqlode/schema_parser.gleam", 1487).
-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.