Example #1
0
 void MoveToNextStatement()
 {
     _statementIndex++;
     if (_statements.Count > _statementIndex)
     {
         _statement = _statements[_statementIndex];
         _statement.Reset();
     }
     else
     {
         _statement = new NpgsqlStatement();
         _statements.Add(_statement);
     }
     _paramIndexMap.Clear();
     _rewrittenSql.Clear();
 }
Example #2
0
        internal PreparedStatement GetOrAddExplicit(NpgsqlStatement statement)
        {
            var sql = statement.SQL;

            PreparedStatement pStatement, statementBeingReplaced = null;

            if (BySql.TryGetValue(sql, out pStatement))
            {
                Debug.Assert(pStatement.State != PreparedState.Unprepared);
                if (pStatement.IsExplicit)
                {
                    // Great, we've found an explicit prepared statement.
                    // We just need to check that the parameter types correspond, since prepared statements are
                    // only keyed by SQL (to prevent pointless allocations). If we have a mismatch, simply run unprepared.
                    return(pStatement.DoParametersMatch(statement.InputParameters)
                        ? pStatement
                        : null);
                }

                // We've found an autoprepare statement (candidate or otherwise)
                switch (pStatement.State)
                {
                case PreparedState.NotYetPrepared:
                    // Found a candidate for autopreparation. Remove it and prepare explicitly.
                    RemoveCandidate(pStatement);
                    break;

                case PreparedState.Prepared:
                    // The statement has already been autoprepared. We need to "promote" it to explicit.
                    statementBeingReplaced = pStatement;
                    break;

                case PreparedState.BeingPrepared:
                    throw new InvalidOperationException($"Found autoprepare statement in state {nameof(PreparedState.BeingPrepared)}");

                case PreparedState.Unprepared:
                    throw new InvalidOperationException($"Found unprepared statement in {nameof(PreparedStatementManager)}");

                default:
                    throw new ArgumentOutOfRangeException();
                }
            }

            // Statement hasn't been prepared yet
            return(BySql[sql] = PreparedStatement.CreateExplicit(this, sql, NextPreparedStatementName(), statement.InputParameters, statementBeingReplaced));
        }
