From 0b0fbb40aa4a1f3dd7cfcc6de4b430ecc947a548 Mon Sep 17 00:00:00 2001 From: Frediano Ziglio Date: Thu, 28 Mar 2024 16:36:19 +0000 Subject: [PATCH 1/7] defncopy: Use memory instead of temporary file Index script is not that huge, just allocate in memory. Signed-off-by: Frediano Ziglio --- src/apps/defncopy.c | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/apps/defncopy.c b/src/apps/defncopy.c index 3bf9d5892..46f1469d0 100644 --- a/src/apps/defncopy.c +++ b/src/apps/defncopy.c @@ -364,17 +364,14 @@ print_ddl(DBPROCESS *dbproc, PROCEDURE *procedure) }; int colmap[TDS_VECTOR_SIZE(colmap_names)]; - FILE *create_index; + char *create_index = NULL; RETCODE erc; int row_code, iresultset, i, ret; int maxnamelen = 0, nrows = 0; char **p_str; - create_index = tmpfile(); - assert(dbproc); assert(procedure); - assert(create_index); /* sp_help returns several result sets. We want just the second one, for now */ for (iresultset=1; (erc = dbresults(dbproc)) != NO_MORE_RESULTS; iresultset++) { @@ -393,6 +390,7 @@ print_ddl(DBPROCESS *dbproc, PROCEDURE *procedure) /* Look for index data */ if (0 == strcmp("index_name", dbcolname(dbproc, 1))) { char *index_name, *index_description, *index_keys, *p, fprimary=0; + char *tmp_str; DBINT datlen; assert(dbnumcols(dbproc) >=3 ); /* column had better be in range */ @@ -440,14 +438,24 @@ print_ddl(DBPROCESS *dbproc, PROCEDURE *procedure) unique[0] = '\0'; sprintf(index_description, "%s %s", unique, pclustering); } - /* Put it to a temporary file; we'll print it after the CREATE TABLE statement. */ + /* Put it to a temporary variable; we'll print it after the CREATE TABLE statement. */ + tmp_str = create_index; + create_index = create_index ? create_index : ""; if (fprimary) { - fprintf(create_index, "ALTER TABLE %s.%s ADD CONSTRAINT %s PRIMARY KEY %s (%s)\nGO\n\n", - procedure->owner, procedure->name, index_name, index_description, index_keys); + ret = asprintf(&create_index, + "%sALTER TABLE %s.%s ADD CONSTRAINT %s PRIMARY KEY %s (%s)\nGO\n\n", + create_index, + procedure->owner, procedure->name, + index_name, index_description, index_keys); } else { - fprintf(create_index, "CREATE %s INDEX %s on %s.%s(%s)\nGO\n\n", - index_description, index_name, procedure->owner, procedure->name, index_keys); + ret = asprintf(&create_index, + "%sCREATE %s INDEX %s on %s.%s(%s)\nGO\n\n", + create_index, + index_description, index_name, + procedure->owner, procedure->name, index_keys); } + assert(ret >= 0); + free(tmp_str); free(index_name); free(index_description); @@ -591,17 +599,15 @@ print_ddl(DBPROCESS *dbproc, PROCEDURE *procedure) printf("\t)\nGO\n\n"); /* print the CREATE INDEX statements */ - rewind(create_index); - while ((i = fgetc(create_index)) != EOF) { - fputc(i, stdout); - } + if (create_index != NULL) + fputs(create_index, stdout); cleanup: p_str = (char **) ddl; for (i=0; i < nrows * (sizeof(struct DDL)/sizeof(char*)); ++i) free(p_str[i]); free(ddl); - fclose(create_index); + free(create_index); return nrows; } From b341ed03b375d0e33490426287139504944c8034 Mon Sep 17 00:00:00 2001 From: Frediano Ziglio Date: Sat, 30 Mar 2024 10:40:54 +0000 Subject: [PATCH 2/7] defncopy: Fix MS column length for N(VAR)CHAR types The length from sp_help is in bytes, not in code units. Signed-off-by: Frediano Ziglio --- src/apps/defncopy.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/apps/defncopy.c b/src/apps/defncopy.c index 46f1469d0..a1610a616 100644 --- a/src/apps/defncopy.c +++ b/src/apps/defncopy.c @@ -369,6 +369,7 @@ print_ddl(DBPROCESS *dbproc, PROCEDURE *procedure) int row_code, iresultset, i, ret; int maxnamelen = 0, nrows = 0; char **p_str; + bool is_ms = false; assert(dbproc); assert(procedure); @@ -481,6 +482,8 @@ print_ddl(DBPROCESS *dbproc, PROCEDURE *procedure) break; } } + if (strcasecmp(name, "Collation") == 0) + is_ms = true; } for (i = 0; i < TDS_VECTOR_SIZE(colmap); ++i) { if (colmap[i] == -1) { @@ -583,6 +586,8 @@ print_ddl(DBPROCESS *dbproc, PROCEDURE *procedure) ltrim(rtrim(ddl[i].length)); if (strcmp(ddl[i].length, "-1") == 0) ret = asprintf(&type, "%s(max)", ddl[i].type); + else if (is_ms && is_in(ddl[i].type, "nchar\0nvarchar\0")) + ret = asprintf(&type, "%s(%d)", ddl[i].type, atoi(ddl[i].length)/2); else ret = asprintf(&type, "%s(%s)", ddl[i].type, ddl[i].length); } From a70f563a3311f1ffd3f595ef6b13cdc0b7908642 Mon Sep 17 00:00:00 2001 From: Frediano Ziglio Date: Sun, 24 Mar 2024 09:58:01 +0000 Subject: [PATCH 3/7] defncopy: Quote strings and identifiers Allows to handle weird characters (like spaces) in strings and identifiers. Handle memory marking in a list to easily free. I plan to move that part of code to common code. Signed-off-by: Frediano Ziglio --- src/apps/defncopy.c | 109 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 100 insertions(+), 9 deletions(-) diff --git a/src/apps/defncopy.c b/src/apps/defncopy.c index a1610a616..b2eb34884 100644 --- a/src/apps/defncopy.c +++ b/src/apps/defncopy.c @@ -129,6 +129,91 @@ static void usage(const char invoked_as[]); static char *rtrim(char *s); static char *ltrim(char *s); +typedef struct tmp_buf { + struct tmp_buf *next; + char buf[1]; +} tmp_buf; + +static tmp_buf *tmp_list = NULL; + +static void* +tmp_malloc(size_t len) +{ + tmp_buf *tmp = malloc(sizeof(tmp_buf*) + len); + if (!tmp) { + fprintf(stderr, "Out of memory\n"); + exit(1); + } + tmp->next = tmp_list; + tmp_list = tmp; + return tmp->buf; +} + +static void +tmp_free(void) +{ + while (tmp_list) { + tmp_buf *next = tmp_list->next; + free(tmp_list); + tmp_list = next; + } +} + +static size_t +count_chars(const char *s, char c) +{ + size_t num = 0; + if (c != 0) { + --s; + while ((s = strchr(s + 1, c)) != NULL) + ++num; + } + return num; +} + +static char * +sql_quote(char *dest, const char *src, char quote_char) +{ + for (; *src; ++src) { + if (*src == quote_char) + *dest++ = *src; + *dest++ = *src; + } + return dest; +} + +static const char * +quote_id(const char *id) +{ + size_t n, len; + char *s, *p; + + n = count_chars(id, ']'); + len = 1 + strlen(id) + n + 1 + 1; + p = s = tmp_malloc(len); + *p++ = '['; + p = sql_quote(p, id, ']'); + *p++ = ']'; + *p = 0; + return s; +} + +static const char * +quote_str(const char *str) +{ + size_t n, len; + char *s, *p; + + n = count_chars(str, '\''); + if (!n) + return str; + len = strlen(str) + n + 1; + s = tmp_malloc(len); + p = sql_quote(s, str, '\''); + *p = 0; + return s; +} + /* global variables */ static OPTIONS options; static char use_statement[512]; @@ -229,8 +314,9 @@ main(int argc, char *argv[]) parse_argument(argv[i], &procedure); - erc = dbfcmd(dbproc, query, procedure.name, procedure.owner, + erc = dbfcmd(dbproc, query, quote_str(procedure.name), quote_str(procedure.owner), (DBTDS(dbproc) == DBTDS_5_0) ? "c.colid2, ":""); + tmp_free(); /* Send the query to the server (we could use dbsqlexec(), instead) */ erc = dbsqlsend(dbproc); @@ -250,7 +336,8 @@ main(int argc, char *argv[]) nrows = print_results(dbproc); if (0 == nrows) { - erc = dbfcmd(dbproc, query_table, procedure.owner, procedure.name); + erc = dbfcmd(dbproc, query_table, quote_str(procedure.owner), quote_str(procedure.name)); + tmp_free(); assert(SUCCEED == erc); erc = dbsqlexec(dbproc); if (erc == FAIL) { @@ -446,17 +533,18 @@ print_ddl(DBPROCESS *dbproc, PROCEDURE *procedure) ret = asprintf(&create_index, "%sALTER TABLE %s.%s ADD CONSTRAINT %s PRIMARY KEY %s (%s)\nGO\n\n", create_index, - procedure->owner, procedure->name, - index_name, index_description, index_keys); + quote_id(procedure->owner), quote_id(procedure->name), + quote_id(index_name), index_description, index_keys); } else { ret = asprintf(&create_index, "%sCREATE %s INDEX %s on %s.%s(%s)\nGO\n\n", create_index, - index_description, index_name, - procedure->owner, procedure->name, index_keys); + index_description, quote_id(index_name), + quote_id(procedure->owner), quote_id(procedure->name), index_keys); } assert(ret >= 0); free(tmp_str); + tmp_free(); free(index_name); free(index_description); @@ -560,7 +648,8 @@ print_ddl(DBPROCESS *dbproc, PROCEDURE *procedure) if (nrows == 0) goto cleanup; - printf("%sCREATE TABLE %s.%s\n", use_statement, procedure->owner, procedure->name); + printf("%sCREATE TABLE %s.%s\n", use_statement, quote_id(procedure->owner), quote_id(procedure->name)); + tmp_free(); for (i=0; i < nrows; i++) { static const char varytypenames[] = "char\0" "nchar\0" @@ -596,8 +685,9 @@ print_ddl(DBPROCESS *dbproc, PROCEDURE *procedure) is_null = is_in(ddl[i].nullable, "1\0yes\0"); /* {(|,} name type [NOT] NULL */ - printf("\t%c %-*s %-15s %3s NULL\n", (i==0? '(' : ','), maxnamelen, ddl[i].name, + printf("\t%c %-*s %-15s %3s NULL\n", (i==0? '(' : ','), maxnamelen+2, quote_id(ddl[i].name), (type? type : ddl[i].type), (is_null? "" : "NOT")); + tmp_free(); free(type); } @@ -861,7 +951,8 @@ msg_handler(DBPROCESS * dbproc, DBINT msgno, int msgstate, int severity, const c if (!endquote) break; *endquote = '\0'; - sprintf(use_statement, "USE %s\nGO\n\n", dbname); + sprintf(use_statement, "USE %s\nGO\n\n", quote_id(dbname)); + tmp_free(); return 0; case 0: /* Ignore print messages */ From 4479de4c8c683e5d4b42aa4103b1982b5bc310ab Mon Sep 17 00:00:00 2001 From: Frediano Ziglio Date: Sat, 30 Mar 2024 21:15:20 +0000 Subject: [PATCH 4/7] defncopy: Trim spaces around nullable flags This fix detection of nullable columns for Sybase. Signed-off-by: Frediano Ziglio --- src/apps/defncopy.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/apps/defncopy.c b/src/apps/defncopy.c index b2eb34884..45015f7d2 100644 --- a/src/apps/defncopy.c +++ b/src/apps/defncopy.c @@ -682,6 +682,7 @@ print_ddl(DBPROCESS *dbproc, PROCEDURE *procedure) } assert(ret >= 0); + ltrim(rtrim(ddl[i].nullable)); is_null = is_in(ddl[i].nullable, "1\0yes\0"); /* {(|,} name type [NOT] NULL */ From 2990e8a053d463100412a4c663fe5ca1ce5e054a Mon Sep 17 00:00:00 2001 From: Frediano Ziglio Date: Sat, 30 Mar 2024 17:24:40 +0000 Subject: [PATCH 5/7] defncopy: Fix for order of index recordset The order of columns is different between MS and Sybase. Also separate code in a function, it was too long. Signed-off-by: Frediano Ziglio --- src/apps/defncopy.c | 226 ++++++++++++++++++++++++++------------------ 1 file changed, 132 insertions(+), 94 deletions(-) diff --git a/src/apps/defncopy.c b/src/apps/defncopy.c index 45015f7d2..415a92ae0 100644 --- a/src/apps/defncopy.c +++ b/src/apps/defncopy.c @@ -419,6 +419,126 @@ is_in(const char *item, const char *list) } } +static void +search_columns(DBPROCESS *dbproc, int *colmap, const char *const *colmap_names, int num_cols) +{ + int i; + + assert(dbproc && colmap && colmap_names); + assert(num_cols > 0); + + /* Find the columns we need */ + for (i = 0; i < num_cols; ++i) + colmap[i] = -1; + for (i = 1; i <= dbnumcols(dbproc); ++i) { + const char *name = dbcolname(dbproc, i); + int j; + + for (j = 0; j < num_cols; ++j) { + if (is_in(name, colmap_names[j])) { + colmap[j] = i; + break; + } + } + } + for (i = 0; i < num_cols; ++i) { + if (colmap[i] == -1) { + fprintf(stderr, "Expected column name %s not found\n", colmap_names[i]); + exit(1); + } + } +} + +static char * +parse_index_row(DBPROCESS *dbproc, PROCEDURE *procedure, char *create_index) +{ + static const char *const colmap_names[3] = { + "index_name\0", + "index_description\0", + "index_keys\0", + }; + int colmap[TDS_VECTOR_SIZE(colmap_names)]; + char *index_name, *index_description, *index_keys, *p, fprimary=0; + char *tmp_str; + DBINT datlen; + int ret, i; + + assert(dbnumcols(dbproc) >=3 ); /* column had better be in range */ + + search_columns(dbproc, colmap, colmap_names, TDS_VECTOR_SIZE(colmap)); + + /* name */ + datlen = dbdatlen(dbproc, 1); + index_name = (char *) calloc(1, 1 + datlen); + assert(index_name); + memcpy(index_name, dbdata(dbproc, 1), datlen); + + /* kind */ + i = colmap[1]; + datlen = dbdatlen(dbproc, i); + index_description = (char *) calloc(1, 1 + datlen); + assert(index_description); + memcpy(index_description, dbdata(dbproc, i), datlen); + + /* columns */ + i = colmap[2]; + datlen = dbdatlen(dbproc, i); + index_keys = (char *) calloc(1, 1 + datlen); + assert(index_keys); + memcpy(index_keys, dbdata(dbproc, i), datlen); + + /* fix up the index attributes; we're going to use the string verbatim (almost). */ + p = strstr(index_description, "located"); + if (p) { + *p = '\0'; /* we don't care where it's located */ + } + /* Microsoft version: [non]clustered[, unique][, primary key] located on PRIMARY */ + p = strstr(index_description, "primary key"); + if (p) { + fprimary = 1; + *p = '\0'; /* we don't care where it's located */ + if ((p = strchr(index_description, ',')) != NULL) + *p = '\0'; /* we use only the first term (clustered/nonclustered) */ + } else { + /* reorder "unique" and "clustered" */ + char nonclustered[] = "nonclustered", unique[] = "unique"; + char *pclustering = nonclustered; + if (NULL == strstr(index_description, pclustering)) { + pclustering += 3; + if (NULL == strstr(index_description, pclustering)) + *pclustering = '\0'; + } + if (NULL == strstr(index_description, unique)) + unique[0] = '\0'; + sprintf(index_description, "%s %s", unique, pclustering); + } + /* Put it to a temporary variable; we'll print it after the CREATE TABLE statement. */ + tmp_str = create_index; + create_index = create_index ? create_index : ""; + if (fprimary) { + ret = asprintf(&create_index, + "%sALTER TABLE %s.%s ADD CONSTRAINT %s PRIMARY KEY %s (%s)\nGO\n\n", + create_index, + quote_id(procedure->owner), quote_id(procedure->name), + quote_id(index_name), index_description, index_keys); + } else { + ret = asprintf(&create_index, + "%sCREATE %s INDEX %s on %s.%s(%s)\nGO\n\n", + create_index, + index_description, quote_id(index_name), + quote_id(procedure->owner), quote_id(procedure->name), index_keys); + } + assert(ret >= 0); + free(tmp_str); + tmp_free(); + + free(index_name); + free(index_description); + free(index_keys); + + return create_index; +} + /* * Get the table information from sp_help, because it's easier to get the index information (eventually). * The column descriptions are in resultset #2, which is where we start. @@ -449,11 +569,10 @@ print_ddl(DBPROCESS *dbproc, PROCEDURE *procedure) "scale\0", "nulls\0nullable\0", }; - int colmap[TDS_VECTOR_SIZE(colmap_names)]; char *create_index = NULL; RETCODE erc; - int row_code, iresultset, i, ret; + int iresultset, i; int maxnamelen = 0, nrows = 0; char **p_str; bool is_ms = false; @@ -463,6 +582,8 @@ print_ddl(DBPROCESS *dbproc, PROCEDURE *procedure) /* sp_help returns several result sets. We want just the second one, for now */ for (iresultset=1; (erc = dbresults(dbproc)) != NO_MORE_RESULTS; iresultset++) { + int row_code; + if (erc == FAIL) { fprintf(stderr, "%s:%d: dbresults(), result set %d failed\n", options.appname, __LINE__, iresultset); goto cleanup; @@ -472,84 +593,13 @@ print_ddl(DBPROCESS *dbproc, PROCEDURE *procedure) while ((row_code = dbnextrow(dbproc)) != NO_MORE_ROWS) { struct DDL *p; char **coldesc[sizeof(struct DDL)/sizeof(char*)]; /* an array of pointers to the DDL elements */ + int colmap[TDS_VECTOR_SIZE(colmap_names)]; assert(row_code == REG_ROW); /* Look for index data */ if (0 == strcmp("index_name", dbcolname(dbproc, 1))) { - char *index_name, *index_description, *index_keys, *p, fprimary=0; - char *tmp_str; - DBINT datlen; - - assert(dbnumcols(dbproc) >=3 ); /* column had better be in range */ - - /* name */ - datlen = dbdatlen(dbproc, 1); - index_name = (char *) calloc(1, 1 + datlen); - assert(index_name); - memcpy(index_name, dbdata(dbproc, 1), datlen); - - /* kind */ - datlen = dbdatlen(dbproc, 2); - index_description = (char *) calloc(1, 1 + datlen); - assert(index_description); - memcpy(index_description, dbdata(dbproc, 2), datlen); - - /* columns */ - datlen = dbdatlen(dbproc, 3); - index_keys = (char *) calloc(1, 1 + datlen); - assert(index_keys); - memcpy(index_keys, dbdata(dbproc, 3), datlen); - - /* fix up the index attributes; we're going to use the string verbatim (almost). */ - p = strstr(index_description, "located"); - if (p) { - *p = '\0'; /* we don't care where it's located */ - } - /* Microsoft version: [non]clustered[, unique][, primary key] located on PRIMARY */ - p = strstr(index_description, "primary key"); - if (p) { - fprimary = 1; - *p = '\0'; /* we don't care where it's located */ - if ((p = strchr(index_description, ',')) != NULL) - *p = '\0'; /* we use only the first term (clustered/nonclustered) */ - } else { - /* reorder "unique" and "clustered" */ - char nonclustered[] = "nonclustered", unique[] = "unique"; - char *pclustering = nonclustered; - if (NULL == strstr(index_description, pclustering)) { - pclustering += 3; - if (NULL == strstr(index_description, pclustering)) - *pclustering = '\0'; - } - if (NULL == strstr(index_description, unique)) - unique[0] = '\0'; - sprintf(index_description, "%s %s", unique, pclustering); - } - /* Put it to a temporary variable; we'll print it after the CREATE TABLE statement. */ - tmp_str = create_index; - create_index = create_index ? create_index : ""; - if (fprimary) { - ret = asprintf(&create_index, - "%sALTER TABLE %s.%s ADD CONSTRAINT %s PRIMARY KEY %s (%s)\nGO\n\n", - create_index, - quote_id(procedure->owner), quote_id(procedure->name), - quote_id(index_name), index_description, index_keys); - } else { - ret = asprintf(&create_index, - "%sCREATE %s INDEX %s on %s.%s(%s)\nGO\n\n", - create_index, - index_description, quote_id(index_name), - quote_id(procedure->owner), quote_id(procedure->name), index_keys); - } - assert(ret >= 0); - free(tmp_str); - tmp_free(); - - free(index_name); - free(index_description); - free(index_keys); - + create_index = parse_index_row(dbproc, procedure, create_index); continue; } @@ -558,27 +608,14 @@ print_ddl(DBPROCESS *dbproc, PROCEDURE *procedure) continue; /* Find the columns we need */ - for (i = 0; i < TDS_VECTOR_SIZE(colmap); ++i) - colmap[i] = -1; - for (i = 1; i <= dbnumcols(dbproc); ++i) { - const char *name = dbcolname(dbproc, i); - int j; - - for (j = 0; j < TDS_VECTOR_SIZE(colmap); ++j) { - if (is_in(name, colmap_names[j])) { - colmap[j] = i; - break; - } - } - if (strcasecmp(name, "Collation") == 0) + search_columns(dbproc, colmap, colmap_names, TDS_VECTOR_SIZE(colmap)); + + /* check if server is Microsoft */ + for (i = 1; i <= dbnumcols(dbproc); ++i) + if (strcasecmp(dbcolname(dbproc, i), "Collation") == 0) { is_ms = true; - } - for (i = 0; i < TDS_VECTOR_SIZE(colmap); ++i) { - if (colmap[i] == -1) { - fprintf(stderr, "Expected column name %s not found\n", colmap_names[i]); - exit(1); + break; } - } /* Make room for the next row */ p = (struct DDL *) realloc(ddl, ++nrows * sizeof(struct DDL)); @@ -662,6 +699,7 @@ print_ddl(DBPROCESS *dbproc, PROCEDURE *procedure) ; char *type = NULL; bool is_null; + int ret; /* get size of decimal, numeric, char, and image types */ ret = 0; From 5ad8d2929a40795913a3390af7244910b85c1d0a Mon Sep 17 00:00:00 2001 From: Frediano Ziglio Date: Sun, 31 Mar 2024 18:13:39 +0100 Subject: [PATCH 6/7] defncopy: Quote key index names Split index keys considering current column names. This is more complicated than just split considering separator (command and space, ", ") however it supports weird cases where we have the separator in the middle of a column name. Signed-off-by: Frediano Ziglio --- src/apps/defncopy.c | 101 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 93 insertions(+), 8 deletions(-) diff --git a/src/apps/defncopy.c b/src/apps/defncopy.c index 415a92ae0..c5fa52a0c 100644 --- a/src/apps/defncopy.c +++ b/src/apps/defncopy.c @@ -121,6 +121,15 @@ typedef struct _procedure char name[512], owner[512]; } PROCEDURE; +typedef struct DDL { + char *name; + char *type; + char *length; + char *precision; + char *scale; + char *nullable; +} DDL; + static int print_ddl(DBPROCESS *dbproc, PROCEDURE *procedure); static int print_results(DBPROCESS *dbproc); static LOGINREC* get_login(int argc, char *argv[], OPTIONS *poptions); @@ -449,8 +458,80 @@ search_columns(DBPROCESS *dbproc, int *colmap, const char *const *colmap_names, } } +static int +find_column_name(const char *start, const DDL *columns, int num_columns) +{ + size_t start_len = strlen(start); + size_t found_len = 0; + int i, found = -1; + + for (i = 0; i < num_columns; ++i) { + const char *const name = columns[i].name; + const size_t name_len = strlen(name); + if (name_len <= start_len && name_len > found_len + && (start[name_len] == 0 || strncmp(start+name_len, ", ", 2) == 0) + && memcmp(name, start, name_len) == 0) { + found_len = name_len; + found = i; + } + } + return found; +} + +/* This function split and quote index keys. + * Index keys are separate by a command and a space (", ") however the + * separator (very unlikely but possible can contain that separator) + * so we use search for column names taking the longer we can. + */ static char * -parse_index_row(DBPROCESS *dbproc, PROCEDURE *procedure, char *create_index) +quote_index_keys(char *index_keys, const DDL *columns, int num_columns) +{ + size_t num_commas = count_chars(index_keys, ','); + size_t num_quotes = count_chars(index_keys, ']'); + size_t max_len = strlen(index_keys) + num_quotes + (num_commas + 1) * 2 + 1; + char *const new_index_keys = malloc(max_len); + char *dest; + bool first = true; + assert(new_index_keys); + dest = new_index_keys; + while (*index_keys) { + int icol = find_column_name(index_keys, columns, num_columns); + /* Sybase put a space at the beginning, handle it */ + if (icol < 0 && index_keys[0] == ' ') { + icol = find_column_name(index_keys + 1, columns, num_columns); + if (icol >= 0) + ++index_keys; + } + if (!first) + *dest++ = ','; + *dest++ = '['; + if (icol >= 0) { + /* found a column matching, use the name */ + dest = sql_quote(dest, columns[icol].name, ']'); + index_keys += strlen(columns[icol].name); + } else { + /* not found, fallback looking for terminator */ + char save; + char *end = strstr(index_keys, ", "); + if (!end) + end = strchr(index_keys, 0); + save = *end; + *end = 0; + dest = sql_quote(dest, index_keys, ']'); + *end = save; + index_keys = end; + } + *dest++ = ']'; + if (strncmp(index_keys, ", ", 2) == 0) + index_keys += 2; + first = false; + } + *dest = 0; + return new_index_keys; +} + +static char * +parse_index_row(DBPROCESS *dbproc, PROCEDURE *procedure, char *create_index, const DDL *columns, int num_columns) { static const char *const colmap_names[3] = { "index_name\0", @@ -487,6 +568,10 @@ parse_index_row(DBPROCESS *dbproc, PROCEDURE *procedure, char *create_index) assert(index_keys); memcpy(index_keys, dbdata(dbproc, i), datlen); + tmp_str = quote_index_keys(index_keys, columns, num_columns); + free(index_keys); + index_keys = tmp_str; + /* fix up the index attributes; we're going to use the string verbatim (almost). */ p = strstr(index_description, "located"); if (p) { @@ -560,7 +645,6 @@ parse_index_row(DBPROCESS *dbproc, PROCEDURE *procedure, char *create_index) static int print_ddl(DBPROCESS *dbproc, PROCEDURE *procedure) { - struct DDL { char *name, *type, *length, *precision, *scale, *nullable; } *ddl = NULL; static const char *const colmap_names[6] = { "column_name\0", "type\0", @@ -570,6 +654,7 @@ print_ddl(DBPROCESS *dbproc, PROCEDURE *procedure) "nulls\0nullable\0", }; + DDL *ddl = NULL; char *create_index = NULL; RETCODE erc; int iresultset, i; @@ -591,15 +676,15 @@ print_ddl(DBPROCESS *dbproc, PROCEDURE *procedure) /* Get the data */ while ((row_code = dbnextrow(dbproc)) != NO_MORE_ROWS) { - struct DDL *p; - char **coldesc[sizeof(struct DDL)/sizeof(char*)]; /* an array of pointers to the DDL elements */ + DDL *p; + char **coldesc[sizeof(DDL)/sizeof(char*)]; /* an array of pointers to the DDL elements */ int colmap[TDS_VECTOR_SIZE(colmap_names)]; assert(row_code == REG_ROW); /* Look for index data */ if (0 == strcmp("index_name", dbcolname(dbproc, 1))) { - create_index = parse_index_row(dbproc, procedure, create_index); + create_index = parse_index_row(dbproc, procedure, create_index, ddl, nrows); continue; } @@ -618,7 +703,7 @@ print_ddl(DBPROCESS *dbproc, PROCEDURE *procedure) } /* Make room for the next row */ - p = (struct DDL *) realloc(ddl, ++nrows * sizeof(struct DDL)); + p = (DDL *) realloc(ddl, ++nrows * sizeof(DDL)); if (p == NULL) { perror("error: insufficient memory for row DDL"); assert(p != NULL); @@ -634,7 +719,7 @@ print_ddl(DBPROCESS *dbproc, PROCEDURE *procedure) coldesc[4] = &ddl[nrows-1].scale; coldesc[5] = &ddl[nrows-1].nullable; - for( i=0; i < sizeof(struct DDL)/sizeof(char*); i++) { + for( i=0; i < sizeof(DDL)/sizeof(char*); i++) { const int col_index = colmap[i]; const DBINT datlen = dbdatlen(dbproc, col_index); const int type = dbcoltype(dbproc, col_index); @@ -738,7 +823,7 @@ print_ddl(DBPROCESS *dbproc, PROCEDURE *procedure) cleanup: p_str = (char **) ddl; - for (i=0; i < nrows * (sizeof(struct DDL)/sizeof(char*)); ++i) + for (i=0; i < nrows * (sizeof(DDL)/sizeof(char*)); ++i) free(p_str[i]); free(ddl); free(create_index); From 3585d56f39c9f99e87a48cbdd80715128f4221fe Mon Sep 17 00:00:00 2001 From: Frediano Ziglio Date: Fri, 29 Mar 2024 11:20:06 +0000 Subject: [PATCH 7/7] defncopy: Add test for the applications Use commands calling programs. Mainly use tsql to create some table, output definition with defncopy and check definition is as expected. Signed-off-by: Frediano Ziglio --- configure.ac | 1 + src/apps/CMakeLists.txt | 2 + src/apps/Makefile.am | 2 +- src/apps/unittests/CMakeLists.txt | 9 + src/apps/unittests/Makefile.am | 13 + src/apps/unittests/defncopy.c | 435 ++++++++++++++++++++++++++++++ 6 files changed, 461 insertions(+), 1 deletion(-) create mode 100644 src/apps/unittests/CMakeLists.txt create mode 100644 src/apps/unittests/Makefile.am create mode 100644 src/apps/unittests/defncopy.c diff --git a/configure.ac b/configure.ac index ab235cdec..33356c19d 100644 --- a/configure.ac +++ b/configure.ac @@ -1003,6 +1003,7 @@ AC_CONFIG_FILES(include/freetds/version.h \ src/odbc/unittests/Makefile \ src/apps/Makefile \ src/apps/fisql/Makefile \ + src/apps/unittests/Makefile \ freetds.spec \ win32/Makefile \ win32/freetds.nsh \ diff --git a/src/apps/CMakeLists.txt b/src/apps/CMakeLists.txt index 4f21d1072..a36e7b374 100644 --- a/src/apps/CMakeLists.txt +++ b/src/apps/CMakeLists.txt @@ -1,3 +1,5 @@ +add_subdirectory(unittests) + set(libs ${lib_NETWORK} ${lib_BASE}) foreach(target freebcp bsqldb defncopy datacopy) diff --git a/src/apps/Makefile.am b/src/apps/Makefile.am index 0d9c79abb..b198efb62 100644 --- a/src/apps/Makefile.am +++ b/src/apps/Makefile.am @@ -1,6 +1,6 @@ AM_CPPFLAGS = -I$(top_srcdir)/include -SUBDIRS = fisql +SUBDIRS = fisql . unittests DIST_SUBDIRS = $(SUBDIRS) diff --git a/src/apps/unittests/CMakeLists.txt b/src/apps/unittests/CMakeLists.txt new file mode 100644 index 000000000..98e530109 --- /dev/null +++ b/src/apps/unittests/CMakeLists.txt @@ -0,0 +1,9 @@ +include_directories(..) + +foreach(target defncopy) + add_executable(a_${target} EXCLUDE_FROM_ALL ${target}.c) + set_target_properties(a_${target} PROPERTIES OUTPUT_NAME ${target}) + target_link_libraries(a_${target} replacements tdsutils ${lib_NETWORK} ${lib_BASE}) + add_test(NAME a_${target} WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} COMMAND a_${target}) + add_dependencies(check a_${target}) +endforeach(target) diff --git a/src/apps/unittests/Makefile.am b/src/apps/unittests/Makefile.am new file mode 100644 index 000000000..0536c2cc3 --- /dev/null +++ b/src/apps/unittests/Makefile.am @@ -0,0 +1,13 @@ +NULL= +TESTS = \ + defncopy$(EXEEXT) \ + $(NULL) + +check_PROGRAMS = $(TESTS) + +defncopy_SOURCES = defncopy.c + +AM_CPPFLAGS = -I$(top_srcdir)/include -I$(srcdir)/.. -I../ -DFREETDS_TOPDIR=\"$(top_srcdir)\" +LDADD = ../../replacements/libreplacements.la $(LTLIBICONV) $(NETWORK_LIBS) +EXTRA_DIST = CMakeLists.txt + diff --git a/src/apps/unittests/defncopy.c b/src/apps/unittests/defncopy.c new file mode 100644 index 000000000..5e5abac32 --- /dev/null +++ b/src/apps/unittests/defncopy.c @@ -0,0 +1,435 @@ +/* FreeTDS - Library of routines accessing Sybase and Microsoft databases + * Copyright (C) 2024 Frediano Ziglio + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +/** + * This tests execute some command using tsql and defncopy to check behaviour + */ + +#undef NDEBUG +#include + +#include +#include +#include + +#if HAVE_STRING_H +#include +#endif /* HAVE_STRING_H */ + +#ifdef HAVE_UNISTD_H +#include +#endif + +#ifdef _WIN32 +#include +#define EXE_SUFFIX ".exe" +#define SDIR_SEPARATOR "\\" +#else +#define EXE_SUFFIX "" +#define SDIR_SEPARATOR "/" +#endif + +#include +#include + +static char USER[512]; +static char SERVER[512]; +static char PASSWORD[512]; +static char DATABASE[512]; + +/* content of output file, from command executed */ +static char *output; + +static bool +read_login_info(void) +{ + FILE *in = NULL; + char line[512]; + char *s1, *s2; + + s1 = getenv("TDSPWDFILE"); + if (s1 && s1[0]) + in = fopen(s1, "r"); + if (!in) + in = fopen("../../../PWD", "r"); + if (!in) { + fprintf(stderr, "Can not open PWD file\n\n"); + return false; + } + + while (fgets(line, sizeof(line), in)) { + s1 = strtok(line, "="); + s2 = strtok(NULL, "\n"); + if (!s1 || !s2) { + continue; + } + if (!strcmp(s1, "UID")) { + strcpy(USER, s2); + } else if (!strcmp(s1, "SRV")) { + strcpy(SERVER, s2); + } else if (!strcmp(s1, "PWD")) { + strcpy(PASSWORD, s2); + } else if (!strcmp(s1, "DB")) { + strcpy(DATABASE, s2); + } + } + fclose(in); + return true; +} + +static void +no_space(void) +{ + fprintf(stderr, "No space left on buffer\n"); + exit(1); +} + +static void +normalize_spaces(char *s) +{ + char *p, *dest, prev; + + /* replace all tabs with spaces */ + for (p = s; *p; ++p) + if (*p == '\t') + *p = ' '; + + /* replace duplicate spaces with a single space */ + prev = 'x'; + for (dest = s, p = s; *p; ++p) { + if (prev == ' ' && *p == ' ') + continue; + *dest++ = prev = *p; + } + *dest = 0; +} + +/* read a file and output on a stream */ +static void +cat(const char *fn, FILE *out) +{ + char line[1024]; + FILE *f = fopen(fn, "r"); + assert(f); + while (fgets(line, sizeof(line), f)) { + fputs(" ", out); + fputs(line, out); + } + fclose(f); +} + +/* read a text file into memory, return it as a string */ +static char * +read_file(const char *fn) +{ + long pos; + char *buf; + size_t readed; + + FILE *f = fopen(fn, "r"); + assert(f); + assert(fseek(f, 0, SEEK_END) == 0); + pos = ftell(f); + assert(pos >= 0); + assert(fseek(f, 0, SEEK_SET) == 0); + buf = malloc(pos + 10); /* allocate some more space */ + assert(buf); + readed = fread(buf, 1, pos+1, f); + assert(readed <= pos); + assert(feof(f)); + fclose(f); + buf[readed] = 0; + return buf; +} + +#define CHECK(n) do {\ + if (dest + (n) > dest_end) \ + no_space(); \ +} while(0) + +static char * +quote_arg(char *dest, char *dest_end, const char *arg) +{ +#ifndef _WIN32 + CHECK(1); + *dest++ = '\''; + for (; *arg; ++arg) { + if (*arg == '\'') { + CHECK(3); + strcpy(dest, "'\\'"); + dest += 3; + } + CHECK(1); + *dest++ = *arg; + } + CHECK(1); + *dest++ = '\''; +#else + CHECK(1); + *dest++ = '\"'; + for (; *arg; ++arg) { + if (*arg == '\\' || *arg == '\"') { + CHECK(1); + *dest++ = '\\'; + } + CHECK(1); + *dest++ = *arg; + } + CHECK(1); + *dest++ = '\"'; +#endif + return dest; +} + +static char * +add_string(char *dest, char *const dest_end, const char *str) +{ + size_t len = strlen(str); + CHECK(len); + memcpy(dest, str, len); + return dest + len; +} + +#undef CHECK + +static char * +add_server(char *dest, char *const dest_end) +{ + dest = add_string(dest, dest_end, " -S "); + dest = quote_arg(dest, dest_end, SERVER); + dest = add_string(dest, dest_end, " -U "); + dest = quote_arg(dest, dest_end, USER); + dest = add_string(dest, dest_end, " -P "); + dest = quote_arg(dest, dest_end, PASSWORD); + if (DATABASE[0]) { + dest = add_string(dest, dest_end, " -D "); + dest = quote_arg(dest, dest_end, DATABASE); + } + return dest; +} + +static void +cleanup(void) +{ + unlink("empty"); + unlink("output"); + unlink("input"); + TDS_ZERO_FREE(output); +} + +static void +tsql(const char *input_data) +{ + char cmd[2048]; + char *const end = cmd + sizeof(cmd) - 1; + char *p; + FILE *f; + + f = fopen("input", "w"); + assert(f); + fputs(input_data, f); + fclose(f); + + strcpy(cmd, ".." SDIR_SEPARATOR "tsql" EXE_SUFFIX " -o q"); + p = strchr(cmd, 0); + p = add_server(p, end); + p = add_string(p, end, "output"); + *p = 0; + printf("Executing: %s\n", cmd); + if (system(cmd) != 0) { + printf("Output is:\n"); + cat("output", stdout); + fprintf(stderr, "Failed command\n"); + exit(1); + } + TDS_ZERO_FREE(output); + output = read_file("output"); +} + +static void +defncopy(const char *object_name) +{ + char cmd[2048]; + char *const end = cmd + sizeof(cmd) - 1; + char *p; + FILE *f; + + // empty input + f = fopen("input", "w"); + assert(f); + fclose(f); + + strcpy(cmd, ".." SDIR_SEPARATOR "defncopy" EXE_SUFFIX); + p = strchr(cmd, 0); + p = add_server(p, end); + p = add_string(p, end, " "); + p = quote_arg(p, end, object_name); + p = add_string(p, end, "output"); + *p = 0; + printf("Executing: %s\n", cmd); + if (system(cmd) != 0) { + printf("Output is:\n"); + cat("output", stdout); + fprintf(stderr, "Failed command\n"); + exit(1); + } + TDS_ZERO_FREE(output); + output = read_file("output"); +} + +/* table with a column name that is also a keyword, should be quoted */ +static void +test_keyword(void) +{ + const char *sql; + static const char clean[] = + "IF OBJECT_ID('dbo.table_with_column_named_key') IS NOT NULL DROP TABLE dbo.table_with_column_named_key\n"; + + tsql(clean); + tsql( +"IF OBJECT_ID('dbo.table_with_column_named_key') IS NOT NULL DROP TABLE dbo.table_with_column_named_key\n" +"GO\n" +"CREATE TABLE dbo.table_with_column_named_key\n" +"(\n" +" [key] nvarchar(4000) NOT NULL\n" +")\n"); + defncopy("dbo.table_with_column_named_key"); + cat("output", stdout); + normalize_spaces(output); + sql = +"CREATE TABLE [dbo].[table_with_column_named_key]\n" +" ( [key] nvarchar(4000) NOT NULL\n" +" )\n" +"GO"; + if (strstr(output, sql) == NULL) { + fprintf(stderr, "Expected SQL string not found\n"); + exit(1); + } + tsql(clean); + tsql(sql); + tsql(clean); +} + +/* table with an index with a space inside */ +static void +test_index_name_with_space(void) +{ + const char *sql; + static const char clean[] = + "IF OBJECT_ID('dbo.tblReportPeriod') IS NOT NULL DROP TABLE dbo.tblReportPeriod\n"; + + tsql(clean); + tsql( +"CREATE TABLE dbo.tblReportPeriod\n" +" ( RecordID int NOT NULL\n" +" , FromDate nvarchar(40) NULL\n" +" , ToDate nvarchar(40) NULL\n" +" )\n" +"CREATE nonclustered INDEX [From Date] on dbo.tblReportPeriod(FromDate)\n"); + defncopy("dbo.tblReportPeriod"); + cat("output", stdout); + normalize_spaces(output); + sql = +"CREATE TABLE [dbo].[tblReportPeriod]\n" +" ( [RecordID] int NOT NULL\n" +" , [FromDate] nvarchar(40) NULL\n" +" , [ToDate] nvarchar(40) NULL\n" +" )\n" +"GO\n" +"\n" +"CREATE nonclustered INDEX [From Date] on [dbo].[tblReportPeriod]([FromDate])"; + if (strstr(output, sql) == NULL) { + fprintf(stderr, "Expected SQL string not found\n"); + exit(1); + } + tsql(clean); + tsql(sql); + tsql(clean); +} + +/* table with an index with a space inside */ +static void +test_weird_index_names(void) +{ + const char *sql, *sql_sybase; + static const char clean[] = + "IF OBJECT_ID('dbo.tblReportPeriod2') IS NOT NULL DROP TABLE dbo.tblReportPeriod2\n"; + + tsql(clean); + tsql( +"CREATE TABLE dbo.tblReportPeriod2\n" +" ( RecordID int NOT NULL\n" +" , [To, ] nvarchar(40) NULL\n" +" , [To] nvarchar(40) NULL\n" +" , [To, , ] nvarchar(40) NULL\n" +" )\n" +"CREATE nonclustered INDEX [From Date] on dbo.tblReportPeriod2([To, ],[To, , ])\n"); + defncopy("dbo.tblReportPeriod2"); + cat("output", stdout); + normalize_spaces(output); + sql = +"CREATE TABLE [dbo].[tblReportPeriod2]\n" +" ( [RecordID] int NOT NULL\n" +" , [To, ] nvarchar(40) NULL\n" +" , [To] nvarchar(40) NULL\n" +" , [To, , ] nvarchar(40) NULL\n" +" )\n" +"GO\n" +"\n" +"CREATE nonclustered INDEX [From Date] on [dbo].[tblReportPeriod2]([To, ],[To, , ])"; + /* Sybase remove spaces at the end */ + sql_sybase = +"CREATE TABLE [dbo].[tblReportPeriod2]\n" +" ( [RecordID] int NOT NULL\n" +" , [To,] nvarchar(40) NULL\n" +" , [To] nvarchar(40) NULL\n" +" , [To, ,] nvarchar(40) NULL\n" +" )\n" +"GO\n" +"\n" +"CREATE nonclustered INDEX [From Date] on [dbo].[tblReportPeriod2]([To,],[To, ,])"; + if (strstr(output, sql) == NULL && strstr(output, sql_sybase) == NULL) { + fprintf(stderr, "Expected SQL string not found\n"); + exit(1); + } + tsql(clean); + tsql(sql); + tsql(clean); +} + +int main(void) +{ + FILE *f; + + cleanup(); + + if (!read_login_info()) + return 1; + + f = fopen("empty", "w"); + if (f) + fclose(f); + + test_keyword(); + test_index_name_with_space(); + test_weird_index_names(); + + cleanup(); + return 0; +}