Skip to content

Commit 16d2c99

Browse files
committed
stored procedures. fixes #19
1 parent 1e9d9a8 commit 16d2c99

30 files changed

+1310
-79
lines changed

.ci/config/config.compression+ssl.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"Data": {
33
"ConnectionString": "server=127.0.0.1;user id=ssltest;password=test;port=3306;database=mysqltest;ssl mode=required;certificate file=.ci/server/ssl-client.pfx;use compression=true;Use Affected Rows=true",
44
"PasswordlessUser": "no_password",
5+
"SupportsCachedProcedures": true,
56
"SupportsJson": true
67
}
78
}

.ci/config/config.compression.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"Data": {
33
"ConnectionString": "server=127.0.0.1;user id=mysqltest;password='test;key=\"val';port=3306;database=mysqltest;UseCompression=true;Use Affected Rows=true",
44
"PasswordlessUser": "no_password",
5+
"SupportsCachedProcedures": true,
56
"SupportsJson": true
67
}
78
}

.ci/config/config.ssl.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"Data": {
33
"ConnectionString": "server=127.0.0.1;user id=ssltest;password=test;port=3306;database=mysqltest;ssl mode=required;certificate file=.ci/server/ssl-client.pfx;Use Affected Rows=true",
44
"PasswordlessUser": "no_password",
5+
"SupportsCachedProcedures": true,
56
"SupportsJson": true
67
}
78
}

.ci/config/config.uds+ssl.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"Data": {
33
"ConnectionString": "server=./.ci/mysqld/mysqld.sock;user id=ssltest;password=test;database=mysqltest;ssl mode=required;certificate file=.ci/server/ssl-client.pfx;Use Affected Rows=true",
44
"PasswordlessUser": "no_password",
5+
"SupportsCachedProcedures": true,
56
"SupportsJson": true
67
}
78
}

