Skip to content

Commit 3db2411

Browse files
Tom Hardinghasura-bot
authored andcommitted
Make ForeignKey mappings non-empty
Previously, these were represented with a HashMap, but supposedly that map can never be empty. Now, it uses NEHashMap, which carries the non-empty invariant behind a smart constructor. PR-URL: hasura/graphql-engine-mono#4481 GitOrigin-RevId: 93ad9aaa9354f25a1ba10e8207ae19614e1e439e
1 parent ce9912f commit 3db2411

7 files changed

Lines changed: 86 additions & 21 deletions

File tree

server/src-lib/Data/HashMap/Strict/NonEmpty.hs

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,36 @@ module Data.HashMap.Strict.NonEmpty
77
singleton,
88
fromHashMap,
99
fromList,
10+
fromNonEmpty,
1011
toHashMap,
12+
toList,
13+
toNonEmpty,
1114

1215
-- * Basic interface
1316
lookup,
1417
(!?),
1518
keys,
1619

1720
-- * Combine
21+
union,
1822
unionWith,
1923

2024
-- * Transformations
2125
mapKeys,
26+
27+
-- * Predicates
28+
isInverseOf,
2229
)
2330
where
2431

32+
import Control.DeepSeq (NFData)
33+
import Data.Aeson (FromJSON, ToJSON)
2534
import Data.HashMap.Strict (HashMap)
2635
import Data.HashMap.Strict qualified as M
36+
import Data.HashMap.Strict.Extended qualified as Extended
2737
import Data.Hashable (Hashable)
38+
import Data.List.NonEmpty (NonEmpty)
39+
import Data.List.NonEmpty qualified as NE
2840
import Prelude hiding (lookup)
2941

3042
-------------------------------------------------------------------------------
@@ -33,7 +45,7 @@ import Prelude hiding (lookup)
3345
-- only provides a restricted set of functionalities. It doesn't
3446
-- provide a 'Monoid' instance, nor an 'empty' function.
3547
newtype NEHashMap k v = NEHashMap {unNEHashMap :: HashMap k v}
36-
deriving newtype (Show, Eq, Ord, Semigroup)
48+
deriving newtype (Show, Eq, FromJSON, Hashable, NFData, Ord, Semigroup, ToJSON)
3749
deriving stock (Functor, Foldable, Traversable)
3850

3951
-------------------------------------------------------------------------------
@@ -58,10 +70,23 @@ fromList :: (Eq k, Hashable k) => [(k, v)] -> Maybe (NEHashMap k v)
5870
fromList [] = Nothing
5971
fromList v = Just $ NEHashMap $ M.fromList v
6072

73+
-- | A variant of 'fromList' that uses 'NonEmpty' inputs.
74+
fromNonEmpty :: (Eq k, Hashable k) => NonEmpty (k, v) -> NEHashMap k v
75+
fromNonEmpty (x NE.:| xs) = NEHashMap (M.fromList (x : xs))
76+
6177
-- | Convert a non-empty map to a 'HashMap'.
6278
toHashMap :: NEHashMap k v -> HashMap k v
6379
toHashMap = unNEHashMap
6480

81+
-- | Convert a non-empty map to a non-empty list of key/value pairs. The closed
82+
-- operations of 'NEHashMap' guarantee that this operation won't fail.
83+
toNonEmpty :: NEHashMap k v -> NonEmpty (k, v)
84+
toNonEmpty = NE.fromList . M.toList . unNEHashMap
85+
86+
-- | Convert a non-empty map to a list of key/value pairs.
87+
toList :: NEHashMap k v -> [(k, v)]
88+
toList = M.toList . unNEHashMap
89+
6590
-------------------------------------------------------------------------------
6691

6792
-- | Return the value to which the specified key is mapped, or 'Nothing' if
@@ -84,6 +109,13 @@ keys = M.keys . unNEHashMap
84109

