# Path: tests/benchmarks/benchmark_utils.py
# def benchmark_parallel_funcs(n_iterations, n_parts, *funcs):
#     """ Run the given `funcs` test functions `n_iterations` times.
# 
#     Every function receives the `n` argument and is supposed to do its job `n` times in a loop.
#     This is to reduce the impact of a repeated function call, and to let your tests initialize before they run.
# 
#     Names of those functions are used in displaying results.
#     """
#     return benchmark_parallel(
#         n_iterations,
#         n_parts,
#         **{f.__name__: f for f in funcs})
# 
# Path: tests/models.py
# def get_big_db_for_benchmarks(n_users, n_articles_per_user, n_comments_per_article):
#     # Connect, create tables
#     engine, Session = init_database(autoflush=True)
#     drop_all(engine)
#     create_all(engine)
# 
#     # Fill DB
#     ssn = Session()
#     ssn.begin()
#     ssn.add_all(content_samples_random(n_users, n_articles_per_user, n_comments_per_article))
#     ssn.commit()
# 
#     # Done
#     return engine, Session
# 
# class User(Base):
#     __tablename__ = 'u'
# 
#     id = Column(Integer, primary_key=True)
#     name = Column(String)
#     tags = Column(pg.ARRAY(String))  # ARRAY field
#     age = Column(Integer)
# 
#     # Maps an SQL expression as a column
#     age_in_10 = column_property(age + 10, deferred=True)
# 
#     master_id = Column(ForeignKey('u.id', ondelete='SET NULL'), nullable=True)
#     master = relationship(lambda: User, remote_side=lambda: User.id, foreign_keys=master_id)
# 
#     @property
#     def user_calculated(self):
#         return self.age + 10
# 
#     def __repr__(self):
#         return 'User(id={}, name={!r})'.format(self.id, self.name)
# 
# class Article(Base):
#     __tablename__ = 'a'
# 
#     id = Column(Integer, primary_key=True)
#     uid = Column(Integer, ForeignKey(User.id))
#     title = Column(String)
#     theme = Column(String)
#     data = Column(pg.JSON)  # JSON field
# 
#     user = relationship(User, backref=backref('articles'))
# 
#     @property
#     def calculated(self):
#         return len(self.title) + self.uid
# 
#     @calculated.setter
#     def calculated(self, value):
#         self.title += value
# 
#     @hybrid_property
#     def hybrid(self):
#         return self.id > 10 and self.user.age > 18
# 
#     @hybrid.expression
#     def hybrid(cls):
#         return and_(cls.id > 10, cls.user.has(User.age > 18))
# 
#     def __repr__(self):
#         return 'Article(id={}, uid={!r}, title={!r})'.format(self.id, self.uid, self.title)


from sqlalchemy.orm import selectinload, joinedload
from tests.benchmarks.benchmark_utils import benchmark_parallel_funcs
from tests.models import get_big_db_for_benchmarks, User, Article
We can also win an additional 2x by not using `to_json()` on rows, but return tuples.
Both this techniques let us fetch the results 3.5x faster than `selectinload()`, 2.7x faster than `joinedload()`.

But this will give us tuples.
If we try to use `to_json()` and fetch keyed objects, it's more convenient, but reduces the performance improvement
to just 1.5x, which brings it close to what SqlAlchemy does.

Conclusion: forming JSON directly in Postgres can potentially speed up some queries 3x. But this is only applicable
to those queries that feed the data to JSON immediately. It's worth doing, but is rather complicated.