Example #3
0
        void ParseRawQuery(
            ReadOnlySpan <char> sql,
            NpgsqlParameterCollection parameters,
            List <NpgsqlStatement> statements,
            bool deriveParameters)
        {
            Debug.Assert(deriveParameters == false || parameters.Count == 0);

            NpgsqlStatement statement      = null !;
            var             statementIndex = -1;

            MoveToNextStatement();

            var currCharOfs = 0;
            var end         = sql.Length;
            var ch          = '\0';
            int dollarTagStart;
            int dollarTagEnd;
            var currTokenBeg      = 0;
            var blockCommentLevel = 0;
            var parenthesisLevel  = 0;

None:
            if (currCharOfs >= end)
            {
                goto Finish;
            }
            var lastChar = ch;

            ch = sql[currCharOfs++];
NoneContinue:
            for (; ; lastChar = ch, ch = sql[currCharOfs++])
            {
                switch (ch)
                {
                case '/':
                    goto BlockCommentBegin;

                case '-':
                    goto LineCommentBegin;

                case '\'':
                    goto Quoted;

                case '$':
                    if (!IsIdentifier(lastChar))
                    {
                        goto DollarQuotedStart;
                    }
                    else
                    {
                        break;
                    }

                case '"':
                    goto DoubleQuoted;

                case ':':
                    if (lastChar != ':')
                    {
                        goto ParamStart;
                    }
                    else
                    {
                        break;
                    }

                case '@':
                    if (lastChar != '@')
                    {
                        goto ParamStart;
                    }
                    else
                    {
                        break;
                    }

                case ';':
                    if (parenthesisLevel == 0)
                    {
                        goto SemiColon;
                    }
                    break;

                case '(':
                    parenthesisLevel++;
                    break;

                case ')':
                    parenthesisLevel--;
                    break;

                case 'e':
                case 'E':
                    if (!IsLetter(lastChar))
                    {
                        goto EscapedStart;
                    }
                    else
                    {
                        break;
                    }
                }

                if (currCharOfs >= end)
                {
                    goto Finish;
                }
            }

ParamStart:
            if (currCharOfs < end)
            {
                lastChar = ch;
                ch       = sql[currCharOfs];
                if (IsParamNameChar(ch))
                {
                    if (currCharOfs - 1 > currTokenBeg)
                    {
                        _rewrittenSql.Append(sql.Slice(currTokenBeg, currCharOfs - 1 - currTokenBeg));
                    }
                    currTokenBeg = currCharOfs++ - 1;
                    goto Param;
                }
                currCharOfs++;
                goto NoneContinue;
            }
            goto Finish;

Param:
            // We have already at least one character of the param name
            for (;;)
            {
                lastChar = ch;
                if (currCharOfs >= end || !IsParamNameChar(ch = sql[currCharOfs]))
                {
                    var paramName = sql.Slice(currTokenBeg + 1, currCharOfs - (currTokenBeg + 1)).ToString();

                    if (!_paramIndexMap.TryGetValue(paramName, out var index))
                    {
                        // Parameter hasn't been seen before in this query
                        if (!parameters.TryGetValue(paramName, out var parameter))
                        {
                            if (deriveParameters)
                            {
                                parameter = new NpgsqlParameter {
                                    ParameterName = paramName
                                };
                                parameters.Add(parameter);
                            }
                            else
                            {
                                // Parameter placeholder does not match a parameter on this command.
                                // Leave the text as it was in the SQL, it may not be a an actual placeholder
                                _rewrittenSql.Append(sql.Slice(currTokenBeg, currCharOfs - currTokenBeg));
                                currTokenBeg = currCharOfs;
                                if (currCharOfs >= end)
                                {
                                    goto Finish;
                                }

                                currCharOfs++;
                                goto NoneContinue;
                            }
                        }

                        if (!parameter.IsInputDirection)
                        {
                            throw new Exception($"Parameter '{paramName}' referenced in SQL but is an out-only parameter");
                        }

                        statement.InputParameters.Add(parameter);
                        index = _paramIndexMap[paramName] = statement.InputParameters.Count;
                    }
                    _rewrittenSql.Append('$');
                    _rewrittenSql.Append(index);
                    currTokenBeg = currCharOfs;

                    if (currCharOfs >= end)
                    {
                        goto Finish;
                    }

                    currCharOfs++;
                    goto NoneContinue;
                }

                currCharOfs++;
            }

Quoted:
            while (currCharOfs < end)
            {
                if (sql[currCharOfs++] == '\'')
                {
                    ch = '\0';
                    goto None;
                }
            }
            goto Finish;

DoubleQuoted:
            while (currCharOfs < end)
            {
                if (sql[currCharOfs++] == '"')
                {
                    ch = '\0';
                    goto None;
                }
            }
            goto Finish;

EscapedStart:
            if (currCharOfs < end)
            {
                lastChar = ch;
                ch       = sql[currCharOfs++];
                if (ch == '\'')
                {
                    goto Escaped;
                }
                goto NoneContinue;
            }
            goto Finish;

Escaped:
            while (currCharOfs < end)
            {
                ch = sql[currCharOfs++];
                switch (ch)
                {
                case '\'':
                    goto MaybeConcatenatedEscaped;

                case '\\':
                {
                    if (currCharOfs >= end)
                    {
                        goto Finish;
                    }
                    currCharOfs++;
                    break;
                }
                }
            }
            goto Finish;

MaybeConcatenatedEscaped:
            while (currCharOfs < end)
            {
                ch = sql[currCharOfs++];
                switch (ch)
                {
                case '\r':
                case '\n':
                    goto MaybeConcatenatedEscaped2;

                case ' ':
                case '\t':
                case '\f':
                    continue;

                default:
                    lastChar = '\0';
                    goto NoneContinue;
                }
            }
            goto Finish;

MaybeConcatenatedEscaped2:
            while (currCharOfs < end)
            {
                ch = sql[currCharOfs++];
                switch (ch)
                {
                case '\'':
                    goto Escaped;

                case '-':
                {
                    if (currCharOfs >= end)
                    {
                        goto Finish;
                    }
                    ch = sql[currCharOfs++];
                    if (ch == '-')
                    {
                        goto MaybeConcatenatedEscapeAfterComment;
                    }
                    lastChar = '\0';
                    goto NoneContinue;
                }

                case ' ':
                case '\t':
                case '\n':
                case '\r':
                case '\f':
                    continue;

                default:
                    lastChar = '\0';
                    goto NoneContinue;
                }
            }
            goto Finish;

MaybeConcatenatedEscapeAfterComment:
            while (currCharOfs < end)
            {
                ch = sql[currCharOfs++];
                if (ch == '\r' || ch == '\n')
                {
                    goto MaybeConcatenatedEscaped2;
                }
            }
            goto Finish;

DollarQuotedStart:
            if (currCharOfs < end)
            {
                ch = sql[currCharOfs];
                if (ch == '$')
                {
                    // Empty tag
                    dollarTagStart = dollarTagEnd = currCharOfs;
                    currCharOfs++;
                    goto DollarQuoted;
                }
                if (IsIdentifierStart(ch))
                {
                    dollarTagStart = currCharOfs;
                    currCharOfs++;
                    goto DollarQuotedInFirstDelim;
                }
                lastChar = '$';
                currCharOfs++;
                goto NoneContinue;
            }
            goto Finish;

DollarQuotedInFirstDelim:
            while (currCharOfs < end)
            {
                lastChar = ch;
                ch       = sql[currCharOfs++];
                if (ch == '$')
                {
                    dollarTagEnd = currCharOfs - 1;
                    goto DollarQuoted;
                }
                if (!IsDollarTagIdentifier(ch))
                {
                    goto NoneContinue;
                }
            }
            goto Finish;

DollarQuoted:
            var tag = sql.Slice(dollarTagStart - 1, dollarTagEnd - dollarTagStart + 2);
            var pos = sql.Slice(dollarTagEnd + 1).IndexOf(tag);

            if (pos == -1)
            {
                currCharOfs = end;
                goto Finish;
            }
            pos        += dollarTagEnd + 1; // If the substring is found adjust the position to be relative to the entire span
            currCharOfs = pos + dollarTagEnd - dollarTagStart + 2;
            ch          = '\0';
            goto None;

LineCommentBegin:
            if (currCharOfs < end)
            {
                ch = sql[currCharOfs++];
                if (ch == '-')
                {
                    goto LineComment;
                }
                lastChar = '\0';
                goto NoneContinue;
            }
            goto Finish;

LineComment:
            while (currCharOfs < end)
            {
                ch = sql[currCharOfs++];
                if (ch == '\r' || ch == '\n')
                {
                    goto None;
                }
            }
            goto Finish;

BlockCommentBegin:
            while (currCharOfs < end)
            {
                ch = sql[currCharOfs++];
                if (ch == '*')
                {
                    blockCommentLevel++;
                    goto BlockComment;
                }
                if (ch != '/')
                {
                    if (blockCommentLevel > 0)
                    {
                        goto BlockComment;
                    }
                    lastChar = '\0';
                    goto NoneContinue;
                }
            }
            goto Finish;

BlockComment:
            while (currCharOfs < end)
            {
                ch = sql[currCharOfs++];
                switch (ch)
                {
                case '*':
                    goto BlockCommentEnd;

                case '/':
                    goto BlockCommentBegin;
                }
            }
            goto Finish;

BlockCommentEnd:
            while (currCharOfs < end)
            {
                ch = sql[currCharOfs++];
                if (ch == '/')
                {
                    if (--blockCommentLevel > 0)
                    {
                        goto BlockComment;
                    }
                    goto None;
                }
                if (ch != '*')
                {
                    goto BlockComment;
                }
            }
            goto Finish;

SemiColon:
            _rewrittenSql.Append(sql.Slice(currTokenBeg, currCharOfs - currTokenBeg - 1));
            statement.SQL = _rewrittenSql.ToString();
            while (currCharOfs < end)
            {
                ch = sql[currCharOfs];
                if (char.IsWhiteSpace(ch))
                {
                    currCharOfs++;
                    continue;
                }
                // TODO: Handle end of line comment? Although psql doesn't seem to handle them...

                currTokenBeg = currCharOfs;
                if (_rewrittenSql.Length > 0)
                {
                    MoveToNextStatement();
                }
                goto None;
            }
            if (statements.Count > statementIndex + 1)
            {
                statements.RemoveRange(statementIndex + 1, statements.Count - (statementIndex + 1));
            }
            return;

Finish:
            _rewrittenSql.Append(sql.Slice(currTokenBeg, end - currTokenBeg));
            statement.SQL = _rewrittenSql.ToString();
            if (statements.Count > statementIndex + 1)
            {
                statements.RemoveRange(statementIndex + 1, statements.Count - (statementIndex + 1));
            }

            void MoveToNextStatement()
            {
                statementIndex++;
                if (statements.Count > statementIndex)
                {
                    statement = statements[statementIndex];
                    statement.Reset();
                }
                else
                {
                    statement = new NpgsqlStatement();
                    statements.Add(statement);
                }
                _paramIndexMap.Clear();
                _rewrittenSql.Clear();
            }
        }