85110
-- | The union of two maps.
86111
--
112+
-- If a key occurs in both maps, the left map @m1@ (first argument) will be
113+
-- preferred.
114+
union :: (Eq k, Hashable k) => NEHashMap k v -> NEHashMap k v -> NEHashMap k v
115+
union (NEHashMap m1) (NEHashMap m2) = NEHashMap $ M.union m1 m2
116+
117+
-- | The union of two maps using a given value-wise union function.
118+
--
87119
-- If a key occurs in both maps, the provided function (first argument) will be
88120
-- used to compute the result.
89121
unionWith :: (Eq k, Hashable k) => (v -> v -> v) -> NEHashMap k v -> NEHashMap k v -> NEHashMap k v
@@ -98,3 +130,15 @@ unionWith fun (NEHashMap m1) (NEHashMap m2) = NEHashMap $ M.unionWith fun m1 m2
98130
-- values is chosen for the conflicting key.
99131
mapKeys :: (Eq k2, Hashable k2) => (k1 -> k2) -> NEHashMap k1 v -> NEHashMap k2 v
100132
mapKeys fun (NEHashMap m) = NEHashMap $ M.mapKeys fun m
133+
134+
-------------------------------------------------------------------------------
135+
136+
-- | Determines whether the left-hand-side and the right-hand-side are inverses of each other.
137+
--
138+
-- More specifically, for two maps @A@ and @B@, 'isInverseOf' is satisfied when both of the
139+
-- following are true:
140+
-- 1. @∀ key ∈ A. A[key] ∈ B ∧ B[A[key]] == key@
141+
-- 2. @∀ key ∈ B. B[key] ∈ A ∧ A[B[key]] == key@
142+
isInverseOf ::
143+
(Eq k, Hashable k, Eq v, Hashable v) => NEHashMap k v -> NEHashMap v k -> Bool
144+
lhs `isInverseOf` rhs = toHashMap lhs `Extended.isInverseOf` toHashMap rhs

server/src-lib/Hasura/Backends/MSSQL/Meta.hs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import Data.Aeson as Aeson
1616
import Data.ByteString.UTF8 qualified as BSUTF8
1717
import Data.FileEmbed (embedFile, makeRelativeToProject)
1818
import Data.HashMap.Strict qualified as HM
19+
import Data.HashMap.Strict.NonEmpty qualified as NEHashMap
1920
import Data.HashSet qualified as HS
2021
import Data.String
2122
import Data.Text qualified as T
@@ -171,7 +172,7 @@ transformColumn columnInfo =
171172

172173
schemaName = SchemaName $ ssName $ sfkcJoinedReferencedSysSchema foreignKeyColumn
173174
_fkForeignTable = TableName (sfkcJoinedReferencedTableName foreignKeyColumn) schemaName
174-
_fkColumnMapping = HM.singleton rciName $ ColumnName $ sfkcJoinedReferencedColumnName foreignKeyColumn
175+
_fkColumnMapping = NEHashMap.singleton rciName $ ColumnName $ sfkcJoinedReferencedColumnName foreignKeyColumn
175176
in ForeignKey {..}
176177

177178
colIsImmutable = scIsComputed columnInfo || scIsIdentity columnInfo
@@ -192,7 +193,7 @@ coalesceKeys :: [ForeignKey 'MSSQL] -> [ForeignKey 'MSSQL]
192193
coalesceKeys = HM.elems . foldl' coalesce HM.empty
193194
where
194195
coalesce mapping fk@(ForeignKey constraint tableName _) = HM.insertWith combine (constraint, tableName) fk mapping
195-
combine oldFK newFK = oldFK {_fkColumnMapping = (HM.union `on` _fkColumnMapping) oldFK newFK}
196+
combine oldFK newFK = oldFK {_fkColumnMapping = (NEHashMap.union `on` _fkColumnMapping) oldFK newFK}
196197

197198
parseScalarType :: Text -> ScalarType
198199
parseScalarType = \case

