Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add fragment support #44

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 28 additions & 11 deletions lib/ayesql/compiler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ defmodule AyeSQL.Compiler do
@typedoc """
Query.
"""
@type query :: {name(), docs(), fragments()}
@type query :: {name(), docs(), fragments(), boolean()}

@typedoc """
Queries.
Expand Down Expand Up @@ -137,21 +137,36 @@ defmodule AyeSQL.Compiler do
end

@spec create_queries(queries()) :: Macro.t() | [Macro.t()]
@spec create_queries(queries(), [Macro.t()]) :: Macro.t() | [Macro.t()]
defp create_queries(queries, acc \\ [])
@spec create_queries(queries(), [Macro.t()], [queries()]) ::
Macro.t() | [Macro.t()]
defp create_queries(queries, acc \\ [], metadata \\ [])

defp create_queries([{nil, nil, fragments}], _acc) do
# Handle anonymous queries (3-tuple format)
defp create_queries([{nil, nil, fragments}], _acc, _metadata) do
create_single_query(fragments)
end

defp create_queries([], acc) do
Enum.reverse(acc)
# Handle named queries (4-tuple format with is_fragment)
defp create_queries([], acc, metadata) do
# Extract fragment names from the original metadata tuples
fragment_names = for {name, _docs, _fragments, true} <- metadata, do: name

fragment_func =
quote do
def fragment_functions(), do: unquote(fragment_names)
end

[fragment_func | Enum.reverse(acc)]
Comment on lines +152 to +159
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we might also want to add

Suggested change
fragment_names = for {name, _docs, _fragments, true} <- metadata, do: name
fragment_func =
quote do
def fragment_functions(), do: unquote(fragment_names)
end
[fragment_func | Enum.reverse(acc)]
fragment_names = for {name, _docs, _fragments, true} <- metadata, do: name
fragment_func =
quote do
def fragment_functions(), do: unquote(fragment_names)
end
query_names = for {name, _docs, _fragments, false} <- metadata, do: name
query_func =
quote do
def query_functions(), do: unquote(query_names)
end
[query_func, fragment_func | Enum.reverse(acc)]

so that we have a direct source of all the non-fragment functions also, that might actually be cleaner for my use case because I also have to filter out __db_runner__, __db_options__ and run to skip explaining those, and with this I could JUST call explain on each function returned by query_functions 🤷‍♂️ just a passing thought, dunno, I don't really need both but might be worth having both.

end

defp create_queries([{_name, _docs, _fragments} = query | queries], acc) do
defp create_queries(
[{_name, _docs, _fragments, _is_fragment} = query | queries],
acc,
metadata
) do
# Keep the original tuple in metadata for later processing
acc = [create_query!(query), create_query(query) | acc]

create_queries(queries, acc)
create_queries(queries, acc, [query | metadata])
end

@spec create_single_query(fragments()) :: Macro.t()
Expand Down Expand Up @@ -181,11 +196,12 @@ defmodule AyeSQL.Compiler do
end

@spec create_query(query()) :: Macro.t()
defp create_query({name, docs, fragments}) do
defp create_query({name, docs, fragments, _is_fragment}) do
fragments = Macro.escape(fragments)

quote do
@doc AyeSQL.Compiler.gen_docs(unquote(docs), unquote(fragments))

@spec unquote(name)(AyeSQL.Core.parameters()) ::
{:ok, AyeSQL.Query.t() | term()}
| {:error, AyeSQL.Error.t() | term()}
Expand Down Expand Up @@ -215,11 +231,12 @@ defmodule AyeSQL.Compiler do
end

@spec create_query!(query()) :: Macro.t()
defp create_query!({name, docs, _}) do
defp create_query!({name, docs, _, _is_fragment}) do
name! = String.to_atom("#{name}!")

quote do
@doc AyeSQL.Compiler.gen_docs!(unquote(docs))

@spec unquote(name!)(AyeSQL.Core.parameters()) ::
AyeSQL.Query.t()
| term()
Expand Down
1 change: 1 addition & 0 deletions lib/ayesql/lexer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ defmodule AyeSQL.Lexer do
@type token_name ::
:"$name"
| :"$docs"
| :"$query_fragment_metadata"
| :"$fragment"
| :"$named_param"

Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule AyeSQL.MixProject do
use Mix.Project

@version "1.1.3"
@version "1.1.4"
@name "AyeSQL"
@description "Library for using raw SQL"
@app :ayesql
Expand Down
10 changes: 7 additions & 3 deletions src/ayesql_lexer.xrl
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ NewLine = (\n|\r)+
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Comments and special directives

FunName = {NewLine}*(\-\-)\sname\:\s[^\n\r]+
FunDocs = {NewLine}*(\-\-)\sdocs\:\s[^\n\r]+
FunName = {NewLine}*{WhiteSpace}*(\-\-)\sname\:\s[^\n\r]+
FunDocs = {NewLine}*{WhiteSpace}*(\-\-)\sdocs\:\s[^\n\r]+
FunFrag = {NewLine}*{WhiteSpace}*(\-\-)\sfragment\:\s[^\n\r]+
Comment = (\-\-)[^\n\r]+

Atom = [a-z_][0-9a-zA-Z_]*
Expand All @@ -26,7 +27,7 @@ Rules.

{FunName} : new_comment(TokenLine, TokenLen, TokenChars).
{FunDocs} : new_comment(TokenLine, TokenLen, TokenChars).

{FunFrag} : new_comment(TokenLine, TokenLen, TokenChars).
{NamedParam} : new_param(TokenLine, TokenLen, TokenChars).
({Comment}?|({String}|{Fragment})+) : new_fragment(TokenLine, TokenLen, TokenChars).

Expand Down Expand Up @@ -67,6 +68,9 @@ new_comment(TokenLine, TokenLen, "-- name: " ++ Value = TokenChars) ->
new_comment(TokenLine, TokenLen, "-- docs: " ++ Value = TokenChars) ->
Documentation = string:trim(Value),
new_token("docs", Documentation, TokenChars, TokenLine, TokenLen);
new_comment(TokenLine, TokenLen, "-- fragment: " ++ Value = TokenChars) ->
Fragment = string:trim(Value),
new_token("query_fragment_metadata", Fragment, TokenChars, TokenLine, TokenLen);
new_comment(_, _, "--" ++ _) ->
skip_token;
new_comment(TokenLine, TokenLen, TokenChars) ->
Expand Down
10 changes: 6 additions & 4 deletions src/ayesql_parser.yrl
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
Nonterminals queries named_queries named_query fragments.
Terminals '$name' '$docs' '$fragment' '$named_param'.
Terminals '$name' '$docs' '$fragment' '$query_fragment_metadata' '$named_param'.
Rootsymbol queries.


queries -> fragments : [ {nil, nil, join_fragments('$1', [])} ].
queries -> named_queries : '$1'.

named_queries -> named_query : [ '$1' ].
named_queries -> named_query named_queries : [ '$1' | '$2' ].

named_query -> '$name' fragments : {extract_value('$1'), nil, join_fragments('$2', [])}.
named_query -> '$name' '$docs' fragments : {extract_value('$1'), extract_value('$2'), join_fragments('$3', [])}.
named_query -> '$name' fragments : {extract_value('$1'), nil, join_fragments('$2', []), false}.
named_query -> '$name' '$docs' fragments : {extract_value('$1'), extract_value('$2'), join_fragments('$3', []), false}.
named_query -> '$name' '$docs' '$query_fragment_metadata' fragments : {extract_value('$1'), extract_value('$2'), join_fragments('$4', []), true}.

fragments -> '$fragment' : [ extract_value('$1') ].
fragments -> '$named_param' : [ extract_value('$1') ].
Expand All @@ -27,6 +27,8 @@ extract_value({'$docs', _, {Value, _, _}}) ->
Value;
extract_value({'$fragment', _, {Value, _, _}}) ->
Value;
extract_value({'$query_fragment_metadata', _, {Value, _, _}}) ->
Value;
extract_value({'$named_param', _, {Value, _, _}}) ->
binary_to_atom(Value).

Expand Down
25 changes: 25 additions & 0 deletions test/ayesql/compiler_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,31 @@ defmodule AyeSQL.CompilerTest do
Compiler.compile_queries("SELECT * FROM table")
end
end

test "should succeed when docs are provided after name" do
contents = """
-- name: function_name
-- docs: Documentation
Query
"""

[tuple | _rest] = Compiler.compile_queries(contents)
assert is_tuple(tuple)
assert elem(tuple, 0) == :def
end

test "should succeed when fragment: true is specified" do
contents = """
-- name: function_name
-- docs: Documentation
-- fragment: true
Query
"""

[tuple | _rest] = Compiler.compile_queries(contents)
assert is_tuple(tuple)
assert elem(tuple, 0) == :def
end
end

describe "eval_query/2" do
Expand Down
10 changes: 10 additions & 0 deletions test/ayesql/lexer_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ defmodule AyeSQL.LexerTest do
end
end

describe "fragment boolean" do
test "gets fragment metadata" do
target = "-- fragment: true"

assert [
{:"$query_fragment_metadata", 1, {"true", ^target, {1, 1}}}
] = Lexer.tokenize(target)
end
end

describe "comments" do
test "ignores comments" do
target = """
Expand Down
13 changes: 13 additions & 0 deletions test/ayesql_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -324,4 +324,17 @@ defmodule AyeSQLTest do
assert {:ok, {_, [], [repo: MyRepo]}} = WithRunner.get_hostnames([])
end
end

test "correctly identifies fragment functions" do
defmodule FragmentTest do
use AyeSQL, runner: TestRunner, repo: MyRepo

defqueries("support/fragments.sql")
end

# Add debug output
IO.puts("Module attributes: #{inspect(FragmentTest.__info__(:attributes))}")

assert FragmentTest.fragment_functions() == [:user_fields]
end
end
5 changes: 5 additions & 0 deletions test/support/fragments.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- Simple fragment query
-- name: user_fields
-- docs: Simple query
-- fragment: true
WHERE user_fields = :user_fields
Loading