public static void BulkInsert <T>(this TdsConnection cnn, IEnumerable <T> objects, string tableName, Dictionary <string, PropertyInfo> columnMapping)
        {
            var writer = cnn.TdsPackage.Writer;
            var reader = cnn.TdsPackage.Reader;
            var parser = cnn.StreamParser;

            MetadataBulkCopy[] metaDataAllColumns = null;

            writer.SendExecuteBatch($"SET FMTONLY ON select * from {tableName} SET FMTONLY OFF", cnn.SqlTransactionId);
            parser.ParseInput(count => { metaDataAllColumns = reader.ColMetaDataBulkCopy(count); });

            writer.ColumnsMetadata = columnMapping != null?GetUsedColumns(metaDataAllColumns, columnMapping) : GetUsedColumns(metaDataAllColumns);

            var bulkInsert = CreateBulkInsertStatement(tableName, writer.ColumnsMetadata);

            writer.SendExecuteBatch(bulkInsert, cnn.SqlTransactionId);
            parser.ParseInput();

            writer.NewPackage(TdsEnums.MT_BULK);
            var columnWriter = new TdsColumnWriter(writer);
            var rowWriter    = RowWriter.GetComplexWriter <T>(columnWriter);

            WriteBulkInsertColMetaData(writer);

            foreach (var o in objects)
            {
                writer.WriteByte(TdsEnums.SQLROW);
                rowWriter(columnWriter, o);
            }

            writer.WriteByteArray(Done);
            writer.SendLastMessage();
            parser.ParseInput();
            if (parser.Status != ParseStatus.Done)
            {
                parser.ParseInput();
            }
        }
        private void TestIntN(object value, int tdsType, bool nulltest, byte precision = 0, byte scale = 0, bool isPlp = false)
        {
            var stream = new TestStream();
            var writer = new TdsPackageWriter(stream);
            var reader = new TdsPackageReader(stream);

            SetupColMetaData(reader, writer, tdsType, precision, scale, isPlp);
            var columwriter  = new TdsColumnWriter(writer);
            var columnReader = new TdsColumnReader(reader);

            object result;

            if (nulltest)
            {
                writer.NewPackage(TdsEnums.MT_RPC);
                ObjectWriter(columwriter, tdsType, value, nulltest);
                writer.SendLastMessage();
                result = ObjectReader(columnReader, value);
                Assert.Null(result);
            }

            writer.NewPackage(TdsEnums.MT_RPC);
            ObjectWriter(columwriter, tdsType, value, false);
            writer.SendLastMessage();
            result = ObjectReader(columnReader, value);
            switch (value)
            {
            case Money v:
                Assert.Equal((decimal)v, result);
                break;

            case Money4 v:
                Assert.Equal((decimal)v, result);
                break;

            case SqlDate v:
                Assert.Equal((DateTime)v, result);
                break;

            case SqlDateTime2 v:
                Assert.Equal((DateTime)v, result);
                break;

            case SqlDateTime4 v:
                Assert.Equal((DateTime)v, result);
                break;

            case SqlImage v:
                Assert.Equal((byte[])v, result);
                break;

            case SqlUnicode v:
                Assert.Equal((string)v, result);
                break;

            case SqlXml v:
                Assert.Equal((string)v, result);
                break;

            case SqlVariant v:
                switch (v.Value)
                {
                case bool b: Assert.Equal(b, result); break;

                case byte b: Assert.Equal(b, result); break;

                case short b: Assert.Equal(b, result); break;

                case int b: Assert.Equal(b, result); break;

                case long b: Assert.Equal(b, result); break;

                case float b: Assert.Equal(b, result); break;

                case double b: Assert.Equal(b, result); break;

                case DateTime b: Assert.Equal(b, result); break;

                case Guid b: Assert.Equal(b, result); break;

                case decimal b: Assert.Equal(b, result); break;

                case byte[] b: Assert.Equal(b, result); break;

                case string b: Assert.Equal(b, result); break;

                case TimeSpan b: Assert.Equal(b, result); break;

                case DateTimeOffset b: Assert.Equal(b, result); break;

                default:
                    Assert.False(true);
                    break;
                }

                break;

            default:
                Assert.Equal(value, result);
                break;
            }

            Assert.Equal(reader.GetReadEndPos(), reader.GetReadPos());
            if (!new[]
            {
                TdsEnums.SQLBIGBINARY,
                TdsEnums.SQLBIGVARBINARY,
                TdsEnums.SQLBIGVARCHAR,
                TdsEnums.SQLBIGCHAR,
                TdsEnums.SQLTEXT,
                TdsEnums.SQLNVARCHAR,
                TdsEnums.SQLNTEXT,
                TdsEnums.SQLNCHAR,
                TdsEnums.SQLXMLTYPE,
                TdsEnums.SQLIMAGE,
            }.Contains(tdsType) && !(value is SqlVariant v1 && (v1.Value is string s || v1.Value is byte[])))
            {
                Assert.InRange(reader.GetReadPos() - 8, 0, TdsEnums.MaxSizeSqlValue);
            }
        }
        private static void ObjectWriter(TdsColumnWriter writer, int tdsType, object v, bool writeNull = true)
        {
            switch (tdsType)
            {
            case TdsEnums.SQLBITN when v is bool b: writer.WriteNullableSqlBit(writeNull ? (bool?)null : b, 0); return;

            case TdsEnums.SQLINTN when v is byte b: writer.WriteNullableSqlByte(writeNull ? (byte?)null : b, 0); return;

            case TdsEnums.SQLINTN when v is short b: writer.WriteNullableSqlInt16(writeNull ? (short?)null : b, 0); return;

            case TdsEnums.SQLINTN when v is int b: writer.WriteNullableSqlInt32(writeNull ? (int?)null : b, 0); return;

            case TdsEnums.SQLINTN when v is long b: writer.WriteNullableSqlInt64(writeNull ? (long?)null : b, 0); return;

            case TdsEnums.SQLMONEYN when v is Money4 b: writer.WriteNullableSqlMoney4(writeNull ? null : (decimal?)b, 0); return;

            case TdsEnums.SQLMONEYN when v is Money b: writer.WriteNullableSqlMoney(writeNull ? null : (decimal?)b, 0); return;

            case TdsEnums.SQLFLTN when v is float b: writer.WriteNullableSqlFloat(writeNull ? null : (float?)b, 0); return;

            case TdsEnums.SQLFLTN when v is double b: writer.WriteNullableSqlDouble(writeNull ? null : (double?)b, 0); return;

            case TdsEnums.SQLDATE when v is SqlDate b: writer.WriteNullableSqlDate(writeNull ? (DateTime?)null : (DateTime)b, 0); return;

            case TdsEnums.SQLTIME when v is TimeSpan b: writer.WriteNullableSqlTime(writeNull ? (TimeSpan?)null : b, 0); return;

            case TdsEnums.SQLDATETIME2 when v is SqlDateTime2 b: writer.WriteNullableSqlDateTime2(writeNull ? (DateTime?)null : (DateTime)b, 0); return;

            case TdsEnums.SQLDATETIMEOFFSET when v is DateTimeOffset b: writer.WriteNullableSqlDateTimeOffset(writeNull ? (DateTimeOffset?)null : b, 0); return;

            case TdsEnums.SQLDATETIMN when v is SqlDateTime4 b: writer.WriteNullableSqlDateTime(writeNull ? (DateTime?)null : (DateTime)b, 0); return;

            case TdsEnums.SQLDATETIMN when v is DateTime b: writer.WriteNullableSqlDateTime(writeNull ? (DateTime?)null : b, 0); return;

            case TdsEnums.SQLDECIMALN when v is decimal b: writer.WriteNullableSqlDecimal(writeNull ? (decimal?)null : b, 0); return;

            case TdsEnums.SQLUNIQUEID when v is Guid b: writer.WriteNullableSqlUniqueId(writeNull ? (Guid?)null : b, 0); return;

            case TdsEnums.SQLBIGBINARY when v is byte[] b: writer.WriteNullableSqlBinary(writeNull ? null : b, 0); return;

            case TdsEnums.SQLBIGVARBINARY when v is byte[] b: writer.WriteNullableSqlBinary(writeNull ? null : b, 0); return;

            case TdsEnums.SQLBIGVARCHAR when v is string b: writer.WriteNullableSqlString(writeNull ? null : b, 0); return;

            case TdsEnums.SQLBIGCHAR when v is string b: writer.WriteNullableSqlString(writeNull ? null : b, 0); return;

            case TdsEnums.SQLTEXT when v is string b: writer.WriteNullableSqlString(writeNull ? null : b, 0); return;

            case TdsEnums.SQLNVARCHAR when v is SqlUnicode b: if (writeNull)
                {
                    writer.WriteNullableSqlString(null, 0);
                }
                else
                {
                    writer.WriteNullableSqlString((string)b, 0);
                } return;

            case TdsEnums.SQLNTEXT when v is SqlUnicode b: if (writeNull)
                {
                    writer.WriteNullableSqlString(null, 0);
                }
                else
                {
                    writer.WriteNullableSqlString((string)b, 0);
                } return;

            case TdsEnums.SQLNCHAR when v is SqlUnicode b: if (writeNull)
                {
                    writer.WriteNullableSqlString(null, 0);
                }
                else
                {
                    writer.WriteNullableSqlString((string)b, 0);
                } return;

            case TdsEnums.SQLXMLTYPE when v is SqlXml b: if (writeNull)
                {
                    writer.WriteNullableSqlString(null, 0);
                }
                else
                {
                    writer.WriteNullableSqlString((string)b, 0);
                } return;

            case TdsEnums.SQLIMAGE when v is SqlImage b: if (writeNull)
                {
                    writer.WriteNullableSqlBinary(null, 0);
                }
                else
                {
                    writer.WriteNullableSqlBinary((byte[])b, 0);
                } return;

            case TdsEnums.SQLBIT: writer.WriteSqlBit((bool)v, 0); return;

            case TdsEnums.SQLINT1 when v is byte b: writer.WriteSqlByte(b, 0); return;

            case TdsEnums.SQLINT2 when v is short b: writer.WriteSqlInt16(b, 0); return;

            case TdsEnums.SQLINT4 when v is int b: writer.WriteSqlInt32(b, 0); return;

            case TdsEnums.SQLINT8 when v is long b: writer.WriteSqlInt64(b, 0); return;

            case TdsEnums.SQLMONEY4 when v is Money4 b: writer.WriteSqlMoney4((decimal)b, 0); return;

            case TdsEnums.SQLMONEY when v is Money b: writer.WriteSqlMoney((decimal)b, 0); return;

            case TdsEnums.SQLFLT4 when v is float b: writer.WriteSqlFloat(b, 0); return;

            case TdsEnums.SQLFLT8 when v is double b: writer.WriteSqlDouble(b, 0); return;

            case TdsEnums.SQLDATETIM4 when v is SqlDateTime4 b: writer.WriteSqlDateTime4((DateTime)b, 0); return;

            case TdsEnums.SQLDATETIME when v is DateTime b: writer.WriteSqlDateTime(b, 0); return;

            case TdsEnums.SQLVARIANT when v is SqlVariant b: if (writeNull)
                {
                    writer.WriteNullableSqlVariant(null, 0);
                }
                else
                {
                    writer.WriteNullableSqlVariant(b.Value, 0);
                } return;
            }

            throw new NotImplementedException();
        }