server/src-lib/Hasura/Backends/MySQL/Meta.hs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import Control.Exception (throw)
99
import Data.ByteString.Char8 qualified as B8
1010
import Data.FileEmbed (embedFile, makeRelativeToProject)
1111
import Data.HashMap.Strict qualified as HM
12+
import Data.HashMap.Strict.NonEmpty qualified as NEHashMap
1213
import Data.HashSet qualified as HS
1314
import Data.Sequence.NonEmpty qualified as SNE
1415
import Data.String (fromString)
@@ -87,7 +88,7 @@ mergeMetadata InformationSchema {..} =
8788
schema = isReferencedTableSchema
8889
}
8990
)
90-
( HM.singleton
91+
( NEHashMap.singleton
9192
(Column isColumnName)
9293
(Column $ fromMaybe "" isReferencedColumnName)
9394
)

server/src-lib/Hasura/Incremental/Internal/Dependency.hs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import Data.Functor.Classes (Eq1 (..), Eq2 (..))
2222
import Data.GADT.Compare
2323
import Data.HashMap.Strict qualified as Map
2424
import Data.HashMap.Strict.InsOrd qualified as OMap
25+
import Data.HashMap.Strict.NonEmpty (NEHashMap)
26+
import Data.HashMap.Strict.NonEmpty qualified as NEHashMap
2527
import Data.HashSet.InsOrd qualified as OSet
2628
import Data.Int
2729
import Data.Scientific (Scientific)
@@ -254,6 +256,9 @@ instance (Cacheable a) => Cacheable (Vector a) where
254256
instance (Cacheable k, Cacheable v) => Cacheable (HashMap k v) where
255257
unchanged accesses = liftEq2 (unchanged accesses) (unchanged accesses)
256258

259+
instance (Cacheable k, Cacheable v) => Cacheable (NEHashMap k v) where
260+
unchanged accesses = unchanged accesses `on` NEHashMap.toHashMap
261+
257262
instance (Cacheable a) => Cacheable (HashSet a) where
258263
unchanged = liftEq . unchanged
259264

server/src-lib/Hasura/RQL/DDL/Relationship.hs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import Control.Lens ((.~))
1616
import Data.Aeson.Types
1717
import Data.HashMap.Strict qualified as Map
1818
import Data.HashMap.Strict.InsOrd qualified as OMap
19+
import Data.HashMap.Strict.NonEmpty qualified as NEHashMap
1920
import Data.HashSet qualified as Set
2021
import Data.Text.Extended
2122
import Data.Tuple (swap)
@@ -140,7 +141,7 @@ objRelP2Setup source qt foreignKeys (RelDef rn ru _) = case ru of
140141
DRRemoteTable
141142
]
142143
<> fmap (drUsingColumnDep @b source qt) (toList columns)
143-
pure (RelInfo rn ObjRel colMap foreignTable False BeforeParent, dependencies)
144+
pure (RelInfo rn ObjRel (NEHashMap.toHashMap colMap) foreignTable False BeforeParent, dependencies)
144145
RUFKeyOn (RemoteTable remoteTable remoteCols) ->
145146
mkFkeyRel ObjRel AfterParent source rn qt remoteTable remoteCols foreignKeys
146147

@@ -218,7 +219,7 @@ mkFkeyRel relType io source rn sourceTable remoteTable remoteColumns foreignKeys
218219
DRRemoteTable
219220
]
220221
<> fmap (drUsingColumnDep @b source remoteTable) (toList remoteColumns)
221-
pure (RelInfo rn relType (reverseMap colMap) remoteTable False io, dependencies)
222+
pure (RelInfo rn relType (reverseMap (NEHashMap.toHashMap colMap)) remoteTable False io, dependencies)
222223
where
223224
reverseMap :: Eq y => Hashable y => HashMap x y -> HashMap y x
224225
reverseMap = Map.fromList . fmap swap . Map.toList
@@ -235,7 +236,7 @@ getRequiredFkey cols fkeys =
235236
[] -> throw400 ConstraintError "no foreign constraint exists on the given column(s)"
236237
_ -> throw400 ConstraintError "more than one foreign key constraint exists on the given column(s)"
237238
where
238-
filteredFkeys = filter ((== Set.fromList (toList cols)) . Map.keysSet . _fkColumnMapping) fkeys
239+
filteredFkeys = filter ((== Set.fromList (toList cols)) . Map.keysSet . NEHashMap.toHashMap . _fkColumnMapping) fkeys
239240

