Skip to content

Instantly share code, notes, and snippets.

@eevee
Last active August 29, 2015 14:23
Show Gist options
  • Save eevee/aac4cf69d55acbd18cbf to your computer and use it in GitHub Desktop.
Save eevee/aac4cf69d55acbd18cbf to your computer and use it in GitHub Desktop.
sqlalchemy doesn't like my zany self-join
# Context: I have a self-referential nested set table and I'm trying to write a
# parent/child relationship. I use DISTINCT ON, so this assumes Postgres.
from sqlalchemy import *
from sqlalchemy.ext import compiler
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import foreign, remote
from sqlalchemy.orm import joinedload
from sqlalchemy.orm import joinedload
from sqlalchemy.orm import relationship
from sqlalchemy.orm import sessionmaker
from sqlalchemy.schema import DDLElement
from sqlalchemy.sql import select
from sqlalchemy.sql import table
# Verbatim from https://bitbucket.org/zzzeek/sqlalchemy/wiki/UsageRecipes/Views
class CreateView(DDLElement):
def __init__(self, name, selectable):
self.name = name
self.selectable = selectable
class DropView(DDLElement):
def __init__(self, name):
self.name = name
@compiler.compiles(CreateView)
def compile(element, compiler, **kw):
return "CREATE VIEW %s AS %s" % (element.name, compiler.sql_compiler.process(element.selectable))
@compiler.compiles(DropView)
def compile(element, compiler, **kw):
return "DROP VIEW %s" % (element.name)
def view(name, metadata, selectable):
t = table(name)
for c in selectable.c:
c._make_proxy(t)
CreateView(name, selectable).execute_at('after-create', metadata)
DropView(name).execute_at('before-drop', metadata)
return t
# End copy-paste
Base = declarative_base()
class Node(Base):
__tablename__ = 'node'
id = Column(Integer, primary_key=True, nullable=False)
left = Column(Integer, nullable=False)
right = Column(Integer, nullable=False)
def __repr__(self):
return "<Node {self.id}>".format(self=self)
_node_parent = Node.__table__.alias()
_node_child = Node.__table__.alias()
node_bridge = view(
'node_parentage',
Base.metadata,
select([_node_child.c.id.label('child_id'), _node_parent.c.id.label('parent_id')])
.select_from(
_node_child.join(
_node_parent,
and_(
_node_parent.c.left < _node_child.c.left,
_node_child.c.right < _node_parent.c.right,
),
)
)
.order_by(_node_child.c.id, _node_parent.c.left)
.distinct(_node_child.c.id)
)
Node.parent = relationship(
Node,
secondary=node_bridge,
primaryjoin=Node.id == node_bridge.c.child_id,
secondaryjoin=Node.id == node_bridge.c.parent_id,
viewonly=True,
uselist=False,
backref='children',
)
engine = create_engine('postgresql:///temp', echo=True)
Base.metadata.drop_all(bind=engine)
Base.metadata.create_all(bind=engine)
session = sessionmaker(bind=engine)()
node1 = Node(id=1, left=1, right=4)
node2 = Node(id=2, left=2, right=3)
node3 = Node(id=3, left=5, right=6)
session.add_all([node1, node2, node3])
session.commit()
# Note that the bridge table itself has the correct results -- the only
# parent/child link is that 1 is the parent of 2.
# Output:
# (2, 1)
for row in session.execute(select(node_bridge.c)):
print(row)
# PROBLEM 3
# This query is totally bogus:
# SELECT node.id AS node_id, node."left" AS node_left, node."right" AS node_right
# FROM node, node_parentage AS node_parentage_1
# WHERE node_parentage_1.child_id = node_parentage_1.child_id
# AND %(param_1)s = node_parentage_1.parent_id
# Output:
# <Node 1>
# <Node 2>
# <Node 3>
print('-' * 80)
q = (
session.query(Node)
.filter(Node.parent == node1)
.all()
)
for node in q:
print(node)
session.rollback()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment