Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Add support for operator classes.

 * docs/index.rst: Add operclass.rst.
 * docs/operclass.rst: New page to document operator classes.
 * pyrseas/database.py: (Database.Dicts.__init__): Add dictionary for
   operator classes.  (Database.link_refs): Add operator classes
   argument to schemas.link_refs call.  (Database.diff_map): Call
   operclasses.diff_map and operclasses._drop.
 * pyrseas/dbobject/operclass.py: New module to implement operator
   classes.
 * pyrseas/dbobject/schema.py (Schema.to_map): Process operator
   classes.  (SchemaDict.from_map): Process input operator classes.
   (SchemaDict.link_refs): Add argument for operator classes.
 * tests/dbobject/__init__.py: Invoke operator class tests.
 * tests/dbobject/test_operclass.py: New test module.
 * tests/dbobject/utils.py: (PostgresDb.clear): Drop existing operator
   classes.
  • Loading branch information...
commit 5f22bca1e6e8cb9b857201570a69a3e8bbf4ec6f 1 parent 39ccee1
@jmafc jmafc authored
View
1  docs/index.rst
@@ -52,6 +52,7 @@ classes and methods are documented mainly for developer use.
function
operator
operfamily
+ operclass
type
table
column
View
38 docs/operclass.rst
@@ -0,0 +1,38 @@
+Operator Classes
+================
+
+.. module:: pyrseas.dbobject.operclass
+
+The :mod:`operclass` module defines two classes: class
+:class:`OperatorClass` derived from :class:`DbSchemaObject` and class
+:class:`OperatorClassDict` derived from :class:`DbObjectDict`.
+
+Operator Class
+--------------
+
+:class:`OperatorClass` is derived from :class:`DbSchemaObject` and
+represents a `PostgreSQL operator class
+<http://www.postgresql.org/docs/current/static/sql-createopclass.html>`_.
+
+.. autoclass:: OperatorClass
+
+.. automethod:: OperatorClass.extern_key
+
+.. automethod:: OperatorClass.identifier
+
+.. automethod:: OperatorClass.to_map
+
+.. automethod:: OperatorClass.create
+
+Operator Class Dictionary
+-------------------------
+
+:class:`OperatorClassDict` is derived from
+:class:`~pyrseas.dbobject.DbObjectDict`. It is a dictionary that
+represents the collection of operator classes in a database.
+
+.. autoclass:: OperatorClassDict
+
+.. automethod:: OperatorClassDict.from_map
+
+.. automethod:: OperatorClassDict.diff_map
View
6 pyrseas/database.py
@@ -20,6 +20,7 @@
from pyrseas.dbobject.index import IndexDict
from pyrseas.dbobject.function import ProcDict
from pyrseas.dbobject.operator import OperatorDict
+from pyrseas.dbobject.operclass import OperatorClassDict
from pyrseas.dbobject.operfamily import OperatorFamilyDict
from pyrseas.dbobject.rule import RuleDict
from pyrseas.dbobject.trigger import TriggerDict
@@ -57,6 +58,7 @@ def __init__(self, dbconn=None):
self.indexes = IndexDict(dbconn)
self.functions = ProcDict(dbconn)
self.operators = OperatorDict(dbconn)
+ self.operclasses = OperatorClassDict(dbconn)
self.operfams = OperatorFamilyDict(dbconn)
self.rules = RuleDict(dbconn)
self.triggers = TriggerDict(dbconn)
@@ -74,7 +76,7 @@ def _link_refs(self, db):
"""Link related objects"""
db.languages.link_refs(db.functions)
db.schemas.link_refs(db.types, db.tables, db.functions, db.operators,
- db.operfams, db.conversions)
+ db.operfams, db.operclasses, db.conversions)
db.tables.link_refs(db.columns, db.constraints, db.indexes,
db.rules, db.triggers)
db.types.link_refs(db.columns, db.constraints, db.functions)
@@ -153,6 +155,7 @@ def diff_map(self, input_map):
stmts.append(self.db.functions.diff_map(self.ndb.functions))
stmts.append(self.db.operators.diff_map(self.ndb.operators))
stmts.append(self.db.operfams.diff_map(self.ndb.operfams))
+ stmts.append(self.db.operclasses.diff_map(self.ndb.operclasses))
stmts.append(self.db.tables.diff_map(self.ndb.tables))
stmts.append(self.db.constraints.diff_map(self.ndb.constraints))
stmts.append(self.db.indexes.diff_map(self.ndb.indexes))
@@ -162,6 +165,7 @@ def diff_map(self, input_map):
stmts.append(self.db.conversions.diff_map(self.ndb.conversions))
stmts.append(self.db.casts.diff_map(self.ndb.casts))
stmts.append(self.db.operators._drop())
+ stmts.append(self.db.operclasses._drop())
stmts.append(self.db.operfams._drop())
stmts.append(self.db.functions._drop())
stmts.append(self.db.types._drop())
View
197 pyrseas/dbobject/operclass.py
@@ -0,0 +1,197 @@
+# -*- coding: utf-8 -*-
+"""
+ pyrseas.operclass
+ ~~~~~~~~~~~~~~~~~
+
+ This module defines two classes: OperatorClass derived from
+ DbSchemaObject and OperatorClassDict derived from DbObjectDict.
+"""
+from pyrseas.dbobject import DbObjectDict, DbSchemaObject, quote_id
+
+
+class OperatorClass(DbSchemaObject):
+ """An operator class"""
+
+ objtype = "OPERATOR CLASS"
+
+ keylist = ['schema', 'name', 'index_method']
+
+ def extern_key(self):
+ """Return the key to be used in external maps for this operator
+
+ :return: string
+ """
+ return '%s %s using %s' % (self.objtype.lower(), self.name,
+ self.index_method)
+
+ def identifier(self):
+ """Return a full identifier for an operator class
+
+ :return: string
+ """
+ return "%s USING %s" % (self.qualname(), self.index_method)
+
+ def to_map(self):
+ """Convert operator class to a YAML-suitable format
+
+ :return: dictionary
+ """
+ dct = self.__dict__.copy()
+ for k in self.keylist:
+ del dct[k]
+ if self.name == self.family:
+ del dct['family']
+ return {self.extern_key(): dct}
+
+ def create(self):
+ """Return SQL statements to CREATE the operator class
+
+ :return: SQL statements
+ """
+ stmts = []
+ dflt = ''
+ if hasattr(self, 'default') and self.default:
+ dflt = "DEFAULT %s" % self.default
+ clauses = []
+ for (strat, oper) in self.operators.items():
+ clauses.append("OPERATOR %d %s" % (strat, oper))
+ for (supp, func) in self.functions.items():
+ clauses.append("FUNCTION %d %s" % (supp, func))
+ if hasattr(self, 'storage'):
+ clauses.append("STORAGE %s" % self.storage)
+ stmts.append("CREATE OPERATOR CLASS %s\n %sFOR TYPE %s USING %s "
+ "AS\n %s" % (
+ self.qualname(), dflt, self.type, self.index_method,
+ ',\n ' .join(clauses)))
+ if hasattr(self, 'description'):
+ stmts.append(self.comment())
+ return stmts
+
+
+class OperatorClassDict(DbObjectDict):
+ "The collection of operator classes in a database"
+
+ cls = OperatorClass
+ query = \
+ """SELECT nspname AS schema, opcname AS name,
+ amname AS index_method, opfname AS family,
+ opcintype::regtype AS type, opcdefault AS default,
+ opckeytype::regtype AS storage, description
+ FROM pg_opclass o JOIN pg_am a ON (opcmethod = a.oid)
+ JOIN pg_opfamily f ON (opcfamily = f.oid)
+ JOIN pg_namespace n ON (opcnamespace = n.oid)
+ LEFT JOIN pg_description d
+ ON (o.oid = d.objoid AND d.objsubid = 0)
+ WHERE (nspname != 'pg_catalog' AND nspname != 'information_schema')
+ ORDER BY nspname, opcname, amname"""
+
+ opquery = \
+ """SELECT nspname AS schema, opcname AS name, amname AS index_method,
+ amopstrategy AS strategy, amopopr::regoperator AS operator
+ FROM pg_opclass o JOIN pg_am a ON (opcmethod = a.oid)
+ JOIN pg_namespace n ON (opcnamespace = n.oid), pg_amop ao,
+ pg_depend
+ WHERE refclassid = 'pg_opclass'::regclass
+ AND classid = 'pg_amop'::regclass AND objid = ao.oid
+ AND refobjid = o.oid
+ AND (nspname != 'pg_catalog' AND nspname != 'information_schema')
+ ORDER BY nspname, opcname, amname, amopstrategy"""
+
+ prquery = \
+ """SELECT nspname AS schema, opcname AS name, amname AS index_method,
+ amprocnum AS support, amproc::regprocedure AS function
+ FROM pg_opclass o JOIN pg_am a ON (opcmethod = a.oid)
+ JOIN pg_namespace n ON (opcnamespace = n.oid), pg_amproc ap,
+ pg_depend
+ WHERE refclassid = 'pg_opclass'::regclass
+ AND classid = 'pg_amproc'::regclass AND objid = ap.oid
+ AND refobjid = o.oid
+ AND (nspname != 'pg_catalog' AND nspname != 'information_schema')
+ ORDER BY nspname, opcname, amname, amprocnum"""
+
+ def _from_catalog(self):
+ """Initialize the dictionary of operator classes from the catalogs"""
+ for opclass in self.fetch():
+ if opclass.storage == '-':
+ del opclass.storage
+ self[opclass.key()] = OperatorClass(**opclass.__dict__)
+ for (sch, opc, idx, strat, oper) in self.dbconn.fetchall(self.opquery):
+ opcls = self[(sch, opc, idx)]
+ if not hasattr(opcls, 'operators'):
+ opcls.operators = {}
+ opcls.operators.update({strat: oper})
+ for (sch, opc, idx, supp, func) in self.dbconn.fetchall(self.prquery):
+ opcls = self[(sch, opc, idx)]
+ if not hasattr(opcls, 'functions'):
+ opcls.functions = {}
+ opcls.functions.update({supp: func})
+
+ def from_map(self, schema, inopcls):
+ """Initalize the dictionary of operator classes from the input map
+
+ :param schema: schema owning the operator classes
+ :param inopcls: YAML map defining the operator classes
+ """
+ for key in inopcls.keys():
+ if not key.startswith('operator class ') or not ' using ' in key:
+ raise KeyError("Unrecognized object type: %s" % key)
+ pos = key.rfind(' using ')
+ opc = key[15:pos] # 15 = len('operator class ')
+ idx = key[pos + 7:] # 7 = len(' using ')
+ inopcl = inopcls[key]
+ self[(schema.name, opc, idx)] = opclass = OperatorClass(
+ schema=schema.name, name=opc, index_method=idx)
+ if not inopcl:
+ raise ValueError("Operator '%s' has no specification" % opc)
+ for attr, val in inopcl.items():
+ setattr(opclass, attr, val)
+ if 'oldname' in inopcl:
+ opclass.oldname = inopcl['oldname']
+ if 'description' in inopcl:
+ opclass.description = inopcl['description']
+
+ def diff_map(self, inopcls):
+ """Generate SQL to transform existing operator classes
+
+ :param inopcls: a YAML map defining the new operator classes
+ :return: list of SQL statements
+
+ Compares the existing operator class definitions, as fetched
+ from the catalogs, to the input map and generates SQL
+ statements to transform the operator classes accordingly.
+ """
+ stmts = []
+ # check input operator classes
+ for (sch, opc, idx) in inopcls.keys():
+ inoper = inopcls[(sch, opc, idx)]
+ # does it exist in the database?
+ if (sch, opc, idx) not in self:
+ if not hasattr(inoper, 'oldname'):
+ # create new operator
+ stmts.append(inoper.create())
+ else:
+ stmts.append(self[(sch, opc, idx)].rename(inoper))
+ else:
+ # check operator objects
+ stmts.append(self[(sch, opc, idx)].diff_map(inoper))
+
+ # check existing operators
+ for (sch, opc, idx) in self.keys():
+ oper = self[(sch, opc, idx)]
+ # if missing, mark it for dropping
+ if (sch, opc, idx) not in inopcls:
+ oper.dropped = False
+
+ return stmts
+
+ def _drop(self):
+ """Actually drop the operator classes
+
+ :return: SQL statements
+ """
+ stmts = []
+ for (sch, opc, idx) in self.keys():
+ oper = self[(sch, opc, idx)]
+ if hasattr(oper, 'dropped'):
+ stmts.append(oper.drop())
+ return stmts
View
19 pyrseas/dbobject/schema.py
@@ -62,6 +62,11 @@ def to_map(self, dbschemas):
for oper in self.operators.keys():
operators.update(self.operators[oper].to_map())
schema[key].update(operators)
+ if hasattr(self, 'operclasses'):
+ operclasses = {}
+ for opf in self.operclasses.keys():
+ operclasses.update(self.operclasses[opf].to_map())
+ schema[key].update(operclasses)
if hasattr(self, 'operfams'):
operfams = {}
for opf in self.operfams.keys():
@@ -144,6 +149,7 @@ def from_map(self, inmap, newdb):
intables = {}
infuncs = {}
inopers = {}
+ inopcls = {}
inopfams = {}
inconvs = {}
for key in inschema.keys():
@@ -159,6 +165,8 @@ def from_map(self, inmap, newdb):
infuncs.update({key: inschema[key]})
elif key.startswith('operator family'):
inopfams.update({key: inschema[key]})
+ elif key.startswith('operator class'):
+ inopcls.update({key: inschema[key]})
elif key.startswith('operator '):
inopers.update({key: inschema[key]})
elif key.startswith('conversion '):
@@ -174,11 +182,12 @@ def from_map(self, inmap, newdb):
newdb.tables.from_map(schema, intables, newdb)
newdb.functions.from_map(schema, infuncs)
newdb.operators.from_map(schema, inopers)
+ newdb.operclasses.from_map(schema, inopcls)
newdb.operfams.from_map(schema, inopfams)
newdb.conversions.from_map(schema, inconvs)
def link_refs(self, dbtypes, dbtables, dbfunctions, dbopers, dbopfams,
- dbconvs):
+ dbopcls, dbconvs):
"""Connect types, tables and functions to their respective schemas
:param dbtypes: dictionary of types and domains
@@ -186,6 +195,7 @@ def link_refs(self, dbtypes, dbtables, dbfunctions, dbopers, dbopfams,
:param dbfunctions: dictionary of functions
:param dbopers: dictionary of operators
:param dbopfams: dictionary of operator families
+ :param dbopcls: dictionary of operator classes
:param dbconvs: dictionary of conversions
Fills in the `domains` dictionary for each schema by
@@ -249,6 +259,13 @@ def link_refs(self, dbtypes, dbtables, dbfunctions, dbopers, dbopfams,
if not hasattr(schema, 'operators'):
schema.operators = {}
schema.operators.update({(opr, lft, rgt): oper})
+ for (sch, opc, idx) in dbopcls.keys():
+ opcl = dbopcls[(sch, opc, idx)]
+ assert self[sch]
+ schema = self[sch]
+ if not hasattr(schema, 'operclasses'):
+ schema.operclasses = {}
+ schema.operclasses.update({(opc, idx): opcl})
for (sch, opf, idx) in dbopfams.keys():
opfam = dbopfams[(sch, opf, idx)]
assert self[sch]
View
2  tests/dbobject/__init__.py
@@ -16,6 +16,7 @@
import test_function
import test_operator
import test_operfamily
+import test_operclass
import test_trigger
import test_rule
import test_conversion
@@ -36,6 +37,7 @@ def suite():
tests.addTest(test_function.suite())
tests.addTest(test_operator.suite())
tests.addTest(test_operfamily.suite())
+ tests.addTest(test_operclass.suite())
tests.addTest(test_trigger.suite())
tests.addTest(test_rule.suite())
tests.addTest(test_conversion.suite())
View
139 tests/dbobject/test_operclass.py
@@ -0,0 +1,139 @@
+# -*- coding: utf-8 -*-
+"""Test operator classes"""
+
+import unittest
+
+from utils import PyrseasTestCase, fix_indent, new_std_map
+
+CREATE_STMT = "CREATE OPERATOR CLASS oc1 FOR TYPE integer USING btree " \
+ "AS OPERATOR 1 <, OPERATOR 3 =, FUNCTION 1 btint4cmp(integer,integer)"
+CREATE_STMT_LONG = "CREATE OPERATOR CLASS oc1 FOR TYPE integer USING btree " \
+ "AS OPERATOR 1 <(integer,integer), OPERATOR 3 =(integer,integer), " \
+ "FUNCTION 1 btint4cmp(integer,integer)"
+DROP_STMT = "DROP OPERATOR CLASS IF EXISTS oc1 USING btree"
+COMMENT_STMT = "COMMENT ON OPERATOR CLASS oc1 USING btree IS " \
+ "'Test operator class oc1'"
+
+
+class OperatorClassToMapTestCase(PyrseasTestCase):
+ """Test mapping of existing operator classes"""
+
+ def test_map_operclass(self):
+ "Map an operator class"
+ expmap = {'type': 'integer', 'operators': {
+ 1: '<(integer,integer)', 3: '=(integer,integer)'},
+ 'functions': {1: 'btint4cmp(integer,integer)'}}
+ dbmap = self.db.execute_and_map(CREATE_STMT)
+ self.assertEqual(dbmap['schema public'][
+ 'operator class oc1 using btree'], expmap)
+
+ def test_map_operclass_comment(self):
+ "Map an operator class comment"
+ self.db.execute(CREATE_STMT)
+ dbmap = self.db.execute_and_map(COMMENT_STMT)
+ self.assertEqual(dbmap['schema public']
+ ['operator class oc1 using btree']['description'],
+ 'Test operator class oc1')
+
+
+class OperatorClassToSqlTestCase(PyrseasTestCase):
+ """Test SQL generation from input operators"""
+
+ def test_create_operclass(self):
+ "Create an operator class"
+ inmap = new_std_map()
+ inmap['schema public'].update({'operator class oc1 using btree': {
+ 'type': 'integer', 'operators': {
+ 1: '<(integer,integer)', 3: '=(integer,integer)'},
+ 'functions': {1: 'btint4cmp(integer,integer)'}}})
+ dbsql = self.db.process_map(inmap)
+ self.assertEqual(fix_indent(dbsql[0]), CREATE_STMT_LONG)
+
+ def test_create_operclass_in_schema(self):
+ "Create a operator within a non-public schema"
+ self.db.execute("CREATE SCHEMA s1")
+ inmap = new_std_map()
+ inmap.update({'schema s1': {'operator class oc1 using btree': {
+ 'type': 'integer', 'operators': {
+ 1: '<(integer,integer)', 3: '=(integer,integer)'},
+ 'functions': {1: 'btint4cmp(integer,integer)'}}}})
+ dbsql = self.db.process_map(inmap)
+ self.assertEqual(fix_indent(dbsql[1]), "CREATE OPERATOR CLASS s1.oc1 "
+ "FOR TYPE integer USING btree AS "
+ "OPERATOR 1 <(integer,integer), "
+ "OPERATOR 3 =(integer,integer), "
+ "FUNCTION 1 btint4cmp(integer,integer)")
+ self.db.execute_commit("DROP SCHEMA s1 CASCADE")
+
+ def test_drop_operclass(self):
+ "Drop an existing operator"
+ self.db.execute_commit(CREATE_STMT)
+ dbsql = self.db.process_map(new_std_map())
+ self.assertEqual(dbsql, ["DROP OPERATOR CLASS oc1 USING btree",
+ "DROP OPERATOR FAMILY oc1 USING btree"])
+
+ def test_operclass_with_comment(self):
+ "Create an operator class with a comment"
+ inmap = new_std_map()
+ inmap['schema public'].update({'operator class oc1 using btree': {
+ 'description': 'Test operator class oc1',
+ 'type': 'integer', 'operators': {
+ 1: '<(integer,integer)', 3: '=(integer,integer)'},
+ 'functions': {1: 'btint4cmp(integer,integer)'}}})
+ dbsql = self.db.process_map(inmap)
+ self.assertEqual(fix_indent(dbsql[0]), CREATE_STMT_LONG)
+ self.assertEqual(dbsql[1], COMMENT_STMT)
+
+ def test_comment_on_operclass(self):
+ "Create a comment for an existing operator class"
+ self.db.execute_commit(CREATE_STMT)
+ inmap = new_std_map()
+ inmap['schema public'].update({'operator class oc1 using btree': {
+ 'description': 'Test operator class oc1',
+ 'type': 'integer', 'operators': {
+ 1: '<(integer,integer)', 3: '=(integer,integer)'},
+ 'functions': {1: 'btint4cmp(integer,integer)'}},
+ 'operator family oc1 using btree': {}})
+ dbsql = self.db.process_map(inmap)
+ self.assertEqual(dbsql, [COMMENT_STMT])
+
+ def test_drop_operclass_comment(self):
+ "Drop the existing comment on an operator class"
+ self.db.execute(CREATE_STMT)
+ self.db.execute_commit(COMMENT_STMT)
+ inmap = new_std_map()
+ inmap['schema public'].update({'operator class oc1 using btree': {
+ 'type': 'integer', 'operators': {
+ 1: '<(integer,integer)', 3: '=(integer,integer)'},
+ 'functions': {1: 'btint4cmp(integer,integer)'}},
+ 'operator family oc1 using btree': {}})
+ dbsql = self.db.process_map(inmap)
+ self.assertEqual(dbsql, [
+ "COMMENT ON OPERATOR CLASS oc1 USING btree IS NULL"])
+
+ def test_change_operclass_comment(self):
+ "Change existing comment on an operator class"
+ self.db.execute(CREATE_STMT)
+ self.db.execute_commit(COMMENT_STMT)
+ inmap = new_std_map()
+ inmap['schema public'].update({'operator class oc1 using btree': {
+ 'description': 'Changed operator class oc1',
+ 'type': 'integer', 'operators': {
+ 1: '<(integer,integer)', 3: '=(integer,integer)'},
+ 'functions': {1: 'btint4cmp(integer,integer)'}},
+ 'operator family oc1 using btree': {}})
+ dbsql = self.db.process_map(inmap)
+ self.assertEqual(dbsql, [
+ "COMMENT ON OPERATOR CLASS oc1 USING btree IS "
+ "'Changed operator class oc1'"])
+
+
+def suite():
+ tests = unittest.TestLoader().loadTestsFromTestCase(
+ OperatorClassToMapTestCase)
+ tests.addTest(unittest.TestLoader().loadTestsFromTestCase(
+ OperatorClassToSqlTestCase))
+ return tests
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='suite')
View
17 tests/dbobject/utils.py
@@ -222,6 +222,23 @@ def clear(self):
opfam[0], opfam[1], opfam[2]))
self.conn.commit()
+ # Operator classes
+ curs = pgexecute(
+ self.conn,
+ """SELECT nspname, opcname, amname
+ FROM pg_opclass o JOIN pg_am a ON (opcmethod = a.oid)
+ JOIN pg_namespace n ON (opcnamespace = n.oid)
+ WHERE (nspname != 'pg_catalog'
+ AND nspname != 'information_schema')""")
+ opcls = curs.fetchall()
+ curs.close()
+ self.conn.rollback()
+ for opcl in opcls:
+ self.execute(
+ "DROP OPERATOR CLASS IF EXISTS %s.%s USING %s CASCADE" % (
+ opcl[0], opcl[1], opcl[2]))
+ self.conn.commit()
+
# Conversions
curs = pgexecute(
self.conn,
Please sign in to comment.
Something went wrong with that request. Please try again.