void MoveToNextStatement() { _statementIndex++; if (_statements.Count > _statementIndex) { _statement = _statements[_statementIndex]; _statement.Reset(); } else { _statement = new NpgsqlStatement(); _statements.Add(_statement); } _paramIndexMap.Clear(); _rewrittenSql.Clear(); }
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)); }
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(); } }
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); }
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); }
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; }