Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

deploy: add support for managing SQL views #33

Draft
wants to merge 1 commit into
base: baseline/master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions src/rex.deploy/src/rex/deploy/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from .sql import (sql_create_schema, sql_drop_schema, sql_rename_schema,
sql_create_extension, sql_drop_extension, sql_comment_on_schema,
sql_create_table, sql_drop_table, sql_rename_table,
sql_create_view, sql_drop_view, sql_rename_view, sql_comment_on_view,
sql_comment_on_table, sql_define_column, sql_add_column,
sql_drop_column, sql_rename_column, sql_copy_column,
sql_set_column_type, sql_set_column_not_null, sql_set_column_default,
Expand Down Expand Up @@ -329,6 +330,10 @@ def add_table(self, name, is_unlogged=False):
"""Adds a table."""
return TableImage(self, name, is_unlogged=is_unlogged)

def add_view(self, name, definition):
"""Adds a view."""
return ViewImage(self, name, definition)

def add_index(self, name, table, columns):
"""Adds an index."""
return IndexImage(self, name, table, columns)
Expand Down Expand Up @@ -370,6 +375,14 @@ def create_table(self, name, definitions, is_unlogged=False):
table.add_column(column_name, type, is_not_null, default)
return table

def create_view(self, name, definition):
"""Creates a view with the given definition."""
qname = (self.name, name)
sql = sql_create_view(qname, definition)
self.cursor.execute(sql)
view = self.add_view(name, definition=definition)
return view

def create_index(self, name, table, columns):
"""Creates an index."""
column_names = [column.name for column in columns]
Expand Down Expand Up @@ -880,6 +893,48 @@ def drop(self):
self.remove()


class ViewImage(NamespacedImage):
"""Database view."""

__slots__ = ('comment', 'definition')

def __init__(self, schema, name, definition):
super(NamespacedImage, self).__init__(schema, name)
self.place(schema.tables)
schema.link(self)
#: View comment.
self.comment = None
#: View definition.
self.definition = definition

def set_comment(self, comment):
"""Sets the comment."""
self.comment = comment
return self

def alter_name(self, name):
"""Renames the view."""
if self.name == name:
return self
sql = sql_rename_view(self.qname, name)
self.cursor.execute(sql)
return self.set_name(name)

def alter_comment(self, comment):
"""Updates the view comment."""
if self.comment == comment:
return self
sql = sql_comment_on_view(self.qname, comment)
self.cursor.execute(sql)
return self.set_comment(comment)

def drop(self):
"""Drops the view."""
sql = sql_drop_view(self.qname)
self.cursor.execute(sql)
self.remove()


class ColumnImage(NamedImage):
"""Database column."""

Expand Down
17 changes: 16 additions & 1 deletion src/rex.deploy/src/rex/deploy/introspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def introspect(cursor):
cursor.execute("""
SELECT c.oid, c.relnamespace, c.relname, c.relpersistence
FROM pg_catalog.pg_class c
WHERE c.relkind IN ('r', 'v') AND
WHERE c.relkind IN ('r') AND
HAS_TABLE_PRIVILEGE(c.oid, 'SELECT')
ORDER BY c.relnamespace, c.relname
""")
Expand All @@ -103,12 +103,27 @@ def introspect(cursor):
table = schema.add_table(relname, is_unlogged=is_unlogged)
class_by_oid[oid] = table_by_oid[oid] = table

# Extract views.
cursor.execute("""
SELECT c.oid, c.relnamespace, c.relname, pg_get_viewdef(c.oid)
FROM pg_catalog.pg_class c
WHERE c.relkind IN ('v') AND
HAS_TABLE_PRIVILEGE(c.oid, 'SELECT')
ORDER BY c.relnamespace, c.relname
""")
for oid, relnamespace, relname, definition in cursor.fetchall():
schema = schema_by_oid[relnamespace]
table = schema.add_view(relname, definition)
class_by_oid[oid] = table_by_oid[oid] = table

# Extract columns.
column_by_num = {}
cursor.execute("""
SELECT a.attrelid, a.attnum, a.attname, a.atttypid, a.atttypmod,
a.attnotnull, a.atthasdef, a.attisdropped
FROM pg_catalog.pg_attribute a
JOIN pg_catalog.pg_class c ON a.attrelid = c.oid
WHERE c.relkind IN ('r')
ORDER BY a.attrelid, a.attnum
""")
for (attrelid, attnum, attname, atttypid,
Expand Down
15 changes: 14 additions & 1 deletion src/rex.deploy/src/rex/deploy/meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
Location, set_location, UStrVal, UChoiceVal, MaybeVal, SeqVal,
RecordVal, Error)
from .fact import LabelVal, TitleVal, AliasVal, AliasSpec, FactDumper
from .image import TableImage, ColumnImage, UniqueKeyImage
from .image import TableImage, ViewImage, ColumnImage, UniqueKeyImage
import operator
import collections
import yaml
Expand Down Expand Up @@ -133,6 +133,17 @@ class TableMeta(Meta):
]


class ViewMeta(Meta):
"""View metadata."""

__slots__ = ()

