Skip to content

Commit

Permalink
Changed over to function usage and started adding function creation c…
Browse files Browse the repository at this point in the history
…ommand zacharyvoase#4
  • Loading branch information
Scott Walton committed May 9, 2013
1 parent 0d34754 commit f5bf416
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 35 deletions.
2 changes: 1 addition & 1 deletion django_postgres/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from django_postgres.view import View
from django_postgres.function import Statement
from django_postgres.function import Function
from django_postgres.bitstrings import BitStringField, BitStringExpression as B, Bits
2 changes: 0 additions & 2 deletions django_postgres/db/sql/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
class NonQuotingQuery(query.Query):
"""Query class that uses the NonQuotingCompiler.
"""
compiler = 'NonQuotingCompiler'

def get_compiler(self, using=None, connection=None):
"""Get the NonQuotingCompiler object.
"""
Expand Down
123 changes: 99 additions & 24 deletions django_postgres/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,84 @@
from django_postgres.db.sql import query


def _split_function_args(function_name):
"""Splits the function name into (name, (arg1type, arg2type))
"""
name, args = function_name.split('(')
name = name.trim()
args = args.trim().replace(')', '').split(',')
return name, tuple(a.trim() for a in args)


def _generate_function(name, args, fields, definition):
"""Generate the SQL for creating the function.
"""
sql = ("CREATE OR REPLACE FUNCTION {name}({args})"
"RETURNS TABLE({fields}) AS"
"{definition}"
"LANGUAGE sql;")

arg_string = ', '.join(args)
field_string = ', '.join(fields)
sql = sql.format(
name=name, args=arg_string, fields=field_string, definition=definition)


def create_function(connection, function_name, function_definition,
update=True, force=False):
"""
Create a named function on a connection.
Returns True if a new function was created (or an existing one updated), or
False if nothing was done.
If ``update`` is True (default), attempt to update an existing function.
If the existing function's definition is incompatible with the new
definition, ``force`` (default: False) controls whether or not to drop the
old view and create the new one.
Beware that setting ``force`` will drop functions with the same name,
irrespective of whether their arguments match.
"""
cursor_wrapper = connection.cursor()
cursor = cursor_wrapper.cursor.cursor

name, args = _split_function_args(function_name)

try:
force_required = False
# Determine if view already exists.
function_query = (
u"SELECT COUNT(*)"
u"FROM pg_catalog.pg_namespace n"
u"JOIN pg_catalog.pg_proc p"
u"ON pronamespace = n.oid"
u"WHERE nspname = 'public' and proname = %s;")
cursor.execute(function_query, [name])
function_exists = cursor.fetchone()[0] > 0
force_required = False

if function_exists and not update:
return 'EXISTS'
elif function_exists:
function_detail_query = (
u"SELECT pronargs"
u"FROM pg_catalog.pg_namespace n"
u"JOIN pg_catalog.pg_proc p"
u"ON pronamespace = n.oid"
u"WHERE nspname = 'public' and proname = %s;")
cursor.execute(function_detail_query, [name])
force_required = cursor.fetchone()[0] != len(args)

if not force_required:
cursor.execute(
_generate_function(name, args, fields, function_definition))
finally:
cursor_wrapper.close()

def _create_model(name, execute, fields=None, app_label='', module='',
options=None):
"""Creates the model that executes the prepared statement to query.
"""Creates the model that executes the function to query.
Most of the settings need to be pre-formatted, as this function will not
check them.
See http://djangosnippets.org/snippets/442/ for more information
Expand Down Expand Up @@ -35,18 +110,18 @@ class Meta:
return type(name, (models.Model,), attrs)


class StatementManager(models.Manager):
"""Adds a prepare() method to the default Manager for Statement. This must
class FunctionManager(models.Manager):
"""Adds a call() method to the default Manager for Function. This must
be called before you can filter the queryset, as it runs the execute
command with the arguments provided.
"""

def prepare(self, args=None):
"""Prepare the statement for filtering by executing it with the
def call(self, args=None):
"""Prepare the function for filtering by executing it with the
arguments passed.
"""
statement_name, statement_args = self.model._meta.db_table.split(u'(')
statement_args = u'(' + statement_args
function_name, function_args = self.model._meta.db_table.split(u'(')
function_args = u'(' + function_args

model_name = self.model.__name__
app_label = self.model._meta.app_label
Expand All @@ -56,37 +131,37 @@ def prepare(self, args=None):
if args:
execute_arguments = ', '.join(unicode(a) for a in args)

execute_statement = u'EXECUTE {name}({args})'.format(
name=statement_name,
execute_function = u'{name}({args})'.format(
name=function_name,
args=execute_arguments)

model = _create_model(
model_name + 'hello', execute_statement, None, app_label, module)
model_name + 'hello', execute_function, None, app_label, module)
return models.query.QuerySet(model, query.NonQuotingQuery(model))

def get_queryset(self):
"""No methods that depend on this can be called until the statement has
been prepared.
"""No methods that depend on this can be called until the function has
been called.
"""
raise exceptions.ObjectDoesNotExist(
u'You must run prepare() before filtering the queryset further')
u'You must run call() before filtering the queryset further')


class Statement(models.Model):
"""Creates Postgres Prepared Statements, which can then be called and
queried from the Django ORM. The default Manager for Statement implements
a prepare() method that must be run for every time you want to prime a
class Function(models.Model):
"""Creates Postgres Prepared Functions, which can then be called and
queried from the Django ORM. The default Manager for Function implements
a call() method that must be run for every time you want to prime a
queryset for execution.
The prepared statement must return a result set with the field names
The called function must return a result set with the field names
matching the fields set in this model definition.
`Meta.db_table` is the name of the prepared statement that will be called.
It should take the form `statement_name (type, type, type)`
The prepared statement definition is stored in the `sql` attribute.
The sql definition is simply the SQL that will be executed by the prepared
statement.
`Meta.db_table` is the name of the function that will be called.
It should take the form `function_name (type, type, type)`
The function definition is stored in the `sql` attribute.
The sql definition is simply the SQL that will be executed by the
function.
"""

objects = StatementManager()
objects = FunctionManager()

class Meta:
abstract = True
Expand Down
4 changes: 2 additions & 2 deletions tests/test_project/functiontest/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
import django_postgres


class UserTypeCounter(django_postgres.Statement):
"""A simple class that tests the prepared statement. Can be called with
class UserTypeCounter(django_postgres.Function):
"""A simple class that tests the function. Can be called with
either True or False as arguments
"""
sql = """SELECT COUNT(*) AS my_count FROM auth_user WHERE
Expand Down
11 changes: 5 additions & 6 deletions tests/test_project/functiontest/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,23 @@
import models


class StatementTestCase(TestCase):
class FunctionTestCase(TestCase):

def test_get_counter(self):
"""Must run prepare on the manager to prepare the statement to be
executed.
"""Must run call on the manager before querying the result.
"""
foo_user = auth.models.User.objects.create(
username='foo', is_superuser=True)
foo_user.set_password('blah')
foo_user.save()

foo_superuser = models.UserTypeCounter.objects.prepare(
foo_superuser = models.UserTypeCounter.objects.call(
(True, ))

self.assertEqual(foo_superuser.get().my_count, 1)

def test_unprepared(self):
"""Cannot execute the statement unless you explicitly prepare it first
def test_uncalled(self):
"""Cannot execute the statement unless you explicitly call it first
"""
foo_user = auth.models.User.objects.create(
username='foo', is_superuser=True)
Expand Down

0 comments on commit f5bf416

Please sign in to comment.