diff --git a/lib/ayesql/compiler.ex b/lib/ayesql/compiler.ex index 267ccaf..6491058 100644 --- a/lib/ayesql/compiler.ex +++ b/lib/ayesql/compiler.ex @@ -32,7 +32,7 @@ defmodule AyeSQL.Compiler do @typedoc """ Query. """ - @type query :: {name(), docs(), fragments()} + @type query :: {name(), docs(), fragments(), boolean()} @typedoc """ Queries. @@ -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)] 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() @@ -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()} @@ -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() diff --git a/lib/ayesql/lexer.ex b/lib/ayesql/lexer.ex index 38bbc2d..9106b2b 100644 --- a/lib/ayesql/lexer.ex +++ b/lib/ayesql/lexer.ex @@ -34,6 +34,7 @@ defmodule AyeSQL.Lexer do @type token_name :: :"$name" | :"$docs" + | :"$query_fragment_metadata" | :"$fragment" | :"$named_param" diff --git a/mix.exs b/mix.exs index 1af465c..1c7ef26 100644 --- a/mix.exs +++ b/mix.exs @@ -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 diff --git a/src/ayesql_lexer.xrl b/src/ayesql_lexer.xrl index c1eb795..d249d1f 100644 --- a/src/ayesql_lexer.xrl +++ b/src/ayesql_lexer.xrl @@ -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_]* @@ -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). @@ -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) -> diff --git a/src/ayesql_parser.yrl b/src/ayesql_parser.yrl index 220a8f8..a00c95e 100644 --- a/src/ayesql_parser.yrl +++ b/src/ayesql_parser.yrl @@ -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') ]. @@ -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). diff --git a/test/ayesql/compiler_test.exs b/test/ayesql/compiler_test.exs index 2801544..24b843d 100644 --- a/test/ayesql/compiler_test.exs +++ b/test/ayesql/compiler_test.exs @@ -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 diff --git a/test/ayesql/lexer_test.exs b/test/ayesql/lexer_test.exs index ff2b5de..6d4998c 100644 --- a/test/ayesql/lexer_test.exs +++ b/test/ayesql/lexer_test.exs @@ -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 = """ diff --git a/test/ayesql_test.exs b/test/ayesql_test.exs index e3b04b7..f94eb3c 100644 --- a/test/ayesql_test.exs +++ b/test/ayesql_test.exs @@ -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 diff --git a/test/support/fragments.sql b/test/support/fragments.sql new file mode 100644 index 0000000..73ca87f --- /dev/null +++ b/test/support/fragments.sql @@ -0,0 +1,5 @@ +-- Simple fragment query +-- name: user_fields +-- docs: Simple query +-- fragment: true +WHERE user_fields = :user_fields