From cd38a9a1fceb5e1218b082f41ab9e4ed4050eab4 Mon Sep 17 00:00:00 2001 From: Gil Mizrahi Date: Wed, 11 May 2022 19:00:10 +0300 Subject: [PATCH] server/postgres + server/mssql: Insert empty objects with default values PR-URL: https://github.com/hasura/graphql-engine-mono/pull/4487 Co-authored-by: Abby Sassel <3883855+sassela@users.noreply.github.com> GitOrigin-RevId: 3413f0b5dbe6ec42fff360d83b5202e4aa4aa86e --- CHANGELOG.md | 1 + server/graphql-engine.cabal | 1 + .../src-lib/Hasura/Backends/MSSQL/ToQuery.hs | 17 +- .../Hasura/Backends/Postgres/SQL/DML.hs | 8 +- server/tests-hspec/Test/InsertDefaultsSpec.hs | 345 ++++++++++++++++++ 5 files changed, 362 insertions(+), 10 deletions(-) create mode 100644 server/tests-hspec/Test/InsertDefaultsSpec.hs diff --git a/CHANGELOG.md b/CHANGELOG.md index e6db7669ca3..2a519f7e675 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - server: fixes remote relationships on actions (fix #8399) - server: fixes url/query date variable bug in REST endpoints - server: makes url/query variables in REST endpoints assume string if other types not applicable +- server: fix inserting empty objects with default values to postgres, citus, and sql server (fix #8475) - console: add remote database relationships for views - console: bug fixes for RS-to-RS relationships - console: allow users to remove prefix / suffix / root field namespace from a remote schema diff --git a/server/graphql-engine.cabal b/server/graphql-engine.cabal index f6e12e75445..c05ffddd30e 100644 --- a/server/graphql-engine.cabal +++ b/server/graphql-engine.cabal @@ -1124,6 +1124,7 @@ test-suite tests-hspec Test.EventTriggersRunSQLSpec Test.HelloWorldSpec Test.InsertCheckPermissionSpec + Test.InsertDefaultsSpec Test.InsertEnumColumnSpec Test.LimitOffsetSpec Test.NestedRelationshipsSpec diff --git a/server/src-lib/Hasura/Backends/MSSQL/ToQuery.hs b/server/src-lib/Hasura/Backends/MSSQL/ToQuery.hs index 8b77510abbc..9584d76feb3 100644 --- a/server/src-lib/Hasura/Backends/MSSQL/ToQuery.hs +++ b/server/src-lib/Hasura/Backends/MSSQL/ToQuery.hs @@ -235,12 +235,17 @@ fromInsert :: Insert -> Printer fromInsert Insert {..} = SepByPrinter NewlinePrinter - [ "INSERT INTO " <+> fromTableName insertTable, - "(" <+> SepByPrinter ", " (map (fromNameText . columnNameText) insertColumns) <+> ")", - fromInsertOutput insertOutput, - "INTO " <+> fromTempTable insertTempTable, - fromValuesList insertValues - ] + $ ["INSERT INTO " <+> fromTableName insertTable] + <> ( if null insertColumns + then [] + else ["(" <+> SepByPrinter ", " (map (fromNameText . columnNameText) insertColumns) <+> ")"] + ) + <> [ fromInsertOutput insertOutput, + "INTO " <+> fromTempTable insertTempTable, + if null insertColumns + then "DEFAULT VALUES" + else fromValuesList insertValues + ] fromSetValue :: SetValue -> Printer fromSetValue = \case diff --git a/server/src-lib/Hasura/Backends/Postgres/SQL/DML.hs b/server/src-lib/Hasura/Backends/Postgres/SQL/DML.hs index 3a0b567d754..16ae4d1a9c4 100644 --- a/server/src-lib/Hasura/Backends/Postgres/SQL/DML.hs +++ b/server/src-lib/Hasura/Backends/Postgres/SQL/DML.hs @@ -1079,10 +1079,10 @@ instance ToSQL SQLInsert where toSQL si = "INSERT INTO" <~> toSQL (siTable si) - <~> "(" - <~> (", " <+> siCols si) - <~> ")" - <~> toSQL (siValues si) + <~> ( if null (siCols si) + then "DEFAULT VALUES" + else "(" <~> (", " <+> siCols si) <~> ")" <~> toSQL (siValues si) + ) <~> maybe "" toSQL (siConflict si) <~> toSQL (siRet si) diff --git a/server/tests-hspec/Test/InsertDefaultsSpec.hs b/server/tests-hspec/Test/InsertDefaultsSpec.hs new file mode 100644 index 00000000000..9400c296060 --- /dev/null +++ b/server/tests-hspec/Test/InsertDefaultsSpec.hs @@ -0,0 +1,345 @@ +{-# LANGUAGE QuasiQuotes #-} + +-- | Test insert with default values +module Test.InsertDefaultsSpec (spec) where + +import Harness.Backend.Citus qualified as Citus +import Harness.Backend.Postgres qualified as Postgres +import Harness.Backend.Sqlserver qualified as Sqlserver +import Harness.GraphqlEngine qualified as GraphqlEngine +import Harness.Quoter.Graphql (graphql) +import Harness.Quoter.Yaml (shouldReturnYaml, yaml) +import Harness.Test.Context qualified as Context +import Harness.Test.Schema qualified as Schema +import Harness.TestEnvironment (TestEnvironment) +import Test.Hspec (SpecWith, it) +import Prelude + +-------------------------------------------------------------------------------- + +-- ** Preamble + +spec :: SpecWith TestEnvironment +spec = do + Context.run + [ postgresContext, + citusContext, + mssqlContext + ] + commonTests + + Context.run [postgresContext, citusContext] postgresTests + Context.run [mssqlContext] mssqlTests + where + postgresContext = + Context.Context + { name = Context.Backend Context.Postgres, + mkLocalTestEnvironment = Context.noLocalTestEnvironment, + setup = Postgres.setup schema, + teardown = Postgres.teardown schema, + customOptions = Nothing + } + citusContext = + Context.Context + { name = Context.Backend Context.Citus, + mkLocalTestEnvironment = Context.noLocalTestEnvironment, + setup = Citus.setup schema, + teardown = Citus.teardown schema, + customOptions = Nothing + } + mssqlContext = + Context.Context + { name = Context.Backend Context.SQLServer, + mkLocalTestEnvironment = Context.noLocalTestEnvironment, + setup = Sqlserver.setup schema, + teardown = Sqlserver.teardown schema, + customOptions = Nothing + } + +-------------------------------------------------------------------------------- + +-- ** Schema + +schema :: [Schema.Table] +schema = + [ alldefaults, + somedefaults, + withrelationship + ] + +alldefaults :: Schema.Table +alldefaults = + Schema.Table + { tableName = "alldefaults", + tableColumns = + [ Schema.column "id" defaultSerialType, + Schema.column "dt" defaultDateTimeType + ], + tablePrimaryKey = ["id"], + tableReferences = [], + tableData = [] + } + +somedefaults :: Schema.Table +somedefaults = + Schema.Table + { tableName = "somedefaults", + tableColumns = + [ Schema.column "id" defaultSerialType, + Schema.column "dt" defaultDateTimeType, + Schema.column "name" Schema.TStr + ], + tablePrimaryKey = ["name"], + tableReferences = [], + tableData = [] + } + +withrelationship :: Schema.Table +withrelationship = + Schema.Table + { tableName = "withrelationship", + tableColumns = + [ Schema.column "id" defaultSerialType, + Schema.column "nickname" Schema.TStr, + Schema.column "time_id" Schema.TInt + ], + tablePrimaryKey = ["nickname"], + tableReferences = [Schema.Reference "time_id" "alldefaults" "id"], + tableData = [] + } + +defaultSerialType :: Schema.ScalarType +defaultSerialType = + Schema.TCustomType $ + Schema.defaultBackendScalarType + { Schema.bstMysql = Nothing, + Schema.bstMssql = Just "INT IDENTITY(1,1)", + Schema.bstCitus = Just "SERIAL", + Schema.bstPostgres = Just "SERIAL", + Schema.bstBigQuery = Nothing + } + +defaultDateTimeType :: Schema.ScalarType +defaultDateTimeType = + Schema.TCustomType $ + Schema.defaultBackendScalarType + { Schema.bstMysql = Nothing, + Schema.bstMssql = Just "DATETIME DEFAULT GETDATE()", + Schema.bstCitus = Just "TIMESTAMP DEFAULT NOW()", + Schema.bstPostgres = Just "TIMESTAMP DEFAULT NOW()", + Schema.bstBigQuery = Nothing + } + +-------------------------------------------------------------------------------- + +-- * Tests + +commonTests :: Context.Options -> SpecWith TestEnvironment +commonTests opts = do + it "Insert empty object with default values" $ \testEnvironment -> + shouldReturnYaml + opts + ( GraphqlEngine.postGraphql + testEnvironment + [graphql| +mutation { + insert_hasura_alldefaults( + objects:[{}] + ){ + affected_rows + } +} +|] + ) + [yaml| +data: + insert_hasura_alldefaults: + affected_rows: 1 +|] + + it "Insert simple object with default values" $ \testEnvironment -> + shouldReturnYaml + opts + ( GraphqlEngine.postGraphql + testEnvironment + [graphql| +mutation { + insert_hasura_somedefaults( + objects:[{ name: "a" }] + ){ + affected_rows + returning { + id + name + } + } +} +|] + ) + [yaml| +data: + insert_hasura_somedefaults: + affected_rows: 1 + returning: + - id: 1 + name: "a" +|] + +postgresTests :: Context.Options -> SpecWith TestEnvironment +postgresTests opts = do + it "Upsert simple object with default values - check empty constraints" $ \testEnvironment -> + shouldReturnYaml + opts + ( GraphqlEngine.postGraphql + testEnvironment + [graphql| +mutation { + insert_hasura_somedefaults( + objects: [{ name: "a" }] + on_conflict: { + constraint: somedefaults_pkey, + update_columns: [] + } + ){ + affected_rows + returning { + id + } + } +} +|] + ) + [yaml| +data: + insert_hasura_somedefaults: + affected_rows: 1 + returning: + - id: 1 +|] + + it "Upsert simple object with default values - check conflict doesn't update" $ \testEnvironment -> + shouldReturnYaml + opts + ( GraphqlEngine.postGraphql + testEnvironment + [graphql| +mutation { + insert_hasura_somedefaults( + objects: [{ name: "a" }] + on_conflict: { + constraint: somedefaults_pkey, + update_columns: [] + } + ){ + affected_rows + returning { + id + } + } +} +|] + ) + [yaml| +data: + insert_hasura_somedefaults: + affected_rows: 0 + returning: [] +|] + + it "Nested insert with empty object" $ \testEnvironment -> + shouldReturnYaml + opts + ( GraphqlEngine.postGraphql + testEnvironment + [graphql| +mutation { + insert_hasura_withrelationship( + objects: [{ nickname: "the a", alldefaults_by_time_id: {data: {} } }] + on_conflict: { + constraint: withrelationship_pkey, + update_columns: [] + } + ){ + affected_rows + returning { + id + nickname + alldefaults_by_time_id { + id + } + } + } +} +|] + ) + [yaml| +data: + insert_hasura_withrelationship: + affected_rows: 2 + returning: + - id: 1 + nickname: "the a" + alldefaults_by_time_id: + id: 1 +|] + +mssqlTests :: Context.Options -> SpecWith TestEnvironment +mssqlTests opts = do + it "Upsert simple object with default values - check empty if_matched" $ \testEnvironment -> + shouldReturnYaml + opts + ( GraphqlEngine.postGraphql + testEnvironment + [graphql| +mutation { + insert_hasura_somedefaults( + objects: [{ name: "a" }] + if_matched: { + match_columns: [], + update_columns: [] + } + ){ + affected_rows + returning { + id + } + } +} +|] + ) + [yaml| +data: + insert_hasura_somedefaults: + affected_rows: 1 + returning: + - id: 1 +|] + + it "Upsert simple object with default values - check conflict doesn't update" $ \testEnvironment -> + shouldReturnYaml + opts + ( GraphqlEngine.postGraphql + testEnvironment + [graphql| +mutation { + insert_hasura_somedefaults( + objects: [{ name: "a" }] + if_matched: { + match_columns: name, + update_columns: [] + } + ){ + affected_rows + returning { + id + } + } +} +|] + ) + [yaml| +data: + insert_hasura_somedefaults: + affected_rows: 0 + returning: [] +|]