Skip to content
Merged
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
29 changes: 21 additions & 8 deletions lib/efsql/cli.ex
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ defmodule Efsql.Cli do

{call, rows, tenants} = Efsql.qall(sql, [], state.tenants)
if state.debug, do: print_debug(call)
print_table(rows, display_limit)
print_table(rows, display_limit, call.columns)
{tenants}
rescue
e ->
Expand Down Expand Up @@ -221,36 +221,49 @@ defmodule Efsql.Cli do
end
end

defp print_table([], _limit) do
defp print_table([], _limit, _columns) do
Owl.IO.puts(Owl.Data.tag("(0 rows)", :light_black))
end

defp print_table(rows, :all) do
print_rows(rows, false)
defp print_table(rows, :all, columns) do
print_rows(rows, false, columns)
end

defp print_table(rows, limit) do
defp print_table(rows, limit, columns) do
{display_rows, more?} =
if length(rows) > limit,
do: {Enum.take(rows, limit), true},
else: {rows, false}

print_rows(display_rows, more?)
print_rows(display_rows, more?, columns)
end

defp print_rows(rows, more?) do
# Columns in select-list order; `select *` (no columns) sorts them.
defp print_rows(rows, more?, columns) do
rows
|> Enum.map(fn row ->
row = if columns, do: Map.new(columns, &{&1, Map.get(row, &1)}), else: row
Map.new(row, fn {k, v} -> {to_string(k), format_value(v)} end)
end)
|> Owl.Table.new(border_style: :solid_rounded, padding_x: 1)
|> Owl.Table.new(
border_style: :solid_rounded,
padding_x: 1,
sort_columns: column_sorter(columns)
)
|> Owl.IO.puts()

n = length(rows)
label = if more?, do: "(#{n} rows, more available — add LIMIT)", else: "(#{n} rows)"
Owl.IO.puts(Owl.Data.tag(label, :light_black))
end

defp column_sorter(nil), do: :asc

defp column_sorter(columns) do
position = columns |> Enum.map(&to_string/1) |> Enum.with_index() |> Map.new()
&(Map.fetch!(position, &1) <= Map.fetch!(position, &2))
end

defp format_value(nil), do: Owl.Data.tag("null", :light_black)
defp format_value(v) when is_binary(v), do: v

Expand Down
6 changes: 5 additions & 1 deletion lib/efsql/physical.ex
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,13 @@ defmodule Efsql.Physical do
* `{:sort, [{:asc | :desc, field}]}` — NULLs last ascending, first descending
* `{:limit, n}`
* `{:project, [field]}` — trim rows to the requested fields

