This commit is contained in:
Aditya Bist 2017-08-10 13:49:30 -07:00 коммит произвёл GitHub
Родитель 656ee05f5b
Коммит e6719e6087
14 изменённых файлов: 338 добавлений и 32 удалений

Просмотреть файл

@ -35,7 +35,7 @@ class NodeObject(metaclass=ABCMeta):
# Render and execute the template
sql = templating.render_template(
templating.get_template_path(template_root, 'nodes.sql', root_server.version),
paths_to_add=[cls._macro_root()],
paths_to_add=cls._macro_root(),
**template_vars
)
cols, rows = root_server.connection.execute_dict(sql)
@ -99,12 +99,12 @@ class NodeObject(metaclass=ABCMeta):
@classmethod
def _macro_root(cls) -> str:
pass
return None
def _get_template(self, connection: querying.ServerConnection, query_file: str, data,
paths_to_add=None, filters_to_add=None) -> str:
""" Helper function to render a template given data and query file """
template_root = self._template_root(connection)
template_root = self._template_root(self.server)
connection_version = querying.get_server_version(connection)
template_path = templating.get_template_path(template_root, query_file, connection_version)
script_template = templating.render_template(template_path, paths_to_add, filters_to_add, **data)

Просмотреть файл

@ -135,14 +135,14 @@ class Schema(node.NodeObject):
@classmethod
def _macro_root(cls) -> str:
return MACRO_ROOT
return [MACRO_ROOT]
# SCRIPTING METHODS ##############################################################
def create_script(self, connection: querying.ServerConnection) -> str:
""" Function to retrieve create scripts for a schema """
data = self._create_query_data()
query_file = "create.sql"
return self._get_template(connection, query_file, data, paths_to_add=[self._macro_root()])
return self._get_template(connection, query_file, data, paths_to_add=self._macro_root())
def delete_script(self, connection: querying.ServerConnection) -> str:
""" Function to retrieve delete scripts for schema """
@ -154,7 +154,7 @@ class Schema(node.NodeObject):
""" Function to retrieve update scripts for schema """
data = self._update_query_data()
query_file = "update.sql"
return self._get_template(connection, query_file, data, paths_to_add=[self._macro_root()])
return self._get_template(connection, query_file, data, paths_to_add=self._macro_root())
# HELPER METHODS ######################################################
def _create_query_data(self) -> dict:

Просмотреть файл

@ -0,0 +1,11 @@
{% macro APPLY(conn, type, role, param, privs, with_grant_privs) -%}
{% if privs %}
GRANT {{ privs|join(', ') }} ON {{ type }} {{ conn|qtIdent(param) }} TO {{ role }};
{% endif %}
{% if with_grant_privs %}
GRANT {{ with_grant_privs|join(', ') }} ON {{ type }} {{ conn|qtIdent(param) }} TO {{ role }} WITH GRANT OPTION;
{% endif %}
{%- endmacro %}
{% macro RESETALL(conn, type, role, param) -%}
REVOKE ALL ON {{ type }} {{ conn|qtIdent(param) }} FROM {{ role }};
{%- endmacro %}

Просмотреть файл

@ -0,0 +1,6 @@
{% macro APPLY(conn, type, name, provider, label) -%}
SECURITY LABEL{% if provider and provider != '' %} FOR {{ conn|qtIdent(provider) }}{% endif %} ON {{ type }} {{ conn|qtIdent(name) }} IS {{ label|qtLiteral }};
{%- endmacro %}
{% macro DROP(conn, type, name, provider) -%}
SECURITY LABEL FOR {{ conn|qtIdent(provider) }} ON {{ type }} {{ conn|qtIdent(name) }} IS NULL;
{%- endmacro %}

Просмотреть файл

