Added postgres backend for gpx importer daemon.
[gpx-import.git] / src / postgres.c
1 /* gpx-import/src/postgres.c
2  *
3  * GPS point insertion into PostgreSQL database, based on the 
4  * existing MySQL backend.
5  *
6  * Copyright Daniel Silverstone <dsilvers@digital-scurf.org>
7  *           CloudMade Ltd <matt@cloudmade.com>
8  *
9  * Written for the OpenStreetMap project.
10  *
11  * This program is free software; you can redistribute it and/or
12  * modify it under the terms of the GNU General Public License as
13  * published by the Free Software Foundation; version 2 of the License
14  * only.
15  *
16  * This program is distributed in the hope that it will be useful, but
17  * WITHOUT ANY WARRANTY; without even the implied warranty of
18  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
19  * General Public License for more details.
20  */
21
22 #include <stdlib.h>
23 #include <libpq-fe.h>
24 #include <stdio.h>
25 #include <string.h>
26 #include <unistd.h>
27 #include <inttypes.h>
28
29 #include "filename.h"
30 #include "db.h"
31 #include "quadtile.h"
32
33 #define STMT_BUFLEN (1024 * 256)
34
35 static PGconn *handle;
36 static char statement_buffer[STMT_BUFLEN];
37 static char escape_buffer[STMT_BUFLEN];
38
39 /* Postgres has slightly different semantics to MySQL when doing queries
40  * than when doing statements, returning different types from the result
41  * status function. This means that we can't share code quite as easily
42  * between these two macros :-(
43  *
44  * Both macros handle transactions by immediately aborting them if 
45  * something goes wrong. This appears to be in-line with the rest of the
46  * semantics in the program.
47  */
48 #define QUERY(R,V...)                                                   \
49   snprintf(statement_buffer, STMT_BUFLEN, V);                           \
50   R = PQexec(handle, statement_buffer);                                 \
51   if (PQresultStatus(R) != PGRES_TUPLES_OK) {                           \
52     ERROR("Failure executing PostgreSQL query: %s",                     \
53           PQresultErrorMessage(R));                                     \
54     PQclear(R);                                                         \
55     PQexec(handle, "ROLLBACK");                                         \
56     return false;                                                       \
57   }
58
59 #define STMT(V...)                                                      \
60   do {                                                                  \
61     PGresult *stmt_result;                                              \
62     snprintf(statement_buffer, STMT_BUFLEN, V);                         \
63     stmt_result = PQexec(handle, statement_buffer);                     \
64     if (PQresultStatus(stmt_result) != PGRES_COMMAND_OK) {              \
65       ERROR("Failure executing PostgreSQL command: %s",                 \
66             PQresultErrorMessage(stmt_result));                         \
67       PQclear(stmt_result);                                             \
68       PQexec(handle, "ROLLBACK");                                       \
69       return false;                                                     \
70     }                                                                   \
71     PQclear(stmt_result);                                               \
72   } while(0)
73
74 #define BLANKOR(S) strdup(((S) ? (S) : ("")))
75
76 bool
77 db_destroy_trace(int64_t jobnr)
78 {
79   STMT("START TRANSACTION");
80   INFO("Destroying job %"PRId64"", jobnr);
81   STMT("DELETE FROM gpx_file_tags WHERE gpx_id=%"PRId64"", jobnr);
82   STMT("DELETE FROM gps_points WHERE gpx_id=%"PRId64"", jobnr);
83   STMT("DELETE FROM gpx_files WHERE id=%"PRId64"", jobnr);
84   // NOTE: Errors aren't checked here - should they be?
85   unlink(make_filename("GPX_PATH_TRACES", jobnr, ".gpx"));
86   unlink(make_filename("GPX_PATH_IMAGES", jobnr, "_icon.gif"));
87   unlink(make_filename("GPX_PATH_IMAGES", jobnr, ".gif"));
88   STMT("COMMIT");
89   return true;
90 }
91
92
93 bool
94 db_insert_gpx(DBJob *job)
95 {
96   PGresult *result;
97   bool do_delete = false;
98   GPXTrackPoint *pt;
99   int64_t gpxnr = job->gpx_id;
100   GPX *gpx = job->gpx;
101   
102   STMT("START TRANSACTION");
103   QUERY(result,"SELECT COUNT(*) FROM gps_points WHERE gpx_id=%"PRId64"", gpxnr);
104   if (atoi(PQgetvalue(result, 0, 0)) != 0) {
105     do_delete = true;
106   }
107   PQclear(result);
108   
109   if (do_delete == true) {
110     WARN("Old rows detected, deleting");
111     STMT("DELETE FROM gps_points WHERE gpx_id=%"PRId64"", gpxnr);
112   }
113   
114   INFO("Inserting %d points", gpx->goodpoints);
115   
116   /* Iterate the points, inserting them into the DB */
117   for (pt = gpx->points; pt != NULL; pt = pt->next) {
118     int string_invalid = 0;
119     PQescapeStringConn(handle, escape_buffer, pt->timestamp,
120                        strlen(pt->timestamp), &string_invalid);
121     if (string_invalid != 0) {
122       ERROR("Failed to escape string `%s', possibly invalid byte sequence.",
123             pt->timestamp);
124     }
125     STMT("INSERT INTO gps_points (gpx_id, trackid, latitude, longitude, timestamp, altitude, tile) " \
126          "VALUES (%"PRId64", %d, %"PRId64", %"PRId64", '%s', %f, %u)",
127          gpxnr, pt->segment, pt->latitude / 100, pt->longitude / 100, escape_buffer, pt->elevation,
128          quadtile_for_coords(pt->latitude, pt->longitude));
129   }
130
131   /* Last up, update the GPX with our lat/long/numpoints etc */
132   STMT("UPDATE gpx_files SET inserted=true, size=%d, latitude=%g, longitude=%g WHERE id=%"PRId64"\n",
133        gpx->goodpoints, (double)gpx->firstlatitude / 1000000000.0, (double)gpx->firstlongitude / 1000000000.0, gpxnr);
134   
135   STMT("COMMIT");
136
137   return true;
138 }
139
140 int64_t
141 db_find_invisible(void)
142 {
143   int64_t ret = -1;
144   PGresult *result;
145
146   STMT("START TRANSACTION");
147   QUERY(result, "SELECT id FROM gpx_files WHERE visible=false LIMIT 1");
148   
149   if ((PQntuples(result) > 0) &&
150       (PQnfields(result) == 1)) {
151     ret = strtol(PQgetvalue(result, 0,0), NULL, 0);
152   }
153   STMT("ROLLBACK");
154
155   PQclear(result);
156
157   return ret;
158 }
159
160 DBJob *
161 db_find_work(int minage)
162 {
163   DBJob *ret = NULL;
164   PGresult *result;
165   int64_t user;
166   
167   STMT("START TRANSACTION");
168   QUERY(result, "SELECT id, name, description, user_id FROM gpx_files WHERE visible=true AND inserted=false AND timestamp <= now() - '%d second'::interval ORDER BY timestamp ASC LIMIT 1", minage);
169   if ((PQntuples(result) > 0) &&
170       (PQnfields(result) == 4)) {
171     ret = calloc(1, sizeof(DBJob));
172     ret->gpx_id = strtol(PQgetvalue(result, 0, 0), NULL, 0);
173     ret->title = BLANKOR(PQgetvalue(result, 0, 1));
174     ret->description = BLANKOR(PQgetvalue(result, 0, 2));
175     user = strtol(PQgetvalue(result, 0, 3), NULL, 0);
176   }
177
178   PQclear(result);
179   
180   if (ret != NULL) {
181     /* Attempt to retrieve the email address */
182     QUERY(result, "SELECT display_name, email FROM users WHERE id=%"PRId64"", user);
183     if ((PQntuples(result) > 0) &&
184         (PQnfields(result) == 2)) {
185       const char *name = PQgetvalue(result, 0, 0);
186       const char *email = PQgetvalue(result, 0, 1);
187       int tlen = strlen(name) + strlen(email) + 4; /* space '<' '>' NULL */
188       ret->email = malloc(tlen);
189       snprintf(ret->email, tlen, "%s <%s>", name, email);
190     } else {
191       db_error(ret, "Database error while retrieving user information for user %"PRId64"", user);
192     }
193
194     PQclear(result);
195   }
196   
197   if (ret != NULL && ret->error == NULL) {
198     /* Attempt to retrieve the tags */
199     QUERY(result, "select array_to_string(array(select tag from gpx_file_tags where gpx_id=%"PRId64"),',')", ret->gpx_id);
200     if ((PQntuples(result) > 0) &&
201         (PQnfields(result) == 1)) {
202       ret->tags = BLANKOR(PQgetvalue(result, 0, 0));
203     } else {
204       db_error(ret, "Database error while retrieving GPX tags for file %"PRId64"\n", ret->gpx_id);
205     }
206   }
207   
208   STMT("ROLLBACK");
209
210   return ret;
211 }
212
213 bool
214 db_connect(void)
215 {
216   char *host, *user, *pass, *db, *port, *options;
217   
218   host = getenv("GPX_PGSQL_HOST");
219   user = getenv("GPX_PGSQL_USER");
220   pass = getenv("GPX_PGSQL_PASS");
221   db = getenv("GPX_PGSQL_DB");
222   port = getenv("GPX_PGSQL_PORT");
223   options = getenv("GPX_PGSQL_OPTIONS");
224   
225   handle = PQsetdbLogin(host, port, options, NULL, db, user, pass);
226
227   if (PQstatus(handle) != CONNECTION_OK) {
228     ERROR("Failure connecting to PostgreSQL server: %s", 
229           PQerrorMessage(handle));
230     PQfinish(handle);
231     return false;
232   }
233   
234   return true;
235 }
236
237 void
238 db_disconnect(void)
239 {
240   PQfinish(handle);
241 }