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 */