Example #4
0
        internal PreparedStatement TryGetAutoPrepared(NpgsqlStatement statement)
        {
            Debug.Assert(_candidates != null);

            var sql = statement.SQL;
            PreparedStatement pStatement;

            if (!BySql.TryGetValue(sql, out pStatement))
            {
                // New candidate. Find an empty candidate slot or eject a least-used one.
                int slotIndex = -1, leastUsages = int.MaxValue;
                var lastUsed = DateTime.MaxValue;
                for (var i = 0; i < _candidates.Length; i++)
                {
                    var candidate = _candidates[i];
                    // ReSharper disable once ConditionIsAlwaysTrueOrFalse
                    // ReSharper disable HeuristicUnreachableCode
                    if (candidate == null)  // Found an unused candidate slot, return immediately
                    {
                        slotIndex = i;
                        break;
                    }
                    // ReSharper restore HeuristicUnreachableCode
                    if (candidate.Usages < leastUsages)
                    {
                        leastUsages = candidate.Usages;
                        slotIndex   = i;
                        lastUsed    = candidate.LastUsed;
                    }
                    else if (candidate.Usages == leastUsages && candidate.LastUsed < lastUsed)
                    {
                        slotIndex = i;
                        lastUsed  = candidate.LastUsed;
                    }
                }

                var leastUsed = _candidates[slotIndex];
                // ReSharper disable once ConditionIsAlwaysTrueOrFalse
                if (leastUsed != null)
                {
                    BySql.Remove(leastUsed.Sql);
                }
                pStatement = BySql[sql] = _candidates[slotIndex] = PreparedStatement.CreateAutoPrepareCandidate(this, sql);
            }

            if (pStatement.IsPrepared)
            {
                // The statement has already been prepared (explicitly or automatically).
                // We just need to check that the parameter types correspond, since prepared statements are
                // only keyed by SQL (to prevent pointless allocations). If we have a mismatch, simply run unprepared.
                return(pStatement.DoParametersMatch(statement.InputParameters)
                    ? pStatement
                    : null);
            }

            if (++pStatement.Usages < UsagesBeforePrepare)
            {
                // Statement still hasn't passed the usage threshold, no automatic preparation.
                // Return null for unprepared exection.
                pStatement.LastUsed = DateTime.UtcNow;
                return(null);
            }

            // Bingo, we've just passed the usage threshold, statement should get prepared
            Log.AutoPreparing(_connector.Id, sql);

            RemoveCandidate(pStatement);

            if (_numAutoPrepared < MaxAutoPrepared)
            {
                // We still have free slots
                _autoPrepared[_numAutoPrepared++] = pStatement;
                pStatement.Name = "_auto" + _numAutoPrepared;
            }
            else
            {
                // We already have the maximum number of prepared statements.
                // Find the least recently used prepared statement and replace it.
                var oldestTimestamp = DateTime.MaxValue;
                var oldestIndex     = -1;
                for (var i = 0; i < _autoPrepared.Length; i++)
                {
                    if (_autoPrepared[i].LastUsed < oldestTimestamp)
                    {
                        oldestIndex     = i;
                        oldestTimestamp = _autoPrepared[i].LastUsed;
                    }
                }
                var lru = _autoPrepared[oldestIndex];
                pStatement.Name = lru.Name;
                pStatement.StatementBeingReplaced = lru;
                _autoPrepared[oldestIndex]        = pStatement;
            }

            // Note that the parameter types are only set at the moment of preparation - in the candidate phase
            // there's no differentiation between overloaded statements, which are a pretty rare case, saving
            // allocations.
            pStatement.SetParamTypes(statement.InputParameters);

            return(pStatement);
        }
