-- | Add metadata tags as comments to SQL queries. module Data.SqlCommenter ( sqlCommenterGoogle, sqlCommenterStandard, Attribute, ) where import Data.List.NonEmpty qualified as NE import Data.Text.Extended (commaSeparated) import Hasura.Prelude import Hasura.QueryTags import Network.URI.Encode qualified as URI -- | query-tags format as defined in the Spec <https://google.github.io/sqlcommenter/spec/#sql-commenter> sqlCommenterGoogle :: QueryTagsAttributes -> QueryTagsComment sqlCommenterGoogle qtAttributes | null attributes = emptyQueryTagsComment | otherwise = QueryTagsComment $ createSQLComment $ generateCommentTags (NE.fromList attributes) where createSQLComment comment = " /* " <> comment <> " */" attributes = _unQueryTagsAttributes qtAttributes -- | Default 'Query Tags' format in the Hasura GraphQL Engine -- Creates simple 'key=value' pairs of query tags. No sorting, No URL encoding. -- If the format of query tags is not mentioned in the metadata then, query-tags -- are formatted using hte below format sqlCommenterStandard :: QueryTagsAttributes -> QueryTagsComment sqlCommenterStandard qtAttributes | null attributes = emptyQueryTagsComment | otherwise = QueryTagsComment $ createSQLComment $ generateComment (NE.fromList attributes) where generateComment attr = commaSeparated [k <> "=" <> v | (k, v) <- NE.toList attr] createSQLComment comment = " /* " <> comment <> " */" attributes = _unQueryTagsAttributes qtAttributes -- | Top-level algorithm to generate the string comment from list of -- 'Attribute's -- Spec <https://google.github.io/sqlcommenter/spec/#sql-commenter> -- See Note [Ambiguous SQLCommenterGoogle Specification] generateCommentTags :: NE.NonEmpty Attribute -> Text generateCommentTags attributes = let -- 1. URL encode the key,value pairs. What the spec calls serialization. -- https://google.github.io/sqlcommenter/spec/#key-serialization-algorithm -- https://google.github.io/sqlcommenter/spec/#value-serialization-algorithm encoded = NE.map urlEncodePair attributes -- 2. Sort the pairs. Spec: https://google.github.io/sqlcommenter/spec/#sorting -- Note the 'sort' from 'Data.List' works by sorting the first element in -- the pair. And sorting on 'Text' is lexicographic. sorted = NE.sort encoded -- 3. Finally, serialize the pairs into a CSV. What the spec calls concatenation -- https://google.github.io/sqlcommenter/spec/#concatenation serialized = serializePairs sorted in serialized where urlEncodePair (k, v) = (URI.encodeText k, URI.encodeText v) serializePairs pairs = commaSeparated [k <> "='" <> v <> "'" | (k, v) <- NE.toList pairs] -- | NOTE: [Ambiguous SQLCommenterGoogle Specification] -- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -- The specification is ambiguos/unclear about the following two steps: -- -- 1. [Comment Escaping](https://google.github.io/sqlcommenter/spec/#comment-escaping) -- ~~~~~~~~~~~~~~~~~~~~ -- The Spec states that: -- -- > If a comment already exists within a SQL statement, we MUST NOT mutate that statement. -- -- Oddly, the implementation of above rule/statement is not uniform among it's various language -- libraries. That along with the fact that the above statement is clear for the scenarios when -- we have properly formed comment, but does not state anything about what to do when the comments -- are malformed is why it was decided to skip this step. -- -- I have created a issue regarding this at https://github.com/google/sqlcommenter/issues/57 -- -- When discussed the above problem with Tiru, it was decided that since in our case we do not have -- any other sql comments in the prepared statement, this step won't affect us thus it's okay to -- skip this. -- -- 2. [Escaping Meta Characters](https://google.github.io/sqlcommenter/spec/#meta-characters) -- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -- The Spec states that: -- -- > Meta characters such as `'` should be escaped with a slash `\`. -- -- And the algorithm in brief is: -- 1. URL encode the Key/Value -- 2. Escape the meta-characters withing the raw value; a single quote `'` becomes `\'` -- -- From the above statement, it could be understood that the meta-characters in context of -- sqlcommenter is `'`. And 'Network.URI.Encode.encodeText' is capable of encoding `'` -- without any need of escaping. -- -- ``` -- Prelude Network.URI.Encode> encodeText"'" -- "%27" -- -- Prelude Network.URI.Encode> encodeText "\'" -- "%27" -- ``` -- -- During the investigation, we also found that the reference implementation of the sqlcommenter -- in other languages did not even do this step: -- 1. https://github.com/google/sqlcommenter/blob/master/python/sqlcommenter-python/google/cloud/sqlcommenter/__init__.py#L29 -- 2. https://github.com/google/sqlcommenter/blob/master/java/sqlcommenter-java/src/main/java/com/google/cloud/sqlcommenter/threadlocalstorage/State.java#L179 -- -- The above can be summarized as: -- 1. The meta-characters in the context of sqlcommenter is `'` -- 2. Reference implementation of sqlcommenter does not include the step -- 3. 'Network.URI.Encode.encodeText' can encode `'` without any need of escaping -- -- Thus, on the basis of the above three facts we decided to ignore the 'Escape Meta Characters' step.