@ -0,0 +1,28 @@
{####################################################}
{# This will be specific macro for Role objects #}
{####################################################}
{% macro APPLY(conn, database, role, param, value) -%}
ALTER {% if role %}ROLE {{ self.conn|qtIdent(role) }}{% if database %} IN DATABASE {{ conn|qtIdent(database) }}{% endif %}{% else %}DATABASE {{ conn|qtIdent(database) }}{% endif %}
SET {{ conn|qtIdent(param) }} TO {{ value|qtLiteral }};
{%- endmacro %}
{% macro RESET(conn, database, role, param) -%}
ALTER {% if role %}ROLE {{ self.conn|qtIdent(role) }}{% if database %} IN DATABASE {{ conn|qtIdent(database) }}{% endif %}{% else %}DATABASE {{ conn|qtIdent(database) }}{% endif %}
RESET {{ conn|qtIdent(param) }};
{%- endmacro %}
{################################################}
{# This will be generic macro for other objects #}
{################################################}
{% macro SET(conn, object_type, object_name, options) -%}
ALTER {{object_type}} {{ conn|qtIdent(object_name) }}
SET ({% for opt in options %}{% if loop.index != 1 %}
, {% endif %}{{ conn|qtIdent(opt.name) }}={{ opt.value|qtLiteral }}{% endfor %});
{%- endmacro %}
{% macro UNSET(conn, object_type, object_name, options) -%}
ALTER {{object_type}} {{ conn|qtIdent(object_name) }}
RESET ({% for opt in options %}{% if loop.index != 1 %}
, {% endif %}{{ conn|qtIdent(opt.name) }}{% endfor %});
{%- endmacro %}

Просмотреть файл

@ -8,10 +8,12 @@ from typing import Optional
from pgsmo.objects.node_object import NodeObject
from pgsmo.objects.server import server as s # noqa
import pgsmo.utils.templating as templating
import pgsmo.utils.querying as querying
class Tablespace(NodeObject):
TEMPLATE_ROOT = templating.get_template_root(__file__, 'templates')
MACRO_ROOT = templating.get_template_root(__file__, 'macros')
@classmethod
def _from_node_query(cls, server: 's.Server', parent: None, **kwargs) -> 'Tablespace':
@ -47,7 +49,81 @@ class Tablespace(NodeObject):
"""Object ID of the user that owns the tablespace"""
return self._owner
@property
def user(self):
return self._full_properties.get("user", "")
@property
def location(self):
return self._full_properties.get("location", "")
@property
def description(self):
return self._full_properties.get("description", "")
@property
def options(self):
return self._full_properties.get("options", "")
@property
def acl(self):
return self._full_properties.get("acl", "")
# IMPLEMENTATION DETAILS ###############################################
@classmethod
def _template_root(cls, server: 's.Server') -> str:
return cls.TEMPLATE_ROOT
@classmethod
def _macro_root(cls) -> str:
return [cls.MACRO_ROOT]
# SCRIPTING METHODS ####################################################
def create_script(self, connection: querying.ServerConnection) -> str:
""" Function to retrieve create scripts for a tablespace """
data = self._create_query_data()
query_file = "create.sql"
return self._get_template(connection, query_file, data)
def delete_script(self, connection: querying.ServerConnection) -> str:
""" Function to retrieve delete scripts for a table"""
data = self._delete_query_data()
query_file = "delete.sql"
return self._get_template(connection, query_file, data)
def update_script(self, connection: querying.ServerConnection) -> str:
""" Function to retrieve update scripts for a table"""
data = self._update_query_data()
query_file = "update.sql"
return self._get_template(connection, query_file, data, paths_to_add=self._macro_root())
# HELPER METHODS #######################################################
def _create_query_data(self):
""" Returns the data needed for create query """
return {"data": {
"name": self.name,
"spcuser": self.user,
"spclocation": self.location
}}
def _delete_query_data(self):
""" Returns the data needed for delete query """
return {"tsname": self.name}
def _update_query_data(self):
""" Returns the data needed for update query """
return {"data": {
"name": self.name,
"spcuser": self.user,
"spclocation": self.location,
"description": self.description,
"spcoptions": self.options,
"spcacl": self.acl
}, "o_data": {
"name": "",
"spcuser": "",
"description": ""
}
}

Просмотреть файл

@ -5,8 +5,8 @@
# This software is released under the PostgreSQL Licence
#}
{### SQL to update tablespace object ###}
{% import 'macros/variable.macros' as VARIABLE %}
{% import 'macros/privilege.macros' as PRIVILEGE %}
{% import 'variable.macros' as VARIABLE %}
{% import 'privilege.macros' as PRIVILEGE %}
{% if data %}
{# ==== To update tablespace name ==== #}
{% if data.name and data.name != o_data.name %}

Просмотреть файл

@ -5,9 +5,9 @@
# This software is released under the PostgreSQL Licence
#}
{### SQL to update tablespace object ###}
{% import 'macros/security.macros' as SECLABEL %}
{% import 'macros/variable.macros' as VARIABLE %}
{% import 'macros/privilege.macros' as PRIVILEGE %}
{% import 'security.macros' as SECLABEL %}
{% import 'variable.macros' as VARIABLE %}
{% import 'privilege.macros' as PRIVILEGE %}
{% if data %}
{# ==== To update tablespace name ==== #}
{% if data.name and data.name != o_data.name %}

Просмотреть файл

@ -80,24 +80,31 @@ def render_template(template_path: str, paths_to_add=None, filters_to_add=None,
:return: The template rendered with the provided context
"""
path, filename = os.path.split(template_path)
if path not in TEMPLATE_ENVIRONMENTS:
# Create the filesystem loader that will look in template folder FIRST
template_root = os.path.dirname(os.path.dirname(template_path))
paths = [path, template_root]
if (paths_to_add is not None):
paths += paths_to_add
loader: FileSystemLoader = FileSystemLoader(paths)
paths = [path]
if (paths_to_add is not None):
paths += paths_to_add
# Create the environment and add the basic filters
new_env: Environment = Environment(loader=loader)
new_env.filters['qtLiteral'] = qt_literal
new_env.filters['qtIdent'] = qt_ident
new_env.filters['qtTypeIdent'] = qt_type_ident
for path in paths:
if path not in TEMPLATE_ENVIRONMENTS:
# Create the filesystem loader that will look in template folder FIRST
template_root = os.path.dirname(os.path.dirname(template_path))
if (template_root not in TEMPLATE_ENVIRONMENTS):
paths.append(template_root)
loader: FileSystemLoader = FileSystemLoader(paths)
# Create the environment and add the basic filters
new_env: Environment = Environment(loader=loader)
new_env.filters['qtLiteral'] = qt_literal
new_env.filters['qtIdent'] = qt_ident
new_env.filters['qtTypeIdent'] = qt_type_ident
TEMPLATE_ENVIRONMENTS[path] = new_env
break
TEMPLATE_ENVIRONMENTS[path] = new_env
if (filters_to_add is not None):
for filter_name, function in filters_to_add.items():
TEMPLATE_ENVIRONMENTS[path].filters[filter_name] = function
env = TEMPLATE_ENVIRONMENTS[path]
to_render = env.get_template(filename)
return to_render.render(context)

Просмотреть файл

@ -29,6 +29,8 @@ class Scripter(object):
# CREATE ##################################################################
# Database
def get_database_create_script(self, metadata) -> str:
""" Get create script for databases """
try:
@ -43,6 +45,8 @@ class Scripter(object):
# need to handle exceptions well
return None
# View
def get_view_create_script(self, metadata) -> str:
""" Get create script for views """
try:
@ -57,6 +61,8 @@ class Scripter(object):
except Exception:
return None
# Table
def get_table_create_script(self, metadata) -> str:
""" Get create script for tables """
try:
@ -69,6 +75,22 @@ class Scripter(object):
except Exception:
return None
# Tablespace
def get_tablespace_create_script(self, metadata) -> str:
try:
# get tablespace
tablespace_name = metadata["name"]
tablespace = self.server.tablespaces[tablespace_name]
# get create script
script = tablespace.create_script(self.connection)
return script
except Exception:
return None
# Schema
def get_schema_create_script(self, metadata) -> str:
""" Get create script for schema """
try:
@ -95,6 +117,9 @@ class Scripter(object):
return None
# DELETE ##################################################################
# Table
def get_table_delete_script(self, metadata) -> str:
""" Get delete script for table """
try:
@ -104,6 +129,8 @@ class Scripter(object):
except Exception:
return None
# View
def get_view_delete_script(self, metadata) -> str:
""" Get delete script for view """
try:
@ -118,6 +145,8 @@ class Scripter(object):
except Exception:
return None
# Database
def get_database_delete_script(self, metadata) -> str:
""" Get delete script for databases """
try:
@ -131,6 +160,8 @@ class Scripter(object):
except Exception:
return None
# Schema
def get_schema_delete_script(self, metadata) -> str:
""" Get delete script for schemas """
try:
@ -143,8 +174,25 @@ class Scripter(object):
except Exception:
return None
# Tablespace
def get_tablespace_delete_script(self, metadata) -> str:
""" Get delete script for tablespaces """
try:
# get tablespace from server
tablespace_name = metadata["name"]
tablespace = self.server.tablespaces[tablespace_name]
# get the delete script
script = tablespace.delete_script(self.connection)
return script
except Exception:
return None
# UPDATE ##################################################################
# Table
def get_table_update_script(self, metadata) -> str:
""" Get update script for tables """
try:
@ -157,6 +205,8 @@ class Scripter(object):
except Exception:
return None
# View
def get_view_update_script(self, metadata) -> str:
""" Get update date script for view """
try:
@ -171,6 +221,8 @@ class Scripter(object):
except Exception:
return None
# Schema
def get_schema_update_script(self, metadata) -> str:
""" Get update script for schemas """
try:
@ -183,6 +235,8 @@ class Scripter(object):
except Exception:
return None
# Role
def get_role_update_script(self, metadata) -> str:
""" Get update script for roles """
try:
@ -193,7 +247,22 @@ class Scripter(object):
# get the create script
script = role.update_script(self.connection)
return script
except:
except Exception:
return None
# Tablespace
def get_tablespace_update_script(self, metadata) -> str:
""" Get update script for tablespaces """
try:
# get tablespace from server
tablespace_name = metadata["name"]
tablespace = self.server.tablespaces[tablespace_name]
# get the delete script
script = tablespace.update_script(self.connection)
return script
except Exception:
return None
# HELPER METHODS ##########################################################

Просмотреть файл

@ -48,16 +48,19 @@ class ScriptingService(object):
def script_as_create(self, connection, metadata: ObjectMetadata) -> str:
""" Function to get script for create operations """
scripter = Scripter(connection)
if (metadata["metadataTypeName"] == 'Database'):
metadataType = metadata["metadataTypeName"]
if (metadataType == 'Database'):
return scripter.get_database_create_script(metadata)
elif (metadata["metadataTypeName"] == 'View'):
elif (metadataType == 'View'):
return scripter.get_view_create_script(metadata)
elif (metadata["metadataTypeName"] == 'Table'):
elif (metadataType == 'Table'):
return scripter.get_table_create_script(metadata)
elif (metadata["metadataTypeName"] == 'Schema'):
elif (metadataType == 'Schema'):
return scripter.get_schema_create_script(metadata)
elif (metadata["metdataTypeName"] == 'Role'):
elif (metadataType == 'Role'):
return scripter.get_role_create_script(metadata)
elif (metadataType == 'Tablespace'):
return scripter.get_tablespace_create_script(metadata)
def script_as_select(self, connection, metadata: ObjectMetadata) -> str:
""" Function to get script for select operations """
@ -74,6 +77,8 @@ class ScriptingService(object):
return scripter.get_table_update_script(metadata)
elif (metadataType == 'Schema'):
return scripter.get_schema_update_script(metadata)
elif (metadataType == 'Tablespace'):
return scripter.get_tablespace_update_script(metadata)
def script_as_delete(self, connection, metadata: ObjectMetadata) -> str:
""" Function to get script for insert operations """
@ -87,6 +92,8 @@ class ScriptingService(object):
return scripter.get_table_delete_script(metadata)
elif (metadataType == 'Schema'):
return scripter.get_schema_delete_script(metadata)
elif (metadataType == 'Tablespace'):
return scripter.get_tablespace_delete_script(metadata)
def _scripting_operation(self, scripting_operation: int, connection, metadata: ObjectMetadata):
"""Helper function to get the correct script based on operation"""

Просмотреть файл

@ -362,7 +362,7 @@ class TestNodeObject(unittest.TestCase):
# Then:
# ... The template path and template renderer should have been called once
mock_template_path.assert_called_once_with('template_root', 'nodes.sql', mock_server.version)
mock_render.assert_called_once_with('path', paths_to_add=[None], **{}) # Params to the renderer should be empty
mock_render.assert_called_once_with('path', paths_to_add=None, **{}) # Params to the renderer should be empty
# ... A query should have been executed
mock_executor.assert_called_once_with('SQL')
@ -405,7 +405,7 @@ class TestNodeObject(unittest.TestCase):
# Then:
# ... The template path and template renderer should have been called once
mock_template_path.assert_called_once_with('template_root', 'nodes.sql', mock_server.version)
mock_render.assert_called_once_with('path', **{'parent_id': 123}, paths_to_add=[None])
mock_render.assert_called_once_with('path', **{'parent_id': 123}, paths_to_add=None)
# ... A query should have been executed
mock_executor.assert_called_once_with('SQL')

Просмотреть файл

@ -42,3 +42,23 @@ class TestTablespace(NodeObjectTestBase, unittest.TestCase):
@property
def parent_expected_to_be_none(self) -> bool:
return True
@property
def full_properties(self):
return {
"user": "user",
"location": "location",
"description": "description",
"options": "options",
"acl": "acl"
}
@property
def property_query(self):
return {
"user": "test",
"location": "some path",
"description": None,
"options": None,
"acl": None
}

Просмотреть файл

@ -27,6 +27,7 @@ from pgsmo.objects.database.database import Database
from pgsmo.objects.server.server import Server
from pgsmo.objects.schema.schema import Schema, TEMPLATE_ROOT
from pgsmo.objects.role.role import Role
from pgsmo.objects.tablespace.tablespace import Tablespace
"""Module for testing the scripting service"""
@ -172,6 +173,9 @@ class TestScriptingService(unittest.TestCase):
# Role
self._test_role_create_script(mock_scripter, service)
# Tablespace
self._test_tablespace_create_script(mock_scripter, service)
def test_script_as_delete(self):
""" Test getting delete script for all objects """
mock_scripter = Scripter(self.connection)
@ -189,6 +193,9 @@ class TestScriptingService(unittest.TestCase):
# Schema
self._test_schema_delete_script(mock_scripter, service)
# Tablespace
self._test_tablespace_delete_script(mock_scripter, service)
def test_script_as_update(self):
""" Test getting update script for all objects """
mock_scripter = Scripter(self.connection)
@ -200,6 +207,9 @@ class TestScriptingService(unittest.TestCase):
# Role
self._test_role_update_script(mock_scripter, service)
# Tablespace
self._test_tablespace_update_script(mock_scripter, service)
# PRIVATE HELPER FUNCTIONS ####################################################
# CREATE SCRIPTS ##############################################################
@ -332,6 +342,30 @@ class TestScriptingService(unittest.TestCase):
# The result shouldn't be none or an empty string
self.assertIsNotNone(result)
def _test_tablespace_create_script(self, scripter, service):
""" Helper function to test create script for schema """
# Set up the mocks
mock_tablespace = Tablespace(None, 'test')
def tablespace_mock_fn(connection):
mock_tablespace._template_root = mock.MagicMock(return_value=Tablespace.TEMPLATE_ROOT)
mock_tablespace._create_query_data = mock.MagicMock(return_value={"data": {"name": "test", "spclocation": None}})
result = mock_tablespace.create_script(connection)
return result
def scripter_mock_fn():
mock_tablespace.create_script = mock.MagicMock(return_value=tablespace_mock_fn(self.connection))
return mock_tablespace.create_script()
scripter.get_tablespace_create_script = mock.MagicMock(return_value=scripter_mock_fn())
service.script_as_create = mock.MagicMock(return_value=scripter.get_tablespace_create_script())
# If I try to get select script for any object
result = service.script_as_create()
# The result shouldn't be none or an empty string
self.assertIsNotNone(result)
# DELETE SCRIPTS ##############################################################
def _test_table_delete_script(self, scripter, service):
@ -431,6 +465,30 @@ class TestScriptingService(unittest.TestCase):
# The result shouldn't be none or an empty string
self.assertNotNoneOrEmpty(result)
def _test_tablespace_delete_script(self, scripter, service):
""" Helper function to test delete script for schemas """
# Set up the mocks
mock_tablespace = Tablespace(None, 'test')
def tablespace_mock_fn(connection):
mock_tablespace._template_root = mock.MagicMock(return_value=Tablespace.TEMPLATE_ROOT)
mock_tablespace._delete_query_data = mock.MagicMock(return_value={"data": {"name": "test"}})
result = mock_tablespace.delete_script(connection)
return result
def scripter_mock_fn():
mock_tablespace.delete_script = mock.MagicMock(return_value=tablespace_mock_fn(self.connection))
return mock_tablespace.delete_script()
scripter.get_tablespace_delete_script = mock.MagicMock(return_value=scripter_mock_fn())
service.script_as_delete = mock.MagicMock(return_value=scripter.get_tablespace_delete_script())
# If I try to get select script for any object
result = service.script_as_delete()
# The result shouldn't be none or an empty string
self.assertNotNoneOrEmpty(result)
# UPDATE SCRIPTS ##############################################################
def _test_schema_update_script(self, scripter, service):
@ -481,6 +539,30 @@ class TestScriptingService(unittest.TestCase):
# The result shouldn't be none or an empty string
self.assertNotNoneOrEmpty(result)
def _test_tablespace_update_script(self, scripter, service):
""" Helper function to test update script for schemas """
# Set up the mocks
mock_tablespace = Tablespace(None, 'test')
def tablespace_mock_fn(connection):
mock_tablespace._template_root = mock.MagicMock(return_value=Tablespace.TEMPLATE_ROOT)
mock_tablespace._update_query_data = mock.MagicMock(return_value={"data": {"name": "test"}, "o_data": {"name": "test"}})
result = mock_tablespace.update_script(connection)
return result
def scripter_mock_fn():
mock_tablespace.update_script = mock.MagicMock(return_value=tablespace_mock_fn(self.connection))
return mock_tablespace.update_script()
scripter.get_tablespace_update_script = mock.MagicMock(return_value=scripter_mock_fn())
service.script_as_update = mock.MagicMock(return_value=scripter.get_tablespace_update_script())
# If I try to get select script for any object
result = service.script_as_update()
# The result shouldn't be none or an empty string
self.assertNotNoneOrEmpty(result)
if __name__ == '__main__':
unittest.main()