Add tests for update mutations to the Data Connector agent test suite

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/8054
GitOrigin-RevId: 6869e8938e7a70128770f959bf34e3a291074d14
This commit is contained in:
Daniel Chambers 2023-02-22 15:17:33 +11:00 committed by hasura-bot
parent 7dfd2d60f9
commit 72e319e486
15 changed files with 851 additions and 59 deletions

View File

@ -96,3 +96,12 @@ source-repository-package
location: https://github.com/hasura/text.git
tag: ba0fd2bf256c996a6c85dbdc8590a6fcde41b8f8
-- This is a version of Sandwich 0.1.2.0 but including
-- this PR https://github.com/codedownio/sandwich/pull/69
-- which adds the ability to skip tests from the command line
source-repository-package
type: git
location: https://github.com/hasura/sandwich.git
tag: v0.1.2.0-incl-skipping.0
subdir: sandwich

View File

@ -110,20 +110,25 @@ Datasets support is enabled via the ENV variables:
* `DATASET_TEMPLATES`
* `DATASET_CLONES`
Templates will be looked up at `${DATASET_TEMPLATES}/${template_name}.db`.
Templates will be looked up at `${DATASET_TEMPLATES}/${template_name}.sqlite` or `${DATASET_TEMPLATES}/${template_name}.sql`. The `.sqlite` templates are just SQLite database files that will be copied as a clone. The `.sql` templates are SQL script files that will be run against a blank SQLite database in order to create a clone.
Clones will be copied to `${DATASET_CLONES}/${clone_name}.db`.
Clones will be copied to `${DATASET_CLONES}/${clone_name}.sqlite`.
## Testing Changes to the Agent
Run:
Ensure you run the agent with `DATASETS=1 DATASET_DELETE=1 MUTATIONS=1` in order to enable testing of mutations.
Then run:
```sh
cabal run dc-api:test:tests-dc-api -- test --agent-base-url http://localhost:8100 --agent-config '{"db": "db.chinook2.sqlite"}'
cabal run dc-api:test:tests-dc-api -- test --agent-base-url http://localhost:8100 sandwich --tui
```
From the HGE repo.
## Known Issues
* Using "returning" in update mutations where you join across relationships that are affected by the update mutation itself may return inconsistent results. This is because of this issue with SQLite: https://sqlite.org/forum/forumpost/9470611066
## TODO
* [x] Prometheus metrics hosted at `/metrics`

View File

