-
Notifications
You must be signed in to change notification settings - Fork 4
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
feat: edgeql structure generation #168
base: edgeql-generation
Are you sure you want to change the base?
Conversation
3a4efaf
to
fcf5085
Compare
fcf5085
to
1034cea
Compare
f43bba2
to
1034cea
Compare
… into edgeql-generation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work, I think this PR is ready for about 70-80 percent, which is great!
Please check my comments and fix them. If you have any questions, feel free to ask them here or in Discord.
I also suggest you run the tests so you can track if there's a issue in the code somewhere without waiting for me to look at PR update.
And to make it easier to debug the code you can run mix edgedb.generate
, it will generate code for queries from test/support/codegen/edgeql
and you can see if there are any bugs/errors in the generation. Also, when you are happy with the results, you should replace the .edgeql.ex.assert
files queries directory with the .edgeql.ex.assert
extension, since they are the main test sources for codegen that make sure that the behavior is correct.
- support for generating Elixir modules from EdgeQL queries via `mix edgedb.generate`. | ||
- abitility to pass atoms as valid arguments for enums. | ||
|
||
### Changed | ||
- `jason` to be required library, but still configurable. | ||
- `EdgeDB.NamedTuple.to_map/2` to include indexes as keys into result map. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is already in the Unreleased
section, probably an artifact after a merge conflict
@@ -61,7 +61,7 @@ defmodule EdgeDB.EdgeQL.Generator do | |||
EEx.function_from_file(:defp, :render_object_template, @object_template, [:assigns]) | |||
EEx.function_from_file(:defp, :render_set_template, @set_template, [:assigns]) | |||
|
|||
@spec generate(Keyword.t()) :: {:ok, %{Path.t() => Path.t()}} | {:error, term()} | |||
@spec generate(Keyword.t()) :: {:ok, list(Path.t())} | {:error, term()} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
generate/1
actually returns a map %{path to request file => path to generated module}
lib/edgedb/edgeql/generator.ex
Outdated
end | ||
|
||
defp codec_to_shape(%Codecs.Duration{}, _codec_storage) do | ||
timex? = Application.get_env(:edgedb, :timex_duration, true) | ||
|
||
typename = "duration()" | ||
|
||
case Code.ensure_loaded?(Timex) do | ||
case Code.loaded?(Timex) do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
probably also wrong conflict resolution. this need to be Code.ensure_loaded?/1
because Code.loadled?/1
appeared only in Elixir >= 1.15
results = case results do | ||
[] -> nil | ||
_ -> Enum.map(results, fn result -> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this code will always execute the _
clause, because EdgeDB.query/4
returns {:ok, EdgeDB.Set.t()}
, so we can just leave Enum.map/2
here
<% else %> | ||
<%= name %>: | ||
<% end %> | ||
@derive Jason.Encoder; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should be rearranged to use the suggestions from credo. And it will be better for readability to add a blank line between the attribute, struct's definition and typespec's definition
<% name -> %> | ||
:<%= name %>, | ||
<%= name %>: result<%= Enum.map_join(@paths, &("[\"#{&1}\"]")) %>["<%= name %>"], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the EdgeDB query from test/support/codegen/edgeql/standart/select_start_types_named_simple.edgeql
this code generates the following mapping of the result:
%Result{
f: result["f"],
e: result["e"],
d: result["d"],
c: result["c"],
a: result["a"],
b: %Result.B{b_b: result["b"]["b_b"], b_a: result["b"]["b_a"]}
}
Nested calls won't be quite as performant (though not too slow), since in this case Elixir would have to first look at the result
property and fetch the "b"
object, and only then the "b_b"
property. And then do the same for the `"b_a" property. But the main problem here is that it handles all types of links in the same way: required, optional and multi. This is a bug and will cause an error when running the code. This code should check the cardinality of a property and do something like that:
# if b is a required link
%Result{
f: result["f"],
e: result["e"],
d: result["d"],
c: result["c"],
a: result["a"],
b: with b <- result["b"] do
%Result.B{b_b: b["b_b"], b_a: b["b_a"]}
end
}
# if b is an optional link
%Result{
f: result["f"],
e: result["e"],
d: result["d"],
c: result["c"],
a: result["a"],
b: with b when not is_nil(b) <- result["b"] do
%Result.B{b_b: b["b_b"], b_a: b["b_a"]}
end
}
# and if b is a set
%Result{
f: result["f"],
e: result["e"],
d: result["d"],
c: result["c"],
a: result["a"],
b: Enum.map(result["b"], fn b ->
%Result.B{b_b: b["b_b"], b_a: b["b_a"]}
end),
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's possible that this problem also affects normal properties, not just relationships, because I noticed for the query from test/support/codegen/edgeql/types/select_result.edgeql
the following mapping
mp_named_tuple_x_int64_y_int64: %{
y: result["mp_named_tuple_x_int64_y_int64"]["y"],
x: result["mp_named_tuple_x_int64_y_int64"]["x"]
},
This should be wrapped in Enum.map/2
for cases where it is a mutli property or an array + this code doesn't account for indexes as fields for named tuples though they are in the typespec
<% end %> | ||
} | ||
} <%= if @object[:is_optional] do %> | ||
| nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line is not necessary. Fields of a structure can be optional, but not the type itself, it will always be an instance of the structure. The code above (that types fields) handles this correctly. Right now the following will be generated for the query from test/support/codegen/edgeql/types/select_result.edgeql
:
defmodule Result do
# ...
defmodule OlAB do
@derive Jason.Encoder
defstruct [:id]
@type t() :: %__MODULE__{id: Tests.Codegen.Queries.Types.SelectResult.uuid()} | nil # this is not required
end
@type t() :: %__MODULE__{
id: Tests.Codegen.Queries.Types.SelectResult.uuid(),
rp_a_str: String.t(),
rp_b_str: String.t(),
rp_c_str: String.t(),
rp_d_str: String.t(),
rp_f_str: String.t(),
ol_a: OlA.t() | nil,
ml_a: list(MlA.t()),
ol_a_b: OlAB.t() | nil, # this is correct
ml_a_b: list(MlAB.t())
}
# ...
end
render_set: @render_set, | ||
module_name: @module_name | ||
) %> | ||
end; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment applies to most of the changes in templates. I noticed that you use ;
at the end of statements, but you don't need that in Elixir. Can you please remove it here and in other places?
should_render_type_for_shape: complex_shape?(raw_shape), | ||
result_type: (complex_shape?(raw_shape) && "result()") || rendered_shape, | ||
should_render_type_for_shape: rendered_schema && complex?, | ||
cardinality_to_function: @cardinality_to_function, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is now useless, right? Since the template code checks the cardinality itself
query_function: @cardinality_to_function[query.result_cardinality], | ||
should_render_type_for_shape: complex_shape?(raw_shape), | ||
result_type: (complex_shape?(raw_shape) && "result()") || rendered_shape, | ||
should_render_type_for_shape: rendered_schema && complex?, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code probably is wrong when handling the case where an empty object is returned from the database. The following code for test/support/codegen/edgeql/types/insert_f_named.edgeql
will be generated for query/3
and query!/3
:
@spec query(
client :: EdgeDB.client(),
args :: args(),
opts :: list(EdgeDB.query_option())
) ::
{:ok, Result.t()}
| {:error, reason}
when reason: any()
@spec query!(
client :: EdgeDB.client(),
args :: args(),
opts :: list(EdgeDB.query_option())
) :: Result.t()
But the Result
module itself won't be generated. In this case, the structure should also be generated, but it will have no fields
368207c
to
f73480c
Compare
f73480c
to
68c149a
Compare
68c149a
to
8575b89
Compare
3cd967d
to
2a56d07
Compare
2a56d07
to
ff48c7c
Compare
https://discordapp.com/channels/841451783728529451/1152298319201173587
Certain optimizations maybe part of a later improvement, for now the goal is get the basics generated and working.