Implement SqliteCommand.Prepare.

This commit is contained in:
Alexander Täschner 2017-07-11 14:54:04 -07:00 коммит произвёл Brice Lambson
Родитель e43d841248
Коммит 6eaf137519
6 изменённых файлов: 572 добавлений и 121 удалений

4
.gitattributes поставляемый
Просмотреть файл

@ -1,2 +1,4 @@
* text=auto
*.sh text=auto eol=lf
*.cs diff=csharp
*.sh eol=lf
*.sln eol=crlf

309
.gitignore поставляемый
Просмотреть файл

@ -1,28 +1,291 @@
.DS_Store
*.db
*.db-journal
/.build/
/global.json
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.suo
*.user
*.iml
/.nuget/
/.vscode/
/.dotnet/
/.idea/
/.vs/
/.vscode/
/artifacts/
/packages/
bin/
obj/
*.userosscache
*.sln.docstates
*.user.sln*
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
# Visual Studio 2015 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# .NET Core
project.lock.json
.settings
*.sublime-*
*.pfx
launchSettings.json
/.build/
.testPublish/
*.project.lock.json
project.fragment.lock.json
artifacts/
**/Properties/launchSettings.json
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# TODO: Comment the next line if you want to checkin your web deploy settings
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/packages/*
# except build/, which is used as an MSBuild target.
!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/packages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
sqlite3.dll
global.json
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
*.mdf
*.ldf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Typescript v1 declaration files
typings/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# JetBrains Rider
.idea/
*.sln.iml
# CodeRush
.cr/
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs

Просмотреть файл

@ -21,6 +21,9 @@ namespace Microsoft.Data.Sqlite
{
private readonly Lazy<SqliteParameterCollection> _parameters = new Lazy<SqliteParameterCollection>(
() => new SqliteParameterCollection());
private readonly ICollection<sqlite3_stmt> _preparedStatements = new List<sqlite3_stmt>();
private SqliteConnection _connection;
private string _commandText;
/// <summary>
/// Initializes a new instance of the <see cref="SqliteCommand" /> class.
@ -76,13 +79,38 @@ namespace Microsoft.Data.Sqlite
/// Gets or sets the SQL to execute against the database.
/// </summary>
/// <value>The SQL to execute against the database.</value>
public override string CommandText { get; set; }
public override string CommandText
{
get => _commandText;
set
{
if (!value.Equals(_commandText))
{
DisposePreparedStatements();
_commandText = value;
}
}
}
/// <summary>
/// Gets or sets the connection used by the command.
/// </summary>
/// <value>The connection used by the command.</value>
public new virtual SqliteConnection Connection { get; set; }
public new virtual SqliteConnection Connection
{
get => _connection;
set
{
if (value != _connection)
{
DisposePreparedStatements();
_connection?.RemoveCommand(this);
_connection = value;
value?.AddCommand(this);
}
}
}
/// <summary>
/// Gets or sets the connection used by the command. Must be a <see cref="SqliteConnection" />.
@ -145,6 +173,19 @@ namespace Microsoft.Data.Sqlite
/// <value>A value indicating how the results are applied to the row being updated.</value>
public override UpdateRowSource UpdatedRowSource { get; set; }
/// <summary>
/// Releases any resources used by the connection and closes it.
/// </summary>
/// <param name="disposing">
/// true to release managed and unmanaged resources; false to release only unmanaged resources.
/// </param>
protected override void Dispose(bool disposing)
{
DisposePreparedStatements();
base.Dispose(disposing);
}
/// <summary>
/// Creates a new parameter.
/// </summary>
@ -164,6 +205,27 @@ namespace Microsoft.Data.Sqlite
/// </summary>
public override void Prepare()
{
if (_connection?.State != ConnectionState.Open)
{
throw new InvalidOperationException(Resources.CallRequiresOpenConnection(nameof(Prepare)));
}
if (string.IsNullOrEmpty(_commandText))
{
throw new InvalidOperationException(Resources.CallRequiresSetCommandText(nameof(Prepare)));
}
if (_preparedStatements.Count != 0)
{
return;
}
using (var enumerator = PrepareAndEnumerateStatements().GetEnumerator())
{
while (enumerator.MoveNext())
{
}
}
}
/// <summary>
@ -193,17 +255,17 @@ namespace Microsoft.Data.Sqlite
throw new ArgumentException(Resources.InvalidCommandBehavior(behavior));
}
if (Connection?.State != ConnectionState.Open)
if (_connection?.State != ConnectionState.Open)
{
throw new InvalidOperationException(Resources.CallRequiresOpenConnection(nameof(ExecuteReader)));
}
if (string.IsNullOrEmpty(CommandText))
if (string.IsNullOrEmpty(_commandText))
{
throw new InvalidOperationException(Resources.CallRequiresSetCommandText(nameof(ExecuteReader)));
}
if (Transaction != Connection.Transaction)
if (Transaction != _connection.Transaction)
{
throw new InvalidOperationException(
Transaction == null
@ -213,88 +275,59 @@ namespace Microsoft.Data.Sqlite
// This is not a guarantee. SQLITE_BUSY can still be thrown before the command timeout.
// This sets a timeout handler but this can be cleared by concurrent commands.
raw.sqlite3_busy_timeout(Connection.Handle, CommandTimeout * 1000);
raw.sqlite3_busy_timeout(_connection.Handle, CommandTimeout * 1000);
var hasChanges = false;
var changes = 0;
var stmts = new Queue<(sqlite3_stmt stmt, bool)>();
var tail = CommandText;
int rc;
var stmts = new Queue<(sqlite3_stmt, bool)>();
do
foreach (var stmt in _preparedStatements.Count == 0
? PrepareAndEnumerateStatements()
: ResetAndEnumerateStatements())
{
var rc = raw.sqlite3_prepare_v2(
Connection.Handle,
tail,
out var stmt,
out tail);
SqliteException.ThrowExceptionForRC(rc, Connection.Handle);
var boundParams = 0;
// Statement was empty, white space, or a comment
if (stmt.ptr == IntPtr.Zero)
if (_parameters.IsValueCreated)
{
if (!string.IsNullOrEmpty(tail))
{
continue;
}
break;
boundParams = _parameters.Value.Bind(stmt);
}
try
var expectedParams = raw.sqlite3_bind_parameter_count(stmt);
if (expectedParams != boundParams)
{
var boundParams = 0;
if (_parameters.IsValueCreated)
var unboundParams = new List<string>();
for (var i = 1; i <= expectedParams; i++)
{
boundParams = _parameters.Value.Bind(stmt);
}
var name = raw.sqlite3_bind_parameter_name(stmt, i);
var expectedParams = raw.sqlite3_bind_parameter_count(stmt);
if (expectedParams != boundParams)
{
var unboundParams = new List<string>();
for (var i = 1; i <= expectedParams; i++)
if (_parameters.IsValueCreated
||
!_parameters.Value.Cast<SqliteParameter>().Any(p => p.ParameterName == name))
{
var name = raw.sqlite3_bind_parameter_name(stmt, i);
if (_parameters.IsValueCreated
||
!_parameters.Value.Cast<SqliteParameter>().Any(p => p.ParameterName == name))
{
unboundParams.Add(name);
}
unboundParams.Add(name);
}
throw new InvalidOperationException(Resources.MissingParameters(string.Join(", ", unboundParams)));
}
var timer = Stopwatch.StartNew();
while (raw.SQLITE_LOCKED == (rc = raw.sqlite3_step(stmt)) || rc == raw.SQLITE_BUSY)
{
if (timer.ElapsedMilliseconds >= CommandTimeout * 1000)
{
break;
}
raw.sqlite3_reset(stmt);
// TODO: Consider having an async path that uses Task.Delay()
Thread.Sleep(150);
}
SqliteException.ThrowExceptionForRC(rc, Connection.Handle);
throw new InvalidOperationException(Resources.MissingParameters(string.Join(", ", unboundParams)));
}
catch
var timer = Stopwatch.StartNew();
while (raw.SQLITE_LOCKED == (rc = raw.sqlite3_step(stmt)) || rc == raw.SQLITE_BUSY)
{
stmt.Dispose();
while (stmts.Count != 0)
if (timer.ElapsedMilliseconds >= CommandTimeout * 1000)
{
stmts.Dequeue().stmt.Dispose();
break;
}
throw;
raw.sqlite3_reset(stmt);
// TODO: Consider having an async path that uses Task.Delay()
Thread.Sleep(150);
}
SqliteException.ThrowExceptionForRC(rc, _connection.Handle);
if (rc == raw.SQLITE_ROW
// NB: This is only a heuristic to separate SELECT statements from INSERT/UPDATE/DELETE statements.
// It will result in false positives, but it's the best we can do without re-parsing SQL
@ -305,15 +338,13 @@ namespace Microsoft.Data.Sqlite
else
{
hasChanges = true;
changes += raw.sqlite3_changes(Connection.Handle);
stmt.Dispose();
changes += raw.sqlite3_changes(_connection.Handle);
}
}
while (!string.IsNullOrEmpty(tail));
var closeConnection = (behavior & CommandBehavior.CloseConnection) != 0;
return new SqliteDataReader(Connection, stmts, hasChanges ? changes : -1, closeConnection);
return new SqliteDataReader(this, stmts, hasChanges ? changes : -1, closeConnection);
}
/// <summary>
@ -396,11 +427,11 @@ namespace Microsoft.Data.Sqlite
/// <exception cref="SqliteException">A SQLite error occurs during execution.</exception>
public override int ExecuteNonQuery()
{
if (Connection?.State != ConnectionState.Open)
if (_connection?.State != ConnectionState.Open)
{
throw new InvalidOperationException(Resources.CallRequiresOpenConnection(nameof(ExecuteNonQuery)));
}
if (CommandText == null)
if (_commandText == null)
{
throw new InvalidOperationException(Resources.CallRequiresSetCommandText(nameof(ExecuteNonQuery)));
}
@ -418,11 +449,11 @@ namespace Microsoft.Data.Sqlite
/// <exception cref="SqliteException">A SQLite error occurs during execution.</exception>
public override object ExecuteScalar()
{
if (Connection?.State != ConnectionState.Open)
if (_connection?.State != ConnectionState.Open)
{
throw new InvalidOperationException(Resources.CallRequiresOpenConnection(nameof(ExecuteScalar)));
}
if (CommandText == null)
if (_commandText == null)
{
throw new InvalidOperationException(Resources.CallRequiresSetCommandText(nameof(ExecuteScalar)));
}
@ -441,5 +472,55 @@ namespace Microsoft.Data.Sqlite
public override void Cancel()
{
}
private IEnumerable<sqlite3_stmt> PrepareAndEnumerateStatements()
{
var tail = _commandText;
do
{
var rc = raw.sqlite3_prepare_v2(
_connection.Handle,
tail,
out sqlite3_stmt stmt,
out tail);
SqliteException.ThrowExceptionForRC(rc, _connection.Handle);
// Statement was empty, white space, or a comment
if (stmt.ptr == IntPtr.Zero)
{
if (!string.IsNullOrEmpty(tail))
{
continue;
}
break;
}
_preparedStatements.Add(stmt);
yield return stmt;
}
while (!string.IsNullOrEmpty(tail));
}
private IEnumerable<sqlite3_stmt> ResetAndEnumerateStatements()
{
foreach (var stmt in _preparedStatements)
{
raw.sqlite3_reset(stmt);
yield return stmt;
}
}
private void DisposePreparedStatements()
{
foreach (var stmt in _preparedStatements)
{
stmt.Dispose();
}
_preparedStatements.Clear();
}
}
}

Просмотреть файл

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Diagnostics;
@ -19,6 +20,8 @@ namespace Microsoft.Data.Sqlite
{
private const string MainDatabaseName = "main";
private readonly IList<WeakReference<SqliteCommand>> _commands = new List<WeakReference<SqliteCommand>>();
private string _connectionString;
private ConnectionState _state;
private sqlite3 _db;
@ -220,6 +223,17 @@ namespace Microsoft.Data.Sqlite
}
Transaction?.Dispose();
foreach (var reference in _commands)
{
if (reference.TryGetTarget(out var command))
{
command.Dispose();
}
}
_commands.Clear();
_db.Dispose2();
_db = null;
SetState(ConnectionState.Closed);
@ -265,6 +279,20 @@ namespace Microsoft.Data.Sqlite
protected override DbCommand CreateDbCommand()
=> CreateCommand();
internal void AddCommand(SqliteCommand command)
=> _commands.Add(new WeakReference<SqliteCommand>(command));
internal void RemoveCommand(SqliteCommand command)
{
for (int i = _commands.Count - 1; i >= 0; i--)
{
if (!_commands[i].TryGetTarget(out var item) || item == command)
{
_commands.RemoveAt(i);
}
}
}
/// <summary>
/// Create custom collation.
/// </summary>

Просмотреть файл

@ -10,6 +10,7 @@ using System.Globalization;
using System.Text;
using Microsoft.Data.Sqlite.Properties;
using SQLitePCL;
using System.Text;
namespace Microsoft.Data.Sqlite
{
@ -20,9 +21,9 @@ namespace Microsoft.Data.Sqlite
{
private static readonly byte[] _emptyByteArray = new byte[0];
private readonly SqliteConnection _connection;
private readonly SqliteCommand _command;
private readonly bool _closeConnection;
private readonly Queue<(sqlite3_stmt stmt, bool)> _stmtQueue;
private readonly Queue<(sqlite3_stmt, bool)> _stmtQueue;
private sqlite3_stmt _stmt;
private bool _hasRows;
private bool _stepped;
@ -30,7 +31,7 @@ namespace Microsoft.Data.Sqlite
private bool _closed;
internal SqliteDataReader(
SqliteConnection connection,
SqliteCommand command,
Queue<(sqlite3_stmt, bool)> stmtQueue,
int recordsAffected,
bool closeConnection)
@ -40,7 +41,7 @@ namespace Microsoft.Data.Sqlite
(_stmt, _hasRows) = stmtQueue.Dequeue();
}
_connection = connection;
_command = command;
_stmtQueue = stmtQueue;
RecordsAffected = recordsAffected;
_closeConnection = closeConnection;
@ -132,7 +133,7 @@ namespace Microsoft.Data.Sqlite
}
var rc = raw.sqlite3_step(_stmt);
SqliteException.ThrowExceptionForRC(rc, _connection.Handle);
SqliteException.ThrowExceptionForRC(rc, _command.Connection.Handle);
_done = rc == raw.SQLITE_DONE;
@ -150,8 +151,6 @@ namespace Microsoft.Data.Sqlite
return false;
}
_stmt.Dispose();
(_stmt, _hasRows) = _stmtQueue.Dequeue();
_stepped = false;
_done = false;
@ -173,17 +172,6 @@ namespace Microsoft.Data.Sqlite
/// </param>
protected override void Dispose(bool disposing)
{
if (_stmt != null)
{
_stmt.Dispose();
_stmt = null;
}
while (_stmtQueue.Count != 0)
{
_stmtQueue.Dequeue().stmt.Dispose();
}
if (!disposing)
{
return;
@ -193,7 +181,7 @@ namespace Microsoft.Data.Sqlite
if (_closeConnection)
{
_connection.Close();
_command.Connection.Close();
}
}

Просмотреть файл

@ -33,18 +33,36 @@ namespace Microsoft.Data.Sqlite
}
}
[Fact]
public void Connection_can_be_nullified()
{
using (var connection = new SqliteConnection("Data Source=:memory:"))
{
connection.Open();
var command = connection.CreateCommand();
command.CommandText = "CREATE TABLE Data (Value);";
command.Prepare();
command.Connection = null;
Assert.Null(command.Connection);
}
}
[Fact]
public void CommandType_text_by_default()
{
Assert.Equal(CommandType.Text, new SqliteCommand().CommandType);
}
[Fact]
public void CommandType_validates_value()
[Theory]
[InlineData(CommandType.StoredProcedure)]
[InlineData(CommandType.TableDirect)]
public void CommandType_validates_value(CommandType commandType)
{
var ex = Assert.Throws<ArgumentException>(() => new SqliteCommand().CommandType = CommandType.StoredProcedure);
var ex = Assert.Throws<ArgumentException>(() => new SqliteCommand().CommandType = commandType);
Assert.Equal(Resources.InvalidCommandType(CommandType.StoredProcedure), ex.Message);
Assert.Equal(Resources.InvalidCommandType(commandType), ex.Message);
}
[Fact]
@ -65,9 +83,49 @@ namespace Microsoft.Data.Sqlite
}
[Fact]
public void Prepare_does_nothing()
public void Prepare_throws_when_no_connection()
{
new SqliteCommand().Prepare();
var ex = Assert.Throws<InvalidOperationException>(() => new SqliteCommand().Prepare());
Assert.Equal(Resources.CallRequiresOpenConnection("Prepare"), ex.Message);
}
[Fact]
public void Prepare_throws_when_connection_closed()
{
using (var connection = new SqliteConnection("Data Source=:memory:"))
{
var ex = Assert.Throws<InvalidOperationException>(() => connection.CreateCommand().Prepare());
Assert.Equal(Resources.CallRequiresOpenConnection("Prepare"), ex.Message);
}
}
[Fact]
public void Prepare_throws_when_no_command_text()
{
using (var connection = new SqliteConnection("Data Source=:memory:"))
{
connection.Open();
var ex = Assert.Throws<InvalidOperationException>(() => connection.CreateCommand().Prepare());
Assert.Equal(Resources.CallRequiresSetCommandText("Prepare"), ex.Message);
}
}
[Fact]
public void Prepare_throws_when_command_text_contains_dependent_commands()
{
using (var connection = new SqliteConnection("Data Source=:memory:"))
{
connection.Open();
var command = connection.CreateCommand();
command.CommandText = "CREATE TABLE Data (Value); INSERT INTO Data VALUES (0);";
var ex = Assert.Throws<SqliteException>(() => command.Prepare());
Assert.Equal(1, ex.SqliteErrorCode);
}
}
[Fact]
@ -167,6 +225,19 @@ namespace Microsoft.Data.Sqlite
}
}
[Fact]
public void ExecuteNonQuery_processes_dependent_commands()
{
using (var connection = new SqliteConnection("Data Source=:memory:"))
{
connection.Open();
var command = connection.CreateCommand();
command.CommandText = "CREATE TABLE Data (Value); INSERT INTO Data VALUES (0);";
command.ExecuteNonQuery();
}
}
[Fact]
public void ExecuteScalar_returns_null_when_empty()
{
@ -259,6 +330,24 @@ namespace Microsoft.Data.Sqlite
}
}
[Fact]
public void ExecuteReader_reuse_statement()
{
using (var connection = new SqliteConnection("Data Source=:memory:"))
{
connection.Open();
var command = connection.CreateCommand();
command.CommandText = "SELECT @Parameter;";
command.Prepare();
command.Parameters.AddWithValue("@Parameter", 1);
Assert.Equal(1L, command.ExecuteScalar());
command.Parameters["@Parameter"].Value = 2;
Assert.Equal(2L, command.ExecuteScalar());
}
}
[Fact]
public void ExecuteReader_throws_when_parameter_unset()
{