Summary

Class:CBAM.SQL.PostgreSQL.Implementation.Parser
Assembly:CBAM.SQL.PostgreSQL.Implementation
File(s):/repo-dir/contents/Source/Code/CBAM.SQL.PostgreSQL.Implementation/VendorSpecific.cs
Covered lines:39
Uncovered lines:132
Coverable lines:171
Total lines:478
Line coverage:22.8%
Branch coverage:23.2%

Coverage History

Metrics

MethodCyclomatic complexity NPath complexity Sequence coverage Branch coverage
.ctor(...)100%0%
Read()200%0%
Peek()100%0%
Dispose(...)100%0%
ParseStringForNextSQLStatement()3100.742%1%
ParseSingleQuotes()1300.643%1%
ParseDoubleQuotes()1400%0%
ParseLineComment()1800%0%
ParseBlockComment()3200%0%
ParseDollarQuotes()5600%0%
CheckSingleQuote()1300.5%1%
CharTerminatesIdentifier(...)400%0%
IsSpace(...)800%0%
IsOperatorChar(...)100%0%
IsIdentifierContinuationCharacter(...)1600%0%
IsDollarQuoteTagStartCharacter(...)1000%0%
IsDollarQuoteTagContinuationCharacter(...)1400%0%
CheckForCircularlyFilledArray(...)1400%0%

File(s)

/repo-dir/contents/Source/Code/CBAM.SQL.PostgreSQL.Implementation/VendorSpecific.cs