.ci/config/config.uds.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"Data": {
33
"ConnectionString": "server=./.ci/mysqld/mysqld.sock;user id=mysqltest;password='test;key=\"val';database=mysqltest;Use Affected Rows=true",
44
"PasswordlessUser": "no_password",
5+
"SupportsCachedProcedures": true,
56
"SupportsJson": true
67
}
78
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using System.Data;
2+
using System.Linq;
3+
using MySql.Data.MySqlClient.Types;
4+
5+
namespace MySql.Data.MySqlClient.Caches
6+
{
7+
internal class CachedParameter
8+
{
9+
public CachedParameter(int ordinalPosition, string mode, string name, string dataType, bool unsigned)
10+
{
11+
Position = ordinalPosition;
12+
if (Position == 0)
13+
{
14+
Direction = ParameterDirection.ReturnValue;
15+
}
16+
else
17+
{
18+
switch (mode.ToLowerInvariant())
19+
{
20+
case "in":
21+
Direction = ParameterDirection.Input;
22+
break;
23+
case "inout":
24+
Direction = ParameterDirection.InputOutput;
25+
break;
26+
case "out":
27+
Direction = ParameterDirection.Output;
28+
break;
29+
}
30+
}
31+
Name = name;
32+
DbType = TypeMapper.Mapper.GetDbTypeMapping(dataType, unsigned).DbTypes?.First() ?? DbType.Object;
33+
}
34+
35+
internal readonly int Position;
36+
internal readonly ParameterDirection Direction;
37+
internal readonly string Name;
38+
internal readonly DbType DbType;
39+
}
40+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Collections.ObjectModel;
4+
using System.Data;
5+
using System.Linq;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using MySql.Data.Protocol.Serialization;
9+
10+
namespace MySql.Data.MySqlClient.Caches
11+
{
12+
internal class CachedProcedure
13+
{
14+
internal static async Task<CachedProcedure> FillAsync(IOBehavior ioBehavior, MySqlConnection connection, string schema, string component, CancellationToken cancellationToken)
15+
{
16+
var cmd = (MySqlCommand) connection.CreateCommand();
17+
18+
cmd.CommandText = @"SELECT ORDINAL_POSITION, PARAMETER_MODE, PARAMETER_NAME, DATA_TYPE, DTD_IDENTIFIER
19+
FROM information_schema.parameters
20+
WHERE SPECIFIC_SCHEMA = @schema AND SPECIFIC_NAME = @component
21+
ORDER BY ORDINAL_POSITION";
22+
cmd.Parameters.Add(new MySqlParameter
23+
{
24+
ParameterName = "@schema",
25+
DbType = DbType.String,
26+
Value = schema
27+
});
28+
cmd.Parameters.Add(new MySqlParameter
29+
{
30+
ParameterName = "@component",
31+
DbType = DbType.String,
32+
Value = component
33+
});
34+
35+
var parameters = new List<CachedParameter>();
36+
using (var reader = (MySqlDataReader) await cmd.ExecuteReaderAsync(CommandBehavior.Default, ioBehavior, cancellationToken).ConfigureAwait(false))
37+
{
38+
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
39+
{
40+
parameters.Add(new CachedParameter(
41+
reader.GetInt32(0),
42+
!reader.IsDBNull(1) ? reader.GetString(1) : null,
43+
!reader.IsDBNull(2) ? reader.GetString(2) : null,
44+
reader.GetString(3),
45+
reader.GetString(4).IndexOf("unsigned", StringComparison.OrdinalIgnoreCase) != -1
46+
));
47+
}
48+
}
49+
50+
return new CachedProcedure(schema, component, parameters.AsReadOnly());
51+
}
52+
53+
protected CachedProcedure(string schema, string component, ReadOnlyCollection<CachedParameter> parameters)
54+
{
55+
m_schema = schema;
56+
m_component = component;
57+
m_parameters = parameters;
58+
}
59+
60+
internal MySqlParameterCollection AlignParamsWithDb(MySqlParameterCollection parameterCollection)
61+
{
62+
var alignedParams = new MySqlParameterCollection();
63+
var returnParam = parameterCollection.FirstOrDefault(x => x.Direction == ParameterDirection.ReturnValue);
64+
65+
foreach (var cachedParam in m_parameters)
66+
{
67+
MySqlParameter alignParam;
68+
if (cachedParam.Direction == ParameterDirection.ReturnValue)
69+
{
70+
if (returnParam == null)
71+
throw new InvalidOperationException($"Attempt to call stored function {FullyQualified} without specifying a return parameter");
72+
alignParam = returnParam;
73+
}
74+
else
75+
{
76+
var index = parameterCollection.NormalizedIndexOf(cachedParam.Name);
77+
if (index == -1)
78+
throw new ArgumentException($"Parameter '{cachedParam.Name}' not found in the collection.");
79+
alignParam = parameterCollection[index];
80+
}
81+
82+
if (alignParam.Direction == default(ParameterDirection))
83+
alignParam.Direction = cachedParam.Direction;
84+
if (alignParam.DbType == default(DbType))
85+
alignParam.DbType = cachedParam.DbType;
86+
87+
// cached parameters are oredered by ordinal position
88+
alignedParams.Add(alignParam);
89+
}
90+
91+
return alignedParams;
92+
}
93+
94+
private string FullyQualified => $"`{m_schema}`.`{m_component}`";
95+
96+
readonly string m_schema;
97+
readonly string m_component;
98+
readonly ReadOnlyCollection<CachedParameter> m_parameters;
99+
}
100+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using System;
2+
using System.Text.RegularExpressions;
3+
4+
namespace MySql.Data.MySqlClient.Caches
5+
{
6+
internal class NormalizedSchema
7+
{
8+
private const string ReQuoted = @"`((?:[^`]|``)+)`";
9+
private const string ReUnQuoted = @"([^\.`]+)";
10+
private static readonly string ReEither = $@"(?:{ReQuoted}|{ReUnQuoted})";
11+
12+
private static readonly Regex NameRe = new Regex(
13+
$@"^\s*{ReEither}\s*(?:\.\s*{ReEither}\s*)?$",
14+
RegexOptions.Compiled);
15+
16+
internal static NormalizedSchema MustNormalize(string name, string defaultSchema = null)
17+
{
18+
var normalized = new NormalizedSchema(name, defaultSchema);
19+
if (normalized.Component == null)
20+
throw new ArgumentException("Could not determine function/procedure name", name);
21+
if (normalized.Component == null)
22+
throw new ArgumentException("Could not determine schema", name);
23+
return normalized;
24+
}
25+
26+
public NormalizedSchema(string name, string defaultSchema=null)
27+
{
28+
var match = NameRe.Match(name);
29+
if (match.Success)
30+
{
31+
if (match.Groups[3].Success)
32+
Component = match.Groups[3].Value.Trim();
33+
else if (match.Groups[4].Success)
34+
Component = match.Groups[4].Value.Trim();
35+
36+
string firstGroup = "";
37+
if (match.Groups[1].Success)
38+
firstGroup = match.Groups[1].Value.Trim();
39+
else if (match.Groups[2].Success)
40+
firstGroup = match.Groups[2].Value.Trim();
41+
if (Component == null)
42+
Component = firstGroup.Trim();
43+
else
44+
Schema = firstGroup.Trim();
45+
46+
if (Schema == null)
47+
Schema = defaultSchema;
48+
}
49+
}
50+
51+
internal readonly string Schema;
52+
internal readonly string Component;
53+
54+
internal string FullyQualified => $"`{Schema}`.`{Component}`";
55+
}
56+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System.Data;
2+
using System.Data.Common;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using MySql.Data.Protocol.Serialization;
6+
7+
namespace MySql.Data.MySqlClient.CommandExecutors
8+
{
9+
internal interface ICommandExecutor
10+
{
11+
Task<int> ExecuteNonQueryAsync(string commandText, MySqlParameterCollection parameterCollection, IOBehavior ioBehavior, CancellationToken cancellationToken);
12+
13+
Task<object> ExecuteScalarAsync(string commandText, MySqlParameterCollection parameterCollection, IOBehavior ioBehavior, CancellationToken cancellationToken);
14+
15+
Task<DbDataReader> ExecuteReaderAsync(string commandText, MySqlParameterCollection parameterCollection, CommandBehavior behavior, IOBehavior ioBehavior, CancellationToken cancellationToken);
16+
}
17+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
using System.Collections.Generic;
2+
using System.Data;
3+
using System.Data.Common;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using MySql.Data.MySqlClient.Types;
7+
using MySql.Data.Protocol.Serialization;
8+
9+
namespace MySql.Data.MySqlClient.CommandExecutors
10+
{
11+
internal class StoredProcedureCommandExecutor : TextCommandExecutor
12+
{
13+
14+
internal StoredProcedureCommandExecutor(MySqlCommand command)
15+
: base(command)
16+
{
17+
m_command = command;
18+
}
19+
20+
public override async Task<DbDataReader> ExecuteReaderAsync(string commandText, MySqlParameterCollection parameterCollection,
21+
CommandBehavior behavior, IOBehavior ioBehavior, CancellationToken cancellationToken)
22+
{
23+
var cachedProcedure = await m_command.Connection.GetCachedProcedure(ioBehavior, commandText, cancellationToken).ConfigureAwait(false);
24+
if (cachedProcedure != null)
25+
parameterCollection = cachedProcedure.AlignParamsWithDb(parameterCollection);
26+
27+
MySqlParameter returnParam = null;
28+
m_outParams = new MySqlParameterCollection();
29+
m_outParamNames = new List<string>();
30+
var inParams = new MySqlParameterCollection();
31+
var argParamNames = new List<string>();
32+
var inOutSetParams = "";
33+
for (var i = 0; i < parameterCollection.Count; i++)
34+
{
35+
var param = parameterCollection[i];
36+
var inName = "@inParam" + i;
37+
var outName = "@outParam" + i;
38+
switch (param.Direction)
39+
{
40+
case 0:
41+
case ParameterDirection.Input:
42+
case ParameterDirection.InputOutput:
43+
var inParam = param.WithParameterName(inName);
44+
inParams.Add(inParam);
45+
if (param.Direction == ParameterDirection.InputOutput)
46+
{
47+
inOutSetParams += $"SET {outName}={inName}; ";
48+
goto case ParameterDirection.Output;
49+
}
50+
argParamNames.Add(inName);
51+
break;
52+
case ParameterDirection.Output:
53+
m_outParams.Add(param);
54+
m_outParamNames.Add(outName);
55+
argParamNames.Add(outName);
56+
break;
57+
case ParameterDirection.ReturnValue:
58+
returnParam = param;
59+
break;
60+
}
61+
}
62+
63+
// if a return param is set, assume it is a funciton. otherwise, assume stored procedure
64+
commandText += "(" + string.Join(", ", argParamNames) +")";
65+
if (returnParam == null)
66+
{
67+
commandText = inOutSetParams + "CALL " + commandText;
68+
if (m_outParams.Count > 0)
69+
{
70+
m_setParamsFlags = true;
71+
m_cancellationToken = cancellationToken;
72+
}
73+
}
74+
else
75+
{
76+
commandText = "SELECT " + commandText;
77+
}
78+
79+
var reader = (MySqlDataReader) await base.ExecuteReaderAsync(commandText, inParams, behavior, ioBehavior, cancellationToken).ConfigureAwait(false);
80+
if (returnParam != null && await reader.ReadAsync(ioBehavior, cancellationToken).ConfigureAwait(false))
81+
returnParam.Value = reader.GetValue(0);
82+
83+
return reader;
84+
}
85+
86+
internal void SetParams()
87+
{
88+
if (!m_setParamsFlags)
89+
return;
90+
m_setParamsFlags = false;
91+
var commandText = "SELECT " + string.Join(", ", m_outParamNames);
92+
using (var reader = (MySqlDataReader) base.ExecuteReaderAsync(commandText, new MySqlParameterCollection(), CommandBehavior.Default, IOBehavior.Synchronous, m_cancellationToken).GetAwaiter().GetResult())
93+
{
94+
reader.Read();
95+
for (var i = 0; i < m_outParams.Count; i++)
96+
{
97+
var param = m_outParams[i];
98+
if (param.DbType != default(DbType))
99+
{
100+
var dbTypeMapping = TypeMapper.Mapper.GetDbTypeMapping(param.DbType);
101+
if (dbTypeMapping != null)
102+
{
103+
param.Value = dbTypeMapping.DoConversion(reader.GetValue(i));
104+
continue;
105+
}
106+
}
107+
param.Value = reader.GetValue(i);
108+
}
109+
}
110+
}
111+
112+
readonly MySqlCommand m_command;
113+
bool m_setParamsFlags;
114+
MySqlParameterCollection m_outParams;
115+
List<string> m_outParamNames;
116+
private CancellationToken m_cancellationToken;
117+
}
118+
}

0 commit comments

Comments
 (0)