240241
drUsingColumnDep ::
241242
forall b.

server/src-lib/Hasura/RQL/DDL/Schema/Enum.hs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ module Hasura.RQL.DDL.Schema.Enum
1515
where
1616

1717
import Data.HashMap.Strict qualified as M
18+
import Data.HashMap.Strict.NonEmpty qualified as NEHashMap
1819
import Data.Sequence qualified as Seq
1920
import Data.Sequence.NonEmpty qualified as NESeq
2021
import Hasura.Prelude
@@ -39,7 +40,7 @@ resolveEnumReferences enumTables =
3940
where
4041
resolveEnumReference :: ForeignKey b -> Maybe (Column b, EnumReference b)
4142
resolveEnumReference foreignKey = do
42-
[(localColumn, foreignColumn)] <- pure $ M.toList (_fkColumnMapping @b foreignKey)
43+
[(localColumn, foreignColumn)] <- pure $ NEHashMap.toList (_fkColumnMapping @b foreignKey)
4344
let foreignKeyTableName = _fkForeignTable foreignKey
4445
(primaryKey, tConfig, enumValues) <- M.lookup foreignKeyTableName enumTables
4546
let tableCustomName = _tcCustomName tConfig

server/src-lib/Hasura/RQL/Types/Table.hs

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ import Data.Aeson.TH
9393
import Data.Aeson.Types (Parser, prependFailure, typeMismatch)
9494
import Data.HashMap.Strict qualified as M
9595
import Data.HashMap.Strict.Extended qualified as M
96+
import Data.HashMap.Strict.NonEmpty (NEHashMap)
97+
import Data.HashMap.Strict.NonEmpty qualified as NEHashMap
9698
import Data.HashSet qualified as HS
9799
import Data.List.Extended (duplicates)
98100
import Data.List.NonEmpty qualified as NE
@@ -761,7 +763,7 @@ $(makeLenses ''PrimaryKey)
761763
data ForeignKey (b :: BackendType) = ForeignKey
762764
{ _fkConstraint :: !(Constraint b),
763765
_fkForeignTable :: !(TableName b),
764-
_fkColumnMapping :: !(HashMap (Column b) (Column b))
766+
_fkColumnMapping :: !(NEHashMap (Column b) (Column b))
765767
}
766768
deriving (Generic)
767769

@@ -861,18 +863,28 @@ instance Backend b => FromJSON (ForeignKeyMetadata b) where
861863
constraint <- o .: "constraint"
862864
foreignTable <- o .: "foreign_table"
863865

864-
columns <- o .: "columns"
865-
foreignColumns <- o .: "foreign_columns"
866-
if length columns == length foreignColumns
867-
then
868-
pure $
869-
ForeignKeyMetadata
870-
ForeignKey
871-
{ _fkConstraint = constraint,
872-
_fkForeignTable = foreignTable,
873-
_fkColumnMapping = M.fromList $ zip columns foreignColumns
874-
}
875-
else fail "columns and foreign_columns differ in length"
866+
columns <-
867+
o .: "columns" >>= \case
868+
x : xs -> pure (x :| xs)
869+
[] -> fail "columns must be non-empty"
870+
871+
foreignColumns <-
872+
o .: "foreign_columns" >>= \case
873+
x : xs -> pure (x :| xs)
874+
[] -> fail "foreign_columns must be non-empty"
875+
876+
unless (length columns == length foreignColumns) do
877+
fail "columns and foreign_columns differ in length"
878+
879+
pure $
880+
ForeignKeyMetadata
881+
ForeignKey
882+
{ _fkConstraint = constraint,
883+
_fkForeignTable = foreignTable,
884+
_fkColumnMapping =
885+
NEHashMap.fromNonEmpty $
886+
NE.zip columns foreignColumns
887+
}
876888

877889
-- | Metadata of any Backend table which is being extracted from source database
878890
data DBTableMetadata (b :: BackendType) = DBTableMetadata

0 commit comments

Comments
 (0)