The problems with this approach: we'll have to make sure that `@property` fields are included into the results if
they were specified in a projection.
"""



# Init DB
engine, Session = get_big_db_for_benchmarks(n_users=100,
                                            n_articles_per_user=5,
                                            n_comments_per_article=3)

# Prepare
N_REPEATS = 500
ssn = Session()


# Tests
def test_selectinload(n):
    """ Load Users+Articles+Comments, with selectinload() """
    for i in range(n):

# Path: tests/benchmarks/benchmark_utils.py
# def benchmark_parallel_funcs(n_iterations, n_parts, *funcs):
#     """ Run the given `funcs` test functions `n_iterations` times.
# 
#     Every function receives the `n` argument and is supposed to do its job `n` times in a loop.
#     This is to reduce the impact of a repeated function call, and to let your tests initialize before they run.
# 
#     Names of those functions are used in displaying results.
#     """
#     return benchmark_parallel(
#         n_iterations,
#         n_parts,
#         **{f.__name__: f for f in funcs})
# 
# Path: tests/models.py
# def get_big_db_for_benchmarks(n_users, n_articles_per_user, n_comments_per_article):
#     # Connect, create tables
#     engine, Session = init_database(autoflush=True)
#     drop_all(engine)
#     create_all(engine)
# 
#     # Fill DB
#     ssn = Session()
#     ssn.begin()
#     ssn.add_all(content_samples_random(n_users, n_articles_per_user, n_comments_per_article))
#     ssn.commit()
# 
#     # Done
#     return engine, Session
# 
# class User(Base):
#     __tablename__ = 'u'
# 
#     id = Column(Integer, primary_key=True)
#     name = Column(String)
#     tags = Column(pg.ARRAY(String))  # ARRAY field
#     age = Column(Integer)
# 
#     # Maps an SQL expression as a column
#     age_in_10 = column_property(age + 10, deferred=True)
# 
#     master_id = Column(ForeignKey('u.id', ondelete='SET NULL'), nullable=True)
#     master = relationship(lambda: User, remote_side=lambda: User.id, foreign_keys=master_id)
# 
#     @property
#     def user_calculated(self):
#         return self.age + 10
# 
#     def __repr__(self):
#         return 'User(id={}, name={!r})'.format(self.id, self.name)
# 
# class Article(Base):
#     __tablename__ = 'a'
# 
#     id = Column(Integer, primary_key=True)
#     uid = Column(Integer, ForeignKey(User.id))
#     title = Column(String)
#     theme = Column(String)
#     data = Column(pg.JSON)  # JSON field
# 
#     user = relationship(User, backref=backref('articles'))
# 
#     @property
#     def calculated(self):
#         return len(self.title) + self.uid
# 
#     @calculated.setter
#     def calculated(self, value):
#         self.title += value
# 
#     @hybrid_property
#     def hybrid(self):
#         return self.id > 10 and self.user.age > 18
# 
#     @hybrid.expression
#     def hybrid(cls):
#         return and_(cls.id > 10, cls.user.has(User.age > 18))
# 
#     def __repr__(self):
#         return 'Article(id={}, uid={!r}, title={!r})'.format(self.id, self.uid, self.title)


# Path: tests/benchmarks/benchmark_compare_orm_overhead_with_pure_jsonb_output.py
from sqlalchemy.orm import selectinload, joinedload
from tests.benchmarks.benchmark_utils import benchmark_parallel_funcs
from tests.models import get_big_db_for_benchmarks, User, Article

We can also win an additional 2x by not using `to_json()` on rows, but return tuples.
Both this techniques let us fetch the results 3.5x faster than `selectinload()`, 2.7x faster than `joinedload()`.

But this will give us tuples.
If we try to use `to_json()` and fetch keyed objects, it's more convenient, but reduces the performance improvement
to just 1.5x, which brings it close to what SqlAlchemy does.

Conclusion: forming JSON directly in Postgres can potentially speed up some queries 3x. But this is only applicable
to those queries that feed the data to JSON immediately. It's worth doing, but is rather complicated.

The problems with this approach: we'll have to make sure that `@property` fields are included into the results if
they were specified in a projection.
"""



# Init DB
engine, Session = get_big_db_for_benchmarks(n_users=100,
                                            n_articles_per_user=5,
                                            n_comments_per_article=3)

# Prepare
N_REPEATS = 500
ssn = Session()


# Tests
def test_selectinload(n):
    """ Load Users+Articles+Comments, with selectinload() """
    for i in range(n):

        users = ssn.query(User).options(