On 06/13/2016 01:15 PM, Drachenfels wrote:
Hi guys,

I found very strange behaviour of my script, after a lot of debugging it
appears that SQLAlchemy complains about joins that I cannot see exactly
why it has any issue with.

Full running example is below in both as inline main.py script and link.

In the nutshell, hierarchy of objects is as follows.

Article has Video, Video has Tag Series, Tag has Slug field
Article has as well Tags as m2m, each tag has Slug field.

What is wrong?

When I do

Article.join(ArticleTag).join(Tag).join(SlugTag).join(Slug).join(Video).join(Tag[Series]).join(SlugTag).join(Slug)

I will get error that SQLAlchemy cannot join Video as it's ambiguous join.

that's not totally surprising? that's an enormous chain of joins, just specify the ON clause for those joins rather than making it guess.


When I Do

Article.join(Video).join(Tag[Series]).join(SlugTag).join(Slug).join(ArticleTag).join(Tag).join(SlugTag).join(Slug).

All works like a charm.

Different solution is to specify that when joining Video, onclause
should be Article.video_uid == Video.uid.

In general Article can have only one Video and nothing else can do it.
So I wonder where SQLAlchemy finds this ambiguity. Is it a bug?

It should be telling you where the ambiguity is.

Just ran your join and we get:

sqlalchemy.exc.InvalidRequestError: Could not find a FROM clause to join from. Tried joining to <class '__main__.Video'>, but got: Can't determine join between 'Join object on Join object on Join object on Join object on article(140307104294928) and article_tag(140307104293200)(140307104296656) and tag(140307104695632)(140307104296720) and slug_tag(140307105025936)(140307104185104) and slug(140307105024208)' and 'video'; tables have more than one foreign key constraint relationship between them. Please specify the 'onclause' of this join explicitly.

Article has a foriegn key to Video and Video has a foreign key to Tag so these can be joined either way.

If perhaps you're expecting this to be unambiguous because Tag has no relationship() to Video, relationship() is not used when you use query.join() without an ON clause. It only uses foreign keys in this mode. You would need to specify an ON clause either as a string relationship name (e.g. "videos") or as the attribute e.g. Article.videos etc.


Keep in mind this code is a small piece of bigger picture, so you might
be wondering why I have this aliasing feature. It's wrapper, that
collects all filters/joins and then compiles them removing for example
duplicated joins). I removed most of it but there might be an artefact
or two.

I haven't considered your code sample with all the right/left join manipulation stuff, your simple example above is explained by the documented behavior of Query.join. If you want joins to work with relationships alone you should consider those using inspect(Class).relationships to find the relationships you'd like to use for each linkage.





Link:

http://sprunge.us/KBeM

Inline code:

# encoding: utf8

from __future__ import unicode_literals

import sys

import sqlalchemy
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()


class Slug(Base):
    __tablename__ = 'slug'

    id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)

    slug = sqlalchemy.Column(sqlalchemy.Unicode(255), nullable=False)
    expiration_date = sqlalchemy.Column(
        sqlalchemy.DateTime(timezone=True), default=None)


class SlugTag(Base):
    __tablename__ = 'slug_tag'

    slug = sqlalchemy.orm.relationship(
        'Slug', primaryjoin='SlugTag.slug_id == Slug.id',
        cascade="all, delete-orphan", uselist=False, single_parent=True)
    obj = sqlalchemy.orm.relationship(
        'Tag', primaryjoin='SlugTag.obj_uid == Tag.uid',
        uselist=False)

    slug_id = sqlalchemy.Column(
        sqlalchemy.Integer,
        sqlalchemy.ForeignKey('slug.id'), nullable=False)
    obj_uid = sqlalchemy.Column(
        sqlalchemy.Unicode(32),
        sqlalchemy.ForeignKey('tag.uid'), nullable=False)

    __table_args__ = (
        sqlalchemy.PrimaryKeyConstraint(
            'slug_id', 'obj_uid', name='slug_tag_pk'),
    )


class Tag(Base):
    __tablename__ = 'tag'

    uid = sqlalchemy.Column(
        sqlalchemy.Unicode(32), primary_key=True, nullable=False)
    title = sqlalchemy.Column(sqlalchemy.Unicode(255), nullable=False)
    type = sqlalchemy.Column(sqlalchemy.Unicode(255))

    slugs_connector = sqlalchemy.orm.relationship(
        'SlugTag', primaryjoin='Tag.uid == SlugTag.obj_uid')

    __table_args__ = (
        sqlalchemy.UniqueConstraint('type', 'title',
name='title_type_unique'),
    )


class Video(Base):
    __tablename__ = 'video'

    SERIES_TYPE = 'series'

    uid = sqlalchemy.Column(
        sqlalchemy.Unicode(32), primary_key=True, nullable=False)
    title = sqlalchemy.Column(sqlalchemy.Unicode(255), nullable=False)
    body = sqlalchemy.Column(sqlalchemy.Unicode)
    duration = sqlalchemy.Column(sqlalchemy.Time)

    series_uid = sqlalchemy.Column(
        sqlalchemy.Unicode(32), sqlalchemy.ForeignKey('tag.uid'),
index=True,
        nullable=False)

    series = sqlalchemy.orm.relationship(
        'Tag', primaryjoin='Video.series_uid == Tag.uid', uselist=False)


