mirror of
https://github.com/enso-org/enso.git
synced 2024-12-28 14:11:56 +03:00
Fix a Bug in the Database Join Implementation (#1614)
This commit is contained in:
parent
5b57960da3
commit
301672df24
@ -124,6 +124,36 @@ type Context
|
||||
set_limit new_limit =
|
||||
Context this.from_spec this.where_filters this.orders this.groups this.meta_index new_limit
|
||||
|
||||
## PRIVATE
|
||||
|
||||
'Lifts' this context into a subquery, so that the original context (with all filters etc.) is
|
||||
encapsulated within the subquery and all external references passed as the second argument,
|
||||
refer directly to that subquery.
|
||||
|
||||
It takes a list of lists of columns that should be included in that subquery (this can for
|
||||
example the list of regular columns, the list of indices etc.)
|
||||
It assumes that columns on these lists all have unique names.
|
||||
|
||||
It returns a new context and the lists transformed in such a way that each column corresponds
|
||||
to one from the original list but it is valid in the new context.
|
||||
|
||||
This is useful as a preprocessing step between combining queries, for example in a join.
|
||||
# as_subquery : Text -> Vector (Vector Internal_Column) -> [IR.Sub_Query, Vector (Vector Internal_Column)]
|
||||
as_subquery alias column_lists =
|
||||
rewrite_internal_column : Internal_Column -> Internal_Column
|
||||
rewrite_internal_column column =
|
||||
Internal_Column column.name column.sql_type (IR.Column alias column.name)
|
||||
|
||||
new_columns = column_lists.map columns->
|
||||
columns.map rewrite_internal_column
|
||||
|
||||
encapsulated_columns = column_lists.flat_map columns->
|
||||
columns.map column-> [column.name, column.expression]
|
||||
new_from = IR.Sub_Query encapsulated_columns this alias
|
||||
|
||||
[new_from, new_columns]
|
||||
|
||||
|
||||
## PRIVATE
|
||||
|
||||
Used as part of the context, specifies the sources of the query.
|
||||
|
@ -156,9 +156,9 @@ type Table
|
||||
## UNSTABLE
|
||||
|
||||
Sets the index of this table, using the column with the provided name.
|
||||
set_index : Text | Column | Vector (Text | Column) -> Table
|
||||
set_index : Text | Column | Vector Text -> Table
|
||||
set_index index = Panic.recover <|
|
||||
new_index = (Helpers.unify_vector_singleton index).map (this.resolve >> .as_internal)
|
||||
new_index = (Helpers.unify_vector_singleton index).map (this.at >> .as_internal)
|
||||
new_ctx = this.context.set_index new_index
|
||||
new_cols = this.internal_columns.filter col->
|
||||
turned_into_index = new_index.exists i-> i.name == col.name
|
||||
@ -286,59 +286,79 @@ type Table
|
||||
when there's a name conflict with a column of `other`.
|
||||
- right_suffix: a suffix that should be added to the columns of `other`
|
||||
when there's a name conflict with a column of `this`.
|
||||
join : Table -> Nothing | Text | Column | Vector (Text | Column) -> Boolean -> Text -> Text -> Table
|
||||
join other on=Nothing drop_unmatched=False left_suffix='_left' right_suffix='_right' = Panic.recover <|
|
||||
Panic.rethrow (Helpers.ensure_name_is_sane left_suffix && Helpers.ensure_name_is_sane right_suffix)
|
||||
if left_suffix == right_suffix then
|
||||
Panic.throw <| Illegal_State_Error "left_suffix must be different from right_suffix"
|
||||
kind = if drop_unmatched then IR.Join_Inner else IR.Join_Left
|
||||
my_index : Vector Internal_Column
|
||||
my_index = case on of
|
||||
Nothing -> this.context.meta_index
|
||||
_ ->
|
||||
(Helpers.unify_vector_singleton on).map (this.resolve >> .as_internal)
|
||||
other_index = other.context.meta_index
|
||||
case my_index.length == other_index.length of
|
||||
False -> Panic.throw <| Illegal_State_Error "Cannot join with multi-indexes of different lengths."
|
||||
True ->
|
||||
# TODO [RW] we may be able to avoid creating subqueries if there are no groups, orders or wheres,
|
||||
# so it may be worth optimizing that here (#1515)
|
||||
new_table_name = this.name + "_" + other.name
|
||||
aliases = case this.name == other.name of
|
||||
True -> [this.name+left_suffix, other.name+right_suffix]
|
||||
False -> [this.name, other.name]
|
||||
left_alias = aliases.first
|
||||
right_alias = aliases.second
|
||||
join : Table | Column -> Nothing | Text | Column | Vector (Text | Column) -> Boolean -> Text -> Text -> Table
|
||||
join other on=Nothing drop_unmatched=False left_suffix='_left' right_suffix='_right' = case other of
|
||||
Column _ _ _ _ _ -> this.join other.to_table on drop_unmatched left_suffix right_suffix
|
||||
Table _ _ _ _ -> Panic.recover <|
|
||||
Panic.rethrow (Helpers.ensure_name_is_sane left_suffix && Helpers.ensure_name_is_sane right_suffix)
|
||||
if left_suffix == right_suffix then
|
||||
Panic.throw <| Illegal_State_Error "left_suffix must be different from right_suffix"
|
||||
kind = if drop_unmatched then IR.Join_Inner else IR.Join_Left
|
||||
|
||||
left_subquery_cols = this.internal_columns_with_index.map c-> [c.name, c.expression]
|
||||
right_subquery_cols = other.internal_columns_with_index.map c-> [c.name, c.expression]
|
||||
left_query = IR.Sub_Query left_subquery_cols this.context left_alias
|
||||
right_query = IR.Sub_Query right_subquery_cols other.context right_alias
|
||||
# Prepare the left and right pairs of indices along which the join will be performed.
|
||||
left_join_index : Vector Internal_Column
|
||||
left_join_index = case on of
|
||||
Nothing -> this.context.meta_index
|
||||
_ ->
|
||||
(Helpers.unify_vector_singleton on).map (this.resolve >> .as_internal)
|
||||
right_join_index = other.context.meta_index
|
||||
if left_join_index.length != right_join_index.length then
|
||||
Panic.throw <| Illegal_State_Error "Cannot join with multi-indexes of different lengths."
|
||||
|
||||
left_renamed_index = my_index.map <|
|
||||
IR.lift_expression_map (IR.substitute_origin this.name left_alias)
|
||||
right_renamed_index = other_index.map <|
|
||||
IR.lift_expression_map (IR.substitute_origin other.name right_alias)
|
||||
on_exprs = left_renamed_index.zip right_renamed_index l-> r->
|
||||
IR.Operation "=" [l.expression, r.expression]
|
||||
# TODO [RW] We may be able to avoid creating subqueries if there are no groups, orders or wheres,
|
||||
# so it may be worth optimizing that here (#1515).
|
||||
new_table_name = this.name + "_" + other.name
|
||||
aliases = case this.name == other.name of
|
||||
True -> [this.name+left_suffix, other.name+right_suffix]
|
||||
False -> [this.name, other.name]
|
||||
left_alias = aliases.first
|
||||
right_alias = aliases.second
|
||||
|
||||
new_index = left_renamed_index
|
||||
new_from = IR.Join kind left_query right_query on_exprs
|
||||
new_limit = Nothing
|
||||
new_ctx = IR.Context new_from [] [] [] new_index new_limit
|
||||
# Ensure that the join indices (which are not directly visible to the user, but must be materialized in the sub-query)
|
||||
# get a fresh set of names, so that they do not collide with other parts of the query.
|
||||
left_used_names = this.internal_columns_with_index.map .name
|
||||
left_join_index_fresh = here.freshen_columns left_used_names left_join_index
|
||||
|
||||
new_names = here.combine_names (this.internal_columns.map .name) (other.internal_columns.map .name) left_suffix right_suffix
|
||||
left_names = new_names.first
|
||||
right_names = new_names.second
|
||||
# Create subqueries that encapsulate the original queries and provide needed columns.
|
||||
# We only include the meta_index from the left table, because only this one will be kept in the result.
|
||||
# The generated new sets of columns refer to the encapsulated expressions within the subquery and are
|
||||
# valid in contexts whose from_spec is this subquery directly or it is a join containing this subquery.
|
||||
# TODO [RW] Not all of these included columns are actually usable from the external context, so
|
||||
# in the future we may consider pruning some of them as additional optimization and simplification of the query.
|
||||
left_config = this.context.as_subquery left_alias [this.internal_columns, this.context.meta_index, left_join_index_fresh]
|
||||
right_config = other.context.as_subquery right_alias [other.internal_columns, right_join_index]
|
||||
|
||||
new_left_columns = this.internal_columns.zip left_names p-> new_name->
|
||||
Internal_Column new_name p.sql_type (IR.Column left_alias p.name)
|
||||
new_right_columns = other.internal_columns.zip right_names p-> new_name->
|
||||
Internal_Column new_name p.sql_type (IR.Column right_alias p.name)
|
||||
left_subquery = left_config.first
|
||||
left_new_columns = left_config.second.at 0
|
||||
left_new_meta_index = left_config.second.at 1
|
||||
left_new_join_index = left_config.second.at 2
|
||||
|
||||
new_columns = new_left_columns + new_right_columns
|
||||
right_subquery = right_config.first
|
||||
right_new_columns = right_config.second.at 0
|
||||
right_new_join_index = right_config.second.at 1
|
||||
|
||||
Table new_table_name this.connection new_columns new_ctx
|
||||
# Generate new names for all columns (including the indices) that will be retained in the created Table.
|
||||
left_names_before = (left_new_meta_index + left_new_columns).map .name
|
||||
right_names_before = right_new_columns.map .name
|
||||
new_names = here.combine_names left_names_before right_names_before left_suffix right_suffix
|
||||
left_indices_count = left_new_meta_index.length
|
||||
left_new_meta_index_names = new_names.first.take_start left_indices_count
|
||||
left_new_columns_names = new_names.first.drop_start left_indices_count
|
||||
right_new_columns_names = new_names.second
|
||||
|
||||
# Rename columns to the newly allocated names
|
||||
new_index = here.rename_columns left_new_meta_index left_new_meta_index_names
|
||||
left_renamed_columns = here.rename_columns left_new_columns left_new_columns_names
|
||||
right_renamed_columns = here.rename_columns right_new_columns right_new_columns_names
|
||||
new_columns = left_renamed_columns + right_renamed_columns
|
||||
|
||||
on_exprs = left_new_join_index.zip right_new_join_index l-> r->
|
||||
IR.Operation "=" [l.expression, r.expression]
|
||||
new_from = IR.Join kind left_subquery right_subquery on_exprs
|
||||
new_limit = Nothing
|
||||
new_ctx = IR.Context new_from [] [] [] new_index new_limit
|
||||
|
||||
Table new_table_name this.connection new_columns new_ctx
|
||||
|
||||
## UNSTABLE
|
||||
|
||||
@ -483,6 +503,7 @@ type Table
|
||||
internal_columns_with_index =
|
||||
this.context.meta_index + this.internal_columns
|
||||
|
||||
|
||||
## PRIVATE
|
||||
|
||||
Inserts a new row to the table. It actually modifies the underlying table
|
||||
@ -552,7 +573,10 @@ type Aggregate_Table
|
||||
ungrouped : Table
|
||||
ungrouped =
|
||||
new_ctx = this.context.set_groups []
|
||||
Table this.name this.connection this.internal_columns new_ctx
|
||||
new_cols = this.internal_columns.filter col->
|
||||
turned_into_index = this.context.meta_index.exists i-> i.name == col.name
|
||||
turned_into_index.not
|
||||
Table this.name this.connection new_cols new_ctx
|
||||
|
||||
type Integrity_Error
|
||||
## UNSTABLE
|
||||
@ -641,3 +665,38 @@ combine_names left_names right_names left_suffix right_suffix =
|
||||
new_left_names = left_pairs.map .second
|
||||
new_right_names = right_pairs.map .second
|
||||
[new_left_names, new_right_names]
|
||||
|
||||
## PRIVATE
|
||||
|
||||
Transforms `preferred_names` names in such a way to not collide with `used_names`. If a name
|
||||
from `preferred_names` does not collide with others, it is kept as is, otherwise numerical
|
||||
suffixes are added.
|
||||
fresh_names : Vector Text -> Vector Text -> Vector Text
|
||||
fresh_names used_names preferred_names =
|
||||
freshen currently_used name ix =
|
||||
new_name = if ix == 0 then name else name+"_"+ix.to_text
|
||||
case currently_used.contains new_name of
|
||||
False -> new_name
|
||||
True -> freshen currently_used name ix+1
|
||||
res = preferred_names . fold [used_names, []] acc-> name->
|
||||
used = acc.first
|
||||
new_name = freshen used name 0
|
||||
[used_names + [new_name], acc.second + [new_name]]
|
||||
res.second
|
||||
|
||||
## PRIVATE
|
||||
|
||||
Transforms the vector of columns, changing names of each column to the corresponding name from the second vector.
|
||||
rename_columns : Vector Internal_Column -> Vector Text -> Vector Internal_Column
|
||||
rename_columns columns new_names =
|
||||
columns.zip new_names col-> name->
|
||||
col.rename name
|
||||
|
||||
## PRIVATE
|
||||
|
||||
Ensures that the provided columns do not clash with the vector of names provided as first argument.
|
||||
Original column names are kept if possible, but if they would clash, the columns are renamed.
|
||||
freshen_columns : Vector Text -> Vector Column -> Vector Column
|
||||
freshen_columns used_names columns =
|
||||
fresh_names = here.fresh_names used_names (columns.map .name)
|
||||
here.rename_columns columns fresh_names
|
||||
|
@ -48,6 +48,14 @@ prepare_visualization x max_rows = Helpers.recover_errors <| case x of
|
||||
Database_Column.Aggregate_Column _ _ _ _ _ ->
|
||||
here.prepare_visualization x.ungrouped.to_table max_rows
|
||||
|
||||
# TODO [RW] Should we truncate Vectors?
|
||||
# We also visualize Vectors and arrays
|
||||
Vector.Vector _ ->
|
||||
truncated = x.take_start max_rows
|
||||
Json.from_pairs [["json", truncated], ["all_rows_count", x.length]] . to_text
|
||||
Array ->
|
||||
here.prepare_visualization (Vector.Vector x) max_rows
|
||||
|
||||
# Anything else will be visualized with the JSON or matrix visualization
|
||||
_ ->
|
||||
Json.from_pairs [["json", x]] . to_text
|
||||
|
@ -2,6 +2,7 @@ from Standard.Base import all
|
||||
|
||||
import Database_Tests.Helpers.Fake_Test_Connection
|
||||
import Standard.Database.Data.Dialect
|
||||
import Standard.Database.Data.Table as Table_Module
|
||||
import Standard.Test
|
||||
|
||||
from Standard.Database import all
|
||||
@ -96,15 +97,19 @@ spec =
|
||||
t3 = test_connection.access_table "T3"
|
||||
Test.specify "should allow joining tables index-on-index" <|
|
||||
r1 = t1.set_index 'A' . join (t2.set_index 'D')
|
||||
r1.to_sql.prepare . should_equal ['SELECT "T1"."B" AS "B", "T1"."C" AS "C", "T2"."E" AS "E", "T2"."F" AS "F" FROM (SELECT "T1"."A" AS "A", "T1"."B" AS "B", "T1"."C" AS "C" FROM "T1" AS "T1") AS "T1" LEFT JOIN (SELECT "T2"."D" AS "D", "T2"."E" AS "E", "T2"."F" AS "F" FROM "T2" AS "T2") AS "T2" ON ("T1"."A" = "T2"."D")', []]
|
||||
r1.to_sql.prepare . should_equal ['SELECT "T1"."B" AS "B", "T1"."C" AS "C", "T2"."E" AS "E", "T2"."F" AS "F" FROM (SELECT "T1"."B" AS "B", "T1"."C" AS "C", "T1"."A" AS "A", "T1"."A" AS "A_1" FROM "T1" AS "T1") AS "T1" LEFT JOIN (SELECT "T2"."E" AS "E", "T2"."F" AS "F", "T2"."D" AS "D" FROM "T2" AS "T2") AS "T2" ON ("T1"."A_1" = "T2"."D")', []]
|
||||
|
||||
Test.specify "should allow joining tables column-on-index" <|
|
||||
r1 = t1.join (t2.set_index 'D') on='B' drop_unmatched=True
|
||||
r1.to_sql.prepare . should_equal ['SELECT "T1"."A" AS "A", "T1"."B" AS "B", "T1"."C" AS "C", "T2"."E" AS "E", "T2"."F" AS "F" FROM (SELECT "T1"."A" AS "A", "T1"."B" AS "B", "T1"."C" AS "C" FROM "T1" AS "T1") AS "T1" INNER JOIN (SELECT "T2"."D" AS "D", "T2"."E" AS "E", "T2"."F" AS "F" FROM "T2" AS "T2") AS "T2" ON ("T1"."B" = "T2"."D")', []]
|
||||
r1.to_sql.prepare . should_equal ['SELECT "T1"."A" AS "A", "T1"."B" AS "B", "T1"."C" AS "C", "T2"."E" AS "E", "T2"."F" AS "F" FROM (SELECT "T1"."A" AS "A", "T1"."B" AS "B", "T1"."C" AS "C", "T1"."B" AS "B_1" FROM "T1" AS "T1") AS "T1" INNER JOIN (SELECT "T2"."E" AS "E", "T2"."F" AS "F", "T2"."D" AS "D" FROM "T2" AS "T2") AS "T2" ON ("T1"."B_1" = "T2"."D")', []]
|
||||
|
||||
Test.specify "should append suffixes to disambiguate column names" <|
|
||||
r1 = t1.join (t3.set_index 'E') on='A'
|
||||
r1.to_sql.prepare . should_equal ['SELECT "T1"."A" AS "A_left", "T1"."B" AS "B", "T1"."C" AS "C", "T3"."A" AS "A_right", "T3"."F" AS "F" FROM (SELECT "T1"."A" AS "A", "T1"."B" AS "B", "T1"."C" AS "C" FROM "T1" AS "T1") AS "T1" LEFT JOIN (SELECT "T3"."E" AS "E", "T3"."A" AS "A", "T3"."F" AS "F" FROM "T3" AS "T3") AS "T3" ON ("T1"."A" = "T3"."E")', []]
|
||||
r1.to_sql.prepare . should_equal ['SELECT "T1"."A" AS "A_left", "T1"."B" AS "B", "T1"."C" AS "C", "T3"."A" AS "A_right", "T3"."F" AS "F" FROM (SELECT "T1"."A" AS "A", "T1"."B" AS "B", "T1"."C" AS "C", "T1"."A" AS "A_1" FROM "T1" AS "T1") AS "T1" LEFT JOIN (SELECT "T3"."A" AS "A", "T3"."F" AS "F", "T3"."E" AS "E" FROM "T3" AS "T3") AS "T3" ON ("T1"."A_1" = "T3"."E")', []]
|
||||
|
||||
r2 = (t3.set_index 'E').join (t2.set_index 'F')
|
||||
r2.index.name . should_equal "E_left"
|
||||
r2.columns . map .name . should_equal ["A", "F", "D", "E_right"]
|
||||
|
||||
Test.specify "should avoid duplicates when disambiguating column names" <|
|
||||
connection =
|
||||
@ -122,7 +127,7 @@ spec =
|
||||
|
||||
Test.specify "should correctly handle self-joins" <|
|
||||
r1 = t1.join (t1.set_index 'A') on='B'
|
||||
r1.to_sql.prepare . should_equal ['SELECT "T1_left"."A" AS "A", "T1_left"."B" AS "B_left", "T1_left"."C" AS "C_left", "T1_right"."B" AS "B_right", "T1_right"."C" AS "C_right" FROM (SELECT "T1"."A" AS "A", "T1"."B" AS "B", "T1"."C" AS "C" FROM "T1" AS "T1") AS "T1_left" LEFT JOIN (SELECT "T1"."A" AS "A", "T1"."B" AS "B", "T1"."C" AS "C" FROM "T1" AS "T1") AS "T1_right" ON ("T1_left"."B" = "T1_right"."A")', []]
|
||||
r1.to_sql.prepare . should_equal ['SELECT "T1_left"."A" AS "A", "T1_left"."B" AS "B_left", "T1_left"."C" AS "C_left", "T1_right"."B" AS "B_right", "T1_right"."C" AS "C_right" FROM (SELECT "T1"."A" AS "A", "T1"."B" AS "B", "T1"."C" AS "C", "T1"."B" AS "B_1" FROM "T1" AS "T1") AS "T1_left" LEFT JOIN (SELECT "T1"."B" AS "B", "T1"."C" AS "C", "T1"."A" AS "A" FROM "T1" AS "T1") AS "T1_right" ON ("T1_left"."B_1" = "T1_right"."A")', []]
|
||||
|
||||
Test.group "[Codegen] Handling Missing Values" <|
|
||||
Test.specify "fill_missing should allow to replace missing values in a column with a constant" <|
|
||||
@ -177,3 +182,17 @@ spec =
|
||||
Test.specify 'should return dataflow error when passed a non-existent column' <|
|
||||
r = t1.sort by='foobar'
|
||||
r.should_fail_with No_Such_Column_Error
|
||||
|
||||
Test.group "Helpers" <|
|
||||
Test.specify "combine_names should combine lists of names" <|
|
||||
v1 = ["A", "B"]
|
||||
v2 = ["A", "C"]
|
||||
combined = Table_Module.combine_names v1 v2 "_1" "_2"
|
||||
combined.first . should_equal ["A_1", "B"]
|
||||
combined.second . should_equal ["A_2", "C"]
|
||||
|
||||
Test.expect_panic_with (Table_Module.combine_names ["A", "A_1"] ["A"] "_1" "_2") Illegal_State_Error
|
||||
Test.specify "fresh_names should provide fresh names" <|
|
||||
used_names = ["A", "A_1"]
|
||||
preferred_names = ["A", "A", "B"]
|
||||
Table_Module.fresh_names used_names preferred_names . should_equal ["A_2", "A_3", "B"]
|
||||
|
@ -165,14 +165,32 @@ spec prefix connection pending=Nothing =
|
||||
Test.specify "should allow self-joins and append suffixes to disambiguate column names" <|
|
||||
r_1 = a.join (a.set_index 'x') on='x' . to_dataframe . sort by='x'
|
||||
r_1.columns.map .name . should_equal ['x', 'y_left', 'y_right']
|
||||
r_1.at 'x' . should_equal [0, 1, 3, 6, 7]
|
||||
r_1.at 'x' . to_vector . should_equal [0, 1, 3, 6, 7]
|
||||
expected_y = ['foo', 'bar', 'spam', 'eggs', 'baz']
|
||||
r_1.at 'y_left' . should_equal expected_y
|
||||
r_1.at 'y_right' . should_equal expected_y
|
||||
r_1.at 'y_left' . to_vector . should_equal expected_y
|
||||
r_1.at 'y_right' . to_vector . should_equal expected_y
|
||||
|
||||
r_2 = a.set_index 'x' . join (a.set_index 'x') left_suffix='_old' right_suffix='_new'
|
||||
r_2.columns.map .name . should_equal ['y_old', 'y_new']
|
||||
|
||||
Test.specify "should correctly handle multi-joins" <|
|
||||
ta = make_table "M_TA" ["id", "name"] ["INTEGER", "VARCHAR"]
|
||||
tb = make_table "M_TB" ["id", "name"] ["INTEGER", "VARCHAR"]
|
||||
tc = make_table "M_TC" ["id_a", "id_b"] ["INTEGER", "INTEGER"]
|
||||
ta.insert [0, "Foo"]
|
||||
ta.insert [1, "Hmm"]
|
||||
tb.insert [2, "Bar"]
|
||||
tb.insert [0, "Hmm"]
|
||||
tc.insert [0, 2]
|
||||
tc.insert [1, 0]
|
||||
ta_2 = ta.set_index "id"
|
||||
tb_2 = tb.set_index "id"
|
||||
res = (tc.join ta_2 on="id_a") . join tb_2 on="id_b" left_suffix="_a" right_suffix="_b"
|
||||
sel = res.select ["name_a", "name_b"]
|
||||
df = sel.to_dataframe . sort by="name_a"
|
||||
df . at "name_a" . to_vector . should_equal ["Foo", "Hmm"]
|
||||
df . at "name_b" . to_vector . should_equal ["Bar", "Hmm"]
|
||||
|
||||
Test.group prefix+"Missing Values" pending=pending <|
|
||||
t4 = make_table "T4" ["A", "B", "C"] ["INT", "BOOLEAN", "VARCHAR"]
|
||||
t4.insert [0, True, ""]
|
||||
@ -332,5 +350,5 @@ spec prefix connection pending=Nothing =
|
||||
r_3 = c.sort order=Sort_Order.Descending missing_last=False
|
||||
r_3.to_vector.should_equal [Nothing, Nothing, 7.3, 3.0, 3.0, 2.2]
|
||||
|
||||
tables = ["T1", "T2", "T3", "T4", "T5", "T6", "T7", "T8", "TA", "TB", "Big", "clothes"]
|
||||
tables = ["T1", "T2", "T3", "T4", "T5", "T6", "T7", "T8", "TA", "TB", "Big", "clothes", "M_TA", "M_TB", "M_TC"]
|
||||
tables.each clean_table
|
||||
|
@ -5,8 +5,12 @@ import Standard.Test
|
||||
import Visualization_Tests.Helpers_Spec
|
||||
import Visualization_Tests.Histogram_Spec
|
||||
import Visualization_Tests.Scatter_Plot_Spec
|
||||
import Visualization_Tests.Sql_Spec
|
||||
import Visualization_Tests.Table_Spec
|
||||
|
||||
main = Test.Suite.runMain <|
|
||||
Helpers_Spec.spec
|
||||
Histogram_Spec.spec
|
||||
Scatter_Plot_Spec.spec
|
||||
Helpers_Spec.spec
|
||||
Histogram_Spec.spec
|
||||
Scatter_Plot_Spec.spec
|
||||
Sql_Spec.spec
|
||||
Table_Spec.spec
|
||||
|
26
test/Visualization_Tests/src/Sql_Spec.enso
Normal file
26
test/Visualization_Tests/src/Sql_Spec.enso
Normal file
@ -0,0 +1,26 @@
|
||||
from Standard.Base import all
|
||||
|
||||
from Standard.Database import all
|
||||
import Standard.Visualization.Sql.Visualization as Visualization
|
||||
import Standard.Test
|
||||
|
||||
visualization_spec connection =
|
||||
connection.execute_update 'CREATE TABLE "T" ("A" VARCHAR, "B" INTEGER, "C" INTEGER)'
|
||||
t = connection.access_table "T"
|
||||
Test.group "Sql Visualization" <|
|
||||
Test.specify "should provide type metadata for interpolations" <|
|
||||
q = t.where ((t.at "B" == 2) && (t.at "A" == True)) . at "C"
|
||||
vis = Visualization.prepare_visualization q
|
||||
int_param = Json.from_pairs [["value", 2], ["actual_type", "Builtins.Main.Integer"], ["expected_sql_type", "INTEGER"], ["expected_enso_type", "Builtins.Main.Integer"]]
|
||||
str_param = Json.from_pairs [["value", True], ["actual_type", "Builtins.Main.Boolean"], ["expected_sql_type", "VARCHAR"], ["expected_enso_type", "Builtins.Main.Text"]]
|
||||
code = 'SELECT "T"."C" AS "C" FROM "T" AS "T" WHERE (("T"."B" = ?) AND ("T"."A" = ?))'
|
||||
json = Json.from_pairs [["dialect", "sqlite"], ["code", code], ["interpolations", [int_param, str_param]]]
|
||||
vis . should_equal json.to_text
|
||||
spec =
|
||||
Enso_Project.data.create_directory
|
||||
file = Enso_Project.data / "sqlite_test.db"
|
||||
file.delete_if_exists
|
||||
connection = Database.open_sqlite_file file
|
||||
here.visualization_spec connection
|
||||
connection.close
|
||||
file.delete
|
125
test/Visualization_Tests/src/Table_Spec.enso
Normal file
125
test/Visualization_Tests/src/Table_Spec.enso
Normal file
@ -0,0 +1,125 @@
|
||||
from Standard.Base import all
|
||||
|
||||
from Standard.Database import all
|
||||
import Standard.Database.Data.Table as Database_Table
|
||||
import Standard.Table.Data.Table as Dataframe_Table
|
||||
import Standard.Visualization.Table.Visualization as Visualization
|
||||
import Standard.Test
|
||||
|
||||
type Foo
|
||||
type Foo x
|
||||
to_json : Json
|
||||
to_json = Json.from_pairs [["x", this.x]]
|
||||
|
||||
visualization_spec connection =
|
||||
connection.execute_update 'CREATE TABLE "T" ("A" VARCHAR, "B" INTEGER, "C" INTEGER)'
|
||||
t = connection.access_table "T"
|
||||
t.insert ['a', 2, 3]
|
||||
t.insert ['a', 2, 5]
|
||||
t.insert ['a', 3, 6]
|
||||
|
||||
make_json header data all_rows ixes_header ixes =
|
||||
p_header = ["header", header]
|
||||
p_data = ["data", data]
|
||||
p_all_rows = ["all_rows_count", all_rows]
|
||||
p_ixes = ["indices", ixes]
|
||||
p_ixes_header = ["indices_header", ixes_header]
|
||||
pairs = [p_header, p_data, p_all_rows, p_ixes, p_ixes_header]
|
||||
Json.from_pairs pairs . to_text
|
||||
|
||||
Test.group "Table Visualization" <|
|
||||
Test.specify "should wrap internal errors" <|
|
||||
bad_table = Database_Table.Table Nothing Nothing Nothing Nothing
|
||||
vis = Visualization.prepare_visualization bad_table 2
|
||||
json = Json.from_pairs [["error", "Method `meta_index` of Nothing could not be found."]]
|
||||
vis . should_equal json.to_text
|
||||
|
||||
Test.specify "should visualize database tables" <|
|
||||
vis = Visualization.prepare_visualization t 1
|
||||
json = make_json header=["A", "B", "C"] data=[['a'], [2], [3]] all_rows=3 ixes_header=[] ixes=[]
|
||||
vis . should_equal json
|
||||
|
||||
t2 = t.set_index "A"
|
||||
vis2 = Visualization.prepare_visualization t2 1
|
||||
json2 = make_json header=["B", "C"] data=[[2], [3]] all_rows=3 ixes_header=["A"] ixes=[['a']]
|
||||
vis2 . should_equal json2
|
||||
|
||||
Test.specify "should visualize database columns" <|
|
||||
vis = Visualization.prepare_visualization (t.at "A") 2
|
||||
json = make_json header=["A"] data=[['a', 'a']] all_rows=3 ixes_header=[] ixes=[]
|
||||
vis . should_equal json
|
||||
|
||||
g = t.group by=["A", "B"] . at "C" . mean
|
||||
vis2 = Visualization.prepare_visualization g 1
|
||||
json2 = make_json header=["C_mean"] data=[[4]] all_rows=2 ixes_header=["A", "B"] ixes=[['a'], [2]]
|
||||
vis2 . should_equal json2
|
||||
|
||||
Test.specify "should visualize database aggregates" <|
|
||||
agg = t.group by="A"
|
||||
vis = Visualization.prepare_visualization agg 1
|
||||
json = make_json header=["B", "C"] data=[[2], [3]] all_rows=3 ixes_header=["A"] ixes=[['a']]
|
||||
vis . should_equal json
|
||||
|
||||
vis2 = Visualization.prepare_visualization (agg.at "C") 1
|
||||
json2 = make_json header=["C"] data=[[3]] all_rows=3 ixes_header=["A"] ixes=[['a']]
|
||||
vis2 . should_equal json2
|
||||
|
||||
t2 = Dataframe_Table.new [["A", [1, 2, 3]], ["B", [4, 5, 6]], ["C", [7, 8, 9]]]
|
||||
Test.specify "should visualize dataframe tables" <|
|
||||
vis = Visualization.prepare_visualization t2 1
|
||||
json = make_json header=["A", "B", "C"] data=[[1], [4], [7]] all_rows=3 ixes_header=[""] ixes=[[0]]
|
||||
vis . should_equal json
|
||||
|
||||
t3 = t2.set_index "A"
|
||||
vis2 = Visualization.prepare_visualization t3 1
|
||||
json2 = make_json header=["B", "C"] data=[[4], [7]] all_rows=3 ixes_header=["A"] ixes=[[1]]
|
||||
vis2 . should_equal json2
|
||||
|
||||
Test.specify "should visualize dataframe columns" <|
|
||||
vis = Visualization.prepare_visualization (t2.at "A") 2
|
||||
json = make_json header=["A"] data=[[1, 2]] all_rows=3 ixes_header=[""] ixes=[[0, 1]]
|
||||
vis . should_equal json
|
||||
|
||||
g = t2.group by="A" . at "C" . mean
|
||||
vis2 = Visualization.prepare_visualization g 1
|
||||
json2 = make_json header=["C_mean"] data=[[7.0]] all_rows=3 ixes_header=["A"] ixes=[[1]]
|
||||
vis2 . should_equal json2
|
||||
|
||||
Test.specify "should visualize dataframe aggregates" <|
|
||||
agg = t2.group by="A"
|
||||
vis = Visualization.prepare_visualization agg 1
|
||||
json = make_json header=["B", "C"] data=[[4], [7]] all_rows=3 ixes_header=["A"] ixes=[[1]]
|
||||
vis . should_equal json
|
||||
|
||||
vis2 = Visualization.prepare_visualization (agg.at "C") 1
|
||||
json2 = make_json header=["C"] data=[[7]] all_rows=3 ixes_header=["A"] ixes=[[1]]
|
||||
vis2 . should_equal json2
|
||||
|
||||
|
||||
Test.specify "should handle Vectors" <|
|
||||
vis = Visualization.prepare_visualization [1, 2, 3] 2
|
||||
json = Json.from_pairs [["json", [1, 2]], ["all_rows_count", 3]]
|
||||
vis . should_equal json.to_text
|
||||
|
||||
vis2 = Visualization.prepare_visualization [[1, 2], [3, 4]] 2
|
||||
json2 = Json.from_pairs [["json", [[1, 2], [3, 4]]], ["all_rows_count", 2]]
|
||||
vis2 . should_equal json2.to_text
|
||||
|
||||
Test.specify "should handle Arrays" <|
|
||||
vis = Visualization.prepare_visualization ([1, 2, 3] . to_array) 2
|
||||
json = Json.from_pairs [["json", [1, 2]], ["all_rows_count", 3]]
|
||||
vis . should_equal json.to_text
|
||||
|
||||
Test.specify "should handle other datatypes" <|
|
||||
vis = Visualization.prepare_visualization (Foo 42) 2
|
||||
json = Json.from_pairs [["json", (Foo 42)]]
|
||||
vis . should_equal json.to_text
|
||||
|
||||
spec =
|
||||
Enso_Project.data.create_directory
|
||||
file = Enso_Project.data / "sqlite_test.db"
|
||||
file.delete_if_exists
|
||||
connection = Database.open_sqlite_file file
|
||||
here.visualization_spec connection
|
||||
connection.close
|
||||
file.delete
|
Loading…
Reference in New Issue
Block a user