fields = [
('label', LabelVal, None),
('title', TitleVal, None),
]


class ColumnMeta(Meta):
"""Column metadata."""

Expand Down Expand Up @@ -162,6 +173,8 @@ def uncomment(image):
"""
if isinstance(image, TableImage):
return TableMeta.parse(image.comment)
if isinstance(image, ViewImage):
return ViewMeta.parse(image.comment)
elif isinstance(image, ColumnImage):
return ColumnMeta.parse(image.comment)
elif isinstance(image, UniqueKeyImage) and image.is_primary:
Expand Down
103 changes: 102 additions & 1 deletion src/rex.deploy/src/rex/deploy/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
plpgsql_primary_key_procedure, plpgsql_integer_random_key,
plpgsql_text_random_key, plpgsql_integer_offset_key,
plpgsql_text_offset_key, plpgsql_text_uuid_key)
from .image import (TableImage, ColumnImage, UniqueKeyImage, CASCADE,
from .image import (TableImage, ColumnImage, ViewImage, UniqueKeyImage, CASCADE,
SET_DEFAULT, BEFORE, AFTER, INSERT, INSERT_UPDATE_DELETE)
from .cluster import Cluster, get_cluster
import datetime
Expand Down Expand Up @@ -257,12 +257,24 @@ def table(self, label):
"""
return TableModel.find(self, label)

def view(self, label):
"""
Finds the view by name.
"""
return ViewModel.find(self, label)

def build_table(self, **kwds):
"""
Creates a new table.
"""
return TableModel.do_build(self, **kwds)

def build_view(self, **kwds):
"""
Creates a new view.
"""
return ViewModel.do_build(self, **kwds)

def facts(self):
"""
Returns a list of facts that reproduce the schema.
Expand Down Expand Up @@ -334,6 +346,8 @@ def find(cls, schema, label):
# Finds a table by name.
names = cls.names(label)
image = schema.image.tables.get(names.name)
if not isinstance(image, TableImage):
image = None
return schema(image)

@classmethod
Expand Down Expand Up @@ -578,6 +592,93 @@ def fact(self, with_related=False):
related=related)


class ViewModel(Model):
"""
Wraps a database view.
"""

__slots__ = ('label', 'title', 'definition')

is_table = False

properties = ['label', 'title', 'definition']

class names:
# Derives names for database objects and the view title.

__slots__ = ('label', 'title', 'name')

def __init__(self, label):
self.label = label
self.title = label_to_title(label)
self.name = mangle(label)

@classmethod
def recognizes(cls, schema, image):
# Verifies if the database object is a table.
if not isinstance(image, ViewImage):
return False
# We expect the table belongs to the `public` schema.
if image.schema is not schema.image:
return False
return True

@classmethod
def find(cls, schema, label):
# Finds a table by name.
names = cls.names(label)
image = schema.image.tables.get(names.name)
if not isinstance(image, ViewImage):
image = None
return schema(image)

@classmethod
def do_build(cls, schema, label, definition, title=None):
# Builds a view.
names = cls.names(label)
image = schema.image.create_view(names.name, definition)
# Save the label and the title if necessary.
saved_label = label if label != names.name else None
saved_title = title if title != names.title else None
meta = uncomment(image)
if meta.update(label=saved_label, title=saved_title):
image.alter_comment(meta.dump())
return cls(schema, image)

def __init__(self, schema, image):
super(ViewModel, self).__init__(schema, image)
assert isinstance(image, ViewImage)
# Extract entity properties.
meta = uncomment(image)
self.label = meta.label or image.name
self.title = meta.title
self.definition = image.definition

def do_modify(self, label, title, definition):
# Updates the state of the view entity.
# Refresh names.
names = self.names(label)
self.image.alter_name(names.name)
# Update saved label and title.
meta = uncomment(self.image)
saved_label = label if label != names.name else None
saved_title = title if title != names.title else None
if meta.update(label=saved_label, title=saved_title):
self.image.alter_comment(meta.dump())

def do_erase(self):
# Drops the entity.
if self.image:
self.image.drop()

def fact(self):
from .table import ViewFact
return ViewFact(
self.label,
definition=self.image.definition,
title=self.title)


class ColumnModel(Model):
"""
Wraps a table column.
Expand Down
27 changes: 27 additions & 0 deletions src/rex.deploy/src/rex/deploy/sql.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,33 @@ def sql_comment_on_table(qname, text):
COMMENT ON TABLE {{ qname|qn }} IS {{ text|v }};
"""

@sql_template
def sql_create_view(qname, definition):
"""
CREATE VIEW {{ qname|qn }} AS ({{ definition }});
"""


@sql_template
def sql_drop_view(qname):
"""
DROP VIEW {{ qname|qn }};
"""


@sql_template
def sql_rename_view(qname, new_name):
"""
ALTER VIEW {{ qname|qn }} RENAME TO {{ new_name|n }};
"""


@sql_template
def sql_comment_on_view(qname, text):
"""
COMMENT ON VIEW {{ qname|qn }} IS {{ text|v }};
"""


@sql_template
def sql_define_column(name, type_qname, is_not_null, default=None):
Expand Down
Loading