From 2efa4997f3a7b40c24fe59f3f6a9bddd0008f5f4 Mon Sep 17 00:00:00 2001 From: Matt McNabb Date: Wed, 28 Jan 2026 15:01:49 +1300 Subject: [PATCH 1/7] bcp out: correct precision for variable precision datetime types Signed-off-by: Matt McNabb --- include/freetds/tds.h | 2 +- src/dblib/bcp.c | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/include/freetds/tds.h b/include/freetds/tds.h index a16580412..26f80200c 100644 --- a/include/freetds/tds.h +++ b/include/freetds/tds.h @@ -693,7 +693,7 @@ struct tds_column */ TDS_TINYINT column_varint_size; /**< size of length when reading from wire (0, 1, 2 or 4) */ - TDS_TINYINT column_prec; /**< precision for decimal/numeric */ + TDS_TINYINT column_prec; /**< precision for decimal/numeric and some datetime types */ TDS_TINYINT column_scale; /**< scale for decimal/numeric */ struct diff --git a/src/dblib/bcp.c b/src/dblib/bcp.c index ad7d3534a..a50e07ac8 100644 --- a/src/dblib/bcp.c +++ b/src/dblib/bcp.c @@ -765,9 +765,18 @@ _bcp_convert_out(DBPROCESS * dbproc, TDSCOLUMN *curcol, BCP_HOSTCOLINFO *hostcol if (is_datetime_type(srctype) && is_ascii_type(hostcol->datatype)) { TDSDATEREC when; + /* Some date/time types have variable precision (e.g. MS datetime2), + * and set curcol->column_prec with that precision. + * DATETIME is printed with precision 3 by MS BCP & ASE BCP however + * curcol->column_prec is not set for that type. + * (It might be better to set column_prec at load time for all + * of the datetime types...) + */ + int prec = curcol->column_prec ? curcol->column_prec : 3; + tds_datecrack(srctype, src, &when); - buflen = (int)tds_strftime((TDS_CHAR *)(*p_data), 256, - bcpdatefmt, &when, 3); + buflen = (int)tds_strftime((TDS_CHAR*)(*p_data), 256, + bcpdatefmt, &when, prec); } else if (srclen == 0 && is_variable_type(curcol->column_type) && is_ascii_type(hostcol->datatype)) { /* From f905073e1207eaec2ff2a5062ce0adfbf1bffeed Mon Sep 17 00:00:00 2001 From: Matt McNabb Date: Tue, 17 Feb 2026 18:25:39 +1300 Subject: [PATCH 2/7] Implement a TDSFILEOUTSTREAM and use it for bcp out MSVC profiler: reduce time spent in fwrite from 41% to 17.3% for a test case of 400MB of data. Signed-off-by: Matt McNabb --- include/freetds/stream.h | 19 ++++++ src/dblib/bcp.c | 31 +++++----- src/tds/stream.c | 121 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 156 insertions(+), 15 deletions(-) diff --git a/include/freetds/stream.h b/include/freetds/stream.h index 50c92625e..3bd8b4f2d 100644 --- a/include/freetds/stream.h +++ b/include/freetds/stream.h @@ -24,6 +24,8 @@ #error Include tds.h first #endif +#include /* FILE * */ +#include /* TDS_ICONV_DIRECTION */ #include /** define a stream of data used for input */ @@ -51,6 +53,23 @@ typedef struct tds_output_stream { size_t buf_len; } TDSOUTSTREAM; +/** Output stream that writes to file. + * Does not open/close fp; and fp can be NULL. + */ +typedef struct tds_fileout_stream { + TDSOUTSTREAM stream; + FILE* fp; + char block[512]; + /** How to buffer the data -- _IONBF, _IOLBF or _IOFBF (default). + * In _IOLBF line will still be flushed on reaching block size. */ + int buff_type; +} TDSFILEOUTSTREAM; + +void tds_fileout_stream_init(TDSFILEOUTSTREAM* s, FILE* fp, int buff_type); + +TDSRET tds_fileout_stream_flush(TDSFILEOUTSTREAM* s); +TDSRET tds_fileout_stream_put(TDSFILEOUTSTREAM* s, void const *src, size_t n); + /** Convert a stream from istream to ostream using a specific conversion */ TDSRET tds_convert_stream(TDSSOCKET * tds, TDSICONV * char_conv, TDS_ICONV_DIRECTION direction, TDSINSTREAM * istream, TDSOUTSTREAM *ostream); diff --git a/src/dblib/bcp.c b/src/dblib/bcp.c index a50e07ac8..fc01ebc6f 100644 --- a/src/dblib/bcp.c +++ b/src/dblib/bcp.c @@ -44,6 +44,7 @@ #include #include #include +#include #include #include #include @@ -856,7 +857,7 @@ bcp_cache_prefix_len(BCP_HOSTCOLINFO *hostcol, const TDSCOLUMN *curcol) } static RETCODE -bcp_write_prefix(FILE *hostfile, BCP_HOSTCOLINFO *hostcol, TDSCOLUMN *curcol, int buflen) +bcp_write_prefix(TDSFILEOUTSTREAM *hoststream, BCP_HOSTCOLINFO *hostcol, TDSCOLUMN *curcol, int buflen) { union { TDS_TINYINT ti; @@ -883,10 +884,7 @@ bcp_write_prefix(FILE *hostfile, BCP_HOSTCOLINFO *hostcol, TDSCOLUMN *curcol, in u.li = buflen; break; } - if (fwrite(&u, plen, 1, hostfile) == 1) - return SUCCEED; - - return FAIL; + return TDS_SUCCEED(tds_fileout_stream_put(hoststream, &u, plen)) ? SUCCEED : FAIL; } /** @@ -903,7 +901,8 @@ bcp_write_prefix(FILE *hostfile, BCP_HOSTCOLINFO *hostcol, TDSCOLUMN *curcol, in static RETCODE _bcp_exec_out(DBPROCESS * dbproc, DBINT * rows_copied) { - FILE *hostfile = NULL; + TDSFILEOUTSTREAM hoststream; + FILE* hostfp = NULL; TDS_UCHAR *data = NULL; int i; @@ -966,11 +965,11 @@ _bcp_exec_out(DBPROCESS * dbproc, DBINT * rows_copied) * TODO above we allocate many buffer just to convert and store * to file.. avoid all that passages... */ - - if (!(hostfile = fopen(dbproc->hostfileinfo->hostfile, "w"))) { + if (!(hostfp = fopen(dbproc->hostfileinfo->hostfile, "w"))) { dbperror(dbproc, SYBEBCUO, errno); goto Cleanup; } + tds_fileout_stream_init(&hoststream, hostfp, _IOFBF); /* fetch a row of data from the server */ @@ -1006,7 +1005,7 @@ _bcp_exec_out(DBPROCESS * dbproc, DBINT * rows_copied) } /* The prefix */ - if (bcp_write_prefix(hostfile, hostcol, curcol, buflen) != SUCCEED) + if (bcp_write_prefix(&hoststream, hostcol, curcol, buflen) != SUCCEED) goto write_error; /* The data */ @@ -1015,23 +1014,25 @@ _bcp_exec_out(DBPROCESS * dbproc, DBINT * rows_copied) } if (buflen > 0) { - if (fwrite(data, buflen, 1, hostfile) != 1) + if ( !TDS_SUCCEED(tds_fileout_stream_put(&hoststream, data, buflen)) ) goto write_error; } /* The terminator */ if (hostcol->terminator && hostcol->term_len > 0) { - if (fwrite(hostcol->terminator, hostcol->term_len, 1, hostfile) != 1) + if ( !TDS_SUCCEED(tds_fileout_stream_put(&hoststream, hostcol->terminator, hostcol->term_len)) ) goto write_error; } } rows_written++; } - if (fclose(hostfile) != 0) { + + tds_fileout_stream_flush(&hoststream); + if (hostfp && fclose(hostfp) != 0) { dbperror(dbproc, SYBEBCUC, errno); goto Cleanup; } - hostfile = NULL; + hostfp = NULL; if (row_of_query + 1 < dbproc->hostfileinfo->firstrow) { /* @@ -1052,8 +1053,8 @@ _bcp_exec_out(DBPROCESS * dbproc, DBINT * rows_copied) dbperror(dbproc, SYBEBCWE, errno); Cleanup: - if (hostfile) - fclose(hostfile); + if (hostfp) + fclose(hostfp); free(data); return FAIL; } diff --git a/src/tds/stream.c b/src/tds/stream.c index b5d9dd1af..76bf723a8 100644 --- a/src/tds/stream.c +++ b/src/tds/stream.c @@ -375,3 +375,124 @@ tds_dynamic_stream_init(TDSDYNAMICSTREAM * stream, void **ptr, size_t allocated) } +static int tds_fileout_stream_write(TDSOUTSTREAM* stream, size_t n) +{ + /* The meaning of this function is that the caller has placed "n" bytes + * at the memory location stream->buffer, and they want that flushed + * to the actual output. + * + * In the context of our buffered stream, this implies also writing + * any earlier buffered data. + */ + size_t extent, n_written; + + TDSFILEOUTSTREAM* s = (TDSFILEOUTSTREAM*)stream; + if (!s->fp) + return n; + + /* Don't read off the end of buffer */ + if (n > stream->buf_len) + n = stream->buf_len; + + /* How much data to output (the buffered data and the new data) */ + extent = (stream->buffer - s->block) + n; + if (extent == 0) + return 0; + + n_written = fwrite(s->block, 1, extent, s->fp); + + if (n_written == 0) + return -1; + + if (n_written >= extent) + { + /* All stored data written; full block available */ + stream->buffer = s->block; + stream->buf_len = sizeof s->block; + } + else + { + /* If they requested to not write the whole buffer for some reason, + * or the write partially failed, we'll have to keep the remaining + * data buffered. + */ + extent -= n_written; + memmove(s->block, s->block + n_written, extent); + stream->buffer = s->block + extent; + stream->buf_len = sizeof s->block - extent; + } + + /* Returning how many characters were written to the file, which might + * be greater than n. (This seems compatible with how this function + * is used at existing call sites). */ + return n_written; +} + +void tds_fileout_stream_init(TDSFILEOUTSTREAM* s, FILE* fp, int buff_type) +{ + s->buff_type = buff_type; + s->fp = fp; + s->stream.write = tds_fileout_stream_write; + s->stream.buffer = s->block; + s->stream.buf_len = sizeof s->block; +} + +TDSRET tds_fileout_stream_flush(TDSFILEOUTSTREAM* s) +{ + return s->stream.write(&s->stream, 0) >= 0 ? TDS_SUCCESS : TDS_FAIL; +} + +static +TDSRET tds_fileout_stream_putbuf(TDSFILEOUTSTREAM* s, void const* src, size_t n) +{ + /* Optimization - If data fully fits in output buffer + * we can bypass the stream copy method */ + if (n <= s->stream.buf_len) + { + memcpy(s->stream.buffer, src, n); + s->stream.buffer += n; + s->stream.buf_len -= n; + if (s->stream.buf_len == 0) + return tds_fileout_stream_flush(s); + return TDS_SUCCESS; + } + else + { + TDSSTATICINSTREAM is; + + /* Copy all of the source data to the output */ + tds_staticin_stream_init(&is, src, n); + return tds_copy_stream(&is.stream, &s->stream); + } +} + +TDSRET tds_fileout_stream_put(TDSFILEOUTSTREAM* s, void const* src, size_t n) +{ + size_t len; + + if (!s->fp || !n) + return TDS_SUCCESS; + + if (s->buff_type == _IONBF) + return fwrite(src, n, 1, s->fp) == 1 ? TDS_SUCCESS : TDS_FAIL; + + if (s->buff_type == _IOFBF) + return tds_fileout_stream_putbuf(s, src, n); + + /* Otherwise, line buffered */ + + /* Flush everything up to the last new-line in the source */ + for (len = n; len; --len) + if (((char const*)src)[len - 1] == '\n') + break; + + TDS_PROPAGATE(tds_fileout_stream_putbuf(s, src, len)); + TDS_PROPAGATE(tds_fileout_stream_flush(s)); + + /* Buffer any remaining partial line */ + if (len == n) + return TDS_SUCCESS; + + return tds_fileout_stream_putbuf(s, (char const*)src + len, n - len); +} + From 566731d63c3b6bb6581544633448f3056138b180 Mon Sep 17 00:00:00 2001 From: Matt McNabb Date: Wed, 18 Feb 2026 15:59:47 +1300 Subject: [PATCH 3/7] Default to not fflush the log file Increase log performance by factor of 3. Set TDSDUMP_FFLUSH to anything, to flush after each line. Should only be needed when debugging segfaults. Signed-off-by: Matt McNabb --- src/tds/log.c | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/tds/log.c b/src/tds/log.c index 5e3bbaf49..5c76fef3d 100644 --- a/src/tds/log.c +++ b/src/tds/log.c @@ -63,6 +63,8 @@ static tds_mutex g_dump_mutex = TDS_MUTEX_INITIALIZER; static FILE* tdsdump_append(void); +static bool g_dumpfile_fflush = false; + #ifdef TDS_ATTRIBUTE_DESTRUCTOR static void __attribute__((destructor)) tds_util_deinit(void) @@ -176,6 +178,9 @@ tdsdump_open(const tds_dir_char *filename) if (result) tds_write_dump = true; + + g_dumpfile_fflush = (getenv("TDSDUMP_FFLUSH") != NULL); + tds_mutex_unlock(&g_dump_mutex); if (result) { @@ -400,7 +405,8 @@ tdsdump_dump_buf_impl(const char* file, unsigned int level_line, const char *msg } fputs("\n", dumpfile); - fflush(dumpfile); + if ( g_dumpfile_fflush ) + fflush(dumpfile); #ifndef TDS_HAVE_MUTEX if (tds_append_mode) { @@ -462,7 +468,8 @@ tdsdump_log_impl(const char* file, unsigned int level_line, const char *fmt, ... vfprintf(dumpfile, fmt, ap); va_end(ap); - fflush(dumpfile); + if (g_dumpfile_fflush) + fflush(dumpfile); #ifndef TDS_HAVE_MUTEX if (tds_append_mode) { From 0572a7f41109678c9112cfa9f4bd978217815cbc Mon Sep 17 00:00:00 2001 From: Matt McNabb Date: Wed, 18 Feb 2026 15:08:33 +1300 Subject: [PATCH 4/7] optimize int32 and 0-100 to string MSVC profiler shows 5% gain in bcp -c from this change Signed-off-by: Matt McNabb --- include/freetds/convert.h | 8 ++++ src/tds/convert.c | 99 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 100 insertions(+), 7 deletions(-) diff --git a/include/freetds/convert.h b/include/freetds/convert.h index cdc89347f..fcbb478fb 100644 --- a/include/freetds/convert.h +++ b/include/freetds/convert.h @@ -91,6 +91,14 @@ TDS_INT tds_convert(const TDSCONTEXT *context, int srctype, const void *src, TDS size_t tds_strftime(char *buf, size_t maxsize, const char *format, const TDSDATEREC * timeptr, int prec); +/* Fast int to string (massively outperforms sprintf in hot loop). + * No null termination; returns number of characters read. + */ +size_t tds_u32toafast_right(char out[10], uint32_t v); +size_t tds_u32toafast(char out[10], uint32_t v); +size_t tds_i32toafast(char out[11], int32_t v); +void tds_02dfast(char out[2], int v); /* v must be 0-99 (non negative) */ + #ifdef __cplusplus #if 0 { diff --git a/src/tds/convert.c b/src/tds/convert.c index 403956465..ff1629fe9 100644 --- a/src/tds/convert.c +++ b/src/tds/convert.c @@ -643,6 +643,83 @@ tds_convert_int8_numeric(unsigned char scale, return tds_numeric_change_prec_scale(&(cr->n), orig_prec, orig_scale); } +static const char DIG2[200] = +"00010203040506070809" +"10111213141516171819" +"20212223242526272829" +"30313233343536373839" +"40414243444546474849" +"50515253545556575859" +"60616263646566676869" +"70717273747576777879" +"80818283848586878889" +"90919293949596979899"; + +static inline void tds02dfast(char out[2], int v) +{ + out[0] = DIG2[v * 2]; + out[1] = DIG2[v * 2 + 1]; +} + +void tds_02dfast(char out[2], int v) +{ + tds02dfast(out, v); +} + +/** Format an unsigned int32 right-justified on a char[10] buffer. + * Returns the amount of leading padding. */ +size_t tds_u32toafast_right(char out[10], uint32_t v) +{ + char* p = out + 10; + + /* Fill buffer from the end so it comes out in the right order */ + while (v >= 100u) { + uint32_t q = v / 100u; + uint32_t r = v - q * 100u; + p -= 2; + tds02dfast(p, r); + v = q; + } + + if (v >= 10u) + { + p -= 2; + tds02dfast(p, v); + } + else if (v > 0u) + *--p = (char)('0' + v); + + return p - out; +} + +/* Returns number of digits written */ +size_t tds_u32toafast(char out[10], uint32_t v) +{ + char buf[10]; + size_t pad; + + /* Small case optimizations */ + if (v < 10u) { *out = (char)('0' + v); return 1; } + if (v < 100u) { tds02dfast(out, v); return 2; } + + pad = tds_u32toafast_right(buf, v); + + memcpy(out, buf + pad, 10 - pad); + return 10 - pad; +} + +size_t tds_i32toafast(char out[11], int32_t x) +{ + size_t sign_length = 0; + if (x < 0) { + *out++ = '-'; + sign_length = 1; + if (TDS_LIKELY(x != (-2147483647 - 1))) + x = -x; + } + return tds_u32toafast(out, x) + sign_length; +} + static TDS_INT tds_convert_int(TDS_INT num, int desttype, CONV_RESULT * cr) { @@ -651,7 +728,7 @@ tds_convert_int(TDS_INT num, int desttype, CONV_RESULT * cr) switch (desttype) { case TDS_CONVERT_CHAR: case CASE_ALL_CHAR: - sprintf(tmp_str, "%d", num); + tmp_str[tds_i32toafast(tmp_str, num)] = 0; return string_to_result(desttype, tmp_str, cr); break; case SYBSINT1: @@ -3077,8 +3154,9 @@ two_digit(char *out, int num) num = 1; if (num > 31) num = 31; - out[0] = num < 10 ? ' ' : num/10 + '0'; - out[1] = num%10 + '0'; + tds02dfast(out, num); + if (out[0] == '0') + out[0] = ' '; } /** @@ -3123,7 +3201,7 @@ tds_strftime(char *buf, size_t maxsize, const char *format, const TDSDATEREC * d #endif /* more characters are required because we replace %z with up to 7 digits */ - our_format = tds_new(char, strlen(format) + 1 + 5 + 1); + our_format = tds_new(char, strlen(format) + 7); if (!our_format) return 0; @@ -3178,9 +3256,16 @@ tds_strftime(char *buf, size_t maxsize, const char *format, const TDSDATEREC * d --pz; if (prec || pz <= our_format || pz[-1] != '.') { - char buf[12]; - - sprintf(buf, "%010d", dr->decimicrosecond & 0x7fffffff); + char buf[10]; + /* decimicrosecond should be a number from 0 to 9,999,999 + * (regardless of prec, which is just the requested + * display precision). In case we are erroneously passed a + * sign, discard the sign. If erroneously given a number + * too big, discard leading digits. + */ + size_t pad = tds_u32toafast_right(buf, dr->decimicrosecond & 0x7fffffff); + if (pad > 3) + memset(buf + 3, '0', pad - 3); memcpy(pz, buf + 3, prec); strcpy(pz + prec, format + (pz - our_format) + z_opt_len); pz += prec; From 8de97baaeb18aacb8feb299c532fc418be5f965f Mon Sep 17 00:00:00 2001 From: Matt McNabb Date: Thu, 19 Feb 2026 13:50:38 +1300 Subject: [PATCH 5/7] Enable use of ENABLE_EXTRA_CHECKS for code subsets Signed-off-by: Matt McNabb --- include/freetds/checks.h | 2 +- src/tds/tds_checks.c | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/include/freetds/checks.h b/include/freetds/checks.h index 825dec9f0..d7b8ef638 100644 --- a/include/freetds/checks.h +++ b/include/freetds/checks.h @@ -57,8 +57,8 @@ void tds_check_freeze_extra(const TDSFREEZE * freeze); # define TDS_MARK_UNDEFINED(ptr, len) do {} while(0) #endif +void tds_extra_assert_check(const char* fn, int line, int cond, const char* cond_str); #if ENABLE_EXTRA_CHECKS -void tds_extra_assert_check(const char *fn, int line, int cond, const char *cond_str); # define tds_extra_assert(cond) \ tds_extra_assert_check(__FILE__, __LINE__, cond, #cond) #else diff --git a/src/tds/tds_checks.c b/src/tds/tds_checks.c index 59c457153..6c7c3903d 100644 --- a/src/tds/tds_checks.c +++ b/src/tds/tds_checks.c @@ -351,6 +351,7 @@ tds_check_freeze_extra(const TDSFREEZE * freeze) break; /* found */ } } +#endif /* ENABLE_EXTRA_CHECKS */ void tds_extra_assert_check(const char *fn, int line, int cond, const char *cond_str) @@ -362,5 +363,3 @@ tds_extra_assert_check(const char *fn, int line, int cond, const char *cond_str) abort(); } - -#endif /* ENABLE_EXTRA_CHECKS */ From 3dc29f9d701594adb4b4e83a492b3c4690b30259 Mon Sep 17 00:00:00 2001 From: Matt McNabb Date: Thu, 19 Feb 2026 13:52:12 +1300 Subject: [PATCH 6/7] Optimize the default bcp datetime formatting. MSVC profiler showed 35% in tds_strftime for a test case. Now 2.7% spent in _bcp_strftime. Signed-off-by: Matt McNabb --- src/dblib/bcp.c | 71 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 4 deletions(-) diff --git a/src/dblib/bcp.c b/src/dblib/bcp.c index fc01ebc6f..6c233c7c7 100644 --- a/src/dblib/bcp.c +++ b/src/dblib/bcp.c @@ -41,6 +41,7 @@ #endif #include +#include #include #include #include @@ -53,6 +54,8 @@ #include #include +#define BCP_DEFAULT_DATEFMT "%Y-%m-%d %H:%M:%S.%z" + #define HOST_COL_CONV_ERROR 1 #define HOST_COL_NULL_ERROR 2 @@ -86,6 +89,7 @@ static int rtrim_u16(uint16_t *str, int len, uint16_t space); static STATUS _bcp_read_hostfile(DBPROCESS * dbproc, FILE * hostfile, bool *row_error, bool skip); static int _bcp_readfmt_colinfo(DBPROCESS * dbproc, char *buf, BCP_HOSTCOLINFO * ci); static int _bcp_get_term_var(const BYTE * pdata, const BYTE * term, int term_len); +static int _bcp_strftime(TDS_UCHAR* buf, size_t maxsize, const char* format, const TDSDATEREC* dr, int prec); /* * "If a host file is being used ... the default data formats are as follows: @@ -776,8 +780,7 @@ _bcp_convert_out(DBPROCESS * dbproc, TDSCOLUMN *curcol, BCP_HOSTCOLINFO *hostcol int prec = curcol->column_prec ? curcol->column_prec : 3; tds_datecrack(srctype, src, &when); - buflen = (int)tds_strftime((TDS_CHAR*)(*p_data), 256, - bcpdatefmt, &when, prec); + buflen = _bcp_strftime(*p_data, 256, bcpdatefmt, &when, prec); } else if (srclen == 0 && is_variable_type(curcol->column_type) && is_ascii_type(hostcol->datatype)) { /* @@ -926,9 +929,13 @@ _bcp_exec_out(DBPROCESS * dbproc, DBINT * rows_copied) tds = dbproc->tds_socket; assert(tds); + /* See if they want a BCP date format different from the default. + * The ASE bcp client does use the locale for bcp out; but all bcp clients + * appear to successfully read this format for input, regardless of locale. + */ bcpdatefmt = getenv("FREEBCP_DATEFMT"); - if (!bcpdatefmt) - bcpdatefmt = "%Y-%m-%d %H:%M:%S.%z"; + if (bcpdatefmt && !strcmp(bcpdatefmt, BCP_DEFAULT_DATEFMT)) + bcpdatefmt = NULL; if (dbproc->bcpinfo->direction == DB_QUERYOUT ) { if (TDS_FAILED(tds_submit_query(tds, tds_dstr_cstr(&dbproc->bcpinfo->tablename)))) @@ -2431,3 +2438,59 @@ _bcp_free_storage(DBPROCESS * dbproc) dbproc->bcpinfo = NULL; } +/** Fast strftime for default bcp format */ +static int _bcp_strftime(TDS_UCHAR* buf, size_t maxsize, const char* format, const TDSDATEREC* dr, int prec) +{ + char* cbuf = (char*)buf; + + if (format) + return (int)tds_strftime(cbuf, maxsize, format, dr, prec); + + cbuf += tds_u32toafast(cbuf, dr->year); + + *cbuf++ = '-'; + tds_02dfast(cbuf, dr->month + 1); + cbuf += 2; + + *cbuf++ = '-'; + tds_02dfast(cbuf, dr->day); + cbuf += 2; + + *cbuf++ = ' '; + tds_02dfast(cbuf, dr->hour); + cbuf += 2; + + *cbuf++ = ':'; + tds_02dfast(cbuf, dr->minute); + cbuf += 2; + + *cbuf++ = ':'; + tds_02dfast(cbuf, dr->second); + cbuf += 2; + + if (prec > 0) + { + char ibuf[10]; + *cbuf++ = '.'; + + /* Mask off a negative sign (input should not contain this anyway) */ + memset(ibuf, '0', tds_u32toafast_right(ibuf, dr->decimicrosecond & 0x7FFFFFFF)); + /* Skip first 3 digits here, as decimicrosecond should not exceed 9,999,999 */ + memcpy(cbuf, ibuf + 3, prec); + cbuf += prec; + } + *cbuf = '\0'; + +#if ENABLE_EXTRA_CHECKS + { + char tbuf[256]; + tds_strftime(tbuf, sizeof tbuf, BCP_DEFAULT_DATEFMT, dr, prec); + if (strcmp(tbuf, (char*)buf)) + { + fprintf(stderr, "_bcp_strftime(%s) does not match tds_strftime(%s)\n", tbuf, (char *)buf); + tds_extra_assert(!"_bcp_strftime mismatch"); + } + } +#endif + return (int)(cbuf - (char*)buf); +} From 8d3353020ee9d0d50e90b978964998a320539192 Mon Sep 17 00:00:00 2001 From: Matt McNabb Date: Thu, 19 Feb 2026 14:20:11 +1300 Subject: [PATCH 7/7] bcp out: Tune block size for file buffer. Gain of about 1% from baseline. Signed-off-by: Matt McNabb --- include/freetds/stream.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/freetds/stream.h b/include/freetds/stream.h index 3bd8b4f2d..407dcb2d4 100644 --- a/include/freetds/stream.h +++ b/include/freetds/stream.h @@ -59,7 +59,7 @@ typedef struct tds_output_stream { typedef struct tds_fileout_stream { TDSOUTSTREAM stream; FILE* fp; - char block[512]; + char block[4096]; /** How to buffer the data -- _IONBF, _IOLBF or _IOFBF (default). * In _IOLBF line will still be flushed on reaching block size. */ int buff_type;