aboutsummaryrefslogtreecommitdiff
path: root/VNLib.Plugins.Extensions.Data/SQL
diff options
context:
space:
mode:
Diffstat (limited to 'VNLib.Plugins.Extensions.Data/SQL')
-rw-r--r--VNLib.Plugins.Extensions.Data/SQL/DbExtensions.cs526
-rw-r--r--VNLib.Plugins.Extensions.Data/SQL/EnumerableTable.cs118
-rw-r--r--VNLib.Plugins.Extensions.Data/SQL/SqlColumnName.cs65
-rw-r--r--VNLib.Plugins.Extensions.Data/SQL/SqlVariable.cs58
-rw-r--r--VNLib.Plugins.Extensions.Data/SQL/TableManager.cs65
5 files changed, 832 insertions, 0 deletions
diff --git a/VNLib.Plugins.Extensions.Data/SQL/DbExtensions.cs b/VNLib.Plugins.Extensions.Data/SQL/DbExtensions.cs
new file mode 100644
index 0000000..e6ee6b1
--- /dev/null
+++ b/VNLib.Plugins.Extensions.Data/SQL/DbExtensions.cs
@@ -0,0 +1,526 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Data
+* File: DbExtensions.cs
+*
+* DbExtensions.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Data;
+using System.Reflection;
+using System.Data.Common;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+
+using VNLib.Utils;
+using VNLib.Utils.Memory.Caching;
+
+namespace VNLib.Plugins.Extensions.Data.SQL
+{
+ /// <summary>
+ /// Provides basic extension methods for ADO.NET abstract classes
+ /// for rapid development
+ /// </summary>
+ public static class DbExtensions
+ {
+ /*
+ * Object rental for propery dictionaries used for custom result objects
+ */
+ private static readonly ObjectRental<Dictionary<string, PropertyInfo>> DictStore;
+
+ static DbExtensions()
+ {
+ //Setup dict store
+ DictStore = ObjectRental.Create<Dictionary<string, PropertyInfo>>(null, static dict => dict.Clear(), 20);
+ }
+
+ /// <summary>
+ /// Creates a new <see cref="DbParameter"/> configured for <see cref="ParameterDirection.Input"/> with the specified value
+ /// and adds it to the command.
+ /// </summary>
+ /// <param name="cmd"></param>
+ /// <param name="name">The parameter name</param>
+ /// <param name="value">The value of the parameter</param>
+ /// <param name="type">The <see cref="DbType"/> of the column</param>
+ /// <param name="nullable">Are null types allowed in the value parameter</param>
+ /// <returns>The created parameter</returns>
+ public static DbParameter AddParameter<T>(this DbCommand cmd, string @name, T @value, DbType @type, bool @nullable = false)
+ {
+ //Create the new parameter from command
+ DbParameter param = cmd.CreateParameter();
+ //Set parameter variables
+ param.ParameterName = name;
+ param.Value = value;
+ param.DbType = type;
+ //Force non null mapping
+ param.SourceColumnNullMapping = nullable;
+ //Specify input parameter
+ param.Direction = ParameterDirection.Input;
+ //Add param to list
+ cmd.Parameters.Add(param);
+ return param;
+ }
+ /// <summary>
+ /// Creates a new <see cref="DbParameter"/> configured for <see cref="ParameterDirection.Input"/> with the specified value
+ /// and adds it to the command.
+ /// </summary>
+ /// <param name="cmd"></param>
+ /// <param name="name">The parameter name</param>
+ /// <param name="value">The value of the parameter</param>
+ /// <param name="type">The <see cref="DbType"/> of the column</param>
+ /// <param name="size">Size of the data value</param>
+ /// <param name="nullable">Are null types allowed in the value parameter</param>
+ /// <returns>The created parameter</returns>
+ public static DbParameter AddParameter<T>(this DbCommand cmd, string @name, T @value, DbType @type, int @size, bool @nullable = false)
+ {
+ DbParameter param = AddParameter(cmd, name, value, type, nullable);
+ //Set size parameter
+ param.Size = size;
+ return param;
+ }
+ /// <summary>
+ /// Creates a new <see cref="DbParameter"/> configured for <see cref="ParameterDirection.Output"/> with the specified value
+ /// and adds it to the command.
+ /// </summary>
+ /// <param name="cmd"></param>
+ /// <param name="name">The parameter name</param>
+ /// <param name="value">The value of the parameter</param>
+ /// <param name="type">The <see cref="DbType"/> of the column</param>
+ /// <param name="nullable">Are null types allowed in the value parameter</param>
+ /// <returns>The created parameter</returns>
+ public static DbParameter AddOutParameter<T>(this DbCommand cmd, string @name, T @value, DbType @type, bool @nullable = false)
+ {
+ //Create the new parameter from command
+ DbParameter param = AddParameter(cmd, name, value, type, nullable);
+ //Specify output parameter
+ param.Direction = ParameterDirection.Output;
+ return param;
+ }
+ /// <summary>
+ /// Creates a new <see cref="DbParameter"/> configured for <see cref="ParameterDirection.Output"/> with the specified value
+ /// and adds it to the command.
+ /// </summary>
+ /// <param name="cmd"></param>
+ /// <param name="name">The parameter name</param>
+ /// <param name="value">The value of the parameter</param>
+ /// <param name="type">The <see cref="DbType"/> of the column</param>
+ /// <param name="size">Size of the data value</param>
+ /// <param name="nullable">Are null types allowed in the value parameter</param>
+ /// <returns>The created parameter</returns>
+ public static DbParameter AddOutParameter<T>(this DbCommand cmd, string @name, T @value, DbType @type, int @size, bool @nullable = false)
+ {
+ DbParameter param = AddOutParameter(cmd, name, value, type, nullable);
+ //Set size parameter
+ param.Size = size;
+ return param;
+ }
+
+ /// <summary>
+ /// Creates a new <see cref="DbCommand"/> for <see cref="CommandType.Text"/> with the specified command
+ /// </summary>
+ /// <param name="db"></param>
+ /// <param name="cmdText">The command to run against the connection</param>
+ /// <returns>The initalized <see cref="DbCommand"/></returns>
+ public static DbCommand CreateTextCommand(this DbConnection db, string cmdText)
+ {
+ //Create the new command
+ DbCommand cmd = db.CreateCommand();
+ cmd.CommandText = cmdText;
+ cmd.CommandType = CommandType.Text; //Specify text command
+ return cmd;
+ }
+ /// <summary>
+ /// Creates a new <see cref="DbCommand"/> for <see cref="CommandType.StoredProcedure"/> with the specified procedure name
+ /// </summary>
+ /// <param name="db"></param>
+ /// <param name="procedureName">The name of the stored proecedure to execute</param>
+ /// <returns>The initalized <see cref="DbCommand"/></returns>
+ public static DbCommand CreateProcedureCommand(this DbConnection db, string procedureName)
+ {
+ //Create the new command
+ DbCommand cmd = db.CreateCommand();
+ cmd.CommandText = procedureName;
+ cmd.CommandType = CommandType.StoredProcedure; //Specify stored procedure
+ return cmd;
+ }
+
+ /// <summary>
+ /// Creates a new <see cref="DbCommand"/> for <see cref="CommandType.Text"/> with the specified command
+ /// on a given transaction
+ /// </summary>
+ /// <param name="db"></param>
+ /// <param name="cmdText">The command to run against the connection</param>
+ /// <param name="transaction">The transaction to execute on</param>
+ /// <returns>The initalized <see cref="DbCommand"/></returns>
+ public static DbCommand CreateTextCommand(this DbConnection db, string cmdText, DbTransaction transaction)
+ {
+ return CreateCommand(db, transaction, CommandType.Text, cmdText);
+ }
+ /// <summary>
+ /// Shortcut to create a command on a transaction with the specifed command type and command
+ /// </summary>
+ /// <param name="db"></param>
+ /// <param name="transaction">The transaction to complete the operation on</param>
+ /// <param name="type">The command type</param>
+ /// <param name="command">The command to execute</param>
+ /// <returns>The intialized db command</returns>
+ public static DbCommand CreateCommand(this DbConnection db, DbTransaction transaction, CommandType type, string command)
+ {
+ //Create the new command
+ DbCommand cmd = db.CreateCommand();
+ cmd.Transaction = transaction;
+ cmd.CommandText = command;
+ cmd.CommandType = type;
+ return cmd;
+ }
+ /// <summary>
+ /// Creates a new <see cref="DbCommand"/> for <see cref="CommandType.StoredProcedure"/> with the specified procedure name
+ /// </summary>
+ /// <param name="db"></param>
+ /// <param name="procedureName">The name of the stored proecedure to execute</param>
+ /// <param name="transaction">The transaction to execute on</param>
+ /// <returns>The initalized <see cref="DbCommand"/></returns>
+ public static DbCommand CreateProcedureCommand(this DbConnection db, string procedureName, DbTransaction transaction)
+ {
+ return CreateCommand(db, transaction, CommandType.StoredProcedure, procedureName);
+ }
+
+ /// <summary>
+ /// Reads all available rows from the reader, adapts columns to public properties with <see cref="SqlColumnName"/>
+ /// attributes, and adds them to the collection
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="reader"></param>
+ /// <param name="container">The container to write created objects to</param>
+ /// <returns>The number of objects created and written to the collection</returns>
+ public static int GetAllObjects<T>(this DbDataReader reader, ICollection<T> container) where T : new()
+ {
+ //make sure its worth collecting object meta
+ if (!reader.HasRows)
+ {
+ return 0;
+ }
+ Type objectType = typeof(T);
+ //Rent a dict of properties that have the column attribute set so we can load the proper results
+ Dictionary<string, PropertyInfo> avialbleProps = DictStore.Rent();
+ //Itterate through public properties
+ foreach (PropertyInfo prop in objectType.GetProperties())
+ {
+ //try to get the column name attribute of the propery
+ SqlColumnName colAtt = prop.GetCustomAttribute<SqlColumnName>(true);
+ //Attribute is valid and coumn name is not empty
+ if (!string.IsNullOrWhiteSpace(colAtt?.ColumnName))
+ {
+ //Store the property for later
+ avialbleProps[colAtt.ColumnName] = prop;
+ }
+ }
+ //Get the column schema
+ ReadOnlyCollection<DbColumn> columns = reader.GetColumnSchema();
+ int count = 0;
+ //Read
+ while (reader.Read())
+ {
+ //Create the new object
+ T ret = new();
+ //Iterate through columns
+ foreach (DbColumn col in columns)
+ {
+ //Get the propery if its specified by its column-name attribute
+ if (avialbleProps.TryGetValue(col.ColumnName, out PropertyInfo prop))
+ {
+ //make sure the column has a value
+ if (col.ColumnOrdinal.HasValue)
+ {
+ //Get the object
+ object val = reader.GetValue(col.ColumnOrdinal.Value);
+ //Set check if the row is DB null, if so set it, otherwise set the value
+ prop.SetValue(ret, Convert.IsDBNull(val) ? null : val);
+ }
+ }
+ }
+ //Add the object to the collection
+ container.Add(ret);
+ //Increment count
+ count++;
+ }
+ //return dict (if an error occurs, just let the dict go and create a new one next time, no stress setting up a try/finally block)
+ DictStore.Return(avialbleProps);
+ return count;
+ }
+ /// <summary>
+ /// Reads all available rows from the reader, adapts columns to public properties with <see cref="SqlColumnName"/>
+ /// attributes, and adds them to the collection
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="reader"></param>
+ /// <param name="container">The container to write created objects to</param>
+ /// <returns>The number of objects created and written to the collection</returns>
+ public static async ValueTask<int> GetAllObjectsAsync<T>(this DbDataReader reader, ICollection<T> container) where T : new()
+ {
+ //make sure its worth collecting object meta
+ if (!reader.HasRows)
+ {
+ return 0;
+ }
+ Type objectType = typeof(T);
+ //Rent a dict of properties that have the column attribute set so we can load the proper results
+ Dictionary<string, PropertyInfo> avialbleProps = DictStore.Rent();
+ //Itterate through public properties
+ foreach (PropertyInfo prop in objectType.GetProperties())
+ {
+ //try to get the column name attribute of the propery
+ SqlColumnName colAtt = prop.GetCustomAttribute<SqlColumnName>(true);
+ //Attribute is valid and coumn name is not empty
+ if (!string.IsNullOrWhiteSpace(colAtt?.ColumnName))
+ {
+ //Store the property for later
+ avialbleProps[colAtt.ColumnName] = prop;
+ }
+ }
+ //Get the column schema
+ ReadOnlyCollection<DbColumn> columns = await reader.GetColumnSchemaAsync();
+ int count = 0;
+ //Read
+ while (await reader.ReadAsync())
+ {
+ //Create the new object
+ T ret = new();
+ //Iterate through columns
+ foreach (DbColumn col in columns)
+ {
+ //Get the propery if its specified by its column-name attribute
+ if (avialbleProps.TryGetValue(col.ColumnName, out PropertyInfo prop))
+ {
+ //make sure the column has a value
+ if (col.ColumnOrdinal.HasValue)
+ {
+ //Get the object
+ object val = reader.GetValue(col.ColumnOrdinal.Value);
+ //Set check if the row is DB null, if so set it, otherwise set the value
+ prop.SetValue(ret, Convert.IsDBNull(val) ? null : val);
+ }
+ }
+ }
+ //Add the object to the collection
+ container.Add(ret);
+ //Increment count
+ count++;
+ }
+ //return dict (if an error occurs, just let the dict go and create a new one next time, no stress setting up a try/finally block)
+ DictStore.Return(avialbleProps);
+ return count;
+ }
+ /// <summary>
+ /// Reads the first available row from the reader, adapts columns to public properties with <see cref="SqlColumnName"/>
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="reader"></param>
+ /// <returns>The created object, or default if no rows are available</returns>
+ public static T GetFirstObject<T>(this DbDataReader reader) where T : new()
+ {
+ //make sure its worth collecting object meta
+ if (!reader.HasRows)
+ {
+ return default;
+ }
+ //Get the object type
+ Type objectType = typeof(T);
+ //Get the column schema
+ ReadOnlyCollection<DbColumn> columns = reader.GetColumnSchema();
+ //Read
+ if (reader.Read())
+ {
+ //Rent a dict of properties that have the column attribute set so we can load the proper results
+ Dictionary<string, PropertyInfo> availbleProps = DictStore.Rent();
+ //Itterate through public properties
+ foreach (PropertyInfo prop in objectType.GetProperties())
+ {
+ //try to get the column name attribute of the propery
+ SqlColumnName colAtt = prop.GetCustomAttribute<SqlColumnName>(true);
+ //Attribute is valid and coumn name is not empty
+ if (colAtt != null && !string.IsNullOrWhiteSpace(colAtt.ColumnName))
+ {
+ //Store the property for later
+ availbleProps[colAtt.ColumnName] = prop;
+ }
+ }
+ //Create the new object
+ T ret = new();
+ //Iterate through columns
+ foreach (DbColumn col in columns)
+ {
+ //Get the propery if its specified by its column-name attribute
+ if (availbleProps.TryGetValue(col.ColumnName, out PropertyInfo prop) && col.ColumnOrdinal.HasValue)
+ {
+ //Get the object
+ object val = reader.GetValue(col.ColumnOrdinal.Value);
+ //Set check if the row is DB null, if so set it, otherwise set the value
+ prop.SetValue(ret, Convert.IsDBNull(val) ? null : val);
+ }
+ }
+ //Return dict, no stress if error occurs, the goal is lower overhead
+ DictStore.Return(availbleProps);
+ //Return the new object
+ return ret;
+ }
+ return default;
+ }
+ /// <summary>
+ /// Reads the first available row from the reader, adapts columns to public properties with <see cref="SqlColumnName"/>
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="reader"></param>
+ /// <returns>The created object, or default if no rows are available</returns>
+ public static async Task<T> GetFirstObjectAsync<T>(this DbDataReader reader) where T : new()
+ {
+ //Read
+ if (await reader.ReadAsync())
+ {
+ //Get the object type
+ Type objectType = typeof(T);
+ //Get the column schema
+ ReadOnlyCollection<DbColumn> columns = await reader.GetColumnSchemaAsync();
+ //Rent a dict of properties that have the column attribute set so we can load the proper results
+ Dictionary<string, PropertyInfo> availbleProps = DictStore.Rent();
+ //Itterate through public properties
+ foreach (PropertyInfo prop in objectType.GetProperties())
+ {
+ //try to get the column name attribute of the propery
+ SqlColumnName colAtt = prop.GetCustomAttribute<SqlColumnName>(true);
+ //Attribute is valid and coumn name is not empty
+ if (colAtt != null && !string.IsNullOrWhiteSpace(colAtt.ColumnName))
+ {
+ //Store the property for later
+ availbleProps[colAtt.ColumnName] = prop;
+ }
+ }
+ //Create the new object
+ T ret = new();
+ //Iterate through columns
+ foreach (DbColumn col in columns)
+ {
+ //Get the propery if its specified by its column-name attribute
+ if (availbleProps.TryGetValue(col.ColumnName, out PropertyInfo prop) && col.ColumnOrdinal.HasValue)
+ {
+ //Get the object
+ object val = reader.GetValue(col.ColumnOrdinal.Value);
+ //Set check if the row is DB null, if so set it, otherwise set the value
+ prop.SetValue(ret, Convert.IsDBNull(val) ? null : val);
+ }
+ }
+ //Return dict, no stress if error occurs, the goal is lower overhead
+ DictStore.Return(availbleProps);
+ //Return the new object
+ return ret;
+ }
+ return default;
+ }
+ /// <summary>
+ /// Executes a nonquery operation with the specified command using the object properties set with the
+ /// <see cref="SqlVariable"/> attributes
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="cmd"></param>
+ /// <param name="obj">The object containing the <see cref="SqlVariable"/> properties to write to command variables</param>
+ /// <returns>The number of rows affected</returns>
+ /// <exception cref="TypeLoadException"></exception>
+ /// <exception cref="ArgumentException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="AmbiguousMatchException"></exception>
+ /// <exception cref="TargetInvocationException"></exception>
+ public static ERRNO ExecuteNonQuery<T>(this DbCommand cmd, T obj) where T : notnull
+ {
+ if (obj == null)
+ {
+ throw new ArgumentNullException(nameof(obj));
+ }
+ //Get the objec type
+ Type objtype = typeof(T);
+ //Itterate through public properties
+ foreach (PropertyInfo prop in objtype.GetProperties())
+ {
+ //try to get the variable attribute of the propery
+ SqlVariable varprops = prop.GetCustomAttribute<SqlVariable>(true);
+ //This property is an sql variable, so lets add it
+ if (varprops == null)
+ {
+ continue;
+ }
+ //If the command type is text, then make sure the variable is actually in the command, if not, ignore it
+ if (cmd.CommandType != CommandType.Text || cmd.CommandText.Contains(varprops.VariableName))
+ {
+ //Add the parameter to the command list
+ cmd.AddParameter(varprops.VariableName, prop.GetValue(obj), varprops.DataType, varprops.Size, varprops.Nullable).Direction = varprops.Direction;
+ }
+ }
+ //Prepare the sql statement
+ cmd.Prepare();
+ //Exect the query and return the results
+ return cmd.ExecuteNonQuery();
+ }
+ /// <summary>
+ /// Executes a nonquery operation with the specified command using the object properties set with the
+ /// <see cref="SqlVariable"/> attributes
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="cmd"></param>
+ /// <param name="obj">The object containing the <see cref="SqlVariable"/> properties to write to command variables</param>
+ /// <returns>The number of rows affected</returns>
+ /// <exception cref="TypeLoadException"></exception>
+ /// <exception cref="ArgumentException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="AmbiguousMatchException"></exception>
+ /// <exception cref="TargetInvocationException"></exception>
+ public static async Task<ERRNO> ExecuteNonQueryAsync<T>(this DbCommand cmd, T obj) where T : notnull
+ {
+ if (obj == null)
+ {
+ throw new ArgumentNullException(nameof(obj));
+ }
+ //Get the objec type
+ Type objtype = typeof(T);
+ //Itterate through public properties
+ foreach (PropertyInfo prop in objtype.GetProperties())
+ {
+ //try to get the variable attribute of the propery
+ SqlVariable varprops = prop.GetCustomAttribute<SqlVariable>(true);
+ //This property is an sql variable, so lets add it
+ if (varprops == null)
+ {
+ continue;
+ }
+ //If the command type is text, then make sure the variable is actually in the command, if not, ignore it
+ if (cmd.CommandType != CommandType.Text || cmd.CommandText.Contains(varprops.VariableName))
+ {
+ //Add the parameter to the command list
+ cmd.AddParameter(varprops.VariableName, prop.GetValue(obj), varprops.DataType, varprops.Size, varprops.Nullable).Direction = varprops.Direction;
+ }
+ }
+ //Prepare the sql statement
+ await cmd.PrepareAsync();
+ //Exect the query and return the results
+ return await cmd.ExecuteNonQueryAsync();
+ }
+ }
+} \ No newline at end of file
diff --git a/VNLib.Plugins.Extensions.Data/SQL/EnumerableTable.cs b/VNLib.Plugins.Extensions.Data/SQL/EnumerableTable.cs
new file mode 100644
index 0000000..23cd889
--- /dev/null
+++ b/VNLib.Plugins.Extensions.Data/SQL/EnumerableTable.cs
@@ -0,0 +1,118 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Data
+* File: EnumerableTable.cs
+*
+* EnumerableTable.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Data;
+using System.Threading;
+using System.Data.Common;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+
+namespace VNLib.Plugins.Extensions.Data.SQL
+{
+ /// <summary>
+ /// A base class for client side async enumerable SQL queries
+ /// </summary>
+ /// <typeparam name="T">The entity type</typeparam>
+ public abstract class EnumerableTable<T> : TableManager, IAsyncEnumerable<T>
+ {
+ const string DEFAULT_ENUM_STATMENT = "SELECT *\r\nFROM @table\r\n;";
+
+ public EnumerableTable(Func<DbConnection> factory, string tableName) : base(factory, tableName)
+ {
+ //Build the default select all statment
+ Enumerate = DEFAULT_ENUM_STATMENT.Replace("@table", tableName);
+ }
+ public EnumerableTable(Func<DbConnection> factory) : base(factory)
+ { }
+
+ /// <summary>
+ /// The command that will be run against the database to return rows for enumeration
+ /// </summary>
+ protected string Enumerate { get; set; }
+
+ /// <summary>
+ /// The isolation level to use when creating the transaction during enumerations
+ /// </summary>
+ protected IsolationLevel TransactionIsolationLevel { get; set; } = IsolationLevel.ReadUncommitted;
+
+ IAsyncEnumerator<T> IAsyncEnumerable<T>.GetAsyncEnumerator(CancellationToken cancellationToken)
+ {
+ return GetAsyncEnumerator(cancellationToken: cancellationToken);
+ }
+
+ /// <summary>
+ /// Transforms a row from the <paramref name="reader"/> into the item type
+ /// to be returned when yielded.
+ /// </summary>
+ /// <param name="reader">The reader to get the item data from</param>
+ /// <param name="cancellationToken">A token to cancel the operation</param>
+ /// <returns>A task that returns the transformed item</returns>
+ /// <remarks>The <paramref name="reader"/> position is set before this method is invoked</remarks>
+ protected abstract Task<T> GetItemAsync(DbDataReader reader, CancellationToken cancellationToken);
+ /// <summary>
+ /// Invoked when an item is no longer in the enumerator scope, in the enumeration process.
+ /// </summary>
+ /// <param name="item">The item to cleanup</param>
+ /// <param name="cancellationToken">A token to cancel the operation</param>
+ /// <returns>A ValueTask that represents the cleanup process</returns>
+ protected abstract ValueTask CleanupItemAsync(T item, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets an <see cref="IAsyncEnumerator{T}"/> to enumerate items within the backing store.
+ /// </summary>
+ /// <param name="closeItems">Cleanup items after each item is enumerated and the enumeration scope has
+ /// returned to the enumerator</param>
+ /// <param name="cancellationToken">A token to cancel the enumeration</param>
+ /// <returns>A <see cref="IAsyncEnumerator{T}"/> to enumerate records within the store</returns>
+ public virtual async IAsyncEnumerator<T> GetAsyncEnumerator(bool closeItems = true, CancellationToken cancellationToken = default)
+ {
+ await using DbConnection db = GetConnection();
+ await db.OpenAsync(cancellationToken);
+ await using DbTransaction transaction = await db.BeginTransactionAsync(cancellationToken);
+ //Start the enumeration command
+ await using DbCommand cmd = db.CreateTextCommand(Enumerate, transaction);
+ await cmd.PrepareAsync(cancellationToken);
+ await using DbDataReader reader = await cmd.ExecuteReaderAsync(cancellationToken);
+ //loop through results and transform each element
+ while (reader.Read())
+ {
+ //get the item
+ T item = await GetItemAsync(reader, cancellationToken);
+ try
+ {
+ yield return item;
+ }
+ finally
+ {
+ if (closeItems)
+ {
+ //Cleanup the item
+ await CleanupItemAsync(item, cancellationToken);
+ }
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/VNLib.Plugins.Extensions.Data/SQL/SqlColumnName.cs b/VNLib.Plugins.Extensions.Data/SQL/SqlColumnName.cs
new file mode 100644
index 0000000..0039fb5
--- /dev/null
+++ b/VNLib.Plugins.Extensions.Data/SQL/SqlColumnName.cs
@@ -0,0 +1,65 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Data
+* File: SqlColumnName.cs
+*
+* SqlColumnName.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+namespace VNLib.Plugins.Extensions.Data.SQL
+{
+ /// <summary>
+ /// Property attribute that specifies the property represents an SQL column in the database
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Property)]
+ public class SqlColumnName : Attribute
+ {
+ public bool Nullable { get; }
+ public bool Unique { get; }
+ public bool PrimaryKey { get; }
+ public string ColumnName { get; init; }
+ /// <summary>
+ /// Specifies the property is an SQL column name
+ /// </summary>
+ /// <param name="columnName">Name of the SQL column</param>
+ /// <param name="primaryKey"></param>
+ /// <param name="nullable"></param>
+ /// <param name="unique"></param>
+ public SqlColumnName(string columnName, bool primaryKey = false, bool nullable = true, bool unique = false)
+ {
+ this.ColumnName = columnName;
+ this.PrimaryKey = primaryKey;
+ this.Nullable = nullable;
+ this.Unique = unique;
+ }
+ }
+
+ /// <summary>
+ /// Allows a type to declare itself as a <see cref="System.Data.DataTable"/> with the specified name
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple =false, Inherited = true)]
+ public class SqlTableName : Attribute
+ {
+ public string TableName { get; }
+
+ public SqlTableName(string tableName) => TableName = tableName;
+ }
+} \ No newline at end of file
diff --git a/VNLib.Plugins.Extensions.Data/SQL/SqlVariable.cs b/VNLib.Plugins.Extensions.Data/SQL/SqlVariable.cs
new file mode 100644
index 0000000..d33854a
--- /dev/null
+++ b/VNLib.Plugins.Extensions.Data/SQL/SqlVariable.cs
@@ -0,0 +1,58 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Data
+* File: SqlVariable.cs
+*
+* SqlVariable.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Data;
+
+namespace VNLib.Plugins.Extensions.Data.SQL
+{
+ /// <summary>
+ /// Property attribute that specifies the property is to be used for a given command variable
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
+ public class SqlVariable : Attribute
+ {
+ public string VariableName { get; init; }
+ public DbType DataType { get; init; }
+ public ParameterDirection Direction { get; init; }
+ public int Size { get; init; }
+ public bool Nullable { get; init; }
+ /// <summary>
+ /// Specifies the property to be used as an SQL variable
+ /// </summary>
+ /// <param name="variableName">Sql statement variable this property will substitute</param>
+ /// <param name="dataType">The sql data the property will represent</param>
+ /// <param name="direction">Data direction during execution</param>
+ /// <param name="size">Column size</param>
+ /// <param name="isNullable">Is this property allowed to be null</param>
+ public SqlVariable(string variableName, DbType dataType, ParameterDirection direction, int size, bool isNullable)
+ {
+ this.VariableName = variableName;
+ this.DataType = dataType;
+ this.Direction = direction;
+ this.Size = size;
+ this.Nullable = isNullable;
+ }
+ }
+}
diff --git a/VNLib.Plugins.Extensions.Data/SQL/TableManager.cs b/VNLib.Plugins.Extensions.Data/SQL/TableManager.cs
new file mode 100644
index 0000000..14c4e64
--- /dev/null
+++ b/VNLib.Plugins.Extensions.Data/SQL/TableManager.cs
@@ -0,0 +1,65 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Data
+* File: TableManager.cs
+*
+* TableManager.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Data.Common;
+
+namespace VNLib.Plugins.Extensions.Data.SQL
+{
+ /// <summary>
+ /// A class that contains basic structures for interacting with an SQL driven database
+ /// </summary>
+ public abstract class TableManager
+ {
+ private readonly Func<DbConnection> Factory;
+ protected string Insert { get; set; }
+ protected string Select { get; set; }
+ protected string Update { get; set; }
+ protected string Delete { get; set; }
+
+ /// <summary>
+ /// The name of the table specified during initialized
+ /// </summary>
+ protected string TableName { get; }
+
+ public TableManager(Func<DbConnection> factory, string tableName)
+ {
+ this.Factory = factory ?? throw new ArgumentNullException(nameof(factory));
+ this.TableName = !string.IsNullOrWhiteSpace(tableName) ? tableName : throw new ArgumentNullException(nameof(tableName));
+ }
+ public TableManager(Func<DbConnection> factory)
+ {
+ this.Factory = factory ?? throw new ArgumentNullException(nameof(factory));
+ this.TableName = "";
+ }
+ /// <summary>
+ /// Opens a new <see cref="DbConnection"/> by invoking the factory callback method
+ /// </summary>
+ /// <returns>The open connection</returns>
+ protected DbConnection GetConnection()
+ {
+ return Factory();
+ }
+ }
+}