Skip to content

Commit

Permalink
Refactoring entry point
Browse files Browse the repository at this point in the history
  • Loading branch information
Benvii committed Apr 27, 2018
1 parent 6c64a83 commit 1029e46
Show file tree
Hide file tree
Showing 8 changed files with 431 additions and 140 deletions.
4 changes: 1 addition & 3 deletions pg_dump_filtered/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,4 @@
# You should have received a copy of the GNU General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.

from pg_dump_filtered.schema_utils import SchemaUtils
from pg_dump_filtered.request_builder import RequestBuilder
from pg_dump_filtered.dump_builder import DumpBuilder
from pg_dump_filtered.pg_dump_filtered import PgDumpFiltered
55 changes: 12 additions & 43 deletions pg_dump_filtered/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@
-h --help Show this screen.
--filters=<SQL> SQL filters. Eg: mytable.mycol = 'value' AND myothertable.toto LIKE 'titi'
--ignored-constraints=<str> List of constraints to be ignored. Eg : "myconstraint,myotherconstraint"
--output=<str> Dump file path. [default: dump.sql]
--debug Set logs to debug.
"""

import logging
import psycopg2
from docopt import docopt
from urllib.parse import urlparse
from path import Path

from pg_dump_filtered import SchemaUtils, RequestBuilder, DumpBuilder, model
from pg_dump_filtered import PgDumpFiltered

MODULE_NAME = 'pg-dump-filtered'

Expand All @@ -59,51 +59,20 @@ def main():
logger.debug("DEBUG")

# Handling arguments
db_uri_parsed = urlparse(args["<db-uri>"])
db_uri_parsed = args["<db-uri>"]
tables_to_export = args["<table-list>"].split(",")
sql_filters = args["--filters"]
ignored_constraints = args["--ignored-constraints"].split(",") if args["--ignored-constraints"] is not None else []
ouput_file = Path(args["--output"])

# database connexion
db_conn = psycopg2.connect(
database=db_uri_parsed.path[1:],
user=db_uri_parsed.username,
password=db_uri_parsed.password,
host=db_uri_parsed.hostname)
dump_service = PgDumpFiltered(
db_uri=db_uri_parsed,
ignored_constraints=ignored_constraints,
sql_filters=sql_filters,
dump_file_path=ouput_file)
dump_service.dump(tables_to_export=tables_to_export)

# getting related tables informations for schema
schema_utils = SchemaUtils(conn=db_conn, ignored_constraints=ignored_constraints)
request_builder = RequestBuilder(schema_utils=schema_utils)
tables_to_request = schema_utils.list_all_related_tables(table_names=tables_to_export)

logger.debug("Table to request : %r", tables_to_request)
input()

from_table_name = tables_to_export[0] # Table that will be used in the FROM statment

# Generating all JOINs, they aren't selective as it would be too difficult to draw a graph of the relations to determine if JOIN is needed or not
join_req = request_builder.generate_join_statments(table_names=tables_to_request, exclude_from_statment=[from_table_name])
logger.debug("#### JOIN REQUEST FOR ALL TABLES ####")
logger.debug(join_req)

# generating select statements
selects = request_builder.generate_all_select_statements(table_to_be_exported=tables_to_request, from_table_name=from_table_name, join_statements=join_req, where_filter=sql_filters)
delete_p_kleys = # TODO

logger.debug("--------------------------------------------")
logger.debug(selects['lot'])
input()

# Starting copies to ouput file
with open('/tmp/dump.sql', 'w') as dump_file:
dump_builder = DumpBuilder(schema_utils=schema_utils, conn=db_conn, dump_file=dump_file)

dump_builder.dump(table_name="lot", select_request=selects['lot'])

# schema_utils.fetch_foreign_keys(table_name="lot")
# schema_utils.fetch_foreign_keys(table_name="lot")

db_conn.close()
dump_service.close()

if __name__ == "__main__":
main()
87 changes: 0 additions & 87 deletions pg_dump_filtered/dump_builder.py

This file was deleted.

17 changes: 17 additions & 0 deletions pg_dump_filtered/helpers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# coding: utf-8

# Copyright (C) 2017 Open Path View, Maison Du Libre
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
# This program 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. If not, see <http://www.gnu.org/licenses/>.

from pg_dump_filtered.helpers.schema_utils import SchemaUtils
from pg_dump_filtered.helpers.request_builder import RequestBuilder
from pg_dump_filtered.helpers.dump_builder import DumpBuilder
181 changes: 181 additions & 0 deletions pg_dump_filtered/helpers/dump_builder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
# coding: utf-8

# Copyright (C) 2017 Open Path View, Maison Du Libre
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
# This program 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. If not, see <http://www.gnu.org/licenses/>.

# Contributors: Benjamin BERNARD <[email protected]>
# Email: [email protected]
# Description: Utils function to generate SQL/PG dump.

import logging
import psycopg2
import psycopg2.extras
from typing import TextIO, List, Dict
from pg_dump_filtered.helpers import SchemaUtils

REQ_SELECT_DUMP = "COPY ({select}) TO STDOUT"

DP_HEADER = """--
-- PostgreSQL database dump
--
-- Dumped from pg_filtered python script
SET statement_timeout = 0;
SET lock_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET client_min_messages = warning;
SET row_security = off;
"""
DP_COPY_HEADER = """
-- Table : {table_name}
COPY public.{table_name} ({cols_names}) FROM stdin;"""
DP_STDIN_END = "\.\n\n"


DP_DISABLE_TABLE_TRIGGERS = """
ALTER TABLE public.{table_name} DISABLE TRIGGER ALL;
"""

DP_ENABLE_TABLE_TRIGGERS = """
ALTER TABLE public.{table_name} ENABLE TRIGGER ALL;
"""

class DumpBuilder():

def __init__(self, schema_utils: SchemaUtils, conn: psycopg2.extensions.connection, dump_file: TextIO):
"""
Instanciate a request builder.
:param schema_utils: Schema utils used to fetch some related schema informations.
:param conn: database connexion.
:param dump_file: File where the dump will be made.
"""
self._schema_utils = schema_utils
self._dump_file = dump_file
self._conn = conn
self.logger = logging.getLogger(__name__)

self._dump_file.write(DP_HEADER)

def _enable_triggers(self, table_name: str):
"""
Enable triggers for table_name.
:param table_name: Table where triggers will be enabled.
"""
self._dump_file.write(DP_ENABLE_TABLE_TRIGGERS.format(table_name=table_name))

def _disable_triggers(self, table_name: str):
"""
Disable triggers for table_name.
:param table_name: Table where triggers will be disabled.
"""
self._dump_file.write(DP_DISABLE_TABLE_TRIGGERS.format(table_name=table_name))

def dump(self, table_name: str, select_request: str):
"""
Generate the COPY statement for a table from a select request.
Dump result wil be appened to the export file.
:param table_name: Table that will be dumped, used to key it's schema.
:param select_request: Select request, which request the table's data.
"""
self.logger.debug("dump for table_name: %s", table_name)

# disable triggers to prevent key relations errors (depending on COPY order in tables)
self._disable_triggers(table_name)

# Making COPY header
cols = self._schema_utils.fetch_cols_names(table_name=table_name)
cols_names_list = ["\"{cname}\"".format(cname=c.column_name) for c in cols] # prevent uppercases columns names errors
cols_names = ", ".join(cols_names_list)
header = DP_COPY_HEADER.format(table_name=table_name, cols_names=cols_names)
self._dump_file.write(header + "\n")
self.logger.debug("Generated COPY statement for dump file : %s", header)

# Execute "dump" request based on select
cur = self._conn.cursor()
cur.copy_expert(REQ_SELECT_DUMP.format(select=select_request), self._dump_file)
self._dump_file.write(DP_STDIN_END)

# Enabling triggers back
self._enable_triggers(table_name)

self.logger.debug("Dump saved into dump_file")

def dump_tables(self, select_requests: Dict[str, str]):
"""
Dump all data corresponding to select requests for each table_name.
:param select_requests: Dictionnary associating table_name => select request.
"""
self.logger.debug("Dumping mutiple tables : %r", select_requests.keys())

for table_name, select in select_requests.items():
self.dump(table_name=table_name, select_request=select)

def generate_primary_keys_delete_statements(self, from_table_name: str, displayed_fields_table_name: str, join_statements: str, where_filter: str=""):
"""
Will generate a delete statement for all selected datas.
:param from_table_name: Table used in from statement.
:param
"""
self.logger.debug("Generating delation statements for table : %s", displayed_fields_table_name)
self._dump_file.write("""-- delete statements for partial dump of table : {table_name}\n""".format(table_name=displayed_fields_table_name))
pkeys_cols = self._schema_utils.fetch_primary_keys(table_name=displayed_fields_table_name)
where = "" if where_filter == "" or where_filter is None else " WHERE " + where_filter

# disabling triggers, ugly but can't do anything else
# this is use to prevent cascade delation as our purpose is to update data not delete all related ones
self._disable_triggers(displayed_fields_table_name)

# select with pkeys cols to gets ids
select_view = ", ".join([c.table_name + "." + c.column_name for c in pkeys_cols])
select_keys_values_req = "SELECT {select_view} FROM {from_table_name} {join_statements} {where}".format(
select_view=select_view, from_table_name=from_table_name, join_statements=join_statements, where=where)
cur = self._conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
self.logger.debug(select_keys_values_req)
cur.execute(select_keys_values_req)
for row in cur:
delete_where = " AND ".join(["{tname}.{cname} = '{id}'".format(tname=c.table_name, cname=c.column_name, id=row[c.column_name]) for c in pkeys_cols])
self._dump_file.write("DELETE FROM public.{table_name} WHERE {where}; \n".format(table_name=displayed_fields_table_name, where=delete_where))

# Setting triggers back
self._enable_triggers(displayed_fields_table_name)

self._dump_file.write("\n")

def generate_all_delete_statements(self, from_table_name: str, table_to_be_exported: List[str], join_statements: str, where_filter: str=""):
"""
Generate all delete statements.
:param table_to_be_exported: Tables that will be exported.
:param from_table_name: Table used in the FROM statement, should not be mentionned in the JOIN statements.
:param where_filter: SQL filters.
:return: Dictionnary of select statement for each table (table_name => select statement)
"""
self.logger.debug("Generating all delete statements for tables : %r, FROM table is: %r", table_to_be_exported, from_table_name)
self.logger.debug("Used filters : %s", where_filter)
for tname in table_to_be_exported:
self.logger.debug("#### Dump for %r ####", tname)
self.generate_primary_keys_delete_statements(
from_table_name=from_table_name,
displayed_fields_table_name=tname,
join_statements=join_statements,
where_filter=where_filter)
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

import logging
from typing import List, Dict
from pg_dump_filtered import SchemaUtils
from pg_dump_filtered.helpers import SchemaUtils

class RequestBuilder():

Expand Down Expand Up @@ -112,9 +112,3 @@ def generate_all_select_statements(self, table_to_be_exported: List[str], from_t
self.logger.debug(req)

return select_requests

def generate_primary_keys_delete_statements(self, from_table_name: str, displayed_fields_table_name: str, join_statements: str, where_filter: str=""):
"""
Will generate a delete statement for all selected datas.
"""
pass
Loading

0 comments on commit 1029e46

Please sign in to comment.