@ -55,6 +55,7 @@ export async function withConnection<Result>(config: Config, mode: number, sqlLo
* $name: "bar"
* });
*/
sqlLogger(query);
db_.all(query, params || {}, (err, data) => {
if (err) {
return reject(err);
@ -67,6 +68,7 @@ export async function withConnection<Result>(config: Config, mode: number, sqlLo
const exec = (sql: string): Promise<void> => {
return new Promise((resolve, reject) => {
sqlLogger(sql);
db_.exec(sql, err => {
if (err) {
reject(err);

View File

@ -196,14 +196,11 @@ if(DATASETS) {
return cloneDataset(sqlLogger, request.params.clone_name, request.body);
});
// Only allow deletion if this is explicitly supported by ENV configuration
if(DATASET_DELETE) {
// TODO: The name param here should be a DatasetCloneName, but this isn't being code-generated.
server.delete<{ Params: { clone_name: string, }, Reply: DatasetDeleteCloneResponse }>("/datasets/clones/:clone_name", async (request, _response) => {
server.log.info({ headers: request.headers, query: request.body, }, "datasets.clones.delete");
return deleteDataset(request.params.clone_name);
});
}
// TODO: The name param here should be a DatasetCloneName, but this isn't being code-generated.
server.delete<{ Params: { clone_name: string, }, Reply: DatasetDeleteCloneResponse }>("/datasets/clones/:clone_name", async (request, _response) => {
server.log.info({ headers: request.headers, query: request.body, }, "datasets.clones.delete");
return deleteDataset(request.params.clone_name);
});
}
server.get("/", async (request, response) => {

View File

@ -147,7 +147,7 @@ function insertString(relationships: Array<TableRelationships>, op: InsertMutati
function deleteString(relationships: Array<TableRelationships>, op: DeleteMutationOperation): string {
return `
DELETE FROM ${op.table}
DELETE FROM ${escapeTableName(op.table)}
WHERE ${whereString(relationships, op.where || EMPTY_AND, op.table)}
RETURNING
${returningString(relationships, op.returning_fields || {}, op.table)} as row,
@ -157,7 +157,7 @@ function deleteString(relationships: Array<TableRelationships>, op: DeleteMutati
function updateString(relationships: Array<TableRelationships>, op: UpdateMutationOperation, info: Array<UpdateInfo>): string {
const result = `
UPDATE ${op.table}
UPDATE ${escapeTableName(op.table)}
SET ${setString(info)}
WHERE ${whereString(relationships, op.where || EMPTY_AND, op.table)}
RETURNING

View File

@ -66,7 +66,7 @@ function validateTableName(tableName: TableName): TableName {
}
/**
* @param ts
* @param ts
* @returns last section of a qualified table array. E.g. [a,b] -> [b]
*/
export function getTableNameSansSchema(ts: Array<string>): Array<string> {
@ -83,9 +83,9 @@ export function escapeTableName(tableName: TableName): string {
}
/**
* @param tableName
* @param tableName
* @returns escaped tableName string with schema qualification removed
*
*
* This is useful in where clauses in returning statements where a qualified table name is invalid SQLite SQL.
*/
export function escapeTableNameSansSchema(tableName: TableName): string {
@ -253,18 +253,18 @@ function generateIdentifierAlias(identifier: string): string {
/**
*
* @param ts Array of Table Relationships
* @param t Table Name
* @param allTableRelationships Array of Table Relationships
* @param tableName Table Name
* @returns Relationships matching table-name
*/
function find_table_relationship(ts: TableRelationships[], t: TableName): TableRelationships {
for(var i = 0; i < ts.length; i++) {
const r = ts[i];
if(tableNameEquals(r.source_table)(t)) {
function find_table_relationship(allTableRelationships: TableRelationships[], tableName: TableName): TableRelationships {
for(var i = 0; i < allTableRelationships.length; i++) {
const r = allTableRelationships[i];
if(tableNameEquals(r.source_table)(tableName)) {
return r;
}
}
throw new Error(`Couldn't find relationship ${ts}, ${t.join(".")} - This shouldn't happen.`);
throw new Error(`Couldn't find table relationships for table ${tableName} - This shouldn't happen.`);
}
function cast_aggregate_function(f: string): string {

View File

@ -162,6 +162,7 @@ test-suite tests-dc-api
Test.Specs.MetricsSpec
Test.Specs.MutationSpec
Test.Specs.MutationSpec.InsertSpec
Test.Specs.MutationSpec.UpdateSpec
Test.Specs.QuerySpec
Test.Specs.QuerySpec.AggregatesSpec
Test.Specs.QuerySpec.BasicSpec

View File

@ -31,6 +31,11 @@ module Hasura.Backends.DataConnector.API.V0.Mutations
_ObjectRelationInsertFieldValue,
_ArrayRelationInsertFieldValue,
UpdateMutationOperation (..),
umoTable,
umoWhere,
umoUpdates,
umoPostUpdateCheck,
umoReturningFields,
RowUpdate (..),
RowColumnOperatorValue (..),
DeleteMutationOperation (..),
@ -506,5 +511,6 @@ instance HasCodec MutationOperationResults where
$(makeLenses ''MutationRequest)
$(makeLenses ''InsertMutationOperation)
$(makeLenses ''UpdateMutationOperation)
$(makeLenses ''MutationResponse)
$(makeLenses ''MutationOperationResults)

View File

@ -17,6 +17,7 @@ module Hasura.Backends.DataConnector.API.V0.Table
tiDeletable,
TableType (..),
ForeignKeys (..),
unForeignKeys,
ConstraintName (..),
Constraint (..),
cForeignTable,
@ -111,13 +112,13 @@ instance HasCodec TableType where
--------------------------------------------------------------------------------
newtype ForeignKeys = ForeignKeys {unForeignKeys :: HashMap ConstraintName Constraint}
newtype ForeignKeys = ForeignKeys {_unForeignKeys :: HashMap ConstraintName Constraint}
deriving stock (Eq, Ord, Show, Generic, Data)
deriving anyclass (NFData, Hashable)
deriving (FromJSON, ToJSON) via Autodocodec ForeignKeys
instance HasCodec ForeignKeys where
codec = dimapCodec ForeignKeys unForeignKeys $ codec @(HashMap ConstraintName Constraint)
codec = dimapCodec ForeignKeys _unForeignKeys $ codec @(HashMap ConstraintName Constraint)
--------------------------------------------------------------------------------
@ -145,3 +146,4 @@ instance HasCodec Constraint where
$(makeLenses ''TableInfo)
$(makeLenses ''Constraint)
$(makeLenses ''ForeignKeys)

View File

@ -39,7 +39,7 @@ where
import Codec.Compression.GZip qualified as GZip
import Command (NameCasing (..), TestConfig (..))
import Control.Arrow (first, (>>>))
import Control.Lens (Index, IxValue, Ixed, Traversal', at, ix, lens, (%~), (&), (?~), (^.), (^..), (^?), _Just)
import Control.Lens (Index, IxValue, Ixed, Traversal', at, ix, (%~), (&), (?~), (^.), (^..), (^?), _Just)
import Data.Aeson (eitherDecodeStrict)
import Data.Aeson qualified as J
import Data.Aeson.Lens (_Bool, _Null, _Number, _String)
@ -211,12 +211,49 @@ invoicesTableName = mkTableName "Invoice"
invoicesRows :: [HashMap API.FieldName API.FieldValue]
invoicesRows = sortBy (API.FieldName "InvoiceId") $ readTableFromXmlIntoRows invoicesTableName
invoicesRowsById :: HashMap Scientific (HashMap API.FieldName API.FieldValue)
invoicesRowsById =
HashMap.fromList $ mapMaybe (\invoice -> (,invoice) <$> invoice ^? field "InvoiceId" . _ColumnFieldNumber) invoicesRows
invoicesTableRelationships :: API.TableRelationships
invoicesTableRelationships =
let invoiceLinesJoinFieldMapping = HashMap.fromList [(API.ColumnName "InvoiceId", API.ColumnName "InvoiceId")]
customersJoinFieldMapping = HashMap.fromList [(API.ColumnName "CustomerId", API.ColumnName "CustomerId")]
in API.TableRelationships
invoicesTableName
( HashMap.fromList
[ (invoiceLinesRelationshipName, API.Relationship invoiceLinesTableName API.ArrayRelationship invoiceLinesJoinFieldMapping),
(customerRelationshipName, API.Relationship customersTableName API.ObjectRelationship customersJoinFieldMapping)
]
)
customerRelationshipName :: API.RelationshipName
customerRelationshipName = API.RelationshipName "Customer"
invoiceLinesTableName :: API.TableName
invoiceLinesTableName = mkTableName "InvoiceLine"
invoiceLinesRows :: [HashMap API.FieldName API.FieldValue]
invoiceLinesRows = sortBy (API.FieldName "InvoiceLineId") $ readTableFromXmlIntoRows invoiceLinesTableName
invoiceLinesTableRelationships :: API.TableRelationships
invoiceLinesTableRelationships =
let invoiceJoinFieldMapping = HashMap.fromList [(API.ColumnName "InvoiceId", API.ColumnName "InvoiceId")]
tracksJoinFieldMapping = HashMap.fromList [(API.ColumnName "TrackId", API.ColumnName "TrackId")]
in API.TableRelationships
invoiceLinesTableName
( HashMap.fromList
[ (invoiceRelationshipName, API.Relationship invoicesTableName API.ObjectRelationship invoiceJoinFieldMapping),
(trackRelationshipName, API.Relationship tracksTableName API.ObjectRelationship tracksJoinFieldMapping)
]
)
invoiceRelationshipName :: API.RelationshipName
invoiceRelationshipName = API.RelationshipName "Invoice"
trackRelationshipName :: API.RelationshipName
trackRelationshipName = API.RelationshipName "Track"
mediaTypesTableName :: API.TableName
mediaTypesTableName = mkTableName "MediaType"
@ -229,6 +266,10 @@ tracksTableName = mkTableName "Track"
tracksRows :: [HashMap API.FieldName API.FieldValue]
tracksRows = sortBy (API.FieldName "TrackId") $ readTableFromXmlIntoRows tracksTableName
tracksRowsById :: HashMap Scientific (HashMap API.FieldName API.FieldValue)
tracksRowsById =
HashMap.fromList $ mapMaybe (\track -> (,track) <$> track ^? field "TrackId" . _ColumnFieldNumber) tracksRows
tracksTableRelationships :: API.TableRelationships
tracksTableRelationships =
let invoiceLinesJoinFieldMapping = HashMap.fromList [(API.ColumnName "TrackId", API.ColumnName "TrackId")]
@ -331,15 +372,22 @@ data TestData = TestData
-- = Invoices table
_tdInvoicesTableName :: API.TableName,
_tdInvoicesRows :: [HashMap API.FieldName API.FieldValue],
_tdInvoicesRowsById :: HashMap Scientific (HashMap API.FieldName API.FieldValue),
_tdInvoicesTableRelationships :: API.TableRelationships,
_tdCustomerRelationshipName :: API.RelationshipName,
-- = InvoiceLines table
_tdInvoiceLinesTableName :: API.TableName,
_tdInvoiceLinesRows :: [HashMap API.FieldName API.FieldValue],
_tdInvoiceLinesTableRelationships :: API.TableRelationships,
_tdInvoiceRelationshipName :: API.RelationshipName,
_tdTrackRelationshipName :: API.RelationshipName,
-- = MediaTypes table
_tdMediaTypesTableName :: API.TableName,
_tdMediaTypesRows :: [HashMap API.FieldName API.FieldValue],
-- = Tracks table
_tdTracksTableName :: API.TableName,
_tdTracksRows :: [HashMap API.FieldName API.FieldValue],
_tdTracksRowsById :: HashMap Scientific (HashMap API.FieldName API.FieldValue),
_tdTracksTableRelationships :: API.TableRelationships,
_tdInvoiceLinesRelationshipName :: API.RelationshipName,
_tdMediaTypeRelationshipName :: API.RelationshipName,
@ -355,6 +403,7 @@ data TestData = TestData
_tdColumnName :: Text -> API.ColumnName,
_tdColumnField :: API.TableName -> Text -> API.Field,
_tdColumnInsertSchema :: API.TableName -> Text -> API.ColumnInsertSchema,
_tdRowColumnOperatorValue :: API.TableName -> Text -> J.Value -> API.RowColumnOperatorValue,
_tdQueryComparisonColumn :: Text -> API.ScalarType -> API.ComparisonColumn,
_tdCurrentComparisonColumn :: Text -> API.ScalarType -> API.ComparisonColumn,
_tdOrderByColumn :: [API.RelationshipName] -> Text -> API.OrderDirection -> API.OrderByElement
@ -387,12 +436,19 @@ mkTestData schemaResponse testConfig =
_tdSupportRepForCustomersRelationshipName = supportRepForCustomersRelationshipName,
_tdInvoicesTableName = formatTableName testConfig invoicesTableName,
_tdInvoicesRows = invoicesRows,
_tdInvoicesRowsById = invoicesRowsById,
_tdInvoicesTableRelationships = formatTableRelationships invoicesTableRelationships,
_tdCustomerRelationshipName = customerRelationshipName,
_tdInvoiceLinesTableName = formatTableName testConfig invoiceLinesTableName,
_tdInvoiceLinesRows = invoiceLinesRows,
_tdInvoiceLinesTableRelationships = formatTableRelationships invoiceLinesTableRelationships,
_tdInvoiceRelationshipName = invoiceRelationshipName,
_tdTrackRelationshipName = trackRelationshipName,
_tdMediaTypesTableName = formatTableName testConfig mediaTypesTableName,
_tdMediaTypesRows = mediaTypesRows,
_tdTracksTableName = formatTableName testConfig tracksTableName,
_tdTracksRows = tracksRows,
_tdTracksRowsById = tracksRowsById,
_tdTracksTableRelationships = formatTableRelationships tracksTableRelationships,
_tdInvoiceLinesRelationshipName = invoiceLinesRelationshipName,
_tdMediaTypeRelationshipName = mediaTypeRelationshipName,
@ -404,6 +460,7 @@ mkTestData schemaResponse testConfig =
_tdColumnName = formatColumnName testConfig . API.ColumnName,
_tdColumnField = columnField schemaResponse testConfig,
_tdColumnInsertSchema = columnInsertSchema schemaResponse testConfig,
_tdRowColumnOperatorValue = rowColumnOperatorValue schemaResponse testConfig,
_tdFindColumnScalarType = \tableName name -> findColumnScalarType schemaResponse tableName (formatColumnName testConfig $ API.ColumnName name),
_tdQueryComparisonColumn = API.ComparisonColumn API.QueryTable . formatColumnName testConfig . API.ColumnName,
_tdCurrentComparisonColumn = API.ComparisonColumn API.CurrentTable . formatColumnName testConfig . API.ColumnName,
@ -425,7 +482,7 @@ mkTestData schemaResponse testConfig =
API.tiName %~ formatTableName testConfig
>>> API.tiColumns . traverse . API.ciName %~ formatColumnName testConfig
>>> API.tiPrimaryKey . traverse %~ formatColumnName testConfig
>>> API.tiForeignKeys . lens API.unForeignKeys (const API.ForeignKeys) . traverse
>>> API.tiForeignKeys . API.unForeignKeys . traverse
%~ ( API.cForeignTable %~ formatTableName testConfig
>>> API.cColumnMapping %~ (HashMap.toList >>> fmap (bimap (formatColumnName testConfig) (formatColumnName testConfig)) >>> HashMap.fromList)
)
@ -443,7 +500,9 @@ data EdgeCasesTestData = EdgeCasesTestData
-- = Utility functions
_ectdTableExists :: API.TableName -> Bool,
_ectdColumnField :: API.TableName -> Text -> API.Field,
_ectdColumnInsertSchema :: API.TableName -> Text -> API.ColumnInsertSchema
_ectdColumnInsertSchema :: API.TableName -> Text -> API.ColumnInsertSchema,
_ectdRowColumnOperatorValue :: API.TableName -> Text -> J.Value -> API.RowColumnOperatorValue,
_ectdCurrentComparisonColumn :: Text -> API.ScalarType -> API.ComparisonColumn
}
mkEdgeCasesTestData :: TestConfig -> API.SchemaResponse -> EdgeCasesTestData
@ -455,7 +514,9 @@ mkEdgeCasesTestData testConfig schemaResponse =
_ectdFindColumnScalarType = \tableName name -> findColumnScalarType schemaResponse tableName (formatColumnName testConfig $ API.ColumnName name),
_ectdTableExists = tableExists,
_ectdColumnField = columnField schemaResponse testConfig,
_ectdColumnInsertSchema = columnInsertSchema schemaResponse testConfig
_ectdColumnInsertSchema = columnInsertSchema schemaResponse testConfig,
_ectdRowColumnOperatorValue = rowColumnOperatorValue schemaResponse testConfig,
_ectdCurrentComparisonColumn = API.ComparisonColumn API.CurrentTable . formatColumnName testConfig . API.ColumnName
}
where
tableExists :: API.TableName -> Bool
@ -497,6 +558,13 @@ columnInsertSchema schemaResponse testConfig tableName columnName =
columnName' = formatColumnName testConfig $ API.ColumnName columnName
scalarType = findColumnScalarType schemaResponse tableName columnName'
rowColumnOperatorValue :: API.SchemaResponse -> TestConfig -> API.TableName -> Text -> J.Value -> API.RowColumnOperatorValue
rowColumnOperatorValue schemaResponse testConfig tableName columnName value =
API.RowColumnOperatorValue columnName' value scalarType
where
columnName' = formatColumnName testConfig $ API.ColumnName columnName
scalarType = findColumnScalarType schemaResponse tableName columnName'
findColumnScalarType :: API.SchemaResponse -> API.TableName -> API.ColumnName -> API.ScalarType
findColumnScalarType API.SchemaResponse {..} tableName columnName =
maybe (error $ "Can't find the scalar type of column " <> show columnName <> " in table " <> show tableName) API._ciType columnInfo

View File

@ -8,11 +8,13 @@ import Hasura.Backends.DataConnector.API
import Test.Data (EdgeCasesTestData, TestData)
import Test.Sandwich (describe)
import Test.Specs.MutationSpec.InsertSpec qualified as InsertSpec
import Test.Specs.MutationSpec.UpdateSpec qualified as UpdateSpec
import Test.TestHelpers (AgentTestSpec)
import Prelude
spec :: TestData -> Maybe EdgeCasesTestData -> Capabilities -> AgentTestSpec
spec testData edgeCasesTestData capabilities@Capabilities {..} = do
describe "mutation API" do
for_ (_cMutations >>= _mcInsertCapabilities) $ \_insertCapabilities ->
for_ (_cMutations >>= _mcInsertCapabilities) $ \_insertCapabilities -> do
InsertSpec.spec testData edgeCasesTestData capabilities
UpdateSpec.spec testData edgeCasesTestData capabilities

View File

@ -3,9 +3,6 @@ module Test.Specs.MutationSpec.InsertSpec (spec) where
import Control.Arrow ((>>>))
import Control.Lens (ix, (&), (.~), (?~), (^?), _Just)
import Control.Monad (when)
import Control.Monad.Catch (MonadThrow)
import Control.Monad.Free (Free)
import Control.Monad.IO.Class (MonadIO)
import Data.Aeson qualified as J
import Data.Foldable (for_)
import Data.Functor ((<&>))
@ -15,19 +12,15 @@ import Data.List (sortOn)
import Data.List.NonEmpty (NonEmpty (..))
import Data.Maybe (fromMaybe, isJust, maybeToList)
import Data.Scientific (Scientific)
import Data.Text qualified as Text
import GHC.Stack (HasCallStack)
import Hasura.Backends.DataConnector.API
import Test.AgentAPI (mutationExpectError, mutationGuarded)
import Test.AgentClient (AgentClientT, HasAgentClient)
import Test.AgentDatasets (HasDatasetContext, chinookTemplate, testingEdgeCasesTemplate, usesDataset)
import Test.AgentTestContext
import Test.AgentDatasets (chinookTemplate, usesDataset)
import Test.Data (EdgeCasesTestData (..), TestData (..))
import Test.Data qualified as Data
import Test.Expectations (mutationResponseShouldBe)
import Test.Sandwich (ExampleT, HasBaseContext, describe, pendingWith, shouldBe)
import Test.Sandwich.Internal (SpecCommand)
import Test.Sandwich (describe, shouldBe)
import Test.TestHelpers (AgentTestSpec, it)
import Test.TestHelpers qualified as Test
import Prelude
spec :: TestData -> Maybe EdgeCasesTestData -> Capabilities -> AgentTestSpec
@ -522,21 +515,7 @@ spec TestData {..} edgeCasesTestData Capabilities {..} = describe "Insert Mutati
response `mutationResponseShouldBe` MutationResponse [expectedResult]
where
edgeCaseTest ::
(HasCallStack, HasAgentClient context, HasAgentTestContext context, HasBaseContext context, MonadThrow m, MonadIO m) =>
(EdgeCasesTestData -> TableName) ->
String ->
(forall testContext. (HasBaseContext testContext, HasAgentClient testContext, HasAgentTestContext testContext, HasDatasetContext testContext) => EdgeCasesTestData -> AgentClientT (ExampleT testContext m) ()) ->
Free (SpecCommand context m) ()
edgeCaseTest expectedTable name test = do
case edgeCasesTestData of
Nothing -> it name $ pendingWith (testingEdgeCasesTemplateName <> " dataset template does not exist")
Just edgeCasesTestData'@EdgeCasesTestData {..} ->
if _ectdTableExists (expectedTable edgeCasesTestData')
then usesDataset testingEdgeCasesTemplate $ it name $ test edgeCasesTestData'
else it name $ pendingWith (Text.unpack (tableNameToText (expectedTable edgeCasesTestData')) <> " table does not exist within the " <> testingEdgeCasesTemplateName <> " dataset")
where
testingEdgeCasesTemplateName = Text.unpack (_unDatasetTemplateName testingEdgeCasesTemplate)
edgeCaseTest = Test.edgeCaseTest edgeCasesTestData
mkSubqueryResponse :: [HashMap FieldName FieldValue] -> FieldValue
mkSubqueryResponse rows =

View File

@ -0,0 +1,698 @@
{-# LANGUAGE QuasiQuotes #-}
module Test.Specs.MutationSpec.UpdateSpec (spec) where
import Control.Lens (ix, (%~), (&), (.~), (?~), (^?))
import Control.Monad (when)
import Data.Aeson qualified as J
import Data.Functor ((<&>))
import Data.HashMap.Strict (HashMap)
import Data.List (sortOn)
import Data.List.NonEmpty (NonEmpty (..))
import Data.Maybe (catMaybes, isJust, mapMaybe, maybeToList)
import Hasura.Backends.DataConnector.API
import Language.GraphQL.Draft.Syntax.QQ qualified as G
import Test.AgentAPI (mutationExpectError, mutationGuarded, queryGuarded)
import Test.AgentDatasets (chinookTemplate, usesDataset)
import Test.Data (EdgeCasesTestData (..), TestData (..))
import Test.Data qualified as Data
import Test.Expectations (mutationResponseShouldBe, rowsShouldBe)
import Test.Sandwich (describe, shouldBe)
import Test.TestHelpers (AgentTestSpec, it)
import Test.TestHelpers qualified as Test
import Prelude
spec :: TestData -> Maybe EdgeCasesTestData -> Capabilities -> AgentTestSpec
spec TestData {..} edgeCasesTestData Capabilities {..} = describe "Update Mutations" $ do
usesDataset chinookTemplate $ it "can set the value of a column on all rows" $ do
let updateOperation =
mkUpdateOperation _tdArtistsTableName
& umoUpdates .~ [SetColumn $ _tdRowColumnOperatorValue _tdArtistsTableName "Name" (J.String "Uniformity")]
let mutationRequest = Data.emptyMutationRequest & mrOperations .~ [UpdateOperation updateOperation]
response <- mutationGuarded mutationRequest
let expectedResult = MutationOperationResults 275 Nothing
response `mutationResponseShouldBe` MutationResponse [expectedResult]
let expectedModifiedRows =
_tdArtistsRows
& fmap (\artist -> artist & Data.field "Name" . Data._ColumnFieldString .~ "Uniformity")
receivedArtists <- Data.sortResponseRowsBy "ArtistId" <$> queryGuarded (artistsQueryRequest (And []))
Data.responseRows receivedArtists `rowsShouldBe` expectedModifiedRows
usesDataset chinookTemplate $ it "can set the value of a column on a specific row" $ do
let whereExp = ApplyBinaryComparisonOperator Equal (_tdCurrentComparisonColumn "ArtistId" artistIdScalarType) (ScalarValue (J.Number 1) artistIdScalarType)
let updateOperation =
mkUpdateOperation _tdArtistsTableName
& umoUpdates .~ [SetColumn $ _tdRowColumnOperatorValue _tdArtistsTableName "Name" (J.String "AySeeDeeSee")]
& umoWhere ?~ whereExp
let mutationRequest = Data.emptyMutationRequest & mrOperations .~ [UpdateOperation updateOperation]
response <- mutationGuarded mutationRequest
let expectedResult = MutationOperationResults 1 Nothing
response `mutationResponseShouldBe` MutationResponse [expectedResult]
let expectedModifiedRows =
_tdArtistsRowsById ^? ix 1
& fmap (\artist -> artist & Data.field "Name" . Data._ColumnFieldString .~ "AySeeDeeSee")
& maybeToList
receivedArtists <- Data.sortResponseRowsBy "ArtistId" <$> queryGuarded (artistsQueryRequest whereExp)
Data.responseRows receivedArtists `rowsShouldBe` expectedModifiedRows
usesDataset chinookTemplate $ it "can set the value of a column on a range of rows" $ do
let whereExp =
And
[ ApplyBinaryComparisonOperator GreaterThan (_tdCurrentComparisonColumn "ArtistId" artistIdScalarType) (ScalarValue (J.Number 10) artistIdScalarType),
ApplyBinaryComparisonOperator LessThanOrEqual (_tdCurrentComparisonColumn "ArtistId" artistIdScalarType) (ScalarValue (J.Number 20) artistIdScalarType)
]
let updateOperation =
mkUpdateOperation _tdArtistsTableName
& umoUpdates .~ [SetColumn $ _tdRowColumnOperatorValue _tdArtistsTableName "Name" (J.String "Nameless")]
& umoWhere ?~ whereExp
let mutationRequest = Data.emptyMutationRequest & mrOperations .~ [UpdateOperation updateOperation]
response <- mutationGuarded mutationRequest
let expectedResult = MutationOperationResults 10 Nothing
response `mutationResponseShouldBe` MutationResponse [expectedResult]
let expectedModifiedRows =
_tdArtistsRows
& filter (\artist -> artist ^? Data.field "ArtistId" . Data._ColumnFieldNumber > Just 10 && artist ^? Data.field "ArtistId" . Data._ColumnFieldNumber <= Just 20)
& fmap (\artist -> artist & Data.field "Name" . Data._ColumnFieldString .~ "Nameless")
receivedArtists <- Data.sortResponseRowsBy "ArtistId" <$> queryGuarded (artistsQueryRequest whereExp)
Data.responseRows receivedArtists `rowsShouldBe` expectedModifiedRows
when ((_cComparisons >>= _ccSubqueryComparisonCapabilities <&> _ctccSupportsRelations) == Just True) $ do
usesDataset chinookTemplate $ it "can set the value of a column on rows filtered by a related table" $ do
let whereExp =
Exists (RelatedTable _tdAlbumsRelationshipName) $
ApplyBinaryComparisonOperator Equal (_tdCurrentComparisonColumn "Title" albumTitleScalarType) (ScalarValue (J.String "Master Of Puppets") albumTitleScalarType)
let updateOperation =
mkUpdateOperation _tdArtistsTableName
& umoUpdates .~ [SetColumn $ _tdRowColumnOperatorValue _tdArtistsTableName "Name" (J.String "Metalika")]
& umoWhere ?~ whereExp
let tableRelationships = [Data.onlyKeepRelationships [_tdAlbumsRelationshipName] _tdArtistsTableRelationships]
let mutationRequest =
Data.emptyMutationRequest
& mrOperations .~ [UpdateOperation updateOperation]
& mrTableRelationships .~ tableRelationships
response <- mutationGuarded mutationRequest
let expectedResult = MutationOperationResults 1 Nothing
response `mutationResponseShouldBe` MutationResponse [expectedResult]
let expectedModifiedRows =
_tdArtistsRows
& filter
( \artist ->
let artistId = artist ^? Data.field "ArtistId" . Data._ColumnFieldNumber
in _tdAlbumsRows
& any
( \album ->
album ^? Data.field "ArtistId" . Data._ColumnFieldNumber == artistId
&& album ^? Data.field "Title" . Data._ColumnFieldString == Just "Master Of Puppets"
)
)
& fmap (\artist -> artist & Data.field "Name" . Data._ColumnFieldString .~ "Metalika")
receivedArtists <- Data.sortResponseRowsBy "ArtistId" <$> queryGuarded (artistsQueryRequest whereExp & qrTableRelationships .~ tableRelationships)
Data.responseRows receivedArtists `rowsShouldBe` expectedModifiedRows
usesDataset chinookTemplate $ it "can set the value of a column differently using multiple operations" $ do
let whereExp1 = ApplyBinaryArrayComparisonOperator In (_tdCurrentComparisonColumn "ArtistId" artistIdScalarType) [J.Number 50, J.Number 51] artistIdScalarType
let updateOperation1 =
mkUpdateOperation _tdArtistsTableName
& umoUpdates .~ [SetColumn $ _tdRowColumnOperatorValue _tdArtistsTableName "Name" (J.String "Renamed 1")]
& umoWhere ?~ whereExp1
let whereExp2 = ApplyBinaryComparisonOperator LessThan (_tdCurrentComparisonColumn "ArtistId" artistIdScalarType) (ScalarValue (J.Number 6) artistIdScalarType)
let updateOperation2 =
mkUpdateOperation _tdArtistsTableName
& umoUpdates .~ [SetColumn $ _tdRowColumnOperatorValue _tdArtistsTableName "Name" (J.String "Renamed 2")]
& umoWhere ?~ whereExp2
let mutationRequest = Data.emptyMutationRequest & mrOperations .~ [UpdateOperation updateOperation1, UpdateOperation updateOperation2]
response <- mutationGuarded mutationRequest
response
`mutationResponseShouldBe` MutationResponse
[ MutationOperationResults 2 Nothing,
MutationOperationResults 5 Nothing
]
let expectedModifiedRows =
[ [_tdArtistsRowsById ^? ix 50, _tdArtistsRowsById ^? ix 51]
& catMaybes
& fmap (\artist -> artist & Data.field "Name" . Data._ColumnFieldString .~ "Renamed 1"),
_tdArtistsRows
& filter (\artist -> artist ^? Data.field "ArtistId" . Data._ColumnFieldNumber < Just 6)
& fmap (\artist -> artist & Data.field "Name" . Data._ColumnFieldString .~ "Renamed 2")
]
& concat
& sortOn (^? Data.field "ArtistId")
receivedArtists <- Data.sortResponseRowsBy "ArtistId" <$> queryGuarded (artistsQueryRequest (Or [whereExp1, whereExp2]))
Data.responseRows receivedArtists `rowsShouldBe` expectedModifiedRows
usesDataset chinookTemplate $ it "multiple update operations are run sequentially" $ do
-- This test performs two updates whose updated row sets overlap each other, so sequential updates should
-- result in the second update applying to the overlapping rows
let whereExp1 = ApplyBinaryArrayComparisonOperator In (_tdCurrentComparisonColumn "Name" artistNameScalarType) [J.String "AC/DC", J.String "Audioslave"] artistNameScalarType
let updateOperation1 =
mkUpdateOperation _tdArtistsTableName
& umoUpdates .~ [SetColumn $ _tdRowColumnOperatorValue _tdArtistsTableName "Name" (J.String "Renamed 1")]
& umoWhere ?~ whereExp1
let whereExp2 = ApplyBinaryComparisonOperator LessThanOrEqual (_tdCurrentComparisonColumn "ArtistId" artistIdScalarType) (ScalarValue (J.Number 5) artistIdScalarType)
let updateOperation2 =
mkUpdateOperation _tdArtistsTableName
& umoUpdates .~ [SetColumn $ _tdRowColumnOperatorValue _tdArtistsTableName "Name" (J.String "Renamed 2")]
& umoWhere ?~ whereExp2
let mutationRequest = Data.emptyMutationRequest & mrOperations .~ [UpdateOperation updateOperation1, UpdateOperation updateOperation2]
response <- mutationGuarded mutationRequest
response
`mutationResponseShouldBe` MutationResponse
[ MutationOperationResults 2 Nothing,
MutationOperationResults 5 Nothing
]
let expectedWhereExp1 artist = artist ^? Data.field "Name" . Data._ColumnFieldString `elem` [Just "AC/DC", Just "Audioslave"]
let expectedWhereExp2 artist = artist ^? Data.field "ArtistId" . Data._ColumnFieldNumber <= Just 5
let expectedModifiedRows =
[ _tdArtistsRows
& filter (\artist -> expectedWhereExp1 artist && not (expectedWhereExp2 artist))
& fmap (\artist -> artist & Data.field "Name" . Data._ColumnFieldString .~ "Renamed 1"),
_tdArtistsRows
& filter expectedWhereExp2
& fmap (\artist -> artist & Data.field "Name" . Data._ColumnFieldString .~ "Renamed 2")
]
& concat
& sortOn (^? Data.field "ArtistId")
-- We can't use whereExp1 since we've renamed the artists!
let alternateWhereExp1 = ApplyBinaryArrayComparisonOperator In (_tdCurrentComparisonColumn "ArtistId" artistIdScalarType) [J.Number 1, J.Number 8] artistIdScalarType
receivedArtists <- Data.sortResponseRowsBy "ArtistId" <$> queryGuarded (artistsQueryRequest (Or [alternateWhereExp1, whereExp2]))
Data.responseRows receivedArtists `rowsShouldBe` expectedModifiedRows
usesDataset chinookTemplate $ it "can increment the value of a column" $ do
let whereExp = ApplyBinaryComparisonOperator Equal (_tdCurrentComparisonColumn "InvoiceId" invoiceIdScalarType) (ScalarValue (J.Number 2) invoiceIdScalarType)
let updateOperation =
mkUpdateOperation _tdInvoiceLinesTableName
& umoUpdates .~ [CustomUpdateColumnOperator incOperator $ _tdRowColumnOperatorValue _tdInvoiceLinesTableName "Quantity" (J.Number 3)]
& umoWhere ?~ whereExp
let mutationRequest = Data.emptyMutationRequest & mrOperations .~ [UpdateOperation updateOperation]
response <- mutationGuarded mutationRequest
let expectedResult = MutationOperationResults 4 Nothing
response `mutationResponseShouldBe` MutationResponse [expectedResult]
let expectedModifiedRows =
_tdInvoiceLinesRows
& filter (\invoiceLine -> invoiceLine ^? Data.field "InvoiceId" . Data._ColumnFieldNumber == Just 2)
& fmap (\invoiceLine -> invoiceLine & Data.field "Quantity" . Data._ColumnFieldNumber %~ (+ 3))
receivedInvoiceLines <- Data.sortResponseRowsBy "InvoiceLineId" <$> queryGuarded (invoiceLinesQueryRequest whereExp)
Data.responseRows receivedInvoiceLines `rowsShouldBe` expectedModifiedRows
usesDataset chinookTemplate $ it "can make multiple updates in one operation" $ do
let whereExp = ApplyBinaryComparisonOperator Equal (_tdCurrentComparisonColumn "InvoiceId" invoiceIdScalarType) (ScalarValue (J.Number 299) invoiceIdScalarType)
let updateOperation =
mkUpdateOperation _tdInvoiceLinesTableName
& umoUpdates
.~ [ CustomUpdateColumnOperator incOperator $ _tdRowColumnOperatorValue _tdInvoiceLinesTableName "UnitPrice" (J.Number 3),
SetColumn $ _tdRowColumnOperatorValue _tdInvoiceLinesTableName "Quantity" (J.Number 2)
]
& umoWhere ?~ whereExp
let mutationRequest = Data.emptyMutationRequest & mrOperations .~ [UpdateOperation updateOperation]
response <- mutationGuarded mutationRequest
let expectedResult = MutationOperationResults 14 Nothing
response `mutationResponseShouldBe` MutationResponse [expectedResult]
let expectedModifiedRows =
_tdInvoiceLinesRows
& filter (\invoiceLine -> invoiceLine ^? Data.field "InvoiceId" . Data._ColumnFieldNumber == Just 299)
& fmap
( \invoiceLine ->
invoiceLine
& Data.field "Quantity" . Data._ColumnFieldNumber .~ 2
& Data.field "UnitPrice" . Data._ColumnFieldNumber %~ (+ 3)
)
receivedInvoiceLines <- Data.sortResponseRowsBy "InvoiceLineId" <$> queryGuarded (invoiceLinesQueryRequest whereExp)
Data.responseRows receivedInvoiceLines `rowsShouldBe` expectedModifiedRows
describe "post-update checks" $ do
usesDataset chinookTemplate $ it "can update when the post-update check passes" $ do
let whereExp =
Or
[ ApplyBinaryComparisonOperator Equal (_tdCurrentComparisonColumn "ArtistId" artistIdScalarType) (ScalarValue (J.Number 1) artistIdScalarType),
ApplyBinaryComparisonOperator Equal (_tdCurrentComparisonColumn "ArtistId" artistIdScalarType) (ScalarValue (J.Number 2) artistIdScalarType)
]
let postUpdateExp =
ApplyBinaryComparisonOperator Equal (_tdCurrentComparisonColumn "Name" artistNameScalarType) (ScalarValue (J.String "Some other name") artistNameScalarType)
let updateOperation =
mkUpdateOperation _tdArtistsTableName
& umoUpdates .~ [SetColumn $ _tdRowColumnOperatorValue _tdArtistsTableName "Name" (J.String "Some other name")]
& umoWhere ?~ whereExp
& umoPostUpdateCheck ?~ postUpdateExp
let mutationRequest = Data.emptyMutationRequest & mrOperations .~ [UpdateOperation updateOperation]
response <- mutationGuarded mutationRequest
let expectedResult = MutationOperationResults 2 Nothing
response `mutationResponseShouldBe` MutationResponse [expectedResult]
let expectedModifiedRows =
[_tdArtistsRowsById ^? ix 1, _tdArtistsRowsById ^? ix 2]
& catMaybes
& fmap (\artist -> artist & Data.field "Name" . Data._ColumnFieldString .~ "Some other name")
receivedArtists <- Data.sortResponseRowsBy "ArtistId" <$> queryGuarded (artistsQueryRequest whereExp)
Data.responseRows receivedArtists `rowsShouldBe` expectedModifiedRows
usesDataset chinookTemplate $ it "fails to update when the post-update check fails" $ do
let whereExp = ApplyBinaryComparisonOperator Equal (_tdCurrentComparisonColumn "InvoiceId" invoiceIdScalarType) (ScalarValue (J.Number 299) invoiceIdScalarType)
let postUpdateExp = ApplyBinaryComparisonOperator LessThanOrEqual (_tdCurrentComparisonColumn "UnitPrice" invoiceLineUnitPriceScalarType) (ScalarValue (J.Number 1.99) invoiceLineUnitPriceScalarType)
let updateOperation =
mkUpdateOperation _tdInvoiceLinesTableName
& umoUpdates .~ [CustomUpdateColumnOperator incOperator $ _tdRowColumnOperatorValue _tdInvoiceLinesTableName "UnitPrice" (J.Number 1)]
& umoWhere ?~ whereExp
& umoPostUpdateCheck ?~ postUpdateExp
let mutationRequest = Data.emptyMutationRequest & mrOperations .~ [UpdateOperation updateOperation]
response <- mutationExpectError mutationRequest
_crType response `shouldBe` MutationPermissionCheckFailure
let expectedUnchangedRows =
_tdInvoiceLinesRows
& filter (\invoiceLine -> invoiceLine ^? Data.field "InvoiceId" . Data._ColumnFieldNumber == Just 299)
receivedInvoiceLines <- Data.sortResponseRowsBy "InvoiceLineId" <$> queryGuarded (invoiceLinesQueryRequest whereExp)
Data.responseRows receivedInvoiceLines `rowsShouldBe` expectedUnchangedRows
when (isJust $ _cComparisons >>= _ccSubqueryComparisonCapabilities) $ do
usesDataset chinookTemplate $ it "can update when post update check against unrelated table passes" $ do
let whereExp = ApplyBinaryComparisonOperator Equal (_tdCurrentComparisonColumn "InvoiceId" invoiceIdScalarType) (ScalarValue (J.Number 299) invoiceIdScalarType)
let postUpdateExp =
Exists (UnrelatedTable _tdCustomersTableName) $
ApplyBinaryComparisonOperator Equal (_tdCurrentComparisonColumn "CustomerId" customerIdScalarType) (ScalarValue (J.Number 2) customerIdScalarType)
let updateOperation =
mkUpdateOperation _tdInvoiceLinesTableName
& umoUpdates .~ [CustomUpdateColumnOperator incOperator $ _tdRowColumnOperatorValue _tdInvoiceLinesTableName "UnitPrice" (J.Number 1)]
& umoWhere ?~ whereExp
& umoPostUpdateCheck ?~ postUpdateExp
let mutationRequest = Data.emptyMutationRequest & mrOperations .~ [UpdateOperation updateOperation]
response <- mutationGuarded mutationRequest
let expectedResult = MutationOperationResults 14 Nothing
response `mutationResponseShouldBe` MutationResponse [expectedResult]
let expectedModifiedRows =
_tdInvoiceLinesRows
& filter (\invoiceLine -> invoiceLine ^? Data.field "InvoiceId" . Data._ColumnFieldNumber == Just 299)
& fmap (\invoiceLine -> invoiceLine & Data.field "UnitPrice" . Data._ColumnFieldNumber %~ (+ 1))
receivedInvoiceLines <- Data.sortResponseRowsBy "InvoiceLineId" <$> queryGuarded (invoiceLinesQueryRequest whereExp)
Data.responseRows receivedInvoiceLines `rowsShouldBe` expectedModifiedRows
usesDataset chinookTemplate $ it "fails to update when post update check against unrelated table fails" $ do
let whereExp = ApplyBinaryComparisonOperator Equal (_tdCurrentComparisonColumn "InvoiceId" invoiceIdScalarType) (ScalarValue (J.Number 299) invoiceIdScalarType)
let postUpdateExp =
Exists (UnrelatedTable _tdCustomersTableName) $
ApplyBinaryComparisonOperator Equal (_tdCurrentComparisonColumn "CustomerId" customerIdScalarType) (ScalarValue (J.Number 666) customerIdScalarType)
let updateOperation =
mkUpdateOperation _tdInvoiceLinesTableName
& umoUpdates .~ [CustomUpdateColumnOperator incOperator $ _tdRowColumnOperatorValue _tdInvoiceLinesTableName "UnitPrice" (J.Number 1)]
& umoWhere ?~ whereExp
& umoPostUpdateCheck ?~ postUpdateExp
let mutationRequest = Data.emptyMutationRequest & mrOperations .~ [UpdateOperation updateOperation]
response <- mutationExpectError mutationRequest
_crType response `shouldBe` MutationPermissionCheckFailure
let expectedUnchangedRows =
_tdInvoiceLinesRows
& filter (\invoiceLine -> invoiceLine ^? Data.field "InvoiceId" . Data._ColumnFieldNumber == Just 299)
receivedInvoiceLines <- Data.sortResponseRowsBy "InvoiceLineId" <$> queryGuarded (invoiceLinesQueryRequest whereExp)
Data.responseRows receivedInvoiceLines `rowsShouldBe` expectedUnchangedRows
when ((_cComparisons >>= _ccSubqueryComparisonCapabilities <&> _ctccSupportsRelations) == Just True) $ do
usesDataset chinookTemplate $ it "can update when post update check against related table passes" $ do
let whereExp = ApplyBinaryComparisonOperator Equal (_tdCurrentComparisonColumn "InvoiceId" invoiceIdScalarType) (ScalarValue (J.Number 299) invoiceIdScalarType)
let postUpdateExp =
Exists (RelatedTable _tdInvoiceRelationshipName) $
ApplyBinaryComparisonOperator Equal (_tdCurrentComparisonColumn "CustomerId" customerIdScalarType) (ScalarValue (J.Number 17) customerIdScalarType)
let updateOperation =
mkUpdateOperation _tdInvoiceLinesTableName
& umoUpdates .~ [SetColumn $ _tdRowColumnOperatorValue _tdInvoiceLinesTableName "InvoiceId" (J.Number 298)]
& umoWhere ?~ whereExp
& umoPostUpdateCheck ?~ postUpdateExp
let tableRelationships = [Data.onlyKeepRelationships [_tdInvoiceRelationshipName] _tdInvoiceLinesTableRelationships]
let mutationRequest =
Data.emptyMutationRequest
& mrOperations .~ [UpdateOperation updateOperation]
& mrTableRelationships .~ tableRelationships
response <- mutationGuarded mutationRequest
let expectedResult = MutationOperationResults 14 Nothing
response `mutationResponseShouldBe` MutationResponse [expectedResult]
let expectedModifiedRows =
_tdInvoiceLinesRows
& filter (\invoiceLine -> invoiceLine ^? Data.field "InvoiceId" . Data._ColumnFieldNumber == Just 299)
& fmap (\invoiceLine -> invoiceLine & Data.field "InvoiceId" . Data._ColumnFieldNumber .~ 298)
-- Need a different where expression for querying the modified rows because the invoice lines have moved
-- to a different invoice and therefore we can't re-query them by the original invoice id any more
let invoiceLineIds = expectedModifiedRows & mapMaybe (^? Data.field "InvoiceLineId" . Data._ColumnFieldNumber) & fmap J.Number
let alternateWhereExp = ApplyBinaryArrayComparisonOperator In (_tdCurrentComparisonColumn "InvoiceLineId" invoiceLineIdScalarType) invoiceLineIds invoiceLineIdScalarType
receivedInvoiceLines <- Data.sortResponseRowsBy "InvoiceLineId" <$> queryGuarded (invoiceLinesQueryRequest alternateWhereExp & qrTableRelationships .~ tableRelationships)
Data.responseRows receivedInvoiceLines `rowsShouldBe` expectedModifiedRows
usesDataset chinookTemplate $ it "fails to update when post update check against related table fails" $ do
let whereExp = ApplyBinaryComparisonOperator Equal (_tdCurrentComparisonColumn "InvoiceId" invoiceIdScalarType) (ScalarValue (J.Number 299) invoiceIdScalarType)
let postUpdateExp =
Exists (RelatedTable _tdInvoiceRelationshipName) $
ApplyBinaryComparisonOperator Equal (_tdCurrentComparisonColumn "CustomerId" customerIdScalarType) (ScalarValue (J.Number 26) customerIdScalarType)
let updateOperation =
mkUpdateOperation _tdInvoiceLinesTableName
& umoUpdates .~ [SetColumn $ _tdRowColumnOperatorValue _tdInvoiceLinesTableName "InvoiceId" (J.Number 298)]
& umoWhere ?~ whereExp
& umoPostUpdateCheck ?~ postUpdateExp
let tableRelationships = [Data.onlyKeepRelationships [_tdInvoiceRelationshipName] _tdInvoiceLinesTableRelationships]
let mutationRequest =
Data.emptyMutationRequest
& mrOperations .~ [UpdateOperation updateOperation]
& mrTableRelationships .~ tableRelationships
response <- mutationExpectError mutationRequest
_crType response `shouldBe` MutationPermissionCheckFailure
let expectedUnchangedRows =
_tdInvoiceLinesRows
& filter (\invoiceLine -> invoiceLine ^? Data.field "InvoiceId" . Data._ColumnFieldNumber == Just 299)
receivedInvoiceLines <- Data.sortResponseRowsBy "InvoiceLineId" <$> queryGuarded (invoiceLinesQueryRequest whereExp)
Data.responseRows receivedInvoiceLines `rowsShouldBe` expectedUnchangedRows
describe "returning" $ do
usesDataset chinookTemplate $ it "returns updated rows" $ do
let whereExp = ApplyBinaryComparisonOperator Equal (_tdCurrentComparisonColumn "InvoiceId" invoiceIdScalarType) (ScalarValue (J.Number 299) invoiceIdScalarType)
let updateOperation =
mkUpdateOperation _tdInvoiceLinesTableName
& umoUpdates
.~ [ SetColumn $ _tdRowColumnOperatorValue _tdInvoiceLinesTableName "InvoiceId" (J.Number 298),
CustomUpdateColumnOperator incOperator $ _tdRowColumnOperatorValue _tdInvoiceLinesTableName "UnitPrice" (J.Number 1)
]
& umoWhere ?~ whereExp
& umoReturningFields .~ invoiceLinesFields
let mutationRequest =
Data.emptyMutationRequest
& mrOperations .~ [UpdateOperation updateOperation]
response <- mutationGuarded mutationRequest
let expectedModifiedRows =
_tdInvoiceLinesRows
& filter (\invoiceLine -> invoiceLine ^? Data.field "InvoiceId" . Data._ColumnFieldNumber == Just 299)
& fmap
( \invoiceLine ->
invoiceLine
& Data.field "InvoiceId" . Data._ColumnFieldNumber .~ 298
& Data.field "UnitPrice" . Data._ColumnFieldNumber %~ (+ 1)
)
let expectedResult = MutationOperationResults 14 (Just expectedModifiedRows)
response `mutationResponseShouldBe` MutationResponse [expectedResult]
usesDataset chinookTemplate $ it "can return rows from an object relationship" $ do
let whereExp = ApplyBinaryComparisonOperator Equal (_tdCurrentComparisonColumn "InvoiceId" invoiceIdScalarType) (ScalarValue (J.Number 299) invoiceIdScalarType)
let updateOperation =
mkUpdateOperation _tdInvoiceLinesTableName
& umoUpdates
.~ [ SetColumn $ _tdRowColumnOperatorValue _tdInvoiceLinesTableName "InvoiceId" (J.Number 298),
CustomUpdateColumnOperator incOperator $ _tdRowColumnOperatorValue _tdInvoiceLinesTableName "UnitPrice" (J.Number 1)
]
& umoWhere ?~ whereExp
& umoReturningFields
.~ invoiceLinesFields
<> Data.mkFieldsMap
[ ( "Track",
( RelField
( RelationshipField _tdTrackRelationshipName $
Data.emptyQuery
& qFields
?~ Data.mkFieldsMap
[ ("TrackId", _tdColumnField _tdTracksTableName "TrackId"),
("Name", _tdColumnField _tdTracksTableName "Name")
]
)
)
)
]
let tableRelationships = [Data.onlyKeepRelationships [_tdTrackRelationshipName] _tdInvoiceLinesTableRelationships]
let mutationRequest =
Data.emptyMutationRequest
& mrOperations .~ [UpdateOperation updateOperation]
& mrTableRelationships .~ tableRelationships
response <- mutationGuarded mutationRequest
let joinInTrack (invoiceLine :: HashMap FieldName FieldValue) =
let track = (invoiceLine ^? Data.field "TrackId" . Data._ColumnFieldNumber) >>= \trackId -> _tdTracksRowsById ^? ix trackId
trackTrimmed = Data.filterColumns ["TrackId", "Name"] $ maybeToList track
in Data.insertField "Track" (mkSubqueryResponse trackTrimmed) invoiceLine
let expectedModifiedRows =
_tdInvoiceLinesRows
& filter (\invoiceLine -> invoiceLine ^? Data.field "InvoiceId" . Data._ColumnFieldNumber == Just 299)
& fmap
( \invoiceLine ->
invoiceLine
& Data.field "InvoiceId" . Data._ColumnFieldNumber .~ 298
& Data.field "UnitPrice" . Data._ColumnFieldNumber %~ (+ 1)
& joinInTrack
)
let expectedResult = MutationOperationResults 14 (Just expectedModifiedRows)
response `mutationResponseShouldBe` MutationResponse [expectedResult]
usesDataset chinookTemplate $ it "can return rows from an array relationship" $ do
let whereExp = ApplyBinaryComparisonOperator Equal (_tdCurrentComparisonColumn "ArtistId" artistIdScalarType) (ScalarValue (J.Number 1) artistIdScalarType)
let updateOperation =
mkUpdateOperation _tdArtistsTableName
& umoUpdates .~ [SetColumn $ _tdRowColumnOperatorValue _tdArtistsTableName "Name" (J.String "AySeeDeeSee")]
& umoWhere ?~ whereExp
& umoReturningFields
.~ artistsFields
<> Data.mkFieldsMap
[ ( "Albums",
( RelField
( RelationshipField _tdAlbumsRelationshipName $
Data.emptyQuery
& qFields
?~ Data.mkFieldsMap
[ ("AlbumId", _tdColumnField _tdAlbumsTableName "AlbumId"),
("ArtistId", _tdColumnField _tdAlbumsTableName "ArtistId"),
("Title", _tdColumnField _tdAlbumsTableName "Title")
]
& qOrderBy ?~ OrderBy mempty (_tdOrderByColumn [] "AlbumId" Ascending :| [])
)
)
)
]
let tableRelationships = [Data.onlyKeepRelationships [_tdAlbumsRelationshipName] _tdArtistsTableRelationships]
let mutationRequest =
Data.emptyMutationRequest
& mrOperations .~ [UpdateOperation updateOperation]
& mrTableRelationships .~ tableRelationships
response <- mutationGuarded mutationRequest
let joinInAlbums (artist :: HashMap FieldName FieldValue) =
let artistId = (artist ^? Data.field "ArtistId" . Data._ColumnFieldNumber)
albums = _tdAlbumsRows & filter (\album -> album ^? Data.field "ArtistId" . Data._ColumnFieldNumber == artistId)
in Data.insertField "Albums" (mkSubqueryResponse albums) artist
let expectedModifiedRows =
_tdArtistsRowsById ^? ix 1
& fmap
( \artist ->
artist
& Data.field "Name" . Data._ColumnFieldString .~ "AySeeDeeSee"
& joinInAlbums
)
& maybeToList
let expectedResult = MutationOperationResults 1 (Just expectedModifiedRows)
response `mutationResponseShouldBe` MutationResponse [expectedResult]
usesDataset chinookTemplate $ it "updated rows are returned even when returned again from across a relationship" $ do
-- In this scenario we move a set of invoice lines from one invoice to another
-- and then return the invoice lines, joined to the invoice and then back to the invoice lines.
-- We expect to see both the existing invoice lines and the moved invoice lines on the invoice.
let whereExp = ApplyBinaryComparisonOperator Equal (_tdCurrentComparisonColumn "InvoiceId" invoiceIdScalarType) (ScalarValue (J.Number 299) invoiceIdScalarType)
let updateOperation =
mkUpdateOperation _tdInvoiceLinesTableName
& umoUpdates
.~ [ SetColumn $ _tdRowColumnOperatorValue _tdInvoiceLinesTableName "InvoiceId" (J.Number 298),
CustomUpdateColumnOperator incOperator $ _tdRowColumnOperatorValue _tdInvoiceLinesTableName "UnitPrice" (J.Number 1)
]
& umoWhere ?~ whereExp
& umoReturningFields
.~ invoiceLinesFields
<> Data.mkFieldsMap
[ ( "Invoice",
( RelField
( RelationshipField _tdInvoiceRelationshipName $
Data.emptyQuery
& qFields
?~ Data.mkFieldsMap
[ ("InvoiceId", _tdColumnField _tdInvoicesTableName "InvoiceId"),
("Total", _tdColumnField _tdInvoicesTableName "Total"),
( "InvoiceLines",
( RelField
( RelationshipField _tdInvoiceLinesRelationshipName $
Data.emptyQuery
& qFields ?~ invoiceLinesFields
& qOrderBy ?~ OrderBy mempty (_tdOrderByColumn [] "InvoiceLineId" Ascending :| [])
)
)
)
]
)
)
)
]
let tableRelationships =
[ Data.onlyKeepRelationships [_tdInvoiceRelationshipName] _tdInvoiceLinesTableRelationships,
Data.onlyKeepRelationships [_tdInvoiceLinesRelationshipName] _tdInvoicesTableRelationships
]
let mutationRequest =
Data.emptyMutationRequest
& mrOperations .~ [UpdateOperation updateOperation]
& mrTableRelationships .~ tableRelationships
response <- mutationGuarded mutationRequest
let modifiedInvoiceLines =
_tdInvoiceLinesRows
& filter (\invoiceLine -> invoiceLine ^? Data.field "InvoiceId" . Data._ColumnFieldNumber == Just 299)
& fmap
( \invoiceLine ->
invoiceLine
& Data.field "InvoiceId" . Data._ColumnFieldNumber .~ 298
& Data.field "UnitPrice" . Data._ColumnFieldNumber %~ (+ 1)
)
let joinInInvoiceLines (invoice :: HashMap FieldName FieldValue) =
let invoiceId = invoice ^? Data.field "InvoiceId" . Data._ColumnFieldNumber
invoiceLines = (_tdInvoiceLinesRows <> modifiedInvoiceLines) & filter (\invoiceLine -> invoiceLine ^? Data.field "InvoiceId" . Data._ColumnFieldNumber == invoiceId)
in Data.insertField "InvoiceLines" (mkSubqueryResponse invoiceLines) invoice
let joinInInvoice (invoiceLine :: HashMap FieldName FieldValue) =
let invoice = (invoiceLine ^? Data.field "InvoiceId" . Data._ColumnFieldNumber) >>= \invoiceId -> _tdInvoicesRowsById ^? ix invoiceId
invoiceTrimmed = Data.filterColumns ["InvoiceId", "Total"] $ maybeToList invoice
in Data.insertField "Invoice" (mkSubqueryResponse (joinInInvoiceLines <$> invoiceTrimmed)) invoiceLine
let expectedModifiedRows = joinInInvoice <$> modifiedInvoiceLines
let expectedResult = MutationOperationResults 14 (Just expectedModifiedRows)
response `mutationResponseShouldBe` MutationResponse [expectedResult]
describe "edge cases" $
edgeCaseTest _ectdNoPrimaryKeyTableName "can update rows in a table with no primary key" $ \EdgeCasesTestData {..} -> do
let firstNameScalarType = _ectdFindColumnScalarType _ectdNoPrimaryKeyTableName "FirstName"
let lastNameScalarType = _ectdFindColumnScalarType _ectdNoPrimaryKeyTableName "LastName"
let whereExp =
And
[ ApplyBinaryComparisonOperator Equal (_ectdCurrentComparisonColumn "FirstName" firstNameScalarType) (ScalarValue (J.String "Will") firstNameScalarType),
ApplyBinaryComparisonOperator Equal (_ectdCurrentComparisonColumn "LastName" lastNameScalarType) (ScalarValue (J.String "Riker") lastNameScalarType)
]
let returning =
Data.mkFieldsMap
[ ("FirstName", _ectdColumnField _ectdNoPrimaryKeyTableName "FirstName"),
("LastName", _ectdColumnField _ectdNoPrimaryKeyTableName "LastName")
]
let updateOperation =
mkUpdateOperation _ectdNoPrimaryKeyTableName
& umoUpdates .~ [SetColumn $ _ectdRowColumnOperatorValue _ectdNoPrimaryKeyTableName "FirstName" (J.String "William")]
& umoWhere ?~ whereExp
& umoReturningFields .~ returning
let mutationRequest = Data.emptyMutationRequest & mrOperations .~ [UpdateOperation updateOperation]
response <- mutationGuarded mutationRequest
let expectedModifiedRows =
[ Data.mkFieldsMap $
[ ("FirstName", mkColumnFieldValue $ J.String "William"),
("LastName", mkColumnFieldValue $ J.String "Riker")
]
]
let expectedResult = MutationOperationResults 1 (Just expectedModifiedRows)
response `mutationResponseShouldBe` MutationResponse [expectedResult]
where
edgeCaseTest = Test.edgeCaseTest edgeCasesTestData
mkUpdateOperation :: TableName -> UpdateMutationOperation
mkUpdateOperation tableName = UpdateMutationOperation tableName Nothing [] Nothing mempty
mkSubqueryResponse :: [HashMap FieldName FieldValue] -> FieldValue
mkSubqueryResponse rows =
mkRelationshipFieldValue $ QueryResponse (Just rows) Nothing
artistsFields :: HashMap FieldName Field
artistsFields =
Data.mkFieldsMap
[ ("ArtistId", _tdColumnField _tdArtistsTableName "ArtistId"),
("Name", _tdColumnField _tdArtistsTableName "Name")
]
artistsQueryRequest :: Expression -> QueryRequest
artistsQueryRequest whereExp =
let query = Data.emptyQuery & qFields ?~ artistsFields & qWhere ?~ whereExp
in QueryRequest _tdArtistsTableName [] query
invoiceLinesFields :: HashMap FieldName Field
invoiceLinesFields =
Data.mkFieldsMap
[ ("InvoiceId", _tdColumnField _tdInvoiceLinesTableName "InvoiceId"),
("InvoiceLineId", _tdColumnField _tdInvoiceLinesTableName "InvoiceLineId"),
("TrackId", _tdColumnField _tdInvoiceLinesTableName "TrackId"),
("UnitPrice", _tdColumnField _tdInvoiceLinesTableName "UnitPrice"),
("Quantity", _tdColumnField _tdInvoiceLinesTableName "Quantity")
]
invoiceLinesQueryRequest :: Expression -> QueryRequest
invoiceLinesQueryRequest whereExp =
let query = Data.emptyQuery & qFields ?~ invoiceLinesFields & qWhere ?~ whereExp
in QueryRequest _tdInvoiceLinesTableName [] query
incOperator :: UpdateColumnOperatorName
incOperator = UpdateColumnOperatorName $ [G.name|inc|]
albumTitleScalarType = _tdFindColumnScalarType _tdAlbumsTableName "Title"
artistIdScalarType = _tdFindColumnScalarType _tdArtistsTableName "ArtistId"
artistNameScalarType = _tdFindColumnScalarType _tdArtistsTableName "Name"
customerIdScalarType = _tdFindColumnScalarType _tdCustomersTableName "CustomerId"
invoiceLineIdScalarType = _tdFindColumnScalarType _tdInvoiceLinesTableName "InvoiceLineId"
invoiceIdScalarType = _tdFindColumnScalarType _tdInvoiceLinesTableName "InvoiceId"
invoiceLineUnitPriceScalarType = _tdFindColumnScalarType _tdInvoiceLinesTableName "UnitPrice"

View File

@ -119,7 +119,7 @@ spec TestData {..} API.Capabilities {..} = describe "schema API" $ do
-- We compare only the constraints and ignore the constraint names since some agents will have
-- different constraint names
let extractConstraintsForComparison table =
sort . HashMap.elems . API.unForeignKeys $ API._tiForeignKeys table
sort . HashMap.elems . API._unForeignKeys $ API._tiForeignKeys table
let actualConstraints = extractConstraintsForComparison <$> tables
let expectedConstraints = Just $ extractConstraintsForComparison expectedTable

View File

@ -3,15 +3,21 @@ module Test.TestHelpers
AgentClientTestSpec,
AgentDatasetTestSpec,
it,
edgeCaseTest,
)
where
import Control.Monad.Catch (MonadThrow)
import Control.Monad.Free (Free)
import Control.Monad.IO.Class (MonadIO)
import Data.Text qualified as Text
import GHC.Stack (HasCallStack)
import Hasura.Backends.DataConnector.API qualified as API
import Test.AgentClient (AgentClientT, HasAgentClient, runAgentClientT)
import Test.AgentDatasets (HasDatasetContext)
import Test.AgentDatasets (HasDatasetContext, testingEdgeCasesTemplate, usesDataset)
import Test.AgentTestContext (HasAgentTestContext)
import Test.Sandwich (ExampleT, HasBaseContext, SpecFree)
import Test.Data (EdgeCasesTestData (..))
import Test.Sandwich (ExampleT, HasBaseContext, SpecFree, pendingWith)
import Test.Sandwich qualified as Sandwich
import Test.Sandwich.Internal (SpecCommand)
import Prelude
@ -24,3 +30,20 @@ type AgentDatasetTestSpec = forall context. (HasBaseContext context, HasAgentTes
it :: (HasCallStack, HasAgentClient context, Monad m) => String -> AgentClientT (ExampleT context m) () -> Free (SpecCommand context m) ()
it label test = Sandwich.it label $ runAgentClientT Nothing test
edgeCaseTest ::
(HasCallStack, HasAgentClient context, HasAgentTestContext context, HasBaseContext context, MonadThrow m, MonadIO m) =>
Maybe EdgeCasesTestData ->
(EdgeCasesTestData -> API.TableName) ->
String ->
(forall testContext. (HasBaseContext testContext, HasAgentClient testContext, HasAgentTestContext testContext, HasDatasetContext testContext) => EdgeCasesTestData -> AgentClientT (ExampleT testContext m) ()) ->
Free (SpecCommand context m) ()
edgeCaseTest edgeCasesTestData expectedTable name test = do
case edgeCasesTestData of
Nothing -> it name $ pendingWith (testingEdgeCasesTemplateName <> " dataset template does not exist")
Just edgeCasesTestData'@EdgeCasesTestData {..} ->
if _ectdTableExists (expectedTable edgeCasesTestData')
then usesDataset testingEdgeCasesTemplate $ it name $ test edgeCasesTestData'
else it name $ pendingWith (Text.unpack (API.tableNameToText (expectedTable edgeCasesTestData')) <> " table does not exist within the " <> testingEdgeCasesTemplateName <> " dataset")
where
testingEdgeCasesTemplateName = Text.unpack (API._unDatasetTemplateName testingEdgeCasesTemplate)