Support composite primary keys for Data Connector [GDW-127]

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/4926
GitOrigin-RevId: 2b6e5052f56a765e0b9a19345fcc4688d7e4700f
This commit is contained in:
Daniel Chambers 2022-07-01 22:20:07 +10:00 committed by hasura-bot
parent 40617719ef
commit b9fb7d8720
12 changed files with 390 additions and 265 deletions

View File

@ -165,39 +165,46 @@ The `GET /schema` endpoint is called whenever the metadata is (re)loaded by `gra
{
"tables": [
{
"name": "artists",
"primary_key": "id",
"name": "Artist",
"primary_key": ["ArtistId"],
"description": "Collection of artists of music",
"columns": [
{
"name": "id",
"type": "string",
"nullable": false
"name": "ArtistId",
"type": "number",
"nullable": false,
"description": "Artist primary key identifier"
},
{
"name": "name",
"name": "Name",
"type": "string",
"nullable": false
"nullable": true,
"description": "The name of the artist"
}
]
},
{
"name": "albums",
"primary_key": "id",
"name": "Album",
"primary_key": ["AlbumId"],
"description": "Collection of music albums created by artists",
"columns": [
{
"name": "id",
"type": "string",
"nullable": false
"name": "AlbumId",
"type": "number",
"nullable": false,
"description": "Album primary key identifier"
},
{
"name": "title",
"name": "Title",
"type": "string",
"nullable": false
"nullable": false,
"description": "The title of the album"
},
{
"name": "artist_id",
"type": "string",
"nullable": false
"name": "ArtistId",
"type": "number",
"nullable": false,
"description": "The ID of the artist that created this album"
}
]
}

View File

@ -38,7 +38,7 @@ const schema: SchemaResponse = {
tables: [
{
name: "Artist",
primary_key: "ArtistId",
primary_key: ["ArtistId"],
description: "Collection of artists of music",
columns: [
{
@ -57,7 +57,7 @@ const schema: SchemaResponse = {
},
{
name: "Album",
primary_key: "AlbumId",
primary_key: ["AlbumId"],
description: "Collection of music albums created by artists",
columns: [
{
@ -82,7 +82,7 @@ const schema: SchemaResponse = {
},
{
name: "Customer",
primary_key: "CustomerId",
primary_key: ["CustomerId"],
description: "Collection of customers who can buy tracks",
columns: [
{
@ -167,7 +167,7 @@ const schema: SchemaResponse = {
},
{
name: "Employee",
primary_key: "EmployeeId",
primary_key: ["EmployeeId"],
description: "Collection of employees who work for the business",
columns: [
{
@ -258,7 +258,7 @@ const schema: SchemaResponse = {
},
{
name: "Genre",
primary_key: "GenreId",
primary_key: ["GenreId"],
description: "Genres of music",
columns: [
{
@ -277,7 +277,7 @@ const schema: SchemaResponse = {
},
{
name: "Invoice",
primary_key: "InvoiceId",
primary_key: ["InvoiceId"],
description: "Collection of invoices of music purchases by a customer",
columns: [
{
@ -338,7 +338,7 @@ const schema: SchemaResponse = {
},
{
name: "InvoiceLine",
primary_key: "InvoiceLineId",
primary_key: ["InvoiceLineId"],
description: "Collection of track purchasing line items of invoices",
columns: [
{
@ -375,7 +375,7 @@ const schema: SchemaResponse = {
},
{
name: "MediaType",
primary_key: "MediaTypeId",
primary_key: ["MediaTypeId"],
description: "Collection of media types that tracks can be encoded in",
columns: [
{
@ -394,7 +394,7 @@ const schema: SchemaResponse = {
},
{
name: "Playlist",
primary_key: "PlaylistId",
primary_key: ["PlaylistId"],
description: "Collection of playlists",
columns: [
{
@ -411,29 +411,28 @@ const schema: SchemaResponse = {
},
]
},
// We don't support composite primary keys yet :(
// {
// name: "PlaylistTrack",
// primary_key: ["PlaylistId", "TrackId"],
// description: "Associations between playlists and tracks",
// columns: [
// {
// name: "PlaylistId",
// type: "number",
// nullable: false,
// description: "The ID of the playlist"
// },
// {
// name: "TrackId",
// type: "number",
// nullable: true,
// description: "The ID of the track"
// },
// ]
// },
{
name: "PlaylistTrack",
primary_key: ["PlaylistId", "TrackId"],
description: "Associations between playlists and tracks",
columns: [
{
name: "PlaylistId",
type: "number",
nullable: false,
description: "The ID of the playlist"
},
{
name: "TrackId",
type: "number",
nullable: true,
description: "The ID of the track"
},
]
},
{
name: "Track",
primary_key: "TrackId",
primary_key: ["TrackId"],
description: "Collection of music tracks",
columns: [
{

View File

@ -578,8 +578,11 @@
},
"primary_key": {
"description": "The primary key of the table",
"items": {
"type": "string"
},
"nullable": true,
"type": "string"
"type": "array"
}
},
"required": [

View File

@ -20,6 +20,6 @@ export type TableInfo = {
/**
* The primary key of the table
*/
primary_key?: string | null;
primary_key?: Array<string> | null;
};

View File

@ -187,7 +187,47 @@
]
},
{
"table": "Playlist"
"table": "Playlist",
"array_relationships": [
{
"name": "PlaylistTracks",
"using": {
"manual_configuration": {
"remote_table": "PlaylistTrack",
"column_mapping": {
"PlaylistId": "PlaylistId"
}
}
}
}
]
},
{
"table": "PlaylistTrack",
"object_relationships": [
{
"name": "Playlist",
"using": {
"manual_configuration": {
"remote_table": "Playlist",
"column_mapping": {
"PlaylistId": "PlaylistId"
}
}
}
},
{
"name": "Track",
"using": {
"manual_configuration": {
"remote_table": "Track",
"column_mapping": {
"TrackId": "TrackId"
}
}
}
}
]
},
{
"table": "Track",
@ -237,6 +277,17 @@
}
}
}
},
{
"name": "PlaylistTracks",
"using": {
"manual_configuration": {
"remote_table": "PlaylistTrack",
"column_mapping": {
"TrackId": "TrackId"
}
}
}
}
]
}
@ -253,4 +304,4 @@
}
}
}
}
}

View File

@ -38,7 +38,7 @@ instance HasCodec TableName where
data TableInfo = TableInfo
{ dtiName :: TableName,
dtiColumns :: [API.V0.ColumnInfo],
dtiPrimaryKey :: Maybe Text,
dtiPrimaryKey :: Maybe [API.V0.ColumnName],
dtiDescription :: Maybe Text
}
deriving stock (Eq, Ord, Show, Generic, Data)

View File

@ -12,6 +12,7 @@ module Data.Sequence.NonEmpty
head,
tail,
toSeq,
fromSeq,
toNonEmpty,
)
where

View File

@ -8,6 +8,7 @@ import Data.Aeson.KeyMap qualified as KM
import Data.Environment (Environment)
import Data.HashMap.Strict qualified as Map
import Data.HashMap.Strict.InsOrd qualified as OMap
import Data.Sequence qualified as Seq
import Data.Sequence.NonEmpty qualified as NESeq
import Data.Text qualified as Text
import Data.Text.Extended (toTxt, (<<>), (<>>))
@ -114,6 +115,7 @@ resolveDatabaseMetadata' ::
resolveDatabaseMetadata' _ sc@(DC.SourceConfig {_scSchema = API.SchemaResponse {..}}) customization =
let tables = Map.fromList $ do
API.TableInfo {..} <- srTables
let primaryKeyColumns = Seq.fromList $ coerce <$> fromMaybe [] dtiPrimaryKey
let meta =
RQL.T.T.DBTableMetadata
{ _ptmiOid = OID 0,
@ -129,7 +131,7 @@ resolveDatabaseMetadata' _ sc@(DC.SourceConfig {_scSchema = API.SchemaResponse {
-- TODO: Add Column Mutability to the 'TableInfo'
rciMutability = RQL.T.C.ColumnMutability False False
},
_ptmiPrimaryKey = dtiPrimaryKey <&> \key -> RQL.T.T.PrimaryKey (RQL.T.T.Constraint () (OID 0)) (NESeq.singleton (coerce key)),
_ptmiPrimaryKey = RQL.T.T.PrimaryKey (RQL.T.T.Constraint () (OID 0)) <$> NESeq.fromSeq primaryKeyColumns,
_ptmiUniqueConstraints = mempty,
_ptmiForeignKeys = mempty,
_ptmiViewInfo = Just $ RQL.T.T.ViewInfo False False False,

View File

@ -5,7 +5,7 @@ module Hasura.Backends.DataConnector.API.V0.TableSpec (spec, genTableName, genTa
import Data.Aeson.QQ.Simple (aesonQQ)
import Hasura.Backends.DataConnector.API.V0
import Hasura.Backends.DataConnector.API.V0.ColumnSpec (genColumnInfo)
import Hasura.Backends.DataConnector.API.V0.ColumnSpec (genColumnInfo, genColumnName)
import Hasura.Prelude
import Hedgehog
import Hedgehog.Gen
@ -33,13 +33,13 @@ spec = do
( TableInfo
(TableName "my_table_name")
[ColumnInfo (ColumnName "id") StringTy False Nothing]
(Just "id")
(Just [ColumnName "id"])
(Just "my description")
)
[aesonQQ|
{ "name": "my_table_name",
"columns": [{"name": "id", "type": "string", "nullable": false}],
"primary_key": "id",
"primary_key": ["id"],
"description": "my description"
}
|]
@ -53,5 +53,5 @@ genTableInfo =
TableInfo
<$> genTableName
<*> Gen.list (linear 0 5) genColumnInfo
<*> Gen.maybe (text (linear 0 10) unicode)
<*> Gen.maybe (Gen.list (linear 0 5) genColumnName)
<*> Gen.maybe (text (linear 0 20) unicode)

View File

@ -1,7 +1,7 @@
[
{
"name": "Artist",
"primary_key": "ArtistId",
"primary_key": ["ArtistId"],
"description": "Collection of artists of music",
"columns": [
{
@ -20,7 +20,7 @@
},
{
"name": "Album",
"primary_key": "AlbumId",
"primary_key": ["AlbumId"],
"description": "Collection of music albums created by artists",
"columns": [
{
@ -45,7 +45,7 @@
},
{
"name": "Customer",
"primary_key": "CustomerId",
"primary_key": ["CustomerId"],
"description": "Collection of customers who can buy tracks",
"columns": [
{
@ -130,7 +130,7 @@
},
{
"name": "Employee",
"primary_key": "EmployeeId",
"primary_key": ["EmployeeId"],
"description": "Collection of employees who work for the business",
"columns": [
{
@ -221,7 +221,7 @@
},
{
"name": "Genre",
"primary_key": "GenreId",
"primary_key": ["GenreId"],
"description": "Genres of music",
"columns": [
{
@ -240,7 +240,7 @@
},
{
"name": "Invoice",
"primary_key": "InvoiceId",
"primary_key": ["InvoiceId"],
"description": "Collection of invoices of music purchases by a customer",
"columns": [
{
@ -301,7 +301,7 @@
},
{
"name": "InvoiceLine",
"primary_key": "InvoiceLineId",
"primary_key": ["InvoiceLineId"],
"description": "Collection of track purchasing line items of invoices",
"columns": [
{
@ -338,7 +338,7 @@
},
{
"name": "MediaType",
"primary_key": "MediaTypeId",
"primary_key": ["MediaTypeId"],
"description": "Collection of media types that tracks can be encoded in",
"columns": [
{
@ -357,7 +357,7 @@
},
{
"name": "Playlist",
"primary_key": "PlaylistId",
"primary_key": ["PlaylistId"],
"description": "Collection of playlists",
"columns": [
{
@ -374,9 +374,28 @@
}
]
},
{
"name": "PlaylistTrack",
"primary_key": ["PlaylistId", "TrackId"],
"description": "Associations between playlists and tracks",
"columns": [
{
"name": "PlaylistId",
"type": "number",
"nullable": false,
"description": "The ID of the playlist"
},
{
"name": "TrackId",
"type": "number",
"nullable": true,
"description": "The ID of the track"
}
]
},
{
"name": "Track",
"primary_key": "TrackId",
"primary_key": ["TrackId"],
"description": "Collection of music tracks",
"columns": [
{

View File

@ -66,7 +66,7 @@ schema =
API.dciDescription = Just "The name of the artist"
}
],
API.dtiPrimaryKey = Just "ArtistId",
API.dtiPrimaryKey = Just [API.ColumnName "ArtistId"],
API.dtiDescription = Just "Collection of artists of music"
},
API.TableInfo
@ -91,7 +91,7 @@ schema =
API.dciDescription = Just "The ID of the artist that created the album"
}
],
API.dtiPrimaryKey = Just "AlbumId",
API.dtiPrimaryKey = Just [API.ColumnName "AlbumId"],
API.dtiDescription = Just "Collection of music albums created by artists"
},
API.TableInfo
@ -110,7 +110,7 @@ schema =
API.dciDescription = Just "The name of the genre"
}
],
API.dtiPrimaryKey = Just "GenreId",
API.dtiPrimaryKey = Just [API.ColumnName "GenreId"],
API.dtiDescription = Just "Genres of music"
},
API.TableInfo
@ -129,7 +129,7 @@ schema =
API.dciDescription = Just "The name of the media type format"
}
],
API.dtiPrimaryKey = Just "MediaTypeId",
API.dtiPrimaryKey = Just [API.ColumnName "MediaTypeId"],
API.dtiDescription = Just "Collection of media types that tracks can be encoded in"
},
API.TableInfo
@ -190,7 +190,7 @@ schema =
API.dciDescription = Just "The price of the track"
}
],
API.dtiPrimaryKey = Just "TrackId",
API.dtiPrimaryKey = Just [API.ColumnName "TrackId"],
API.dtiDescription = Just "Collection of music tracks"
}
]

View File

@ -79,6 +79,22 @@ tables:
remote_table: Album
column_mapping:
ArtistId: ArtistId
- table: Playlist
- table: PlaylistTrack
object_relationships:
- name: Playlist
using:
manual_configuration:
remote_table: Playlist
column_mapping:
PlaylistId: PlaylistId
- name: Track
using:
manual_configuration:
remote_table: Track
column_mapping:
TrackId: TrackId
- table: Track
configuration: {}
|]
@ -114,24 +130,24 @@ tests opts = describe "Queries" $ do
( GraphqlEngine.postGraphql
testEnvironment
[graphql|
query getAlbum {
albums(limit: 3, order_by: {id: asc}) {
id
title
}
}
|]
query getAlbum {
albums(limit: 3, order_by: {id: asc}) {
id
title
}
}
|]
)
[yaml|
data:
albums:
- id: 1
title: For Those About To Rock We Salute You
- id: 2
title: Balls to the Wall
- id: 3
title: Restless and Wild
|]
data:
albums:
- id: 1
title: For Those About To Rock We Salute You
- id: 2
title: Balls to the Wall
- id: 3
title: Restless and Wild
|]
it "works with a primary key" $ \(testEnvironment, _) ->
shouldReturnYaml
@ -139,20 +155,20 @@ data:
( GraphqlEngine.postGraphql
testEnvironment
[graphql|
query getAlbum {
albums_by_pk(id: 1) {
id
title
}
}
|]
query getAlbum {
albums_by_pk(id: 1) {
id
title
}
}
|]
)
[yaml|
data:
albums_by_pk:
- id: 1
title: "For Those About To Rock We Salute You"
|]
data:
albums_by_pk:
- id: 1
title: "For Those About To Rock We Salute You"
|]
it "works with non existent primary key" $ \(testEnvironment, _) ->
shouldReturnYaml
@ -160,18 +176,45 @@ data:
( GraphqlEngine.postGraphql
testEnvironment
[graphql|
query getAlbum {
albums_by_pk(id: 999999) {
id
title
}
}
|]
query getAlbum {
albums_by_pk(id: 999999) {
id
title
}
}
|]
)
[yaml|
data:
albums_by_pk: []
|]
data:
albums_by_pk: []
|]
it "works with a composite primary key" $ \(testEnvironment, _) ->
shouldReturnYaml
opts
( GraphqlEngine.postGraphql
testEnvironment
[graphql|
query getAlbum {
PlaylistTrack_by_pk(PlaylistId: 1, TrackId: 2) {
Playlist {
Name
}
Track {
Name
}
}
}
|]
)
[yaml|
data:
PlaylistTrack_by_pk:
- Playlist:
Name: "Music"
Track:
Name: "Balls to the Wall"
|]
it "works with pagination" $ \(testEnvironment, _) ->
shouldReturnYaml
@ -179,20 +222,20 @@ data:
( GraphqlEngine.postGraphql
testEnvironment
[graphql|
query getAlbum {
albums (limit: 3, offset: 2) {
id
}
}
|]
query getAlbum {
albums (limit: 3, offset: 2) {
id
}
}
|]
)
[yaml|
data:
albums:
- id: 3
- id: 4
- id: 5
|]
data:
albums:
- id: 3
- id: 4
- id: 5
|]
describe "Array Relationships" $ do
it "joins on album id" $ \(testEnvironment, _) ->
@ -201,26 +244,26 @@ data:
( GraphqlEngine.postGraphql
testEnvironment
[graphql|
query getArtist {
artists_by_pk(id: 1) {
id
name
albums {
title
}
}
}
|]
query getArtist {
artists_by_pk(id: 1) {
id
name
albums {
title
}
}
}
|]
)
[yaml|
data:
artists_by_pk:
- name: AC/DC
id: 1
albums:
- title: For Those About To Rock We Salute You
- title: Let There Be Rock
|]
data:
artists_by_pk:
- name: AC/DC
id: 1
albums:
- title: For Those About To Rock We Salute You
- title: Let There Be Rock
|]
describe "Object Relationships" $ do
it "joins on artist id" $ \(testEnvironment, _) ->
@ -229,25 +272,25 @@ data:
( GraphqlEngine.postGraphql
testEnvironment
[graphql|
query getAlbum {
albums_by_pk(id: 1) {
id
title
artist {
name
}
}
}
|]
query getAlbum {
albums_by_pk(id: 1) {
id
title
artist {
name
}
}
}
|]
)
[yaml|
data:
albums_by_pk:
- id: 1
title: "For Those About To Rock We Salute You"
artist:
name: "AC/DC"
|]
data:
albums_by_pk:
- id: 1
title: "For Those About To Rock We Salute You"
artist:
name: "AC/DC"
|]
describe "Where Clause Tests" $ do
it "works with '_in' predicate" $ \(testEnvironment, _) ->
@ -256,24 +299,24 @@ data:
( GraphqlEngine.postGraphql
testEnvironment
[graphql|
query getAlbum {
albums(where: {id: {_in: [1, 3, 5]}}) {
id
title
}
}
|]
query getAlbum {
albums(where: {id: {_in: [1, 3, 5]}}) {
id
title
}
}
|]
)
[yaml|
data:
albums:
- id: 1
title: For Those About To Rock We Salute You
- id: 3
title: Restless and Wild
- id: 5
title: Big Ones
|]
data:
albums:
- id: 1
title: For Those About To Rock We Salute You
- id: 3
title: Restless and Wild
- id: 5
title: Big Ones
|]
it "works with '_nin' predicate" $ \(testEnvironment, _) ->
shouldReturnYaml
@ -281,22 +324,22 @@ data:
( GraphqlEngine.postGraphql
testEnvironment
[graphql|
query getAlbum {
albums(where: {id: {_in: [1, 3, 5]}, title: {_nin: ["Big Ones"]}}) {
id
title
}
}
|]
query getAlbum {
albums(where: {id: {_in: [1, 3, 5]}, title: {_nin: ["Big Ones"]}}) {
id
title
}
}
|]
)
[yaml|
data:
albums:
- id: 1
title: For Those About To Rock We Salute You
- id: 3
title: Restless and Wild
|]
data:
albums:
- id: 1
title: For Those About To Rock We Salute You
- id: 3
title: Restless and Wild
|]
it "works with '_eq' predicate" $ \(testEnvironment, _) ->
shouldReturnYaml
@ -304,20 +347,20 @@ data:
( GraphqlEngine.postGraphql
testEnvironment
[graphql|
query getAlbum {
albums(where: {id: {_eq: 1}}) {
id
title
}
}
|]
query getAlbum {
albums(where: {id: {_eq: 1}}) {
id
title
}
}
|]
)
[yaml|
data:
albums:
- id: 1
title: For Those About To Rock We Salute You
|]
data:
albums:
- id: 1
title: For Those About To Rock We Salute You
|]
it "works with '_neq' predicate" $ \(testEnvironment, _) ->
shouldReturnYaml
@ -325,22 +368,22 @@ data:
( GraphqlEngine.postGraphql
testEnvironment
[graphql|
query getAlbum {
albums(where: {id: {_neq: 2, _in: [1, 2, 3]}}) {
id
title
}
}
|]
query getAlbum {
albums(where: {id: {_neq: 2, _in: [1, 2, 3]}}) {
id
title
}
}
|]
)
[yaml|
data:
albums:
- id: 1
title: For Those About To Rock We Salute You
- id: 3
title: Restless and Wild
|]
data:
albums:
- id: 1
title: For Those About To Rock We Salute You
- id: 3
title: Restless and Wild
|]
it "works with '_lt' predicate" $ \(testEnvironment, _) ->
shouldReturnYaml
@ -348,20 +391,20 @@ data:
( GraphqlEngine.postGraphql
testEnvironment
[graphql|
query getAlbum {
albums(where: {id: {_lt: 2}}) {
id
title
}
}
|]
query getAlbum {
albums(where: {id: {_lt: 2}}) {
id
title
}
}
|]
)
[yaml|
data:
albums:
- id: 1
title: For Those About To Rock We Salute You
|]
data:
albums:
- id: 1
title: For Those About To Rock We Salute You
|]
it "works with '_lte' predicate" $ \(testEnvironment, _) ->
shouldReturnYaml
@ -369,22 +412,22 @@ data:
( GraphqlEngine.postGraphql
testEnvironment
[graphql|
query getArtists {
artists(where: {id: {_lte: 2}}) {
id
name
}
}
|]
query getArtists {
artists(where: {id: {_lte: 2}}) {
id
name
}
}
|]
)
[yaml|
data:
artists:
- id: 1
name: AC/DC
- id: 2
name: Accept
|]
data:
artists:
- id: 1
name: AC/DC
- id: 2
name: Accept
|]
it "works with '_gt' predicate" $ \(testEnvironment, _) ->
shouldReturnYaml
@ -392,20 +435,20 @@ data:
( GraphqlEngine.postGraphql
testEnvironment
[graphql|
query getArtists {
artists(where: {id: {_gt: 274}}) {
id
name
}
}
|]
query getArtists {
artists(where: {id: {_gt: 274}}) {
id
name
}
}
|]
)
[yaml|
data:
artists:
- id: 275
name: Philip Glass Ensemble
|]
data:
artists:
- id: 275
name: Philip Glass Ensemble
|]
it "works with '_gte' predicate" $ \(testEnvironment, _) ->
shouldReturnYaml
@ -413,19 +456,19 @@ data:
( GraphqlEngine.postGraphql
testEnvironment
[graphql|
query getArtists {
artists(where: {id: {_gte: 274}}) {
id
name
}
}
|]
query getArtists {
artists(where: {id: {_gte: 274}}) {
id
name
}
}
|]
)
[yaml|
data:
artists:
- id: 274
name: Nash Ensemble
- id: 275
name: Philip Glass Ensemble
|]
data:
artists:
- id: 274
name: Nash Ensemble
- id: 275
name: Philip Glass Ensemble
|]