Skip to content

Commit

Permalink
Implement Insert update action for update_database_table. (#6990)
Browse files Browse the repository at this point in the history
This adds the spec for all update actions, but implements the common input validation framework and `Insert`. Tests for remaining actions are marked as pending - these will be implemented in a subsequent PR.
  • Loading branch information
radeusgd authored Jun 14, 2023
1 parent 70cdb15 commit d9ed63f
Show file tree
Hide file tree
Showing 23 changed files with 725 additions and 155 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import Standard.Base.Errors.Illegal_State.Illegal_State
import Standard.Base.Runtime.Managed_Resource.Managed_Resource

import Standard.Base.Metadata.Widget
from Standard.Base.Metadata.Widget import Single_Choice
from Standard.Base.Metadata.Widget import Single_Choice, Vector_Editor
from Standard.Base.Metadata.Choice import Option
import Standard.Base.Metadata.Display

import Standard.Table.Data.Table.Table as Materialized_Table
import Standard.Table.Data.Type.Value_Type.Value_Type

import project.Data.Column_Description.Column_Description
import project.Data.SQL_Query.SQL_Query
import project.Data.SQL_Statement.SQL_Statement
import project.Data.SQL_Type.SQL_Type
Expand Down Expand Up @@ -201,10 +202,8 @@ type Connection
- table_name: the name of the table to create. If not provided, a random
name will be generated for temporary tables. If `temporary=False`, then
a name must be provided.
- structure: the structure of the table. This can be provided as a vector
of pairs of column names and types or an existing `Table` to copy the
structure from it. Note that if a `Table` is provided, only its column
structure is inherited - no table content is copied.
- structure: the structure of the table, provided as either an existing
`Table` (no data will be copied) or a `Vector` of `Column_Description`.
- primary_key: the names of the columns to use as the primary key. The
first column from the table is used by default. If it is set to
`Nothing` or an empty vector, no primary key will be created.
Expand All @@ -231,8 +230,9 @@ type Connection
structure provided, `Missing_Input_Columns` error is raised.
- An `SQL_Error` may be reported if there is a failure on the database
side.
create_table : Text|Nothing -> Vector (Pair Text Value_Type) | Database_Table | Materialized_Table -> Vector Text | Nothing -> Boolean -> Boolean -> Problem_Behavior -> Database_Table ! Table_Already_Exists
create_table self (table_name : Text | Nothing = Nothing) (structure : Vector | Database_Table | Materialized_Table) (primary_key : (Vector Text | Nothing) = [first_column_in_structure structure]) (temporary : Boolean = False) (allow_existing : Boolean = False) (on_problems:Problem_Behavior = Problem_Behavior.Report_Warning) =
@structure make_structure_creator
create_table : Text|Nothing -> Vector Column_Description | Database_Table | Materialized_Table -> Vector Text | Nothing -> Boolean -> Boolean -> Problem_Behavior -> Database_Table ! Table_Already_Exists
create_table self (table_name : Text | Nothing = Nothing) (structure : Vector Column_Description | Database_Table | Materialized_Table) (primary_key : (Vector Text | Nothing) = [first_column_in_structure structure]) (temporary : Boolean = False) (allow_existing : Boolean = False) (on_problems:Problem_Behavior = Problem_Behavior.Report_Warning) =
created_table_name = create_table_structure self table_name structure primary_key temporary allow_existing on_problems
self.query (SQL_Query.Table_Name created_table_name)

Expand Down Expand Up @@ -306,7 +306,13 @@ make_table_name_selector connection =
tables_to_display = connection.tables.at "Name" . to_vector
Single_Choice display=Display.Always values=(tables_to_display.map t-> Option t t.pretty)

## PRIVATE
make_structure_creator : Widget
make_structure_creator =
item_editor = Single_Choice display=Display.Always values=[Option "new column" "(Column_Description.Value)"]
Vector_Editor item_editor=item_editor item_default=item_editor.values.first.value display=Display.Always

## PRIVATE
first_column_in_structure structure = case structure of
_ : Vector -> structure.first.first
_ : Vector -> structure.first.name
_ -> structure.column_names.first
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from Standard.Base import all

## ADVANCED
Specifies additional properties for a column created using
`Connection.create_table`.

The support for the constraints may vary between databases.
type Column_Constraint
## ADVANCED
Specifies a default value for the column.

Argument:
- `sql_expression`: The SQL expression to use as the default value. Note that this is a raw SQL expression, so if you want to set a default to a string, you must include the quotes. The quoting style may depend on the database. Never pass unsanitized data to this parameter.

! SQL Injection

Since `sql_expression` is a raw SQL expression, note that malicious
data can cause execution of arbitrary SQL queries. Only use this
parameter with trusted data.
Default_Expression (sql_expression : Text)

## PRIVATE
DEPRECATED This will be replaced by extending the `Value_Type` with the concept of non-nullable types. TODO in #5872

Specifies that the column does not accept `NULL` values.
Not_Null
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from Standard.Base import all

import Standard.Table.Data.Type.Value_Type.Value_Type

import project.Data.Column_Constraint.Column_Constraint

## Describes a column structure for `Connection.create_table`.
type Column_Description

## Describes a column structure for `Connection.create_table`.

Arguments:
- name: The name of the column.
- value_type: The type of the column.
- constraints: Additional SQL constraints for the column.
Value (name:Text) (value_type:Value_Type) (constraints : Vector Column_Constraint = [])
36 changes: 36 additions & 0 deletions distribution/lib/Standard/Database/0.0.0-dev/src/Errors.enso
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,39 @@ type Non_Unique_Primary_Key
to_display_text : Text
to_display_text self =
"The primary key " + self.primary_key.to_display_text + " is not unique. The key "+self.clashing_primary_key.to_display_text+" corresponds to "+self.clashing_example_row_count.to_text+" rows."

type Unmatched_Rows
## PRIVATE
Indicates that the `Update` operation encountered input rows that did not
have any matching rows in the target table.
Error

## PRIVATE
Pretty print the rows already present error.
to_display_text : Text
to_display_text self =
"The `Update` operation encountered input rows that did not have any matching rows in the target table. The operation has been rolled back. Consider `Update_Or_Insert` if you want to insert rows that do not match any existing rows."

type Rows_Already_Present
## PRIVATE
Indicates that the `Insert` operation encountered input rows that already
had matching rows in the target table.
Error (count : Integer)

## PRIVATE
Pretty print the rows already present error.
to_display_text : Text
to_display_text self =
"The `Insert` operation encountered " + self.count.to_text + " input rows that already had matching rows in the target table. The operation has been rolled back."

type Multiple_Target_Rows_Matched_For_Update
## PRIVATE
Indicates that the source table had rows matching multiple rows in the
target table by the specified key.
Error

## PRIVATE
Pretty print the multiple target rows matched for update error.
to_display_text : Text
to_display_text self =
"The update operation encountered input rows that matched multiple rows in the target table. The operation has been rolled back. You may need to use a more specific key for matching."
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import project.Connection.Connection.Connection
import project.Data.Table.Table
import project.Data.Update_Action.Update_Action
from project.Errors import all
from project.Extensions.Upload_Default_Helpers import default_key_columns
from project.Internal.Upload_Table import all

## Creates a new database table from this table.
Expand Down Expand Up @@ -80,9 +79,27 @@ Table.select_into_database_table self connection table_name=Nothing primary_key=
table, an `Unmatched_Columns` error is raised.
- If a column in the source table has a type that cannot be trivially
widened to the corresponding column in the target table, a
`Column_Type_Mismatch` error is raised.
`Column_Type_Mismatch` error is raised. If the types do not match but can
be widened, an `Inexact_Type_Coercion` is reported.
- If `update_action` is `Insert` and a row with the same key already exists
in the target table, a `Rows_Already_Present` error is raised.
- If the `update_action` is `Update` and some rows in the source have no
corresponding rows in the target table, a `Unmatched_Rows` error is
raised.
- If the source table contains multiple rows for the same key, a
`Non_Unique_Primary_Key` error is raised.
- If a row in the source table matches multiple rows in the target table, a
`Multiple_Target_Rows_Matched_For_Update` error is raised.
- If another database error occurs, an `SQL_Error` is raised.

If any error was raised, the data in the target table is not modified.
Table.update_database_table : Connection -> Text -> Update_Action -> Vector Text-> Boolean -> Table ! Table_Not_Found | Unmatched_Columns | Missing_Input_Columns | Column_Type_Mismatch | SQL_Error | Illegal_Argument
Table.update_database_table connection (table_name : Text) (update_action : Update_Action = Update_Action.Update_Or_Insert) (key_columns : Vector = default_key_columns connection table_name) (error_on_missing_columns : Boolean = False) =
common_update_table self connection table_name update_action key_columns error_on_missing_columns

? Type Widening

Smaller types can be widened to a larger type, for example 32-bit integer
column can be widened to a 64-bit integer column, but not vice versa
(because larger numbers could not fit the smaller type and the type of the
column in the target table cannot be changed).
Table.update_database_table : Connection -> Text -> Update_Action -> Vector Text | Nothing -> Boolean -> Problem_Behavior -> Table ! Table_Not_Found | Unmatched_Columns | Missing_Input_Columns | Column_Type_Mismatch | SQL_Error | Illegal_Argument
Table.update_database_table self connection (table_name : Text) (update_action : Update_Action = Update_Action.Update_Or_Insert) (key_columns : Vector | Nothing = default_key_columns connection table_name) (error_on_missing_columns : Boolean = False) (on_problems : Problem_Behavior = Problem_Behavior.Report_Warning) =
common_update_table self connection table_name update_action key_columns error_on_missing_columns on_problems

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import project.Connection.Connection.Connection
import project.Data.Table.Table as Database_Table
import project.Data.Update_Action.Update_Action
from project.Errors import all
from project.Extensions.Upload_Default_Helpers import default_key_columns
from project.Internal.Upload_Table import all

## Creates a new database table from this in-memory table.
Expand Down Expand Up @@ -62,9 +61,7 @@ Table.select_into_database_table self connection table_name=Nothing primary_key=
- key_columns: the names of the columns to use identify correlate rows from
the source table with rows in the target table. This key is used to
determine if a row from the source table exists in the target or is a new
one. The key can be an empty list if the action is `Insert` - then all rows
are treated as new (but the Database may still reject them with `SQL_Error`
if they violate an integrity constraint).
one.
- error_on_missing_columns: if set to `False` (the default), any columns
missing from the source table will be left unchanged or initialized with
the default value if inserting. If a missing column has no default value,
Expand All @@ -76,18 +73,34 @@ Table.select_into_database_table self connection table_name=Nothing primary_key=

- If `key_columns` are not present in either the source or target tables, a
`Missing_Input_Columns` error is raised.
- If no `key_columns` are specified and the `update_action` is other than
`Insert`, an `Illegal_Argument` error is raised.
- If the target table does not exist, a `Table_Not_Found` error is raised.
- If `error_on_missing_columns` is set to `True` and a column is missing
from the source table, a `Missing_Input_Columns` error is raised.
- If the source table contains columns that are not present in the target
table, an `Unmatched_Columns` error is raised.
- If a column in the source table has a type that cannot be trivially
widened to the corresponding column in the target table, a
`Column_Type_Mismatch` error is raised.
`Column_Type_Mismatch` error is raised. If the types do not match but can
be widened, an `Inexact_Type_Coercion` is reported.
- If `update_action` is `Insert` and a row with the same key already exists
in the target table, a `Rows_Already_Present` error is raised.
- If the `update_action` is `Update` and some rows in the source have no
corresponding rows in the target table, a `Unmatched_Rows` error is
raised.
- If the source table contains multiple rows for the same key, a
`Non_Unique_Primary_Key` error is raised.
- If a row in the source table matches multiple rows in the target table, a
`Multiple_Target_Rows_Matched_For_Update` error is raised.
- If another database error occurs, an `SQL_Error` is raised.

If any error was raised, the data in the target table is not modified.
Table.update_database_table : Connection -> Text -> Update_Action -> Vector Text-> Boolean -> Database_Table ! Table_Not_Found | Unmatched_Columns | Missing_Input_Columns | Column_Type_Mismatch | SQL_Error | Illegal_Argument
Table.update_database_table connection (table_name : Text) (update_action : Update_Action = Update_Action.Update_Or_Insert) (key_columns : Vector = default_key_columns connection table_name) (error_on_missing_columns : Boolean = False) =
common_update_table self connection table_name update_action key_columns error_on_missing_columns

? Type Widening

Smaller types can be widened to a larger type, for example 32-bit integer
column can be widened to a 64-bit integer column, but not vice versa
(because larger numbers could not fit the smaller type and the type of the
column in the target table cannot be changed).
Table.update_database_table : Connection -> Text -> Update_Action -> Vector Text | Nothing -> Boolean -> Problem_Behavior -> Database_Table ! Table_Not_Found | Unmatched_Columns | Missing_Input_Columns | Column_Type_Mismatch | SQL_Error | Illegal_Argument
Table.update_database_table self connection (table_name : Text) (update_action : Update_Action = Update_Action.Update_Or_Insert) (key_columns : Vector | Nothing = default_key_columns connection table_name) (error_on_missing_columns : Boolean = False) (on_problems : Problem_Behavior = Problem_Behavior.Report_Warning) =
common_update_table self connection table_name update_action key_columns error_on_missing_columns on_problems
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ from Standard.Base import all
import Standard.Base.Errors.Illegal_State.Illegal_State

import project.Data.SQL.Builder
import project.Data.Column_Constraint.Column_Constraint
import project.Internal.IR.Context.Context
import project.Internal.IR.SQL_Expression.SQL_Expression
import project.Internal.IR.From_Spec.From_Spec
Expand Down Expand Up @@ -435,10 +436,11 @@ generate_query dialect query = case query of
generate_create_table dialect name columns primary_key temporary
Query.Drop_Table name ->
Builder.code "DROP TABLE " ++ dialect.wrap_identifier name
Query.Insert_From_Select table_name select_query -> case select_query of
Query.Insert_From_Select table_name column_names select_query -> case select_query of
Query.Select _ _ ->
inner_query = generate_query dialect select_query
Builder.code "INSERT INTO " ++ dialect.wrap_identifier table_name ++ " " ++ inner_query
columns_part = Builder.join ", " (column_names.map dialect.wrap_identifier) . paren
Builder.code "INSERT INTO " ++ dialect.wrap_identifier table_name ++ " " ++ columns_part ++ " " ++ inner_query
_ -> Panic.throw (Illegal_State.Error "The inner query of `Query.Insert_From_Select` must be `Query.Select`, but it was "+select_query.to_display_text+". This is a bug in the Database library.")
_ -> Error.throw <| Unsupported_Database_Operation.Error "Unsupported query type: "+query.to_text

Expand Down Expand Up @@ -491,13 +493,20 @@ make_concat make_raw_concat_expr make_contains_expr has_quote args =
## PRIVATE
Generates the SQL code corresponding to a CREATE TABLE query.
generate_create_table dialect name columns primary_key temporary =
column_definitions = columns.map descriptor->
name = descriptor.first
sql_type_as_text = descriptor.second
dialect.wrap_identifier name ++ " " ++ sql_type_as_text
column_definitions = columns.map (generate_column_description dialect)
modifiers = if primary_key.is_nothing then [] else
[Builder.code ", PRIMARY KEY (" ++ Builder.join ", " (primary_key.map dialect.wrap_identifier) ++ ")"]
table_type = if temporary then "TEMPORARY TABLE" else "TABLE"
create_prefix = Builder.code ("CREATE "+table_type+" ") ++ dialect.wrap_identifier name
create_body = (Builder.join ", " column_definitions) ++ (Builder.join "" modifiers)
create_prefix ++ " (" ++ create_body ++ ")"

## PRIVATE
Generates a description of a single column for a CREATE TABLE query.
generate_column_description dialect descriptor =
modifiers = descriptor.constraints.map constraint-> case constraint of
Column_Constraint.Default_Expression raw_expression -> (Builder.code "DEFAULT ") ++ (Builder.code raw_expression . paren)
Column_Constraint.Not_Null -> Builder.code "NOT NULL"
suffix = if modifiers.is_empty then Builder.empty else
Builder.code " " ++ (Builder.join " " modifiers)
(dialect.wrap_identifier descriptor.name) ++ " " ++ descriptor.sql_type ++ suffix
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from Standard.Base import all

import project.Data.Column_Constraint.Column_Constraint

## PRIVATE
type Create_Column_Descriptor
## PRIVATE
A description of a column for the `Create_Table` query.
Value (name : Text) (sql_type : Text) (constraints : Vector Column_Constraint)
Loading

0 comments on commit d9ed63f

Please sign in to comment.