diff --git a/lib/ayesql/compiler.ex b/lib/ayesql/compiler.ex index 47129b1..90cb004 100644 --- a/lib/ayesql/compiler.ex +++ b/lib/ayesql/compiler.ex @@ -19,6 +19,11 @@ defmodule AyeSQL.Compiler do """ @type fragments :: [fragment() | param()] + @typedoc """ + Query type. + """ + @type type :: nil | atom() + @typedoc """ Query name. """ @@ -32,7 +37,7 @@ defmodule AyeSQL.Compiler do @typedoc """ Query. """ - @type query :: {name(), docs(), fragments()} + @type query :: {name(), type(), docs(), fragments()} @typedoc """ Queries. @@ -140,7 +145,7 @@ defmodule AyeSQL.Compiler do @spec create_queries(queries(), [Macro.t()]) :: Macro.t() | [Macro.t()] defp create_queries(queries, acc \\ []) - defp create_queries([{nil, nil, fragments}], _acc) do + defp create_queries([{nil, _any, nil, fragments}], _acc) do create_single_query(fragments) end @@ -148,7 +153,10 @@ defmodule AyeSQL.Compiler do Enum.reverse(acc) end - defp create_queries([{_name, _docs, _fragments} = query | queries], acc) do + defp create_queries( + [{_name, _type, _docs, _fragments} = query | queries], + acc + ) do acc = [create_query!(query), create_query(query) | acc] create_queries(queries, acc) @@ -181,7 +189,47 @@ defmodule AyeSQL.Compiler do end @spec create_query(query()) :: Macro.t() - defp create_query({name, docs, fragments}) do + defp create_query({name, :script, docs, fragments}) 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()} + @spec unquote(name)(AyeSQL.Core.parameters(), AyeSQL.Core.options()) :: + {:ok, AyeSQL.Query.t() | term()} + | {:error, AyeSQL.Error.t() | term()} + def unquote(name)(params, options \\ []) + + def unquote(name)(params, options) do + options = Keyword.merge(__MODULE__.__db_options__(), options) + + {index, options} = Keyword.pop(options, :index, 1) + {run?, options} = Keyword.pop(options, :run, true) + + content = AyeSQL.AST.expand(__MODULE__, unquote(fragments)) + context = AyeSQL.AST.Context.new(index: index) + + with {:ok, %{statement: statement} = query} <- + AyeSQL.Core.evaluate(content, params, context) do + if run? do + statement + |> String.split(";", trim: true) + |> Enum.map(&String.trim(&1)) + |> Enum.each(&__MODULE__.run!(%{query | statement: &1}, options)) + + {:ok, []} + else + {:ok, query} + end + end + end + end + end + + @spec create_query(query()) :: Macro.t() + defp create_query({name, :normal, docs, fragments}) do fragments = Macro.escape(fragments) quote do @@ -215,7 +263,7 @@ defmodule AyeSQL.Compiler do end @spec create_query!(query()) :: Macro.t() - defp create_query!({name, docs, _}) do + defp create_query!({name, _type, docs, _}) do name! = String.to_atom("#{name}!") quote do @@ -297,7 +345,7 @@ defmodule AyeSQL.Compiler do no_return() defp raise_error( contents, - {line, _, ['syntax error before: ', info]}, + {line, _, [~c"syntax error before: ", info]}, options ) do error_context = options[:error_context] || 2 diff --git a/src/ayesql_lexer.xrl b/src/ayesql_lexer.xrl index c1eb795..60aa4a5 100644 --- a/src/ayesql_lexer.xrl +++ b/src/ayesql_lexer.xrl @@ -11,6 +11,7 @@ NewLine = (\n|\r)+ FunName = {NewLine}*(\-\-)\sname\:\s[^\n\r]+ FunDocs = {NewLine}*(\-\-)\sdocs\:\s[^\n\r]+ +FunType = {NewLine}*(\-\-)\stype\:\s[^\n\r]+ Comment = (\-\-)[^\n\r]+ Atom = [a-z_][0-9a-zA-Z_]* @@ -26,6 +27,7 @@ Rules. {FunName} : new_comment(TokenLine, TokenLen, TokenChars). {FunDocs} : new_comment(TokenLine, TokenLen, TokenChars). +{FunType} : new_comment(TokenLine, TokenLen, TokenChars). {NamedParam} : new_param(TokenLine, TokenLen, TokenChars). ({Comment}?|({String}|{Fragment})+) : new_fragment(TokenLine, TokenLen, TokenChars). @@ -64,6 +66,9 @@ new_fragment(TokenLine, TokenLen, TokenChars) -> new_comment(TokenLine, TokenLen, "-- name: " ++ Value = TokenChars) -> Identifier = string:trim(Value), new_token("name", Identifier, TokenChars, TokenLine, TokenLen); +new_comment(TokenLine, TokenLen, "-- type: " ++ Value = TokenChars) -> + Identifier = string:trim(Value), + new_token("type", Identifier, TokenChars, TokenLine, TokenLen); new_comment(TokenLine, TokenLen, "-- docs: " ++ Value = TokenChars) -> Documentation = string:trim(Value), new_token("docs", Documentation, TokenChars, TokenLine, TokenLen); diff --git a/src/ayesql_parser.yrl b/src/ayesql_parser.yrl index 220a8f8..95e6aba 100644 --- a/src/ayesql_parser.yrl +++ b/src/ayesql_parser.yrl @@ -1,33 +1,48 @@ Nonterminals queries named_queries named_query fragments. -Terminals '$name' '$docs' '$fragment' '$named_param'. +Terminals '$name' '$type' '$docs' '$fragment' '$named_param'. Rootsymbol queries. -queries -> fragments : [ {nil, nil, join_fragments('$1', [])} ]. +queries -> fragments : [ {nil, normal, 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_name('$1'), normal, nil, join_fragments('$2', [])}. +named_query -> '$name' '$docs' fragments + : {extract_name('$1'), normal, extract_docs('$2'), join_fragments('$3', [])}. +named_query -> '$name' '$type' fragments + : {extract_name('$1'), extract_type('$2'), nil, join_fragments('$3', [])}. +named_query -> '$name' '$type' '$docs' fragments + : {extract_name('$1'), extract_type('$2'), extract_docs('$3'), join_fragments('$4', [])}. -fragments -> '$fragment' : [ extract_value('$1') ]. -fragments -> '$named_param' : [ extract_value('$1') ]. -fragments -> '$fragment' fragments : [ extract_value('$1') | '$2' ]. -fragments -> '$named_param' fragments : [ extract_value('$1') | '$2' ]. +fragments -> '$fragment' : [ extract_fragment('$1') ]. +fragments -> '$fragment' fragments : [ extract_fragment('$1') | '$2' ]. +fragments -> '$named_param' : [ extract_named('$1') ]. +fragments -> '$named_param' fragments : [ extract_named('$1') | '$2' ]. Erlang code. -extract_value({'$name', _, {Value, _, _}}) -> - binary_to_atom(Value); -extract_value({'$docs', _, {<<>>, _, _}}) -> +extract_type({'$type', _, {<<>>, _, _}}) -> + normal; +extract_type({'$type', _, {Value, _, _}}) -> + binary_to_atom(Value). + +extract_name({'$name', _, {Value, _, _}}) -> + Value1 = [X || <> <= Value, not lists:member(X, "#")], + list_to_atom(Value1). + +extract_docs({'$docs', _, {<<>>, _, _}}) -> nil; -extract_value({'$docs', _, {Value, _, _}}) -> - Value; -extract_value({'$fragment', _, {Value, _, _}}) -> - Value; -extract_value({'$named_param', _, {Value, _, _}}) -> +extract_docs({'$docs', _, {Value, _, _}}) -> + Value. + +extract_fragment({'$fragment', _, {Value, _, _}}) -> + Value. + +extract_named({'$named_param', _, {Value, _, _}}) -> binary_to_atom(Value). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff --git a/test/ayesql_test.exs b/test/ayesql_test.exs index e3b04b7..c32bff1 100644 --- a/test/ayesql_test.exs +++ b/test/ayesql_test.exs @@ -110,6 +110,16 @@ defmodule AyeSQLTest do end end + describe "when query is a script" do + import AyeSQL, only: [defqueries: 3] + + defqueries(Script, "support/script.sql", runner: TestRunner) + + test "can expand query with empty params list" do + assert Script.set_schema([]) == {:ok, []} + end + end + describe "when query does not need parameters" do import AyeSQL, only: [defqueries: 3] diff --git a/test/support/script.sql b/test/support/script.sql new file mode 100644 index 0000000..9f10b2b --- /dev/null +++ b/test/support/script.sql @@ -0,0 +1,5 @@ +-- Runs multiple sql statements +-- name: set_schema +-- type: script +CREATE SCHEMA schema1; +CREATE SCHEMA schema2;