diff --git a/DataGateway.Service.Tests/SqlTests/MsSqlRestApiTests.cs b/DataGateway.Service.Tests/SqlTests/MsSqlRestApiTests.cs index 476081b5fe..986cd84714 100644 --- a/DataGateway.Service.Tests/SqlTests/MsSqlRestApiTests.cs +++ b/DataGateway.Service.Tests/SqlTests/MsSqlRestApiTests.cs @@ -190,7 +190,7 @@ public class MsSqlRestApiTests : RestApiTestBase // the insertion. $"SELECT [categoryid],[pieceid],[categoryName],[piecesAvailable]," + $"[piecesRequired] FROM { _Composite_NonAutoGenPK } " + - $"WHERE [categoryid] = 5 AND [pieceid] = 2 AND [categoryName] = 'Thriller' " + + $"WHERE [categoryid] = 5 AND [pieceid] = 2 AND [categoryName] = 'FairyTales' " + $"AND [piecesAvailable] = 0 AND [piecesRequired] = 0 " + $"FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER" }, @@ -237,9 +237,9 @@ public class MsSqlRestApiTests : RestApiTestBase }, { "PutOne_Update_CompositeNonAutoGenPK_Test", - $"SELECT [categoryid], [pieceid], [categoryName],[piecesAvailable]," + + $"SELECT [categoryid], [pieceid], [categoryName], [piecesAvailable]," + $"[piecesRequired] FROM { _Composite_NonAutoGenPK } " + - $"WHERE [categoryid] = 2 AND [pieceid] = 1 AND [categoryName] = 'History' " + + $"WHERE [categoryid] = 2 AND [pieceid] = 1 AND [categoryName] = 'SciFi' " + $"AND [piecesAvailable] = 10 AND [piecesRequired] = 5 " + $"FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER" }, @@ -265,8 +265,9 @@ public class MsSqlRestApiTests : RestApiTestBase }, { "PutOne_Insert_AutoGenNonPK_Test", - $"SELECT [id], [title], [volume] FROM { _integration_AutoGenNonPK_TableName } " + + $"SELECT [id], [title], [volume], [categoryName] FROM { _integration_AutoGenNonPK_TableName } " + $"WHERE id = { STARTING_ID_FOR_TEST_INSERTS } AND [title] = 'Star Trek' " + + $"AND [categoryName] = 'Suspense' " + $"AND [volume] IS NOT NULL " + $"FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER" }, @@ -274,7 +275,7 @@ public class MsSqlRestApiTests : RestApiTestBase "PutOne_Insert_CompositeNonAutoGenPK_Test", $"SELECT [categoryid], [pieceid], [categoryName],[piecesAvailable]," + $"[piecesRequired] FROM { _Composite_NonAutoGenPK } " + - $"WHERE [categoryid] = 3 AND [pieceid] = 1 AND [categoryName] = 'comics' " + + $"WHERE [categoryid] = 3 AND [pieceid] = 1 AND [categoryName] = 'SciFi' " + $"AND [piecesAvailable] = 2 AND [piecesRequired] = 1 " + $"FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER" }, @@ -297,7 +298,7 @@ public class MsSqlRestApiTests : RestApiTestBase "PatchOne_Insert_CompositeNonAutoGenPK_Test", $"SELECT [categoryid], [pieceid], [categoryName],[piecesAvailable]," + $"[piecesRequired] FROM { _Composite_NonAutoGenPK } " + - $"WHERE [categoryid] = 4 AND [pieceid] = 1 AND [categoryName] = 'Suspense' " + + $"WHERE [categoryid] = 4 AND [pieceid] = 1 AND [categoryName] = 'FairyTales' " + $"AND [piecesAvailable] = 5 AND [piecesRequired] = 4 " + $"FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER" }, @@ -305,7 +306,7 @@ public class MsSqlRestApiTests : RestApiTestBase "PatchOne_Insert_Default_Test", $"SELECT [categoryid], [pieceid], [categoryName],[piecesAvailable]," + $"[piecesRequired] FROM { _Composite_NonAutoGenPK } " + - $"WHERE [categoryid] = 7 AND [pieceid] = 1 AND [categoryName] = 'Drama' " + + $"WHERE [categoryid] = 7 AND [pieceid] = 1 AND [categoryName] = 'SciFi' " + $"AND [piecesAvailable] = 0 AND [piecesRequired] = 0 " + $"FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER" }, @@ -332,7 +333,7 @@ public class MsSqlRestApiTests : RestApiTestBase "PatchOne_Update_CompositeNonAutoGenPK_Test", $"SELECT [categoryid], [pieceid], [categoryName],[piecesAvailable]," + $"[piecesRequired] FROM { _Composite_NonAutoGenPK } " + - $"WHERE [categoryid] = 1 AND [pieceid] = 1 AND [categoryName] = 'books' " + + $"WHERE [categoryid] = 1 AND [pieceid] = 1 AND [categoryName] = 'SciFi' " + $"AND [piecesAvailable]= 10 AND [piecesRequired] = 0 " + $"FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER" }, diff --git a/DataGateway.Service.Tests/SqlTests/MySqlRestApiTests.cs b/DataGateway.Service.Tests/SqlTests/MySqlRestApiTests.cs index bef5f8c4f0..3a806a8fa1 100644 --- a/DataGateway.Service.Tests/SqlTests/MySqlRestApiTests.cs +++ b/DataGateway.Service.Tests/SqlTests/MySqlRestApiTests.cs @@ -353,7 +353,7 @@ SELECT JSON_OBJECT('categoryid', categoryid, 'pieceid', pieceid, 'categoryName', FROM ( SELECT categoryid, pieceid, categoryName,piecesAvailable,piecesRequired FROM " + _Composite_NonAutoGenPK + @" - WHERE categoryid = 5 AND pieceid = 2 AND categoryName ='Thriller' AND piecesAvailable = 0 + WHERE categoryid = 5 AND pieceid = 2 AND categoryName ='FairyTales' AND piecesAvailable = 0 AND piecesRequired = 0 ) AS subq " @@ -436,7 +436,7 @@ SELECT JSON_OBJECT('categoryid', categoryid, 'pieceid', pieceid, 'categoryName', FROM ( SELECT categoryid, pieceid, categoryName,piecesAvailable,piecesRequired FROM " + _Composite_NonAutoGenPK + @" - WHERE categoryid = 2 AND pieceid = 1 AND categoryName ='History' AND piecesAvailable = 10 + WHERE categoryid = 2 AND pieceid = 1 AND categoryName ='SciFi' AND piecesAvailable = 10 AND piecesRequired = 5 ) AS subq " @@ -466,9 +466,9 @@ AND issue_number is NULL }, { "PutOne_Insert_AutoGenNonPK_Test", - @"SELECT JSON_OBJECT('id', id, 'title', title, 'volume', volume ) AS data + @"SELECT JSON_OBJECT('id', id, 'title', title, 'volume', volume, 'categoryName', categoryName) AS data FROM ( - SELECT id, title, volume + SELECT id, title, volume, categoryName FROM " + _integration_AutoGenNonPK_TableName + @" WHERE id = " + $"{STARTING_ID_FOR_TEST_INSERTS}" + @" AND title = 'Star Trek' AND volume IS NOT NULL @@ -483,7 +483,7 @@ SELECT JSON_OBJECT('categoryid', categoryid, 'pieceid', pieceid, 'categoryName', FROM ( SELECT categoryid, pieceid, categoryName,piecesAvailable,piecesRequired FROM " + _Composite_NonAutoGenPK + @" - WHERE categoryid = 3 AND pieceid = 1 AND categoryName ='comics' AND piecesAvailable = 2 + WHERE categoryid = 3 AND pieceid = 1 AND categoryName ='SciFi' AND piecesAvailable = 2 AND piecesRequired = 1 ) AS subq " @@ -520,7 +520,7 @@ SELECT JSON_OBJECT('categoryid', categoryid, 'pieceid', pieceid, 'categoryName', FROM ( SELECT categoryid, pieceid, categoryName,piecesAvailable,piecesRequired FROM " + _Composite_NonAutoGenPK + @" - WHERE categoryid = 4 AND pieceid = 1 AND categoryName ='Suspense' AND piecesAvailable = 5 + WHERE categoryid = 4 AND pieceid = 1 AND categoryName ='FairyTales' AND piecesAvailable = 5 AND piecesRequired = 4 ) AS subq " @@ -533,7 +533,7 @@ SELECT JSON_OBJECT('categoryid', categoryid, 'pieceid', pieceid, 'categoryName', FROM ( SELECT categoryid, pieceid, categoryName,piecesAvailable,piecesRequired FROM " + _Composite_NonAutoGenPK + @" - WHERE categoryid = 7 AND pieceid = 1 AND categoryName ='Drama' AND piecesAvailable = 0 + WHERE categoryid = 7 AND pieceid = 1 AND categoryName ='SciFi' AND piecesAvailable = 0 AND piecesRequired = 0 ) AS subq " @@ -581,7 +581,7 @@ SELECT JSON_OBJECT('categoryid', categoryid, 'pieceid', pieceid, 'categoryName', FROM ( SELECT categoryid, pieceid, categoryName,piecesAvailable,piecesRequired FROM " + _Composite_NonAutoGenPK + @" - WHERE categoryid = 1 AND pieceid = 1 AND categoryName ='books' AND piecesAvailable = 10 + WHERE categoryid = 1 AND pieceid = 1 AND categoryName ='SciFi' AND piecesAvailable = 10 AND piecesRequired = 0 ) AS subq " diff --git a/DataGateway.Service.Tests/SqlTests/PostgreSqlRestApiTests.cs b/DataGateway.Service.Tests/SqlTests/PostgreSqlRestApiTests.cs index d775523c81..5d96147a26 100644 --- a/DataGateway.Service.Tests/SqlTests/PostgreSqlRestApiTests.cs +++ b/DataGateway.Service.Tests/SqlTests/PostgreSqlRestApiTests.cs @@ -336,7 +336,7 @@ SELECT to_jsonb(subq) AS data FROM ( SELECT categoryid, pieceid, ""categoryName"", ""piecesAvailable"", ""piecesRequired"" FROM " + _Composite_NonAutoGenPK + @" - WHERE categoryid = 5 AND pieceid = 2 AND ""categoryName"" = 'Thriller' + WHERE categoryid = 5 AND pieceid = 2 AND ""categoryName"" = 'FairyTales' AND ""piecesAvailable"" = 0 AND ""piecesRequired"" = 0 ) AS subq " @@ -417,7 +417,7 @@ SELECT to_jsonb(subq) AS data FROM ( SELECT categoryid, pieceid, ""categoryName"", ""piecesAvailable"", ""piecesRequired"" FROM " + _Composite_NonAutoGenPK + @" - WHERE categoryid = 2 AND pieceid = 1 AND ""categoryName"" = 'History' + WHERE categoryid = 2 AND pieceid = 1 AND ""categoryName"" = 'SciFi' AND ""piecesAvailable"" = 10 AND ""piecesRequired"" = 5 ) AS subq " @@ -457,7 +457,7 @@ SELECT to_jsonb(subq) AS data @" SELECT to_jsonb(subq) AS data FROM ( - SELECT id, title, volume + SELECT id, title, volume, ""categoryName"" FROM " + _integration_AutoGenNonPK_TableName + @" WHERE id = " + STARTING_ID_FOR_TEST_INSERTS + @" AND title = 'Star Trek' AND volume IS NOT NULL @@ -471,7 +471,7 @@ SELECT to_jsonb(subq) AS data FROM ( SELECT categoryid, pieceid, ""categoryName"", ""piecesAvailable"", ""piecesRequired"" FROM " + _Composite_NonAutoGenPK + @" - WHERE categoryid = 3 AND pieceid = 1 AND ""categoryName"" = 'comics' + WHERE categoryid = 3 AND pieceid = 1 AND ""categoryName"" = 'SciFi' AND ""piecesAvailable"" = 2 AND ""piecesRequired"" = 1 ) AS subq " @@ -506,7 +506,7 @@ SELECT to_jsonb(subq) AS data FROM ( SELECT categoryid, pieceid, ""categoryName"", ""piecesAvailable"", ""piecesRequired"" FROM " + _Composite_NonAutoGenPK + @" - WHERE categoryid = 4 AND pieceid = 1 AND ""categoryName"" = 'Suspense' + WHERE categoryid = 4 AND pieceid = 1 AND ""categoryName"" = 'FairyTales' AND ""piecesAvailable"" = 5 AND ""piecesRequired"" = 4 ) AS subq " @@ -518,7 +518,7 @@ SELECT to_jsonb(subq) AS data FROM ( SELECT categoryid, pieceid, ""categoryName"", ""piecesAvailable"", ""piecesRequired"" FROM " + _Composite_NonAutoGenPK + @" - WHERE categoryid = 7 AND pieceid = 1 AND ""categoryName"" = 'Drama' + WHERE categoryid = 7 AND pieceid = 1 AND ""categoryName"" = 'SciFi' AND ""piecesAvailable"" = 0 AND ""piecesRequired"" = 0 ) AS subq " @@ -563,7 +563,7 @@ SELECT to_jsonb(subq) AS data FROM ( SELECT categoryid, pieceid, ""categoryName"", ""piecesAvailable"", ""piecesRequired"" FROM " + _Composite_NonAutoGenPK + @" - WHERE categoryid = 1 AND pieceid = 1 AND ""categoryName"" = 'books' + WHERE categoryid = 1 AND pieceid = 1 AND ""categoryName"" = 'SciFi' AND ""piecesAvailable"" = 10 AND ""piecesRequired"" = 0 ) AS subq " diff --git a/DataGateway.Service.Tests/SqlTests/RestApiTestBase.cs b/DataGateway.Service.Tests/SqlTests/RestApiTestBase.cs index e642b04a50..6246e65a7a 100644 --- a/DataGateway.Service.Tests/SqlTests/RestApiTestBase.cs +++ b/DataGateway.Service.Tests/SqlTests/RestApiTestBase.cs @@ -391,7 +391,7 @@ await SetupAndRunRestApiTest( { ""categoryid"": ""5"", ""pieceid"": ""2"", - ""categoryName"":""Thriller"" + ""categoryName"":""FairyTales"" }"; expectedLocationHeader = $"categoryid/5/pieceid/2"; @@ -521,7 +521,7 @@ await SetupAndRunRestApiTest( requestBody = @" { - ""categoryName"":""History"", + ""categoryName"":""SciFi"", ""piecesAvailable"":""10"", ""piecesRequired"":""5"" }"; @@ -633,7 +633,8 @@ await SetupAndRunRestApiTest( // that is autogenerated. requestBody = @" { - ""title"": ""Star Trek"" + ""title"": ""Star Trek"", + ""categoryName"" : ""Suspense"" }"; expectedLocationHeader = $"id/{STARTING_ID_FOR_TEST_INSERTS}"; @@ -651,7 +652,7 @@ await SetupAndRunRestApiTest( requestBody = @" { - ""categoryName"":""comics"", + ""categoryName"":""SciFi"", ""piecesAvailable"":""2"", ""piecesRequired"":""1"" }"; @@ -719,7 +720,7 @@ await SetupAndRunRestApiTest( requestBody = @" { - ""categoryName"": ""Suspense"", + ""categoryName"": ""FairyTales"", ""piecesAvailable"":""5"", ""piecesRequired"":""4"" }"; @@ -739,7 +740,7 @@ await SetupAndRunRestApiTest( requestBody = @" { - ""categoryName"": ""Drama"" + ""categoryName"": ""SciFi"" }"; expectedLocationHeader = $"categoryid/7/pieceid/1"; diff --git a/DataGateway.Service.Tests/SqlTests/SqlMetadataProviderTests.cs b/DataGateway.Service.Tests/SqlTests/SqlMetadataProviderTests.cs index b8ad107b93..f5692fab60 100644 --- a/DataGateway.Service.Tests/SqlTests/SqlMetadataProviderTests.cs +++ b/DataGateway.Service.Tests/SqlTests/SqlMetadataProviderTests.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.IO; using Azure.DataGateway.Service.Models; using Azure.DataGateway.Service.Services; @@ -31,6 +32,13 @@ SqlGraphQLFileMetadataProvider expectedMetadataProvider actualTableDefinition.PrimaryKey, $"Did not find the expected primary keys for table {tableName}"); + CollectionAssert.AreEqual( + new SortedDictionary + (expectedTableDefinition.ForeignKeys), + new SortedDictionary + (actualTableDefinition.ForeignKeys), + $"Did not find the expected foreign keys for table {tableName}"); + foreach ((string columnName, ColumnDefinition expectedColumnDefinition) in expectedTableDefinition.Columns) { ColumnDefinition actualColumnDefinition; diff --git a/DataGateway.Service.Tests/SqlTests/SqlTestBase.cs b/DataGateway.Service.Tests/SqlTests/SqlTestBase.cs index 721378f77a..ccfd48b5cb 100644 --- a/DataGateway.Service.Tests/SqlTests/SqlTestBase.cs +++ b/DataGateway.Service.Tests/SqlTests/SqlTestBase.cs @@ -60,30 +60,30 @@ protected static async Task InitializeTestFixture(TestContext context, string te { case TestCategory.POSTGRESQL: _queryBuilder = new PostgresQueryBuilder(); - _metadataStoreProvider = new SqlGraphQLFileMetadataProvider( - config, - new PostgreSqlMetadataProvider(config)); _defaultSchemaName = "public"; _dbExceptionParser = new PostgresDbExceptionParser(); _queryExecutor = new QueryExecutor(config, _dbExceptionParser); + _metadataStoreProvider = new SqlGraphQLFileMetadataProvider( + config, + new PostgreSqlMetadataProvider(config, _queryExecutor, _queryBuilder)); break; case TestCategory.MSSQL: _queryBuilder = new MsSqlQueryBuilder(); - _metadataStoreProvider = new SqlGraphQLFileMetadataProvider( - config, - new MsSqlMetadataProvider(config)); _defaultSchemaName = "dbo"; _dbExceptionParser = new DbExceptionParserBase(); _queryExecutor = new QueryExecutor(config, _dbExceptionParser); + _metadataStoreProvider = new SqlGraphQLFileMetadataProvider( + config, + new MsSqlMetadataProvider(config, _queryExecutor, _queryBuilder)); break; case TestCategory.MYSQL: _queryBuilder = new MySqlQueryBuilder(); - _metadataStoreProvider = new SqlGraphQLFileMetadataProvider( - config, - new MySqlMetadataProvider(config)); _defaultSchemaName = "mysql"; _dbExceptionParser = new MySqlDbExceptionParser(); _queryExecutor = new QueryExecutor(config, _dbExceptionParser); + _metadataStoreProvider = new SqlGraphQLFileMetadataProvider( + config, + new MySqlMetadataProvider(config, _queryExecutor, _queryBuilder)); break; } diff --git a/DataGateway.Service.Tests/sql-config-test.json b/DataGateway.Service.Tests/sql-config-test.json index 0483b5625b..d4057be540 100644 --- a/DataGateway.Service.Tests/sql-config-test.json +++ b/DataGateway.Service.Tests/sql-config-test.json @@ -153,7 +153,8 @@ "ForeignKeys": { "book_publisher_fk": { "ReferencedTable": "publishers", - "Columns": [ "publisher_id" ] + "ReferencedColumns": [ "id" ], + "ReferencingColumns": [ "publisher_id" ] } }, "HttpVerbs": { @@ -174,6 +175,16 @@ } } }, + "book_website_placements": { + "PrimaryKey": [ "id" ], + "ForeignKeys": { + "book_website_placement_book_fk": { + "ReferencedTable": "books", + "ReferencedColumns": [ "id" ], + "ReferencingColumns": [ "book_id" ] + } + } + }, "authors": { "PrimaryKey": [ "id" ], "Columns": { @@ -212,7 +223,8 @@ "ForeignKeys": { "review_book_fk": { "ReferencedTable": "books", - "Columns": [ "book_id" ] + "ReferencedColumns": [ "id" ], + "ReferencingColumns": [ "book_id" ] } }, "HttpVerbs": { @@ -234,11 +246,13 @@ "ForeignKeys": { "book_author_link_book_fk": { "ReferencedTable": "books", - "Columns": [ "book_id" ] + "ReferencedColumns": [ "id" ], + "ReferencingColumns": [ "book_id" ] }, "book_author_link_author_fk": { "ReferencedTable": "authors", - "Columns": [ "author_id" ] + "ReferencedColumns": [ "id" ], + "ReferencingColumns": [ "author_id" ] } } }, @@ -259,8 +273,6 @@ "IsNullable": true } }, - "ForeignKeys": { - }, "HttpVerbs": { "GET": { "AuthorizationType": "Anonymous" @@ -295,6 +307,10 @@ "Type": "bigint", "IsAutoGenerated": true, "IsNullable": false + }, + "categoryName": { + "Type": "text", + "IsNullable": false } }, "HttpVerbs": { @@ -311,6 +327,73 @@ "Authorization": "Authenticated" } } + }, + "stocks": { + "PrimaryKey": [ "categoryid", "pieceid" ], + "Columns": { + "categoryid": { + "Type": "bigint", + "IsAutoGenerated": false, + "IsNullable": false + }, + "pieceid": { + "Type": "bigint", + "IsAutoGenerated": false, + "IsNullable": false + }, + "categoryName": { + "Type": "text", + "IsNullable": false + }, + "piecesAvailable": { + "Type": "bigint", + "IsAutoGenerated": false, + "IsNullable": true, + "HasDefault": true + }, + "piecesRequired": { + "Type": "bigint", + "IsAutoGenerated": false, + "IsNullable": false, + "HasDefault": true + } + }, + "ForeignKeys": { + "stocks_comics_fk": { + "ReferencedTable": "comics", + "ReferencedColumns": [ "categoryName" ], + "ReferencingColumns": [ "categoryName" ] + } + } + }, + "stocks_price": { + "PrimaryKey": [ "categoryid", "pieceid", "instant" ], + "Columns": { + "categoryid": { + "Type": "bigint", + "IsAutoGenerated": false, + "IsNullable": false + }, + "pieceid": { + "Type": "bigint", + "IsAutoGenerated": false, + "IsNullable": false + }, + "instant": { + "Type": "timestamp" + }, + "price": { + "Type": "float", + "IsNullable": true + } + }, + "ForeignKeys": { + "stocks_price_stocks_fk": { + "ReferencedTable": "stocks", + "ReferencedColumns": [ "categoryid", "pieceid" ], + "ReferencingColumns": [ "categoryid", "pieceid" ] + } + } } } } diff --git a/DataGateway.Service/Configurations/SqlConfigValidatorExceptions.cs b/DataGateway.Service/Configurations/SqlConfigValidatorExceptions.cs index 492b47094c..9821f65c15 100644 --- a/DataGateway.Service/Configurations/SqlConfigValidatorExceptions.cs +++ b/DataGateway.Service/Configurations/SqlConfigValidatorExceptions.cs @@ -313,7 +313,7 @@ private void ValidateRefColumnsExistInRefTable(List referencedColumns, s /// private void ValidateFKColumnsHaveMatchingTableColumns(ForeignKeyDefinition foreignKey, TableDefinition table) { - IEnumerable unmatchedFkCols = foreignKey.Columns.Except(table.Columns.Keys); + IEnumerable unmatchedFkCols = foreignKey.ReferencingColumns.Except(table.Columns.Keys); if (unmatchedFkCols.Any()) { diff --git a/DataGateway.Service/Configurations/SqlConfigValidatorMain.cs b/DataGateway.Service/Configurations/SqlConfigValidatorMain.cs index 32f2fcb596..ec76d06c7b 100644 --- a/DataGateway.Service/Configurations/SqlConfigValidatorMain.cs +++ b/DataGateway.Service/Configurations/SqlConfigValidatorMain.cs @@ -182,8 +182,8 @@ private void ValidateForeignKeyColumns(ForeignKeyDefinition foreignKey, TableDef if (HasExplicitColumns(foreignKey)) { - ValidateNoDuplicateFkColumns(foreignKey.Columns, refColumns: false); - columns = foreignKey.Columns; + ValidateNoDuplicateFkColumns(foreignKey.ReferencingColumns, refColumns: false); + columns = foreignKey.ReferencingColumns; ValidateFKColumnsHaveMatchingTableColumns(foreignKey, table); } else diff --git a/DataGateway.Service/Configurations/SqlConfigValidatorUtil.cs b/DataGateway.Service/Configurations/SqlConfigValidatorUtil.cs index b98a38685e..67b733e9f2 100644 --- a/DataGateway.Service/Configurations/SqlConfigValidatorUtil.cs +++ b/DataGateway.Service/Configurations/SqlConfigValidatorUtil.cs @@ -227,7 +227,7 @@ private static bool TableHasForeignKey(TableDefinition table) /// private static bool HasExplicitColumns(ForeignKeyDefinition fk) { - return fk.Columns.Count > 0; + return fk.ReferencingColumns.Count > 0; } /// @@ -502,7 +502,7 @@ private static IEnumerable GetPkAndFkColumns(TableDefinition table) foreach (KeyValuePair nameFKPair in table.ForeignKeys) { ForeignKeyDefinition foreignKey = nameFKPair.Value; - columns.AddRange(foreignKey.Columns); + columns.AddRange(foreignKey.ReferencingColumns); } return columns; diff --git a/DataGateway.Service/Models/DatabaseSchema.cs b/DataGateway.Service/Models/DatabaseSchema.cs index b46bc28620..1bd16854aa 100644 --- a/DataGateway.Service/Models/DatabaseSchema.cs +++ b/DataGateway.Service/Models/DatabaseSchema.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Azure.DataGateway.Service.Authorization; namespace Azure.DataGateway.Service.Models @@ -50,10 +51,29 @@ public class ForeignKeyDefinition /// /// The list of columns of the table that make up the foreign key. - /// If this list is empty, the primary key columns of the + /// If this list is empty, the primary key columns of the referencing /// table are implicitly assumed to be the foreign key columns. /// - public List Columns { get; set; } = new(); + public List ReferencingColumns { get; set; } = new(); + + public override bool Equals(object? other) + { + return Equals(other as ForeignKeyDefinition); + } + + public bool Equals(ForeignKeyDefinition? other) + { + return other != null && + ReferencedTable.Equals(other.ReferencedTable) && + Enumerable.SequenceEqual(ReferencedColumns, other.ReferencedColumns) && + Enumerable.SequenceEqual(ReferencingColumns, other.ReferencingColumns); + } + + public override int GetHashCode() + { + return HashCode.Combine( + ReferencedTable, ReferencedColumns, ReferencingColumns); + } } public class AuthorizationRule diff --git a/DataGateway.Service/MsSqlBooks.sql b/DataGateway.Service/MsSqlBooks.sql index 1eafcccba6..ab3571deff 100644 --- a/DataGateway.Service/MsSqlBooks.sql +++ b/DataGateway.Service/MsSqlBooks.sql @@ -6,8 +6,9 @@ DROP TABLE IF EXISTS website_users; DROP TABLE IF EXISTS books; DROP TABLE IF EXISTS publishers; DROP TABLE IF EXISTS magazines; -DROP TABLE IF EXISTS comics; +DROP TABLE IF EXISTS stocks_price; DROP TABLE IF EXISTS stocks; +DROP TABLE IF EXISTS comics; --Autogenerated id seed are set at 5001 for consistency with Postgres --This allows for tests using the same id values for both languages @@ -63,18 +64,27 @@ CREATE TABLE magazines( CREATE TABLE comics( id bigint PRIMARY KEY, title varchar(max) NOT NULL, - volume bigint IDENTITY(5001,1) + volume bigint IDENTITY(5001,1), + categoryName varchar(100) NOT NULL UNIQUE ); CREATE TABLE stocks( categoryid bigint NOT NULL, pieceid bigint NOT NULL, - categoryName varchar(max) NOT NULL, + categoryName varchar(100) NOT NULL, piecesAvailable bigint DEFAULT 0, piecesRequired bigint DEFAULT 0 NOT NULL, PRIMARY KEY(categoryid,pieceid) ); +CREATE TABLE stocks_price( + categoryid bigint NOT NULL, + pieceid bigint NOT NULL, + instant char(10) NOT NULL, + price float, + PRIMARY KEY(categoryid, pieceid, instant) +); + ALTER TABLE books ADD CONSTRAINT book_publisher_fk FOREIGN KEY (publisher_id) @@ -105,6 +115,18 @@ FOREIGN KEY (author_id) REFERENCES authors (id) ON DELETE CASCADE; +ALTER TABLE stocks +ADD CONSTRAINT stocks_comics_fk +FOREIGN KEY (categoryName) +REFERENCES comics (categoryName) +ON DELETE CASCADE; + +ALTER TABLE stocks_price +ADD CONSTRAINT stocks_price_stocks_fk +FOREIGN KEY (categoryid, pieceid) +REFERENCES stocks (categoryid, pieceid) +ON DELETE CASCADE; + SET IDENTITY_INSERT publishers ON INSERT INTO publishers(id, name) VALUES (1234, 'Big Company'), (2345, 'Small Town Publisher'), (2323, 'TBD Publishing One'), (2324, 'TBD Publishing Two Ltd'); SET IDENTITY_INSERT publishers OFF @@ -128,5 +150,6 @@ INSERT INTO reviews(id, book_id, content) VALUES (567, 1, 'Indeed a great book') SET IDENTITY_INSERT reviews OFF INSERT INTO website_users(id, username) VALUES (1, 'George'), (2, NULL), (3, ''), (4, 'book_lover_95'), (5, 'null'); -INSERT INTO stocks(categoryid, pieceid,categoryName) VALUES (1, 1, 'books'), (2, 1, 'magazines'); INSERT INTO magazines(id, title, issue_number) VALUES (1, 'Vogue', 1234), (11, 'Sports Illustrated', NULL), (3, 'Fitness', NULL); +INSERT INTO comics(id, title, categoryName) VALUES (1, 'Star Trek', 'SciFi'), (2, 'Cinderella', 'FairyTales'); +INSERT INTO stocks(categoryid, pieceid, categoryName) VALUES (1, 1, 'SciFi'), (2, 1, 'FairyTales'); diff --git a/DataGateway.Service/MySqlBooks.sql b/DataGateway.Service/MySqlBooks.sql index fd79c6b6d6..5b518296bf 100644 --- a/DataGateway.Service/MySqlBooks.sql +++ b/DataGateway.Service/MySqlBooks.sql @@ -6,8 +6,9 @@ DROP TABLE IF EXISTS website_users; DROP TABLE IF EXISTS books; DROP TABLE IF EXISTS publishers; DROP TABLE IF EXISTS magazines; -DROP TABLE IF EXISTS comics; +DROP TABLE IF EXISTS stocks_price; DROP TABLE IF EXISTS stocks; +DROP TABLE IF EXISTS comics; CREATE TABLE publishers( id bigint AUTO_INCREMENT PRIMARY KEY, @@ -60,16 +61,25 @@ CREATE TABLE magazines( CREATE TABLE comics( id bigint PRIMARY KEY, title text NOT NULL, - volume bigint AUTO_INCREMENT UNIQUE KEY + volume bigint AUTO_INCREMENT UNIQUE KEY, + categoryName varchar(100) NOT NULL UNIQUE ); CREATE TABLE stocks( categoryid bigint NOT NULL, pieceid bigint NOT NULL, - categoryName text NOT NULL, + categoryName varchar(100) NOT NULL, piecesAvailable bigint DEFAULT (0), piecesRequired bigint DEFAULT (0) NOT NULL, - PRIMARY KEY(categoryid,pieceid) + PRIMARY KEY(categoryid, pieceid) +); + +CREATE TABLE stocks_price( + categoryid bigint NOT NULL, + pieceid bigint NOT NULL, + instant char(10) NOT NULL, + price float, + PRIMARY KEY(categoryid, pieceid, instant) ); ALTER TABLE books @@ -102,6 +112,18 @@ FOREIGN KEY (author_id) REFERENCES authors (id) ON DELETE CASCADE; +ALTER TABLE stocks +ADD CONSTRAINT stocks_comics_fk +FOREIGN KEY (categoryName) +REFERENCES comics (categoryName) +ON DELETE CASCADE; + +ALTER TABLE stocks_price +ADD CONSTRAINT stocks_price_stocks_fk +FOREIGN KEY (categoryid, pieceid) +REFERENCES stocks (categoryid, pieceid) +ON DELETE CASCADE; + INSERT INTO publishers(id, name) VALUES (1234, 'Big Company'), (2345, 'Small Town Publisher'), (2323, 'TBD Publishing One'), (2324, 'TBD Publishing Two Ltd'); INSERT INTO authors(id, name, birthdate) VALUES (123, 'Jelte', '2001-01-01'), (124, 'Aniruddh', '2002-02-02'); INSERT INTO books(id, title, publisher_id) VALUES (1, 'Awesome book', 1234), (2, 'Also Awesome book', 1234), (3, 'Great wall of china explained', 2345), (4, 'US history in a nutshell', 2345), (5, 'Chernobyl Diaries', 2323), (6, 'The Palace Door', 2324), (7, 'The Groovy Bar', 2324), (8, 'Time to Eat', 2324); @@ -109,8 +131,9 @@ INSERT INTO book_website_placements(book_id, price) VALUES (1, 100), (2, 50), (3 INSERT INTO website_users(id, username) VALUES (1, 'George'), (2, NULL), (3, ''), (4, 'book_lover_95'), (5, 'null'); INSERT INTO book_author_link(book_id, author_id) VALUES (1, 123), (2, 124), (3, 123), (3, 124), (4, 123), (4, 124); INSERT INTO reviews(id, book_id, content) VALUES (567, 1, 'Indeed a great book'), (568, 1, 'I loved it'), (569, 1, 'best book I read in years'); -INSERT INTO stocks(categoryid, pieceid,categoryName) VALUES (1, 1, 'books'), (2, 1, 'magazines'); INSERT INTO magazines(id, title, issue_number) VALUES (1, 'Vogue', 1234), (11, 'Sports Illustrated', NULL), (3, 'Fitness', NULL); +INSERT INTO comics(id, title, categoryName) VALUES (1, 'Star Trek', 'SciFi'), (2, 'Cinderella', 'FairyTales'); +INSERT INTO stocks(categoryid, pieceid, categoryName) VALUES (1, 1, 'SciFi'), (2, 1, 'FairyTales'); -- Starting with id > 5000 is chosen arbitrarily so that the incremented id-s won't conflict with the manually inserted ids in this script -- AUTO_INCREMENT is set to 5001 so the next autogenerated id will be 5001 @@ -120,4 +143,3 @@ ALTER TABLE publishers AUTO_INCREMENT = 5001; ALTER TABLE authors AUTO_INCREMENT = 5001; ALTER TABLE reviews AUTO_INCREMENT = 5001; ALTER TABLE comics AUTO_INCREMENT = 5001 - diff --git a/DataGateway.Service/PostgreSqlBooks.sql b/DataGateway.Service/PostgreSqlBooks.sql index fe54731d5b..cdfd47081c 100644 --- a/DataGateway.Service/PostgreSqlBooks.sql +++ b/DataGateway.Service/PostgreSqlBooks.sql @@ -6,8 +6,9 @@ DROP TABLE IF EXISTS website_users; DROP TABLE IF EXISTS books; DROP TABLE IF EXISTS publishers; DROP TABLE IF EXISTS magazines; -DROP TABLE IF EXISTS comics; +DROP TABLE IF EXISTS stocks_price; DROP TABLE IF EXISTS stocks; +DROP TABLE IF EXISTS comics; CREATE TABLE publishers( id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, @@ -59,18 +60,27 @@ CREATE TABLE magazines( CREATE TABLE comics( id bigint PRIMARY KEY, title text NOT NULL, - volume bigint GENERATED BY DEFAULT AS IDENTITY + volume bigint GENERATED BY DEFAULT AS IDENTITY, + "categoryName" varchar(100) NOT NULL UNIQUE ); CREATE TABLE stocks( categoryid bigint NOT NULL, pieceid bigint NOT NULL, - "categoryName" text NOT NULL, + "categoryName" varchar(100) NOT NULL, "piecesAvailable" bigint DEFAULT 0, "piecesRequired" bigint DEFAULT 0 NOT NULL, PRIMARY KEY(categoryid, pieceid) ); +CREATE TABLE stocks_price( + categoryid bigint NOT NULL, + pieceid bigint NOT NULL, + instant char(10) NOT NULL, + price float, + PRIMARY KEY(categoryid, pieceid, instant) +); + ALTER TABLE books ADD CONSTRAINT book_publisher_fk FOREIGN KEY (publisher_id) @@ -101,6 +111,18 @@ FOREIGN KEY (author_id) REFERENCES authors (id) ON DELETE CASCADE; +ALTER TABLE stocks +ADD CONSTRAINT stocks_comics_fk +FOREIGN KEY ("categoryName") +REFERENCES comics ("categoryName") +ON DELETE CASCADE; + +ALTER TABLE stocks_price +ADD CONSTRAINT stocks_price_stocks_fk +FOREIGN KEY (categoryid, pieceid) +REFERENCES stocks (categoryid, pieceid) +ON DELETE CASCADE; + INSERT INTO publishers(id, name) VALUES (1234, 'Big Company'), (2345, 'Small Town Publisher'), (2323, 'TBD Publishing One'), (2324, 'TBD Publishing Two Ltd'); INSERT INTO authors(id, name, birthdate) VALUES (123, 'Jelte', '2001-01-01'), (124, 'Aniruddh', '2002-02-02'); INSERT INTO books(id, title, publisher_id) VALUES (1, 'Awesome book', 1234), (2, 'Also Awesome book', 1234), (3, 'Great wall of china explained', 2345), (4, 'US history in a nutshell', 2345), (5, 'Chernobyl Diaries', 2323), (6, 'The Palace Door', 2324), (7, 'The Groovy Bar', 2324), (8, 'Time to Eat', 2324); @@ -108,9 +130,9 @@ INSERT INTO book_website_placements(book_id, price) VALUES (1, 100), (2, 50), (3 INSERT INTO website_users(id, username) VALUES (1, 'George'), (2, NULL), (3, ''), (4, 'book_lover_95'), (5, 'null'); INSERT INTO book_author_link(book_id, author_id) VALUES (1, 123), (2, 124), (3, 123), (3, 124), (4, 123), (4, 124); INSERT INTO reviews(id, book_id, content) VALUES (567, 1, 'Indeed a great book'), (568, 1, 'I loved it'), (569, 1, 'best book I read in years'); -INSERT INTO stocks(categoryid, pieceid, "categoryName") VALUES (1, 1, 'books'), (2, 1, 'magazines'); INSERT INTO magazines(id, title, issue_number) VALUES (1, 'Vogue', 1234), (11, 'Sports Illustrated', NULL), (3, 'Fitness', NULL); - +INSERT INTO comics(id, title, "categoryName") VALUES (1, 'Star Trek', 'SciFi'), (2, 'Cinderella', 'FairyTales'); +INSERT INTO stocks(categoryid, pieceid, "categoryName") VALUES (1, 1, 'SciFi'), (2, 1, 'FairyTales'); --Starting with id > 5000 is chosen arbitrarily so that the incremented id-s won't conflict with the manually inserted ids in this script --Sequence counter is set to 5000 so the next autogenerated id will be 5001 diff --git a/DataGateway.Service/Resolvers/BaseSqlQueryBuilder.cs b/DataGateway.Service/Resolvers/BaseSqlQueryBuilder.cs index 3f82d9c12e..af17195218 100644 --- a/DataGateway.Service/Resolvers/BaseSqlQueryBuilder.cs +++ b/DataGateway.Service/Resolvers/BaseSqlQueryBuilder.cs @@ -12,6 +12,9 @@ namespace Azure.DataGateway.Service.Resolvers /// public abstract class BaseSqlQueryBuilder { + public const string SCHEMA_NAME_PARAM = "schemaName"; + public const string TABLE_NAME_PARAM = "tableName"; + /// /// Adds database specific quotes to string identifier /// @@ -263,5 +266,63 @@ public string JoinPredicateStrings(params string?[] predicateStrings) return string.Join(" AND ", validPredicates); } + + /// + public virtual string BuildForeignKeyInfoQuery(int numberOfParameters) + { + string[] schemaNameParams = + CreateParams(kindOfParam: SCHEMA_NAME_PARAM, numberOfParameters); + + string[] tableNameParams = + CreateParams(kindOfParam: TABLE_NAME_PARAM, numberOfParameters); + string tableSchemaParamsForInClause = string.Join(", @", schemaNameParams); + string tableNameParamsForInClause = string.Join(", @", tableNameParams); + + // The view REFERENTIAL_CONSTRAINTS has a row for each referential key CONSTRAINT_NAME and + // its corresponding UNIQUE_CONSTRAINT_NAME to which it references. + // These are only constraint names so we need to join with the view KEY_COLUMN_USAGE to get the + // constraint columns - one inner join for the columns from the 'Referencing table' + // and the other join for the columns from the 'Referenced Table'. + string foreignKeyQuery = $@" + SELECT + ReferentialConstraints.CONSTRAINT_NAME {QuoteIdentifier(nameof(ForeignKeyDefinition))}, + ReferencingColumnUsage.TABLE_NAME {QuoteIdentifier(nameof(TableDefinition))}, + ReferencingColumnUsage.COLUMN_NAME {QuoteIdentifier(nameof(ForeignKeyDefinition.ReferencingColumns))}, + ReferencedColumnUsage.TABLE_NAME {QuoteIdentifier(nameof(ForeignKeyDefinition.ReferencedTable))}, + ReferencedColumnUsage.COLUMN_NAME {QuoteIdentifier(nameof(ForeignKeyDefinition.ReferencedColumns))} + FROM + INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS ReferentialConstraints + INNER JOIN + INFORMATION_SCHEMA.KEY_COLUMN_USAGE ReferencingColumnUsage + ON ReferentialConstraints.CONSTRAINT_CATALOG = ReferencingColumnUsage.CONSTRAINT_CATALOG + AND ReferentialConstraints.CONSTRAINT_SCHEMA = ReferencingColumnUsage.CONSTRAINT_SCHEMA + AND ReferentialConstraints.CONSTRAINT_NAME = ReferencingColumnUsage.CONSTRAINT_NAME + INNER JOIN + INFORMATION_SCHEMA.KEY_COLUMN_USAGE ReferencedColumnUsage + ON ReferentialConstraints.UNIQUE_CONSTRAINT_CATALOG = ReferencedColumnUsage.CONSTRAINT_CATALOG + AND ReferentialConstraints.UNIQUE_CONSTRAINT_SCHEMA = ReferencedColumnUsage.CONSTRAINT_SCHEMA + AND ReferentialConstraints.UNIQUE_CONSTRAINT_NAME = ReferencedColumnUsage.CONSTRAINT_NAME + AND ReferencingColumnUsage.ORDINAL_POSITION = ReferencedColumnUsage.ORDINAL_POSITION + WHERE + ReferencingColumnUsage.TABLE_SCHEMA IN (@{tableSchemaParamsForInClause}) + AND ReferencingColumnUsage.TABLE_NAME IN (@{tableNameParamsForInClause})"; + + Console.WriteLine($"Foreign Key Query: {foreignKeyQuery}"); + return foreignKeyQuery; + } + + /// + /// Creates a list of named parameters with incremental suffixes + /// starting from 0 to numberOfParameters - 1. + /// e.g. tableName0, tableName1 + /// + /// The kind of parameter being created acting + /// as the prefix common to all parameters. + /// The number of parameters to create. + /// The created list + public static string[] CreateParams(string kindOfParam, int numberOfParameters) + { + return Enumerable.Range(0, numberOfParameters).Select(i => kindOfParam + i).ToArray(); + } } } diff --git a/DataGateway.Service/Resolvers/IQueryBuilder.cs b/DataGateway.Service/Resolvers/IQueryBuilder.cs index c18c2ee831..8a1c8039af 100644 --- a/DataGateway.Service/Resolvers/IQueryBuilder.cs +++ b/DataGateway.Service/Resolvers/IQueryBuilder.cs @@ -35,5 +35,11 @@ public interface IQueryBuilder /// query. /// public string Build(SqlUpsertQueryStructure structure); + + /// + /// Builds the query to obtain foreign key information with the given + /// number of parameters. + /// + public string BuildForeignKeyInfoQuery(int numberOfParameters); } } diff --git a/DataGateway.Service/Resolvers/MySqlQueryBuilder.cs b/DataGateway.Service/Resolvers/MySqlQueryBuilder.cs index 76b8df8e3a..387a5eceff 100644 --- a/DataGateway.Service/Resolvers/MySqlQueryBuilder.cs +++ b/DataGateway.Service/Resolvers/MySqlQueryBuilder.cs @@ -14,8 +14,11 @@ namespace Azure.DataGateway.Service.Resolvers public class MySqlQueryBuilder : BaseSqlQueryBuilder, IQueryBuilder { private static DbCommandBuilder _builder = new MySqlCommandBuilder(); + public const string DATABASE_NAME_PARAM = "databaseName"; - /// + /// + /// Adds database specific quotes to string identifier + /// protected override string QuoteIdentifier(string ident) { return _builder.QuoteIdentifier(ident); @@ -118,6 +121,35 @@ public string Build(SqlUpsertQueryStructure structure) } } + /// + public override string BuildForeignKeyInfoQuery(int numberOfParameters) + { + string[] databaseNameParams = CreateParams(DATABASE_NAME_PARAM, numberOfParameters); + string[] tableNameParams = CreateParams(TABLE_NAME_PARAM, numberOfParameters); + string tableSchemaParamsForInClause = string.Join(", @", databaseNameParams); + string tableNameParamsForInClause = string.Join(", @", tableNameParams); + + // For MySQL, the view KEY_COLUMN_USAGE provides all the information we need + // so there is no need to join with any other view. + string foreignKeyQuery = $@" + SELECT + CONSTRAINT_NAME {QuoteIdentifier(nameof(ForeignKeyDefinition))}, + TABLE_NAME {QuoteIdentifier(nameof(TableDefinition))}, + COLUMN_NAME {QuoteIdentifier(nameof(ForeignKeyDefinition.ReferencingColumns))}, + REFERENCED_TABLE_NAME {QuoteIdentifier(nameof(ForeignKeyDefinition.ReferencedTable))}, + REFERENCED_COLUMN_NAME {QuoteIdentifier(nameof(ForeignKeyDefinition.ReferencedColumns))} + FROM + INFORMATION_SCHEMA.KEY_COLUMN_USAGE + WHERE + TABLE_SCHEMA IN (@{tableSchemaParamsForInClause}) + AND TABLE_NAME IN (@{tableNameParamsForInClause}) + AND REFERENCED_TABLE_NAME IS NOT NULL + AND REFERENCED_COLUMN_NAME IS NOT NULL;"; + + Console.WriteLine($"Foreign Key Query is : {foreignKeyQuery}"); + return foreignKeyQuery; + } + /// /// Makes the query segments to store PK during an update /// diff --git a/DataGateway.Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs b/DataGateway.Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs index 28b3f872e9..02bb6593b5 100644 --- a/DataGateway.Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs +++ b/DataGateway.Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs @@ -628,7 +628,7 @@ void AddGraphQLFields(IReadOnlyList Selections) /// private static List GetFkColumns(ForeignKeyDefinition fk, TableDefinition table) { - return fk.Columns.Count > 0 ? fk.Columns : table.PrimaryKey; + return fk.ReferencingColumns.Count > 0 ? fk.ReferencingColumns : table.PrimaryKey; } /// diff --git a/DataGateway.Service/Services/EdmModelBuilder.cs b/DataGateway.Service/Services/EdmModelBuilder.cs index 9f757e74bb..bc8533e5d9 100644 --- a/DataGateway.Service/Services/EdmModelBuilder.cs +++ b/DataGateway.Service/Services/EdmModelBuilder.cs @@ -51,18 +51,28 @@ private EdmModelBuilder BuildEntityTypes(DatabaseSchema schema) // need to convert our column system type to an Edm type Type columnSystemType = schema.Tables[entityName].Columns[column].SystemType; EdmPrimitiveTypeKind type = EdmPrimitiveTypeKind.None; - if (ReferenceEquals(typeof(string), columnSystemType)) + if (columnSystemType.IsArray) { - type = EdmPrimitiveTypeKind.String; + columnSystemType = columnSystemType.GetElementType()!; } - else if (ReferenceEquals(typeof(long), columnSystemType)) - { - type = EdmPrimitiveTypeKind.Int64; - } - else + + switch (Type.GetTypeCode(columnSystemType)) { - throw new ArgumentException($"No resolver for column type" + - $" {columnSystemType.Name}"); + case TypeCode.String: + type = EdmPrimitiveTypeKind.String; + break; + case TypeCode.Int64: + type = EdmPrimitiveTypeKind.Int64; + break; + case TypeCode.Single: + type = EdmPrimitiveTypeKind.Single; + break; + case TypeCode.Double: + type = EdmPrimitiveTypeKind.Double; + break; + default: + throw new ArgumentException($"No resolver for column type" + + $" {columnSystemType.Name}"); } // if column is in our list of keys we add as a key to entity diff --git a/DataGateway.Service/Services/MetadataProviders/ISqlMetadataProvider.cs b/DataGateway.Service/Services/MetadataProviders/ISqlMetadataProvider.cs index 5a2496580e..13bcd6f685 100644 --- a/DataGateway.Service/Services/MetadataProviders/ISqlMetadataProvider.cs +++ b/DataGateway.Service/Services/MetadataProviders/ISqlMetadataProvider.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Data; using System.Threading.Tasks; using Azure.DataGateway.Service.Models; @@ -28,5 +29,15 @@ public Task PopulateTableDefinitionAsync( string schemaName, string tableName, TableDefinition tableDefinition); + + /// + /// Fills the table definition with information of the foreign keys + /// for all the tables. + /// + /// Name of the default schema. + /// Dictionary of all tables. + public Task PopulateForeignKeyDefinitionAsync( + string schemaName, + Dictionary tables); } } diff --git a/DataGateway.Service/Services/MetadataProviders/MsSqlMetadataProvider.cs b/DataGateway.Service/Services/MetadataProviders/MsSqlMetadataProvider.cs index c0060c0148..8c3981f066 100644 --- a/DataGateway.Service/Services/MetadataProviders/MsSqlMetadataProvider.cs +++ b/DataGateway.Service/Services/MetadataProviders/MsSqlMetadataProvider.cs @@ -1,4 +1,5 @@ using Azure.DataGateway.Service.Configurations; +using Azure.DataGateway.Service.Resolvers; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Options; @@ -13,8 +14,11 @@ namespace Azure.DataGateway.Service.Services public class MsSqlMetadataProvider : SqlMetadataProvider { - public MsSqlMetadataProvider(IOptions dataGatewayConfig) - : base(dataGatewayConfig) + public MsSqlMetadataProvider( + IOptions dataGatewayConfig, + IQueryExecutor queryExecutor, + IQueryBuilder sqlQueryBuilder) + : base(dataGatewayConfig, queryExecutor, sqlQueryBuilder) { } diff --git a/DataGateway.Service/Services/MetadataProviders/MySqlMetadataProvider.cs b/DataGateway.Service/Services/MetadataProviders/MySqlMetadataProvider.cs index 7d3b365dc5..58f36a2c41 100644 --- a/DataGateway.Service/Services/MetadataProviders/MySqlMetadataProvider.cs +++ b/DataGateway.Service/Services/MetadataProviders/MySqlMetadataProvider.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading.Tasks; using Azure.DataGateway.Service.Configurations; +using Azure.DataGateway.Service.Resolvers; using Microsoft.Extensions.Options; using MySqlConnector; @@ -13,8 +14,11 @@ namespace Azure.DataGateway.Service.Services /// public class MySqlMetadataProvider : SqlMetadataProvider, ISqlMetadataProvider { - public MySqlMetadataProvider(IOptions dataGatewayConfig) - : base(dataGatewayConfig) + public MySqlMetadataProvider( + IOptions dataGatewayConfig, + IQueryExecutor queryExecutor, + IQueryBuilder sqlQueryBuilder) + : base(dataGatewayConfig, queryExecutor, sqlQueryBuilder) { } @@ -52,5 +56,39 @@ protected override async Task GetColumnsAsync( return allColumns; } + + /// + /// For MySql, the table name is only a 2 part name. + /// The database name from the connection string needs to be used instead of schemaName. + /// + protected override Dictionary + GetForeignKeyQueryParams( + string[] schemaNames, + string[] tableNames) + { + using MySqlConnection conn = new(ConnectionString); + Dictionary parameters = new(); + + string[] databaseNameParams = + BaseSqlQueryBuilder.CreateParams( + kindOfParam: MySqlQueryBuilder.DATABASE_NAME_PARAM, + schemaNames.Count()); + string[] tableNameParams = + BaseSqlQueryBuilder.CreateParams( + kindOfParam: BaseSqlQueryBuilder.TABLE_NAME_PARAM, + tableNames.Count()); + + for (int i = 0; i < schemaNames.Count(); ++i) + { + parameters.Add(databaseNameParams[i], conn.Database); + } + + for (int i = 0; i < tableNames.Count(); ++i) + { + parameters.Add(tableNameParams[i], tableNames[i]); + } + + return parameters; + } } } diff --git a/DataGateway.Service/Services/MetadataProviders/PostgreSqlMetadataProvider.cs b/DataGateway.Service/Services/MetadataProviders/PostgreSqlMetadataProvider.cs index 3610fcd9ab..cb9bab262c 100644 --- a/DataGateway.Service/Services/MetadataProviders/PostgreSqlMetadataProvider.cs +++ b/DataGateway.Service/Services/MetadataProviders/PostgreSqlMetadataProvider.cs @@ -1,4 +1,5 @@ using Azure.DataGateway.Service.Configurations; +using Azure.DataGateway.Service.Resolvers; using Microsoft.Extensions.Options; using Npgsql; @@ -13,8 +14,11 @@ namespace Azure.DataGateway.Service.Services public class PostgreSqlMetadataProvider : SqlMetadataProvider { - public PostgreSqlMetadataProvider(IOptions dataGatewayConfig) - : base(dataGatewayConfig) + public PostgreSqlMetadataProvider( + IOptions dataGatewayConfig, + IQueryExecutor queryExecutor, + IQueryBuilder sqlQueryBuilder) + : base(dataGatewayConfig, queryExecutor, sqlQueryBuilder) { } } diff --git a/DataGateway.Service/Services/MetadataProviders/SqlGraphQLFileMetadataProvider.cs b/DataGateway.Service/Services/MetadataProviders/SqlGraphQLFileMetadataProvider.cs index 7e4b75a213..06f21a7255 100644 --- a/DataGateway.Service/Services/MetadataProviders/SqlGraphQLFileMetadataProvider.cs +++ b/DataGateway.Service/Services/MetadataProviders/SqlGraphQLFileMetadataProvider.cs @@ -106,6 +106,9 @@ private async Task EnrichDatabaseSchemaWithTableMetadata() $"is not supported."); } } + + await _sqlMetadataProvider!.PopulateForeignKeyDefinitionAsync(schemaName, GraphQLResolverConfig.DatabaseSchema.Tables); + } private void InitFilterParser() diff --git a/DataGateway.Service/Services/MetadataProviders/SqlMetadataProvider.cs b/DataGateway.Service/Services/MetadataProviders/SqlMetadataProvider.cs index 36d311b191..9f276e4f45 100644 --- a/DataGateway.Service/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/DataGateway.Service/Services/MetadataProviders/SqlMetadataProvider.cs @@ -6,7 +6,9 @@ using System.Text; using System.Threading.Tasks; using Azure.DataGateway.Service.Configurations; +using Azure.DataGateway.Service.Exceptions; using Azure.DataGateway.Service.Models; +using Azure.DataGateway.Service.Resolvers; using Microsoft.Extensions.Options; namespace Azure.DataGateway.Service.Services @@ -20,18 +22,31 @@ public class SqlMetadataProvider : ISqlMeta where DataAdapterT : DbDataAdapter, new() where CommandT : DbCommand, new() { + // nullable since Mock tests do not need it. + // TODO: Refactor the Mock tests to remove the nullability here + // once the runtime config is implemented tracked by #353. + private readonly IQueryExecutor? _queryExecutor; + private const int NUMBER_OF_RESTRICTIONS = 4; protected const string TABLE_TYPE = "BASE TABLE"; protected string ConnectionString { get; init; } + // nullable since Mock tests don't need this. + protected IQueryBuilder SqlQueryBuilder { get; init; } + protected DataSet EntitiesDataSet { get; init; } - public SqlMetadataProvider(IOptions dataGatewayConfig) + public SqlMetadataProvider( + IOptions dataGatewayConfig, + IQueryExecutor queryExecutor, + IQueryBuilder queryBuilder) { ConnectionString = dataGatewayConfig.Value.DatabaseConnection.ConnectionString; EntitiesDataSet = new(); + SqlQueryBuilder = queryBuilder; + _queryExecutor = queryExecutor; } /// @@ -41,6 +56,7 @@ public SqlMetadataProvider() { ConnectionString = new(string.Empty); EntitiesDataSet = new(); + SqlQueryBuilder = new MsSqlQueryBuilder(); } /// @@ -78,6 +94,71 @@ public virtual async Task PopulateTableDefinitionAsync( columnsInTable); } + /// + public async Task PopulateForeignKeyDefinitionAsync( + string defaultSchemaName, + Dictionary tables) + { + // Build the query required to get the foreign key information. + string queryForForeignKeyInfo = + ((BaseSqlQueryBuilder)SqlQueryBuilder).BuildForeignKeyInfoQuery(tables.Count); + + // Build the array storing all the schemaNames, for now the defaultSchemaName. + string[] schemaNames = Enumerable.Range(1, tables.Count).Select(x => defaultSchemaName).ToArray(); + + // Build the parameters dictionary for the foreign key info query + // consisting of all schema names and table names. + Dictionary parameters = + GetForeignKeyQueryParams(schemaNames, tables.Keys.ToArray()); + + // Execute the foreign key info query. + using DbDataReader reader = + await _queryExecutor!.ExecuteQueryAsync(queryForForeignKeyInfo, parameters); + + // Extract the first row from the result. + Dictionary? foreignKeyInfo = + await _queryExecutor!.ExtractRowFromDbDataReader(reader); + + // While the result is not null + // keep populating the table definition for all tables with all foreign keys. + while (foreignKeyInfo != null) + { + string twoPartTableName = (string)foreignKeyInfo[nameof(TableDefinition)]!; + TableDefinition? tableDefinition; + string foreignKeyName = (string)foreignKeyInfo[nameof(ForeignKeyDefinition)]!; + ForeignKeyDefinition? foreignKeyDefinition; + + if (tables.TryGetValue(twoPartTableName, out tableDefinition)) + { + if (!tableDefinition.ForeignKeys.TryGetValue(foreignKeyName, out foreignKeyDefinition)) + { + // If this is the first column in this foreign key for this table, + // add the referenced table to the tableDefinition. + foreignKeyDefinition = new(); + foreignKeyDefinition.ReferencedTable = + (string)foreignKeyInfo[nameof(ForeignKeyDefinition.ReferencedTable)]!; + tableDefinition.ForeignKeys.Add(foreignKeyName, foreignKeyDefinition); + } + + // add the referenced and referencing columns to the foreign key definition. + foreignKeyDefinition.ReferencedColumns.Add( + (string)foreignKeyInfo[nameof(ForeignKeyDefinition.ReferencedColumns)]!); + foreignKeyDefinition.ReferencingColumns.Add( + (string)foreignKeyInfo[nameof(ForeignKeyDefinition.ReferencingColumns)]!); + } + else + { + // This should not happen. + throw new DataGatewayException( + message: "Foreign key information is retrieved for a table that is not to be exposed.", + statusCode: System.Net.HttpStatusCode.InternalServerError, + subStatusCode: DataGatewayException.SubStatusCodes.UnexpectedError); + } + + foreignKeyInfo = await _queryExecutor.ExtractRowFromDbDataReader(reader); + } + } + /// public virtual async Task GetTableWithSchemaFromDataSetAsync( string schemaName, @@ -175,5 +256,40 @@ protected void PopulateColumnDefinitionWithHasDefault( } } } + + /// + /// Builds the dictionary of parameters and their values required for the + /// foreign key query. + /// + /// + /// + /// The dictionary populated with parameters. + protected virtual Dictionary + GetForeignKeyQueryParams( + string[] schemaNames, + string[] tableNames) + { + Dictionary parameters = new(); + string[] schemaNameParams = + BaseSqlQueryBuilder.CreateParams( + kindOfParam: BaseSqlQueryBuilder.SCHEMA_NAME_PARAM, + schemaNames.Count()); + string[] tableNameParams = + BaseSqlQueryBuilder.CreateParams( + kindOfParam: BaseSqlQueryBuilder.TABLE_NAME_PARAM, + tableNames.Count()); + + for (int i = 0; i < schemaNames.Count(); ++i) + { + parameters.Add(schemaNameParams[i], schemaNames[i]); + } + + for (int i = 0; i < tableNames.Count(); ++i) + { + parameters.Add(tableNameParams[i], tableNames[i]); + } + + return parameters; + } } } diff --git a/DataGateway.Service/sql-config.json b/DataGateway.Service/sql-config.json index 5f134b7bb2..e7aeebe34e 100644 --- a/DataGateway.Service/sql-config.json +++ b/DataGateway.Service/sql-config.json @@ -179,12 +179,6 @@ } }, "books": { - "ForeignKeys": { - "book_publisher_fk": { - "ReferencedTable": "publishers", - "Columns": [ "publisher_id" ] - } - }, "HttpVerbs": { "GET": { "AuthorizationType": "Authenticated" @@ -204,12 +198,6 @@ } }, "book_website_placements": { - "ForeignKeys": { - "book_website_placement_book_fk": { - "ReferencedTable": "books", - "Columns": [ "book_id" ] - } - }, "HttpVerbs": { "GET": { "AuthorizationType": "Anonymous" @@ -255,12 +243,6 @@ } }, "reviews": { - "ForeignKeys": { - "review_book_fk": { - "ReferencedTable": "books", - "Columns": [ "book_id" ] - } - }, "HttpVerbs": { "GET": { "AuthorizationType": "Anonymous" @@ -268,20 +250,8 @@ } }, "book_author_link": { - "ForeignKeys": { - "book_author_link_book_fk": { - "ReferencedTable": "books", - "Columns": [ "book_id" ] - }, - "book_author_link_author_fk": { - "ReferencedTable": "authors", - "Columns": [ "author_id" ] - } - } }, "magazines": { - "ForeignKeys": { - }, "HttpVerbs": { "GET": { "AuthorizationType": "Anonymous" @@ -315,6 +285,8 @@ "Authorization": "Authenticated" } } + }, + "stocks_price": { } } }