#LineLine coverage
 1/*
 2 * Copyright 2017 Stanislav Muhametsin. All rights Reserved.
 3 *
 4 * Licensed  under the  Apache License,  Version 2.0  (the "License");
 5 * you may not use  this file  except in  compliance with the License.
 6 * You may obtain a copy of the License at
 7 *
 8 *   http://www.apache.org/licenses/LICENSE-2.0
 9 *
 10 * Unless required by applicable law or agreed to in writing, software
 11 * distributed  under the  License is distributed on an "AS IS" BASIS,
 12 * WITHOUT  WARRANTIES OR CONDITIONS  OF ANY KIND, either  express  or
 13 * implied.
 14 *
 15 * See the License for the specific language governing permissions and
 16 * limitations under the License.
 17 */
 18using System;
 19using System.Collections.Generic;
 20using System.IO;
 21using System.Text;
 22using System.Threading.Tasks;
 23using UtilPack;
 24
 25using TReader = UtilPack.PeekablePotentiallyAsyncReader<System.Char?>;
 26
 27namespace CBAM.SQL.PostgreSQL.Implementation
 28{
 29   internal static class Parser
 30   {
 31      // Helper class to keep track of how many chars has been read from the underlying reader
 32      private sealed class TextReaderWrapper : TextReader
 33      {
 34         private readonly TextReader _reader;
 35         private Int32 _charsRead;
 36
 037         internal TextReaderWrapper( TextReader reader )
 38         {
 039            ArgumentValidator.ValidateNotNull( "Reader", reader );
 40
 041            this._reader = reader;
 042            this._charsRead = 0;
 043         }
 44
 45         public Int32 CharsRead
 46         {
 47            get
 48            {
 049               return this._charsRead;
 50            }
 51         }
 52
 53         public override Int32 Read()
 54         {
 055            var retVal = this._reader.Read();
 056            if ( retVal != -1 )
 57            {
 058               ++this._charsRead;
 59            }
 060            return retVal;
 61         }
 62
 63         public override Int32 Peek()
 64         {
 065            return this._reader.Peek();
 66         }
 67
 68         protected override void Dispose( bool disposing )
 69         {
 70            // Do nothing - we don't want to close underlying reader.
 071         }
 72      }
 73
 74      // Returns amount of characters read
 75      internal static async ValueTask<Int32[]> ParseStringForNextSQLStatement(
 76         TReader reader,
 77         Boolean standardConformingStrings,
 78         Func<Int32> onParameter
 79         )
 80      {
 6281         var parenthesisLevel = 0;
 6282         List<Int32> paramIndicesList = null;
 6283         var queryEndEncountered = false;
 12484         Char? prev1 = null, prev2 = null;
 85         Char? c;
 86
 167187         while ( !queryEndEncountered && ( c = await reader.TryReadNextAsync() ).HasValue )
 88         {
 160989            switch ( c )
 90            {
 91               case '\'':
 9092                  await ParseSingleQuotes( reader, standardConformingStrings, prev1, prev2 );
 9093                  break;
 94               case '"':
 095                  await ParseDoubleQuotes( reader );
 096                  break;
 97               case '-':
 098                  await ParseLineComment( reader );
 099                  break;
 100               case '/':
 0101                  await ParseBlockComment( reader );
 0102                  break;
 103               case '$':
 0104                  await ParseDollarQuotes( reader, prev1 );
 0105                  break;
 106               case '(':
 28107                  ++parenthesisLevel;
 28108                  break;
 109               case ')':
 24110                  --parenthesisLevel;
 24111                  break;
 112               case '?':
 14113                  if ( onParameter != null )
 114                  {
 14115                     if ( paramIndicesList == null )
 116                     {
 10117                        paramIndicesList = new List<Int32>();
 118                     }
 14119                     paramIndicesList.Add( onParameter() );
 120                  }
 14121                  break;
 122               case ';':
 2123                  if ( parenthesisLevel == 0 )
 124                  {
 2125                     queryEndEncountered = true;
 126                  }
 127                  break;
 128            }
 1609129            prev2 = prev1;
 1609130            prev1 = c;
 131
 132         }
 133
 62134         return paramIndicesList == null ? null : paramIndicesList.ToArray();
 135
 62136      }
 137
 138
 139      // See http://www.postgresql.org/docs/9.1/static/sql-syntax-lexical.html for String Constants with C-style Escapes
 140      // Returns index of the single quote character ending this single quote sequence
 141      internal static async ValueTask<Boolean> ParseSingleQuotes(
 142         TReader reader,
 143         Boolean standardConformingStrings,
 144         Char? prev1,
 145         Char? prev2
 146         )
 147      {
 148         Char? c;
 90149         if ( !standardConformingStrings
 90150            && prev1.HasValue
 90151            && prev2.HasValue
 90152            && ( prev1 == 'e' || prev1 == 'E' )
 90153            && CharTerminatesIdentifier( prev2.Value )
 90154            )
 155         {
 156            // C-Style escaping
 157            // Treat backslashes as escape character
 0158            Char prev = '\0';
 0159            while ( ( c = await reader.TryReadNextAsync() ).HasValue )
 160            {
 0161               if ( c != '\\' && prev != '\\' && await CheckSingleQuote( reader, c.Value ) )
 162               {
 163                  break;
 164               }
 0165               prev = c.Value;
 166            }
 0167         }
 168         else
 169         {
 170            // Don't treat backslashes as escape character
 1063171            while ( ( c = await reader.TryReadNextAsync() ).HasValue && !await CheckSingleQuote( reader, c.Value ) ) ;
 172         }
 173
 90174         return true;
 90175      }
 176
 177      internal static async ValueTask<Boolean> ParseDoubleQuotes(
 178         TReader reader
 179         )
 180      {
 181         Char? c;
 0182         while ( ( c = await reader.TryReadNextAsync() ).HasValue )
 183         {
 0184            if ( c == '"' )
 185            {
 186               // Check for double-doublequote
 0187               if ( ( await reader.TryPeekAsync() ).IsOfValue( '"' ) )
 188               {
 0189                  await reader.ReadNextAsync();
 190               }
 191               else
 192               {
 193                  break;
 194               }
 195            }
 196         }
 197
 0198         return true;
 0199      }
 200
 201      internal static async ValueTask<Boolean> ParseLineComment(
 202         TReader reader
 203         )
 204      {
 0205         if ( ( await reader.TryPeekAsync() ).IsOfValue( '-' ) )
 206         {
 207            // Line comment starting
 208            Char? c;
 0209            while ( ( c = await reader.TryReadNextAsync() ).HasValue && c != '\r' && c != '\n' ) ;
 210         }
 0211         return true;
 0212      }
 213
 214
 215      internal static async ValueTask<Boolean> ParseBlockComment(
 216         TReader reader
 217         )
 218      {
 0219         if ( ( await reader.TryPeekAsync() ).IsOfValue( '*' ) )
 220         {
 221            // Block comment starting
 222            // SQL spec says block comments nest
 0223            var level = 1;
 0224            await reader.ReadNextAsync();
 0225            Char? prev = null;
 0226            Char? cur = null;
 0227            var levelChanged = false;
 0228            while ( level != 0 && ( cur = await reader.ReadNextAsync() ).HasValue )
 229            {
 0230               var oldLevel = level;
 0231               if ( !levelChanged ) // Don't process '*/*' or '/*/' twice
 232               {
 0233                  if ( prev.HasValue )
 234                  {
 0235                     if ( prev == '*' && cur == '/' )
 236                     {
 237                        // Block comment ending
 0238                        --level;
 0239                     }
 0240                     else if ( prev == '/' && cur == '*' )
 241                     {
 242                        // Nested block comment
 0243                        ++level;
 244                     }
 245                  }
 246               }
 247
 0248               levelChanged = level != oldLevel;
 0249               prev = cur;
 250            }
 0251         }
 252
 0253         return true;
 0254      }
 255
 256      // See http://www.postgresql.org/docs/9.1/static/sql-syntax-lexical.html for dollar quote spec
 257      internal static async ValueTask<Boolean> ParseDollarQuotes(
 258         TReader reader,
 259         Char? prev
 260         )
 261      {
 0262         var c = await reader.TryPeekAsync();
 0263         if ( c.HasValue && ( !prev.HasValue || !IsIdentifierContinuationCharacter( prev.Value ) ) )
 264         {
 0265            Char[] tag = null;
 0266            if ( c == '$' )
 267            {
 0268               tag = Empty<Char>.Array;
 0269            }
 0270            else if ( IsDollarQuoteTagStartCharacter( c.Value ) )
 271            {
 0272               var list = new List<Char>();
 0273               while ( ( c = await reader.TryPeekAsync() ).HasValue )
 274               {
 0275                  if ( c == '$' )
 276                  {
 0277                     tag = list.ToArray();
 0278                     break;
 279                  }
 0280                  else if ( !IsDollarQuoteTagContinuationCharacter( c.Value ) )
 281                  {
 282                     break;
 283                  }
 284                  else
 285                  {
 0286                     list.Add( await reader.ReadNextAsync() );
 287                  }
 288               }
 0289            }
 290
 0291            if ( tag != null )
 292            {
 293               // Read the tag-ending dollar sign
 0294               await reader.ReadNextAsync();
 0295               var tagLen = tag.Length;
 296
 0297               var isEmptyTag = tagLen == 0;
 0298               var array = isEmptyTag ? null : new Char[tagLen];
 0299               var arrayIdx = tagLen - 1;
 0300               while ( ( c = await reader.TryReadNextAsync() ).HasValue )
 301               {
 0302                  if ( c == '$' )
 303                  {
 304                     // Check if this is double-dollar-sign for empty tag, or that previous characters are same as tag
 0305                     if ( isEmptyTag && prev == '$' )
 306                     {
 307                        break;
 308                     }
 0309                     else if ( !isEmptyTag && CheckForCircularlyFilledArray( tag, tagLen, array, arrayIdx ) )
 310                     {
 311                        break;
 312                     }
 313                  }
 314
 0315                  if ( !isEmptyTag )
 316                  {
 0317                     if ( tag.Length > 1 )
 318                     {
 0319                        if ( arrayIdx == tag.Length - 1 )
 320                        {
 0321                           arrayIdx = 0;
 0322                        }
 323                        else
 324                        {
 0325                           ++arrayIdx;
 326                        }
 327                     }
 0328                     array[arrayIdx] = (Char) c;
 329                  }
 330
 0331                  prev = c;
 332               }
 0333            }
 0334         }
 335
 0336         return true;
 0337      }
 338
 339      // Returns true if this character ends string literal
 340      private static async ValueTask<Boolean> CheckSingleQuote(
 341         TReader reader,
 342         Char prevChar
 343         )
 344      {
 1063345         var retVal = prevChar == '\'';
 1063346         if ( retVal )
 347         {
 348            Char? peek;
 90349            if ( ( peek = await reader.TryPeekAsync() ).HasValue )
 350            {
 351               // Check for double quotes
 89352               if ( peek == '\'' )
 353               {
 0354                  await reader.ReadNextAsync();
 0355                  retVal = false;
 0356               }
 89357               else if ( peek == '\n' || peek == '\r' )
 358               {
 359                  // Check for newline-separated string literal ( http://www.postgresql.org/docs/9.1/static/sql-syntax-l
 0360                  while ( peek.HasValue && peek == '\n' || peek == '\r' )
 361                  {
 0362                     peek = await reader.ReadNextAsync();
 363                  }
 364
 0365                  if ( peek.HasValue && peek == '\'' )
 366                  {
 0367                     retVal = false;
 368                  }
 369               }
 370            }
 371         }
 372
 1063373         return retVal;
 1063374      }
 375
 376      // Returns true if character terminates identifier in backend parser
 377      private static Boolean CharTerminatesIdentifier( Char c )
 378      {
 0379         return c == '"' || IsSpace( c ) || IsOperatorChar( c );
 380      }
 381
 382      // The functions below must be kept in sync with logic of pgsql/src/backend/parser/scan.l
 383
 384      // Returns true if character is treated as space character in backend parser
 385      internal static Boolean IsSpace( Char c )
 386      {
 0387         return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f';
 388      }
 389
 390      // Returns true if the given character is a valid character for an operator in backend parser
 391      private static Boolean IsOperatorChar( Char c )
 392      {
 393         /*
 394          * Extracted from operators defined by {self} and {op_chars}
 395          * in pgsql/src/backend/parser/scan.l.
 396          */
 0397         return ",()[].;:+-*/%^<>=~!@#&|`?".IndexOf( c ) != -1;
 398      }
 399
 400      // Checks wehether character is valid as second or later character of an identifier
 401      private static Boolean IsIdentifierContinuationCharacter( Char c )
 402      {
 0403         return ( c >= 'a' && c <= 'z' )
 0404            || ( c >= 'A' && c <= 'Z' )
 0405            || c == '_'
 0406            || c > 127
 0407            || ( c >= '0' && c <= '9' )
 0408            || c == '$';
 409      }
 410
 411      // Checks wthether character is valid as first character of dollar quote tag
 412      private static Boolean IsDollarQuoteTagStartCharacter( Char c )
 413      {
 0414         return ( c >= 'a' && c <= 'z' )
 0415            || ( c >= 'A' && c <= 'Z' )
 0416            || c == '_'
 0417            || c > 127;
 418      }
 419
 420      // Checks whether character is valid as second or later character of dollar quote tag
 421      private static Boolean IsDollarQuoteTagContinuationCharacter( Char c )
 422      {
 0423         return ( c >= 'a' && c <= 'z' )
 0424            || ( c >= 'A' && c <= 'Z' )
 0425            || c == '_'
 0426            || ( c >= '0' && c <= '9' )
 0427            || c > 127;
 428
 429      }
 430
 431      // auxArrayIndex = index of last set character in auxArray
 432      internal static Boolean CheckForCircularlyFilledArray( Char[] referenceDataArray, Int32 refLen, Char[] auxArray, I
 433      {
 0434         var min = auxArrayIndex + 1 - refLen;
 0435         var i = refLen - 1;
 0436         if ( min >= 0 )
 437         {
 438            // Enough to check that last auxLen chars are same (do check backwards)
 0439            for ( var j = auxArrayIndex; i >= 0; --i, --j )
 440            {
 0441               if ( referenceDataArray[i] != auxArray[j] )
 442               {
 0443                  return false;
 444               }
 445            }
 0446         }
 447         else
 448         {
 0449            var j = auxArrayIndex;
 0450            for ( ; j >= 0; --j, --i )
 451            {
 0452               if ( referenceDataArray[i] != auxArray[j] )
 453               {
 0454                  return false;
 455               }
 456            }
 457
 0458            for ( j = auxArray.Length - 1; i >= 0; --i, --j )
 459            {
 0460               if ( referenceDataArray[i] != auxArray[j] )
 461               {
 0462                  return false;
 463               }
 464            }
 465         }
 466
 0467         return true;
 468      }
 469   }
 470}
 471
 472public static partial class E_CBAM
 473{
 474   internal static Boolean IsOfValue( this Char? nullable, Char value )
 475   {
 476      return nullable.HasValue && nullable.Value == value;
 477   }
 478}