diff --git a/CMakeLists.txt b/CMakeLists.txt index e8624fa..f7039fd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -56,6 +56,8 @@ add_executable(pgquarrel src/table.h src/textsearch.c src/textsearch.h + src/transform.c + src/transform.h src/trigger.c src/trigger.h src/type.c diff --git a/README.md b/README.md index afb003a..005effa 100644 --- a/README.md +++ b/README.md @@ -241,7 +241,7 @@ Features TRANSFORM - not implemented + complete @@ -330,6 +330,7 @@ The following command-line options are provided (all are optional): * `subscription`: subscription comparison (default: false). * `table`: table comparison (default: true). * `text-search`: text search comparison (default: false). +* `transform`: transform comparison (default: false). * `trigger`: trigger comparison (default: true). * `type`: type comparison (default: true). * `view`: view comparison (default: true). @@ -374,6 +375,7 @@ statistics = false subscription = false table = true text-search = false +transform = false trigger = true type = true view = true diff --git a/src/common.h b/src/common.h index 5b16085..443c39d 100644 --- a/src/common.h +++ b/src/common.h @@ -89,6 +89,7 @@ typedef struct QuarrelGeneralOptions bool subscription; bool table; bool textsearch; + bool transform; bool trigger; bool type; bool view; diff --git a/src/quarrel.c b/src/quarrel.c index 93eb10c..9105530 100644 --- a/src/quarrel.c +++ b/src/quarrel.c @@ -34,6 +34,7 @@ * server: complete * subscription: partial * table: partial + * transform: complete * trigger: partial * type: partial * text search configuration: partial @@ -45,7 +46,6 @@ * * UNSUPPORTED * ~~~~~~~~~~~~~ - * transform * * UNCERTAIN * ~~~~~~~~~~~~~ @@ -88,6 +88,7 @@ #include "subscription.h" #include "table.h" #include "textsearch.h" +#include "transform.h" #include "trigger.h" #include "type.h" #include "usermapping.h" @@ -111,7 +112,8 @@ PQLStatistic qstat = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0 }; FILE *fout; /* output file */ @@ -163,6 +165,7 @@ static void quarrelTextSearchConfigs(); static void quarrelTextSearchDicts(); static void quarrelTextSearchParsers(); static void quarrelTextSearchTemplates(); +static void quarrelTransforms(); static void quarrelTriggers(); static void quarrelBaseTypes(); static void quarrelCompositeTypes(); @@ -282,6 +285,8 @@ help(void) (opts.general.table) ? "true" : "false"); printf(" --text-search=BOOL text search (default: %s)\n", (opts.general.textsearch) ? "true" : "false"); + printf(" --transform=BOOL transform (default: %s)\n", + (opts.general.transform) ? "true" : "false"); printf(" --trigger=BOOL trigger (default: %s)\n", (opts.general.trigger) ? "true" : "false"); printf(" --type=BOOL type (default: %s)\n", @@ -352,6 +357,7 @@ loadConfig(const char *cf, QuarrelOptions *options) options->general.subscription = false; /* general - subscription */ options->general.table = true; /* general - table */ options->general.textsearch = false; /* general - text-search */ + options->general.transform = false; /* general - transform */ options->general.trigger = true; /* general - trigger */ options->general.type = true; /* general - type */ options->general.view = true; /* general - view */ @@ -576,6 +582,10 @@ loadConfig(const char *cf, QuarrelOptions *options) mini_file_get_value(config, "general", "text-search")); + if (mini_file_get_value(config, "general", "transform") != NULL) + options->general.transform = parseBoolean("transform", mini_file_get_value(config, + "general", "transform")); + if (mini_file_get_value(config, "general", "trigger") != NULL) options->general.trigger = parseBoolean("trigger", mini_file_get_value(config, "general", "trigger")); @@ -3666,6 +3676,86 @@ quarrelTextSearchTemplates() freeTextSearchTemplates(tstemplates2, ntstemplates2); } +static void +quarrelTransforms() +{ + PQLTransform *transforms1 = NULL; /* target */ + PQLTransform *transforms2 = NULL; /* source */ + int ntransforms1 = 0; /* # of transforms */ + int ntransforms2 = 0; + int i, j; + + transforms1 = getTransforms(conn1, &ntransforms1); + transforms2 = getTransforms(conn2, &ntransforms2); + + for (i = 0; i < ntransforms1; i++) + logNoise("server1: transform for %s.%s language %s", transforms1[i].trftype.schemaname, transforms1[i].trftype.objectname, transforms1[i].languagename); + + for (i = 0; i < ntransforms2; i++) + logNoise("server2: transform for %s.%s language %s", transforms2[i].trftype.schemaname, transforms2[i].trftype.objectname, transforms2[i].languagename); + + /* + * We have two sorted lists. Let's figure out which elements are not in the + * other list. + * We have two sorted lists. The strategy is transverse both lists only once + * to figure out transforms not presented in the other list. + */ + i = j = 0; + while (i < ntransforms1 || j < ntransforms2) + { + /* End of transforms1 list. Print transforms2 list until its end. */ + if (i == ntransforms1) + { + logDebug("transform for %s.%s language %s: server2", transforms2[i].trftype.schemaname, transforms2[i].trftype.objectname, transforms2[i].languagename); + + dumpCreateTransform(fpre, &transforms2[j]); + + j++; + qstat.transformadded++; + } + /* End of transforms2 list. Print transforms1 list until its end. */ + else if (j == ntransforms2) + { + logDebug("transform for %s.%s language %s: server1", transforms1[i].trftype.schemaname, transforms1[i].trftype.objectname, transforms1[i].languagename); + + dumpDropTransform(fpost, &transforms1[i]); + + i++; + qstat.transformremoved++; + } + else if (compareNamesAndRelations(&transforms1[i].trftype, &transforms2[j].trftype, transforms1[i].languagename, transforms2[j].languagename) == 0) + { + logDebug("transform for %s.%s language %s: server1 server2", transforms1[i].trftype.schemaname, transforms1[i].trftype.objectname, transforms1[i].languagename); + + dumpAlterTransform(fpre, &transforms1[i], &transforms2[j]); + + i++; + j++; + } + else if (compareNamesAndRelations(&transforms1[i].trftype, &transforms2[j].trftype, transforms1[i].languagename, transforms2[j].languagename) < 0) + { + logDebug("transform for %s.%s language %s: server1", transforms1[i].trftype.schemaname, transforms1[i].trftype.objectname, transforms1[i].languagename); + + dumpDropTransform(fpost, &transforms1[i]); + + i++; + qstat.transformremoved++; + } + else if (compareNamesAndRelations(&transforms1[i].trftype, &transforms2[j].trftype, transforms1[i].languagename, transforms2[j].languagename) > 0) + { + logDebug("transform for %s.%s language %s: server2", transforms2[i].trftype.schemaname, transforms2[i].trftype.objectname, transforms2[i].languagename); + + dumpCreateTransform(fpre, &transforms2[j]); + + j++; + qstat.transformadded++; + } + } + + freeTransforms(transforms1, ntransforms1); + freeTransforms(transforms2, ntransforms2); +} + static void quarrelTriggers() { @@ -4525,6 +4615,7 @@ int main(int argc, char *argv[]) {"subscription", required_argument, NULL, 41}, {"table", required_argument, NULL, 31}, {"text-search", required_argument, NULL, 32}, + {"transform", required_argument, NULL, 44}, {"trigger", required_argument, NULL, 33}, {"type", required_argument, NULL, 34}, {"view", required_argument, NULL, 35}, @@ -4767,6 +4858,10 @@ int main(int argc, char *argv[]) gopts.foreigntable = parseBoolean("foreign-table", optarg); gopts_given.foreigntable = true; break; + case 44: + gopts.transform = parseBoolean("transform", optarg); + gopts_given.transform = true; + break; default: fprintf(stderr, "Try \"%s --help\" for more information.\n", PGQ_NAME); exit(EXIT_FAILURE); @@ -4854,6 +4949,8 @@ int main(int argc, char *argv[]) options.table = gopts.table; if (gopts_given.textsearch) options.textsearch = gopts.textsearch; + if (gopts_given.transform) + options.transform = gopts.transform; if (gopts_given.trigger) options.trigger = gopts.trigger; if (gopts_given.type) @@ -5047,6 +5144,8 @@ int main(int argc, char *argv[]) quarrelTextSearchDicts(); quarrelTextSearchConfigs(); } + if (options.transform) + quarrelTransforms(); if (options.statistics) quarrelStatistics(); diff --git a/src/quarrel.h b/src/quarrel.h index bb99a80..392712c 100644 --- a/src/quarrel.h +++ b/src/quarrel.h @@ -91,6 +91,8 @@ typedef struct PQLStatistic int tsparserremoved; int tstemplateadded; int tstemplateremoved; + int transformadded; + int transformremoved; int trgadded; int trgremoved; int typeadded; diff --git a/src/transform.c b/src/transform.c new file mode 100644 index 0000000..d693a72 --- /dev/null +++ b/src/transform.c @@ -0,0 +1,202 @@ +/*---------------------------------------------------------------------- + * + * pgquarrel -- comparing database schemas + * + * transform.c + * Generate TRANSFORM commands + * + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * CREATE TRANSFORM + * DROP TRANSFORM + * COMMENT ON TRANSFORM + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * Copyright (c) 2015-2018, Euler Taveira + * + * --------------------------------------------------------------------- + */ +#include "transform.h" + + +PQLTransform * +getTransforms(PGconn *c, int *n) +{ + PQLTransform *t; + PGresult *res; + int i; + + logNoise("transform: server version: %d", PQserverVersion(c)); + + /* bail out if we do not support it */ + if (PQserverVersion(c) < 90500) + { + logWarning("ignoring transforms because server does not support it"); + return NULL; + } + + res = PQexec(c, "SELECT t.oid, n.nspname AS typschema, y.typname AS typname, (SELECT lanname FROM pg_language WHERE oid = t.trflang) AS lanname, p.oid AS fromsqloid, x.nspname AS fromsqlschema, p.proname AS fromsqlname, pg_get_function_arguments(t.trffromsql) AS fromsqlargs, q.oid AS tosqloid, z.nspname AS tosqlschema, q.proname AS tosqlname, pg_get_function_args(t.trftosql) AS tosqlargs, obj_description(t.oid, 'pg_transform') AS description FROM pg_transform t INNER JOIN pg_type y ON (t.trftype = y.oid) INNER JOIN pg_namespace n ON (n.oid = y.typnamespace) LEFT JOIN pg_proc p ON (t.trffromsql = p.oid) LEFT JOIN pg_namespace x ON (x.oid = p.pronamespace) LEFT JOIN pg_proc q ON (t.trftosql = q.oid) LEFT JOIN pg_namespace z ON (z.oid = q.pronamespace) ORDER BY typschema, typname, lanname"); + + if (PQresultStatus(res) != PGRES_TUPLES_OK) + { + logError("query failed: %s", PQresultErrorMessage(res)); + PQclear(res); + PQfinish(c); + /* XXX leak another connection? */ + exit(EXIT_FAILURE); + } + + *n = PQntuples(res); + if (*n > 0) + t = (PQLTransform *) malloc(*n * sizeof(PQLTransform)); + else + t = NULL; + + logDebug("number of transforms in server: %d", *n); + + for (i = 0; i < *n; i++) + { + char *withoutescape; + + t[i].trftype.oid = strtoul(PQgetvalue(res, i, PQfnumber(res, "oid")), NULL, 10); + t[i].trftype.schemaname = strdup(PQgetvalue(res, i, PQfnumber(res, "typschema"))); + t[i].trftype.objectname = strdup(PQgetvalue(res, i, PQfnumber(res, "typname"))); + t[i].languagename = strdup(PQgetvalue(res, i, PQfnumber(res, "lanname"))); + + if (PQgetisnull(res, i, PQfnumber(res, "fromsqlname"))) + { + t[i].fromsql.schemaname = NULL; + t[i].fromsql.objectname = NULL; + t[i].fromsqlargs = NULL; + } + else + { + t[i].fromsql.oid = strtoul(PQgetvalue(res, i, PQfnumber(res, "fromsqloid")), NULL, 10); + t[i].fromsql.schemaname = strdup(PQgetvalue(res, i, PQfnumber(res, "fromsqlschema"))); + t[i].fromsql.objectname = strdup(PQgetvalue(res, i, PQfnumber(res, "fromsqlname"))); + t[i].fromsqlargs = strdup(PQgetvalue(res, i, PQfnumber(res, "fromsqlargs"))); + } + + if (PQgetisnull(res, i, PQfnumber(res, "tosqlname"))) + { + t[i].tosql.schemaname = NULL; + t[i].tosql.objectname = NULL; + t[i].tosqlargs = NULL; + } + else + { + t[i].tosql.oid = strtoul(PQgetvalue(res, i, PQfnumber(res, "tosqloid")), NULL, 10); + t[i].tosql.schemaname = strdup(PQgetvalue(res, i, PQfnumber(res, "tosqlschema"))); + t[i].tosql.objectname = strdup(PQgetvalue(res, i, PQfnumber(res, "tosqlname"))); + t[i].tosqlargs = strdup(PQgetvalue(res, i, PQfnumber(res, "tosqlargs"))); + } + + if (PQgetisnull(res, i, PQfnumber(res, "description"))) + t[i].comment = NULL; + else + { + withoutescape = PQgetvalue(res, i, PQfnumber(res, "description")); + t[i].comment = PQescapeLiteral(c, withoutescape, strlen(withoutescape)); + if (t[i].comment == NULL) + { + logError("escaping comment failed: %s", PQerrorMessage(c)); + PQclear(res); + PQfinish(c); + /* XXX leak another connection? */ + exit(EXIT_FAILURE); + } + } + + logDebug("transform for type \"%s\".\"%s\" language \"%s\"", t[i].trftype.schemaname, t[i].trftype.objectname, t[i].languagename); + } + + PQclear(res); + + return t; +} + +void +freeTransforms(PQLTransform *t, int n) +{ + if (n > 0) + { + int i; + + for (i = 0; i < n; i++) + { + free(t[i].trftype.schemaname); + free(t[i].trftype.objectname); + free(t[i].languagename); + if (t[i].fromsql.schemaname) + free(t[i].fromsql.schemaname); + if (t[i].fromsql.objectname) + free(t[i].fromsql.objectname); + if (t[i].fromsqlargs) + free(t[i].fromsqlargs); + if (t[i].tosql.schemaname) + free(t[i].tosql.schemaname); + if (t[i].tosql.objectname) + free(t[i].tosql.objectname); + if (t[i].tosqlargs) + free(t[i].tosqlargs); + if (t[i].comment) + PQfreemem(t[i].comment); + } + + free(t); + } +} + +void +dumpDropTransform(FILE *output, PQLTransform *t) +{ + char *typeschema = formatObjectIdentifier(t->trftype.schemaname); + char *typename = formatObjectIdentifier(t->trftype.objectname); + char *langname = formatObjectIdentifier(t->languagename); + + fprintf(output, "\n\n"); + fprintf(output, "DROP TRANSFORM FOR %s.%s LANGUAGE %s;", typeschema, typename, langname); + + free(typeschema); + free(typename); + free(langname); +} + +void +dumpCreateTransform(FILE *output, PQLTransform *t) +{ + char *typeschema = formatObjectIdentifier(t->trftype.schemaname); + char *typename = formatObjectIdentifier(t->trftype.objectname); + char *langname = formatObjectIdentifier(t->languagename); + char *fromsqlschema = formatObjectIdentifier(t->fromsql.schemaname); + char *fromsqlname = formatObjectIdentifier(t->fromsql.objectname); + char *tosqlschema = formatObjectIdentifier(t->tosql.schemaname); + char *tosqlname = formatObjectIdentifier(t->tosql.objectname); + + fprintf(output, "\n\n"); + fprintf(output, "CREATE TRANSFORM FOR %s.%s LANGUAGE %s (", typeschema, typename, langname); + if (t->fromsql.objectname != NULL) + fprintf(output, "FROM SQL WITH FUNCTION %s.%s", fromsqlschema, fromsqlname); + if (t->tosql.objectname != NULL) + fprintf(output, "TO SQL WITH FUNCTION %s.%s", tosqlschema, tosqlname); + fprintf(output, ");"); + + /* comment */ + if (options.comment && t->comment != NULL) + { + fprintf(output, "\n\n"); + fprintf(output, "COMMENT ON TRANSFORM FOR %s.%s LANGUAGE %s IS %s;", typeschema, typename, langname, t->comment); + } + + free(typeschema); + free(typename); + free(langname); + free(fromsqlschema); + free(fromsqlname); + free(tosqlschema); + free(tosqlname); +} + +void +dumpAlterTransform(FILE *output, PQLTransform *a, PQLTransform *b) +{ +} diff --git a/src/transform.h b/src/transform.h new file mode 100644 index 0000000..824a59f --- /dev/null +++ b/src/transform.h @@ -0,0 +1,33 @@ +/*---------------------------------------------------------------------- + * + * pgquarrel -- comparing database schemas + * + * Copyright (c) 2015-2018, Euler Taveira + * + * --------------------------------------------------------------------- + */ +#ifndef TRANSFORM_H +#define TRANSFORM_H + +#include "common.h" + +typedef struct PQLTransform +{ + PQLObject trftype; + char *languagename; + PQLObject fromsql; + char *fromsqlargs; + PQLObject tosql; + char *tosqlargs; + char *comment; +} PQLTransform; + +PQLTransform *getTransforms(PGconn *c, int *n); + +void dumpDropTransform(FILE *output, PQLTransform *t); +void dumpCreateTransform(FILE *output, PQLTransform *t); +void dumpAlterTransform(FILE *output, PQLTransform *a, PQLTransform *b); + +void freeTransforms(PQLTransform *t, int n); + +#endif /* TRANSFORM_H */