diff --git a/postgres/postgres.c b/postgres/postgres.c new file mode 100644 index 0000000..fd0d38a --- /dev/null +++ b/postgres/postgres.c @@ -0,0 +1,293 @@ +/* + * This file is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License, version 2, + * as published by the Free Software Foundation. + * + * In addition to the permissions in the GNU General Public License, + * the authors give you unlimited permission to link the compiled + * version of this file into combinations with other programs, + * and to distribute those combinations without any restriction + * coming from the use of this file. (The General Public License + * restrictions do apply in other respects; for example, they cover + * modification of the file, and distribution when not linked into + * a combined executable.) + * + * This file 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include <assert.h> +#include <string.h> +#include <git2.h> +#include <git2/odb_backend.h> +#include <postgresql/libpq-fe.h> + +#define GIT2_TABLE_NAME "git2_odb" +#define GIT2_SCHEMA_NAME "git2" + +typedef struct { + git_odb_backend parent; + PGconn *db; +} postgres_backend; + +int postgres_backend__read_header(size_t *len_p, git_otype *type_p, git_odb_backend *_backend, const git_oid *oid) +{ + postgres_backend *backend; + int error; + PGresult *result; + + assert(len_p && type_p && _backend && oid); + + backend = (postgres_backend *)_backend; + error = GIT_ERROR; + + result = PQexecParams(backend->db, "SELECT type, size FROM " GIT2_SCHEMA_NAME "." GIT2_TABLE_NAME " WHERE oid = $1;", 1, NULL, (const char**)(&(oid->id)), NULL, NULL, 0); + if(PQresultStatus(result) != PGRES_TUPLES_OK){ + return GIT_ERROR; + } + + if(PQntuples(result) < 1){ + error = GIT_ENOTFOUND; + } + else{ + assert(PQntuples(result) == 1); + + *type_p = (git_otype)strtol(PQgetvalue(result, 0, 0), NULL, 10); + *len_p = (git_otype)strtol(PQgetvalue(result, 0, 1), NULL, 10); + error = GIT_SUCCESS; + } + + PQclear(result); + return error; +} + +int postgres_backend__read(void **data_p, size_t *len_p, git_otype *type_p, git_odb_backend *_backend, const git_oid *oid) +{ + postgres_backend *backend; + int error; + PGresult *result; + + assert(data_p && len_p && type_p && _backend && oid); + + backend = (postgres_backend *)_backend; + error = GIT_ERROR; + + result = PQexecParams(backend->db, "SELECT type, size, data FROM " GIT2_SCHEMA_NAME "." GIT2_TABLE_NAME " WHERE oid = $1;", 1, NULL, (const char**)(&(oid->id)), NULL, NULL, 0); + if(PQresultStatus(result) != PGRES_TUPLES_OK){ + return GIT_ERROR; + } + + if(PQntuples(result) < 1){ + error = GIT_ENOTFOUND; + } + else{ + assert(PQntuples(result) == 1); + + *type_p = (git_otype)strtol(PQgetvalue(result, 0, 0), NULL, 10); + *len_p = (git_otype)strtol(PQgetvalue(result, 0, 1), NULL, 10); + *data_p = malloc(*len_p); + + if (*data_p == NULL) { + error = GIT_ENOMEM; + } else { + memcpy(*data_p, PQgetvalue(result, 0, 2), *len_p); + error = GIT_SUCCESS; + } + + error = GIT_SUCCESS; + } + + PQclear(result); + return error; +} + +int postgres_backend__read_prefix(git_oid *out_oid, void **data_p, size_t *len_p, git_otype *type_p, git_odb_backend *_backend, + const git_oid *short_oid, unsigned int len) { + if (len >= GIT_OID_HEXSZ) { + /* Just match the full identifier */ + int error = postgres_backend__read(data_p, len_p, type_p, _backend, short_oid); + if (error == GIT_SUCCESS) + git_oid_cpy(out_oid, short_oid); + + return error; + } else if (len < GIT_OID_HEXSZ) { + return GIT_ENOTIMPLEMENTED; + } +} + +int postgres_backend__exists(git_odb_backend *_backend, const git_oid *oid) +{ + postgres_backend *backend; + int found; + PGresult *result; + + assert(_backend && oid); + + backend = (postgres_backend *)_backend; + found = 0; + + result = PQexecParams(backend->db, "SELECT type, size, data FROM " GIT2_SCHEMA_NAME "." GIT2_TABLE_NAME " WHERE oid = $1;", 1, NULL, (const char**)(&(oid->id)), NULL, NULL, 0); + if(PQresultStatus(result) != PGRES_TUPLES_OK){ + return GIT_ERROR; + } + + if(PQntuples(result) > 0){ + found = 1; + } + + PQclear(result); + return found; +} + +int postgres_backend__write(git_oid *id, git_odb_backend *_backend, const void *data, size_t len, git_otype type) +{ + int error; + postgres_backend *backend; + PGresult *result; + + //this is a rather ugly construct to avoid having to know about postgres' internal integer representation + char type_str[128]; + char size_str[128]; + + const char *values[4] = {(char*)id->id, type_str, size_str, (char*)data}; + const int lengths[4] = {0, 0, 0, len}; + const int formats[4] = {0, 0, 0, 1}; + + assert(id && _backend && data); + + backend = (postgres_backend *)_backend; + + if ((error = git_odb_hash(id, data, len, type)) < 0) + return error; + + snprintf(type_str, sizeof(type_str), "%d", type); + snprintf(size_str, sizeof(size_str), "%d", len); + + result = PQexecParams(backend->db, "INSERT INTO " GIT2_SCHEMA_NAME "." GIT2_TABLE_NAME " VALUES ($1, $2, $3, $4);", 4, NULL, values, lengths, formats, 0); + + error = PQresultStatus(result); + PQclear(result); + + return (error == PGRES_COMMAND_OK || error == PGRES_TUPLES_OK) ? GIT_SUCCESS : GIT_ERROR; +} + +void postgres_backend__free(git_odb_backend *_backend) +{ + postgres_backend *backend; + assert(_backend); + backend = (postgres_backend *)_backend; + + PQfinish(backend->db); + + free(backend); +} + +static int create_table(PGconn *db) +{ + PGresult *result; + + //This should use CREATE SCHEMA IF NOT EXISTS, but that was added in Postgres 9.3 + static const char *schema_create = + "CREATE SCHEMA " GIT2_SCHEMA_NAME; + + //FIXME: using bytea for BLOBs might have some implications on the + //need to escape/unescape data before writes / after reads. + //However, it might well be that this is already handled by sending + //the parameter as binary. This should be tested in postgres_backend__write + static const char *sql_creat = + "CREATE TABLE " GIT2_SCHEMA_NAME "." GIT2_TABLE_NAME " (" + "oid TEXT PRIMARY KEY NOT NULL," + "type INTEGER NOT NULL," + "size INTEGER NOT NULL," + "data bytea);"; + + result = PQexec(db, schema_create); + if(PQresultStatus(result) != PGRES_COMMAND_OK && PQresultStatus(result) != PGRES_TUPLES_OK){ + return GIT_ERROR; + } + + result = PQexec(db, sql_creat); + if(PQresultStatus(result) != PGRES_COMMAND_OK && PQresultStatus(result) != PGRES_TUPLES_OK){ + return GIT_ERROR; + } + + return GIT_SUCCESS; +} + +static int init_db(PGconn *db) +{ + PGresult *result; + static const char *sql_check = "SELECT EXISTS ( SELECT 1 FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace " + " WHERE n.nspname = '"GIT2_SCHEMA_NAME"' AND c.relname = '"GIT2_TABLE_NAME"' AND c.relkind = 'r');"; + + result = PQexec(db, sql_check); + if(PQresultStatus(result) != PGRES_TUPLES_OK || PQntuples(result) < 1){ + PQclear(result); + return create_table(db); + } + + return GIT_SUCCESS; +} + +int pq_connect(PGconn **db, const char *host, unsigned port, const char *dbname, const char *user, const char *password){ + char port_str[10]; + + snprintf(port_str, sizeof(port_str), "%d", port); + + //This allows the application to use the .pgpass mechanism by supplying a NULL password + char const *keywords[] = {"host", "port", "dbname", "user", (password) ? "password":NULL, NULL}; + const char *values[] = {host, port_str, dbname, user, password, NULL}; + + *db = PQconnectdbParams(keywords, (char const**)values, 0); + + if(!(*db)){ + return 1; + } + + if(PQstatus(*db) != CONNECTION_OK){ + PQfinish(*db); + return 1; + } + + return 0; +} + +int git_odb_backend_postgres(git_odb_backend **backend_out, const char *pg_host, + const char *pg_user, const char *pg_passwd, const char *pg_db, unsigned int pg_port) +{ + postgres_backend *backend; + int error; + + backend = calloc(1, sizeof(postgres_backend)); + if (backend == NULL) + return GIT_ENOMEM; + + if(pq_connect(&(backend->db), pg_host, pg_port, pg_db, pg_user, pg_passwd)){ + goto cleanup; + } + + // check for and possibly create the database + error = init_db(backend->db); + if (error < 0) + goto cleanup; + + backend->parent.read = &postgres_backend__read; + backend->parent.read_header = &postgres_backend__read_header; + backend->parent.write = &postgres_backend__write; + backend->parent.exists = &postgres_backend__exists; + backend->parent.free = &postgres_backend__free; + + *backend_out = (git_odb_backend *)backend; + return GIT_SUCCESS; + + cleanup: + postgres_backend__free((git_odb_backend *)backend); + return GIT_ERROR; +}