class ArticleTag(Base):
    """Represent m2m table between Tags and Article.

    association obj for mapping m2m articles<->tags
    """
    __tablename__ = 'article_tag'

    article_uid = sqlalchemy.Column(
        sqlalchemy.Unicode(32), sqlalchemy.ForeignKey('article.uid'),
        primary_key=True)
    tag_uid = sqlalchemy.Column(
        sqlalchemy.Unicode(32), sqlalchemy.ForeignKey('tag.uid'),
        primary_key=True)

    tag = sqlalchemy.orm.relationship('Tag', backref='articles')


class Article(Base):
    __tablename__ = 'article'

    uid = sqlalchemy.Column(
        sqlalchemy.Unicode(32), primary_key=True, nullable=False)
    title = sqlalchemy.Column(sqlalchemy.Unicode(255), nullable=False)
    meta_description = sqlalchemy.Column(sqlalchemy.Unicode(255))
    meta_title = sqlalchemy.Column(sqlalchemy.Unicode(255))

    body = sqlalchemy.Column(sqlalchemy.Unicode)

    video_uid = sqlalchemy.Column(
        sqlalchemy.Unicode(32), sqlalchemy.ForeignKey('video.uid'),
        index=True)

    video = sqlalchemy.orm.relationship(
        'Video', primaryjoin='Article.video_uid == Video.uid',
        uselist=False, backref='articles')
    article_tags = sqlalchemy.orm.relationship('ArticleTag',
backref='article')


def _alias_onclause(join, onclause):
    if hasattr(onclause, 'clauses'):
        for clause in onclause.clauses:
            _alias_onclause(join, clause)

        return

    ltable = getattr(onclause.left, 'table', None)
    rtable = getattr(onclause.right, 'table', None)

    if ltable is join.__table__:
        onclause.left = getattr(join, onclause.left.name)

    if rtable is join.__table__:
        onclause.right = getattr(join, onclause.right.name)


def main():
    engine = create_engine('sqlite:///:memory:')
    Session = sessionmaker(bind=engine)()

    Base.metadata.create_all(engine)

    joins = []

    # join #A

    joins.append((ArticleTag, ArticleTag.article_uid == Article.uid))
    joins.append((Tag, None))

    aslugtag = sqlalchemy.orm.util.AliasedClass(SlugTag)
    aslug = sqlalchemy.orm.util.AliasedClass(Slug)

    joins.append((aslugtag, None))
    joins.append((
        aslug,
        (aslug.id == aslugtag.slug_id) & (aslug.expiration_date.is_(None))))

    # end of join #A

    # join #B

    # if success True there is extra onclause
    success = False

    if success:
        joins.append((Video, Video.uid == Article.video_uid))
    else:
        joins.append((Video, None))

    joins.append((
        Tag, (Video.series_uid == Tag.uid) & (Tag.type ==
Video.SERIES_TYPE)))

    aslugtag = sqlalchemy.orm.util.AliasedClass(SlugTag)
    aslug = sqlalchemy.orm.util.AliasedClass(Slug)

    joins.append((aslugtag, None))
    joins.append((
        aslug,
        (aslug.id == aslugtag.slug_id) & (aslug.expiration_date.is_(None))))

    # end of join #B

    inner_joins = []

    qry = Session.query(Article)

    for join, onclause in joins:
        if join in inner_joins:
            join = sqlalchemy.orm.util.AliasedClass(join)

            if onclause is not None:
                _alias_onclause(join, onclause)

        if onclause is None:
            qry = qry.join(join)
        else:
            qry = qry.join(join, onclause)

        inner_joins.append(join)

    print qry.count()
    print('sys', sys.version)
    print('SQLAlchemy', sqlalchemy.__version__)

if __name__ == '__main__':
    main()

--
You received this message because you are subscribed to the Google
Groups "sqlalchemy" group.
To unsubscribe from this group and stop receiving emails from it, send
an email to sqlalchemy+unsubscr...@googlegroups.com
<mailto:sqlalchemy+unsubscr...@googlegroups.com>.
To post to this group, send email to sqlalchemy@googlegroups.com
<mailto:sqlalchemy@googlegroups.com>.
Visit this group at https://groups.google.com/group/sqlalchemy.
For more options, visit https://groups.google.com/d/optout.

--
You received this message because you are subscribed to the Google Groups 
"sqlalchemy" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to sqlalchemy+unsubscr...@googlegroups.com.
To post to this group, send email to sqlalchemy@googlegroups.com.
Visit this group at https://groups.google.com/group/sqlalchemy.
For more options, visit https://groups.google.com/d/optout.

Reply via email to