Example #5
0
        internal PreparedStatement?TryGetAutoPrepared(NpgsqlStatement statement)
        {
            var sql = statement.SQL;

            if (!BySql.TryGetValue(sql, out var pStatement))
            {
                // New candidate. Find an empty candidate slot or eject a least-used one.
                int slotIndex = -1, leastUsages = int.MaxValue;
                var lastUsed = DateTime.MaxValue;
                for (var i = 0; i < _candidates.Length; i++)
                {
                    var candidate = _candidates[i];
                    // ReSharper disable once ConditionIsAlwaysTrueOrFalse
                    // ReSharper disable HeuristicUnreachableCode
                    if (candidate == null)  // Found an unused candidate slot, return immediately
                    {
                        slotIndex = i;
                        break;
                    }
                    // ReSharper restore HeuristicUnreachableCode
                    if (candidate.Usages < leastUsages)
                    {
                        leastUsages = candidate.Usages;
                        slotIndex   = i;
                        lastUsed    = candidate.LastUsed;
                    }
                    else if (candidate.Usages == leastUsages && candidate.LastUsed < lastUsed)
                    {
                        slotIndex = i;
                        lastUsed  = candidate.LastUsed;
                    }
                }

                var leastUsed = _candidates[slotIndex];
                // ReSharper disable once ConditionIsAlwaysTrueOrFalse
                if (leastUsed != null)
                {
                    BySql.Remove(leastUsed.Sql);
                }
                pStatement = BySql[sql] = _candidates[slotIndex] = PreparedStatement.CreateAutoPrepareCandidate(this, sql);
            }

            switch (pStatement.State)
            {
            case PreparedState.NotPrepared:
                break;

            case PreparedState.Prepared:
            case PreparedState.BeingPrepared:
                // The statement has already been prepared (explicitly or automatically), or has been selected
                // for preparation (earlier identical statement in the same command).
                // We just need to check that the parameter types correspond, since prepared statements are
                // only keyed by SQL (to prevent pointless allocations). If we have a mismatch, simply run unprepared.
                if (!pStatement.DoParametersMatch(statement.InputParameters))
                {
                    return(null);
                }
                // Prevent this statement from being replaced within this batch
                pStatement.LastUsed = DateTime.MaxValue;
                return(pStatement);

            case PreparedState.BeingUnprepared:
                // The statement is being replaced by an earlier statement in this same batch.
                return(null);

            default:
                Debug.Fail($"Unexpected {nameof(PreparedState)} in auto-preparation: {pStatement.State}");
                break;
            }

            if (++pStatement.Usages < UsagesBeforePrepare)
            {
                // Statement still hasn't passed the usage threshold, no automatic preparation.
                // Return null for unprepared execution.
                pStatement.LastUsed = DateTime.UtcNow;
                return(null);
            }

            // Bingo, we've just passed the usage threshold, statement should get prepared
            Log.Trace($"Automatically preparing statement: {sql}", _connector.Id);

            if (_numAutoPrepared < MaxAutoPrepared)
            {
                // We still have free slots
                _autoPrepared[_numAutoPrepared++] = pStatement;
                pStatement.Name = "_auto" + _numAutoPrepared;
            }
            else
            {
                // We already have the maximum number of prepared statements.
                // Find the least recently used prepared statement and replace it.
                var oldestTimestamp = DateTime.MaxValue;
                var oldestIndex     = -1;
                for (var i = 0; i < _autoPrepared.Length; i++)
                {
                    if (_autoPrepared[i].LastUsed < oldestTimestamp)
                    {
                        oldestIndex     = i;
                        oldestTimestamp = _autoPrepared[i].LastUsed;
                    }
                }

                if (oldestIndex == -1)
                {
                    // We're here if we couldn't find a prepared statement to replace, because all of them are already
                    // being prepared in this batch.
                    return(null);
                }

                var lru = _autoPrepared[oldestIndex];
                pStatement.Name = lru.Name;
                pStatement.StatementBeingReplaced = lru;
                lru.State = PreparedState.BeingUnprepared;
                _autoPrepared[oldestIndex] = pStatement;
            }

            RemoveCandidate(pStatement);

            // Make sure this statement isn't replaced by a later statement in the same batch.
            pStatement.LastUsed = DateTime.MaxValue;

            // Note that the parameter types are only set at the moment of preparation - in the candidate phase
            // there's no differentiation between overloaded statements, which are a pretty rare case, saving
            // allocations.
            pStatement.SetParamTypes(statement.InputParameters);

            return(pStatement);
        }
Example #6
0
        void ProcessRawQuery()
        {
            NpgsqlStatement statement;

            switch (CommandType)
            {
            case CommandType.Text:
                Debug.Assert(_connection?.Connector != null);
                _connection.Connector.SqlParser.ParseRawQuery(CommandText, _connection == null || _connection.UseConformantStrings, _parameters, _statements);
                if (_statements.Count > 1 && _parameters.HasOutputParameters)
                {
                    throw new NotSupportedException("Commands with multiple queries cannot have out parameters");
                }
                break;

            case CommandType.TableDirect:
                if (_statements.Count == 0)
                {
                    statement = new NpgsqlStatement();
                }
                else
                {
                    statement = _statements[0];
                    statement.Reset();
                    _statements.Clear();
                }
                _statements.Add(statement);
                statement.SQL = "SELECT * FROM " + CommandText;
                break;

            case CommandType.StoredProcedure:
                var inputList = _parameters.Where(p => p.IsInputDirection).ToList();
                var numInput  = inputList.Count;
                var sb        = new StringBuilder();
                sb.Append("SELECT * FROM ");
                sb.Append(CommandText);
                sb.Append('(');
                bool hasWrittenFirst = false;
                for (var i = 1; i <= numInput; i++)
                {
                    var param = inputList[i - 1];
                    if (param.AutoAssignedName || param.CleanName == "")
                    {
                        if (hasWrittenFirst)
                        {
                            sb.Append(',');
                        }
                        sb.Append('$');
                        sb.Append(i);
                        hasWrittenFirst = true;
                    }
                }
                for (var i = 1; i <= numInput; i++)
                {
                    var param = inputList[i - 1];
                    if (!param.AutoAssignedName && param.CleanName != "")
                    {
                        if (hasWrittenFirst)
                        {
                            sb.Append(',');
                        }
                        sb.Append('"');
                        sb.Append(param.CleanName.Replace("\"", "\"\""));
                        sb.Append("\" := ");
                        sb.Append('$');
                        sb.Append(i);
                        hasWrittenFirst = true;
                    }
                }
                sb.Append(')');

                if (_statements.Count == 0)
                {
                    statement = new NpgsqlStatement();
                }
                else
                {
                    statement = _statements[0];
                    statement.Reset();
                    _statements.Clear();
                }
                statement.SQL = sb.ToString();
                statement.InputParameters.AddRange(inputList);
                _statements.Add(statement);
                break;

            default:
                throw new InvalidOperationException($"Internal Npgsql bug: unexpected value {CommandType} of enum {nameof(CommandType)}. Please file a bug.");
            }

            foreach (var s in _statements)
            {
                if (s.InputParameters.Count > 65535)
                {
                    throw new Exception("A statement cannot have more than 65535 parameters");
                }
            }

            _isParsed = true;
        }