A plan also carries `columns`, the result columns in select-list order,
so a result table can show them as written; nil for `select *`, whose
fields are whatever the rows hold.
"""

defmodule Plan do
defstruct access: nil, ops: []
defstruct access: nil, ops: [], columns: nil
end
end
10 changes: 8 additions & 2 deletions lib/efsql/planner.ex
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,16 @@ defmodule Efsql.Planner do
{:project, logical.projection}
)

%Plan{plan | ops: plan.ops ++ ops}
%Plan{plan | ops: plan.ops ++ ops, columns: columns(logical.projection)}
end

def plan(%Logical.Select{} = logical, options) do
def plan(%Logical.Select{} = logical, options),
do: %Plan{plan_rows(logical, options) | columns: columns(logical.projection)}

defp columns(:star), do: nil
defp columns(fields), do: Enum.uniq(fields)

defp plan_rows(%Logical.Select{} = logical, options) do
%Logical.Select{predicates: preds, order: sort, projection: projection} = logical
star? = projection == :star
{pks, ins, pushables, residuals} = classify(preds)
Expand Down
24 changes: 19 additions & 5 deletions lib/efsql/tui/app.ex
Original file line number Diff line number Diff line change
Expand Up @@ -691,9 +691,19 @@ defmodule Efsql.Tui.App do

defp ivalue_key(%Model{ifield_cursor: ix, size: {_rows, cols}}), do: {ix, cols}

@doc """
The inspected row's fields: in select-list order when the query has one,
else sorted.
"""
def inspector_fields(%Model{irow: row, plan: %Efsql.Physical.Plan{columns: [_ | _] = columns}}) do
listed = Enum.filter(columns, &Map.has_key?(row, &1))
listed ++ Enum.sort(Map.keys(row) -- listed)
end

def inspector_fields(%Model{irow: row}), do: row |> Map.keys() |> Enum.sort()

defp ivalue_lines(%Model{irow: row, size: {_rows, cols}} = model) do
fields = row |> Map.keys() |> Enum.sort()
selected = Enum.at(fields, model.ifield_cursor)
selected = Enum.at(inspector_fields(model), model.ifield_cursor)

row
|> Map.get(selected)
Expand Down Expand Up @@ -751,7 +761,7 @@ defmodule Efsql.Tui.App do
end

defp done(model, :query, {:ok, {plan, rows, tenants, elapsed}}) do
columns = columns(rows)
columns = columns(plan, rows)
cells = render_cells(rows, columns)

model = %{
Expand All @@ -775,9 +785,13 @@ defmodule Efsql.Tui.App do

defp done(model, _tag, _result), do: {%{model | busy: nil}, []}

defp columns([]), do: []
defp columns(_plan, []), do: []

# In select-list order; `select *` has no list, so its fields are sorted
# with id first.
defp columns(%Efsql.Physical.Plan{columns: [_ | _] = columns}, _rows), do: columns

defp columns(rows) do
defp columns(_plan, rows) do
keys = rows |> Enum.flat_map(&Map.keys/1) |> Enum.uniq() |> Enum.sort()
if :id in keys, do: [:id | List.delete(keys, :id)], else: keys
end
Expand Down
2 changes: 1 addition & 1 deletion lib/efsql/tui/view.ex
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ defmodule Efsql.Tui.View do
# -- inspector --

defp inspector(%Model{irow: row} = model, height, cols) do
fields = row |> Map.keys() |> Enum.sort()
fields = App.inspector_fields(model)
selected = Enum.at(fields, model.ifield_cursor)

list =
Expand Down
12 changes: 12 additions & 0 deletions test/planner_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,16 @@ defmodule EfsqlTest.Integration.Planner do
assert {:index_scan, %Ecto.Query{wheres: [%{expr: {:==, [], [_, "Alice"]}}]}, _opts} =
plan.access
end

test "the plan carries the select list's column order", context do
tenant_id = context[:tenant_id]

assert plan("select notes, name, id, name from #{tenant_id}.users;").columns ==
[:notes, :name, :id]

assert plan("select * from #{tenant_id}.users;").columns == nil

grouped = "select count(*) as n, notes from #{tenant_id}.users group by notes order by n;"
assert plan(grouped).columns == [:n, :notes]
end
end
28 changes: 28 additions & 0 deletions test/tui_app_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,34 @@ defmodule Efsql.Tui.AppTest do
assert model.mode == :query
end

test "result columns follow the select list" do
model = %{activated() | mode: :query}
rows = [%{id: "0001", name: "Alice", "count(*)": 3}]

plan = %Efsql.Physical.Plan{
access: {:pk_range, nil, nil, nil, []},
ops: [],
columns: [:name, :"count(*)", :id]
}

{model, _} = feed(model, [{:done, :query, {:ok, {plan, rows, %{}, 1}}}])
assert model.columns == [:name, :"count(*)", :id]
assert frame_text(model) =~ ~r/name\s+count\(\*\)\s+id/

# the inspector lists fields in the same order
{model, _} = feed(model, [{:key, :tab}, {:key, :enter}])
assert App.inspector_fields(model) == [:name, :"count(*)", :id]
end

test "select * has no select list, so id comes first and the rest sort" do
model = %{activated() | mode: :query}
rows = [%{name: "Alice", id: "0001", age: 30}]

plan = %Efsql.Physical.Plan{access: {:pk_range, nil, nil, nil, []}, ops: []}
{model, _} = feed(model, [{:done, :query, {:ok, {plan, rows, %{}, 1}}}])
assert model.columns == [:id, :age, :name]
end

test "uuid primary keys are not truncated in results" do
model = %{activated() | mode: :query}
uuid = "00ab2aa8-2bba-4102-bf8c-ced5d3142f8a"
Expand Down
Loading