flowmaticdb 2.2.0__tar.gz → 2.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/PKG-INFO +25 -13
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/README.md +20 -12
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/pyproject.toml +6 -1
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/adapters/_postgres.py +1 -1
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/dialects/_postgres.py +23 -2
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/_join.py +3 -3
- flowmaticdb-2.2.2/src/flowmaticdb/query/_joins_mixin.py +106 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb.egg-info/PKG-INFO +25 -13
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb.egg-info/requires.txt +5 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/tests/test_conditions_port.py +20 -17
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/tests/test_dialect_postgres.py +1 -1
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/tests/test_integration_mysql.py +5 -8
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/tests/test_integration_postgres.py +3 -3
- flowmaticdb-2.2.2/tests/test_joins.py +80 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/tests/test_select_query.py +1 -2
- flowmaticdb-2.2.0/src/flowmaticdb/query/_joins_mixin.py +0 -114
- flowmaticdb-2.2.0/tests/test_joins.py +0 -37
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/setup.cfg +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/__init__.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/_exceptions.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/_helpers.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/_query_with_params.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/adapters/__init__.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/adapters/_base.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/adapters/_mysql.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/adapters/_sqlite.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/database/__init__.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/database/_abc.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/database/_database.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/database/_db.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/database/_table.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/dialects/__init__.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/dialects/_base.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/dialects/_mysql.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/dialects/_sql_dialect.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/dialects/_sqlite.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/migrations/__init__.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/migrations/_loader.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/migrations/_migration_abc.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/migrations/_migrator.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/migrations/_template.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/py.typed +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/__init__.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/_alter_table.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/_condition.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/_condition_group.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/_condition_mixin.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/_create_table.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/_ddl_mixins.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/_delete.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/_drop_table.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/_having_mixin.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/_insert.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/_on_conflict.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/_order_by.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/_query.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/_select.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/_simple_mixins.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/_union.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/_update.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/_where_mixin.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/ddl/__init__.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/ddl/_add_column.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/ddl/_add_foreign_key_constraint.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/ddl/_add_primary_keys.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/ddl/_add_unique_constraint.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/ddl/_alter.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/ddl/_alter_column.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/ddl/_column.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/ddl/_constraint.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/ddl/_drop_column.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/ddl/_drop_constraint.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/ddl/_foreign_key_constraint.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/ddl/_raw_alter.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/ddl/_raw_constraint.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/ddl/_rename_column.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/ddl/_unique_constraint.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/enums/__init__.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/enums/_chain.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/enums/_condition.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/enums/_join.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/enums/_order_by_dir.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/enums/_referential_action.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/enums/_type.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/enums/_union.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/expressions/__init__.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/expressions/_alias.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/expressions/_current_timestamp.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/expressions/_excluded.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/expressions/_expression.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/expressions/_identifier.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/expressions/_raw.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/expressions/_sql.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/query/expressions/_sub_query.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/result/__init__.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/result/_base.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/result/_mysql.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/result/_postgres.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/result/_result.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb/result/_sqlite.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb.egg-info/SOURCES.txt +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb.egg-info/dependency_links.txt +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/src/flowmaticdb.egg-info/top_level.txt +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/tests/test_adapters_port.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/tests/test_alter_table_query.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/tests/test_conditions.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/tests/test_database_port.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/tests/test_ddl_port.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/tests/test_delete_query.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/tests/test_dialect_mysql.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/tests/test_dialect_sql.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/tests/test_dialect_sql_port.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/tests/test_dialect_sqlite.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/tests/test_dialect_subclasses_port.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/tests/test_expressions.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/tests/test_helpers_port.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/tests/test_insert_query.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/tests/test_integration_sqlite.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/tests/test_query_core_port.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/tests/test_query_with_params.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/tests/test_result_abstract.py +0 -0
- {flowmaticdb-2.2.0 → flowmaticdb-2.2.2}/tests/test_update_query.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flowmaticdb
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.2
|
|
4
4
|
Summary: A Python database abstraction layer supporting PostgreSQL, SQLite, and MySQL.
|
|
5
5
|
Author: Flowmatic, UniForceMusic
|
|
6
6
|
License: MIT
|
|
@@ -29,6 +29,10 @@ Provides-Extra: dev
|
|
|
29
29
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
30
30
|
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
31
31
|
Requires-Dist: ruff>=0.1; extra == "dev"
|
|
32
|
+
Provides-Extra: all
|
|
33
|
+
Requires-Dist: asyncpg>=0.29; extra == "all"
|
|
34
|
+
Requires-Dist: mysql-connector-python>=9.0; extra == "all"
|
|
35
|
+
Requires-Dist: psycopg[binary]>=3.1; extra == "all"
|
|
32
36
|
|
|
33
37
|
# Transparency
|
|
34
38
|
This package is a port of [PHP Sentience Database](https://github.com/Sentience-Framework/database)
|
|
@@ -433,21 +437,26 @@ from flowmaticdb import raw, identifier
|
|
|
433
437
|
|
|
434
438
|
query = db.select("users").columns(["users.id", "posts.title"])
|
|
435
439
|
|
|
436
|
-
# INNER JOIN with ON conditions
|
|
437
|
-
join
|
|
438
|
-
|
|
439
|
-
|
|
440
|
+
# INNER JOIN with ON conditions — the callback receives the Join,
|
|
441
|
+
# every join method returns the query so you can keep chaining
|
|
442
|
+
query.inner_join_table(
|
|
443
|
+
"posts",
|
|
444
|
+
lambda join: join
|
|
445
|
+
.on(["users", "id"], ["p", "user_id"]) # ON users.id = p.user_id
|
|
446
|
+
.or_on(["p", "status"], ["'published'"]), # OR p.status = 'published'
|
|
447
|
+
"p",
|
|
448
|
+
)
|
|
440
449
|
|
|
441
450
|
# LEFT JOIN
|
|
442
|
-
query.
|
|
451
|
+
query.left_join_table("comments", lambda join: join.on(["p", "id"], ["c", "post_id"]), "c")
|
|
443
452
|
|
|
444
|
-
# CROSS JOIN
|
|
453
|
+
# CROSS JOIN (never takes ON conditions)
|
|
445
454
|
query.cross_join("sessions")
|
|
446
455
|
|
|
447
456
|
# LATERAL joins
|
|
448
|
-
query.
|
|
449
|
-
query.
|
|
450
|
-
query.
|
|
457
|
+
query.left_join_lateral_sub_query(sub_query, "sq")
|
|
458
|
+
query.inner_join_lateral_sub_query(sub_query, "sq")
|
|
459
|
+
query.cross_join_lateral_sub_query(sub_query, "sq")
|
|
451
460
|
|
|
452
461
|
# Raw join SQL (e.g. for aggregates)
|
|
453
462
|
query.join(raw("LEFT JOIN (SELECT user_id, count(*) AS cnt FROM orders GROUP BY user_id) AS o ON o.user_id = users.id"))
|
|
@@ -458,9 +467,12 @@ query.join(raw("LEFT JOIN (SELECT user_id, count(*) AS cnt FROM orders GROUP BY
|
|
|
458
467
|
`Join` objects support all the same condition methods as WHERE:
|
|
459
468
|
|
|
460
469
|
```python
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
join
|
|
470
|
+
query.inner_join(
|
|
471
|
+
"orders",
|
|
472
|
+
lambda join: join
|
|
473
|
+
.where_equals(["orders", "user_id"], ["users", "id"])
|
|
474
|
+
.where_greater_than("orders.total", 100),
|
|
475
|
+
)
|
|
464
476
|
```
|
|
465
477
|
|
|
466
478
|
---
|
|
@@ -401,21 +401,26 @@ from flowmaticdb import raw, identifier
|
|
|
401
401
|
|
|
402
402
|
query = db.select("users").columns(["users.id", "posts.title"])
|
|
403
403
|
|
|
404
|
-
# INNER JOIN with ON conditions
|
|
405
|
-
join
|
|
406
|
-
|
|
407
|
-
|
|
404
|
+
# INNER JOIN with ON conditions — the callback receives the Join,
|
|
405
|
+
# every join method returns the query so you can keep chaining
|
|
406
|
+
query.inner_join_table(
|
|
407
|
+
"posts",
|
|
408
|
+
lambda join: join
|
|
409
|
+
.on(["users", "id"], ["p", "user_id"]) # ON users.id = p.user_id
|
|
410
|
+
.or_on(["p", "status"], ["'published'"]), # OR p.status = 'published'
|
|
411
|
+
"p",
|
|
412
|
+
)
|
|
408
413
|
|
|
409
414
|
# LEFT JOIN
|
|
410
|
-
query.
|
|
415
|
+
query.left_join_table("comments", lambda join: join.on(["p", "id"], ["c", "post_id"]), "c")
|
|
411
416
|
|
|
412
|
-
# CROSS JOIN
|
|
417
|
+
# CROSS JOIN (never takes ON conditions)
|
|
413
418
|
query.cross_join("sessions")
|
|
414
419
|
|
|
415
420
|
# LATERAL joins
|
|
416
|
-
query.
|
|
417
|
-
query.
|
|
418
|
-
query.
|
|
421
|
+
query.left_join_lateral_sub_query(sub_query, "sq")
|
|
422
|
+
query.inner_join_lateral_sub_query(sub_query, "sq")
|
|
423
|
+
query.cross_join_lateral_sub_query(sub_query, "sq")
|
|
419
424
|
|
|
420
425
|
# Raw join SQL (e.g. for aggregates)
|
|
421
426
|
query.join(raw("LEFT JOIN (SELECT user_id, count(*) AS cnt FROM orders GROUP BY user_id) AS o ON o.user_id = users.id"))
|
|
@@ -426,9 +431,12 @@ query.join(raw("LEFT JOIN (SELECT user_id, count(*) AS cnt FROM orders GROUP BY
|
|
|
426
431
|
`Join` objects support all the same condition methods as WHERE:
|
|
427
432
|
|
|
428
433
|
```python
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
join
|
|
434
|
+
query.inner_join(
|
|
435
|
+
"orders",
|
|
436
|
+
lambda join: join
|
|
437
|
+
.where_equals(["orders", "user_id"], ["users", "id"])
|
|
438
|
+
.where_greater_than("orders.total", 100),
|
|
439
|
+
)
|
|
432
440
|
```
|
|
433
441
|
|
|
434
442
|
---
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "flowmaticdb"
|
|
7
|
-
version = "2.2.
|
|
7
|
+
version = "2.2.2"
|
|
8
8
|
description = "A Python database abstraction layer supporting PostgreSQL, SQLite, and MySQL."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -50,6 +50,11 @@ dev = [
|
|
|
50
50
|
"mypy>=1.0",
|
|
51
51
|
"ruff>=0.1",
|
|
52
52
|
]
|
|
53
|
+
all = [
|
|
54
|
+
"asyncpg>=0.29",
|
|
55
|
+
"mysql-connector-python>=9.0",
|
|
56
|
+
"psycopg[binary]>=3.1",
|
|
57
|
+
]
|
|
53
58
|
|
|
54
59
|
[tool.setuptools.packages.find]
|
|
55
60
|
where = ["src"]
|
|
@@ -9,7 +9,7 @@ from datetime import date, datetime
|
|
|
9
9
|
from datetime import time as time_of_day
|
|
10
10
|
from typing import TYPE_CHECKING, Any, TypeVar
|
|
11
11
|
|
|
12
|
-
from flowmaticdb
|
|
12
|
+
from flowmaticdb import AdapterError
|
|
13
13
|
from flowmaticdb._query_with_params import REGEX_PATTERN
|
|
14
14
|
from flowmaticdb.adapters._base import AdapterABC
|
|
15
15
|
from flowmaticdb.result import AsyncpgResult, PsycopgResult, ResultABC
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import dataclasses
|
|
4
4
|
import re
|
|
5
5
|
from collections.abc import Mapping
|
|
6
|
+
from datetime import UTC, datetime
|
|
6
7
|
from typing import Any, ClassVar
|
|
7
8
|
|
|
8
9
|
from flowmaticdb.dialects._sql_dialect import SQLDialect
|
|
@@ -12,6 +13,10 @@ from flowmaticdb.query.enums import ConditionEnum, TypeEnum
|
|
|
12
13
|
|
|
13
14
|
_TZ_OFFSET_RE = re.compile(r"([+-]\d{2})$")
|
|
14
15
|
|
|
16
|
+
# TIMESTAMP WITHOUT TIME ZONE columns and pre-timestamptz data still come back
|
|
17
|
+
# without an offset; those are read as UTC.
|
|
18
|
+
_NAIVE_DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S.%f"
|
|
19
|
+
|
|
15
20
|
|
|
16
21
|
class PostgresqlDialect(SQLDialect):
|
|
17
22
|
escape_chars: ClassVar[Mapping[str, str]] = {
|
|
@@ -33,7 +38,7 @@ class PostgresqlDialect(SQLDialect):
|
|
|
33
38
|
self.on_conflict = True
|
|
34
39
|
self.returning = True
|
|
35
40
|
self.lateral = True
|
|
36
|
-
self.datetime_format = "%Y-%m-%d %H:%M:%S.%f"
|
|
41
|
+
self.datetime_format = "%Y-%m-%d %H:%M:%S.%f%z"
|
|
37
42
|
self._version_gate()
|
|
38
43
|
|
|
39
44
|
def _version_gate(self) -> None:
|
|
@@ -118,9 +123,25 @@ class PostgresqlDialect(SQLDialect):
|
|
|
118
123
|
def cast_bool(self, value: bool) -> bool | int:
|
|
119
124
|
return value
|
|
120
125
|
|
|
126
|
+
def cast_datetime(self, value: Any) -> str:
|
|
127
|
+
# %z renders nothing for a naive datetime, which would defeat the point
|
|
128
|
+
# of a timestamptz column -- read those as UTC, matching how naive
|
|
129
|
+
# strings are parsed back.
|
|
130
|
+
if isinstance(value, datetime) and value.tzinfo is None:
|
|
131
|
+
value = value.replace(tzinfo=UTC)
|
|
132
|
+
return super().cast_datetime(value)
|
|
133
|
+
|
|
121
134
|
def parse_datetime(self, value: Any) -> Any:
|
|
122
135
|
if isinstance(value, str):
|
|
123
136
|
value = _TZ_OFFSET_RE.sub(r"\1:00", value)
|
|
137
|
+
try:
|
|
138
|
+
return datetime.strptime(value, self.datetime_format)
|
|
139
|
+
except ValueError:
|
|
140
|
+
pass
|
|
141
|
+
try:
|
|
142
|
+
return datetime.strptime(value, _NAIVE_DATETIME_FORMAT).replace(tzinfo=UTC)
|
|
143
|
+
except ValueError:
|
|
144
|
+
pass
|
|
124
145
|
return super().parse_datetime(value)
|
|
125
146
|
|
|
126
147
|
def type(self, type_enum: TypeEnum, bits: int | None = None) -> str:
|
|
@@ -128,5 +149,5 @@ class PostgresqlDialect(SQLDialect):
|
|
|
128
149
|
if type_enum == TypeEnum.FLOAT:
|
|
129
150
|
return "DOUBLE PRECISION" if size > 32 else "REAL"
|
|
130
151
|
if type_enum == TypeEnum.DATETIME:
|
|
131
|
-
return "
|
|
152
|
+
return "TIMESTAMPTZ"
|
|
132
153
|
return super().type(type_enum, bits)
|
|
@@ -8,7 +8,7 @@ from flowmaticdb.query._condition import Condition
|
|
|
8
8
|
from flowmaticdb.query._condition_group import ConditionGroupABC
|
|
9
9
|
from flowmaticdb.query._condition_mixin import ConditionMixin
|
|
10
10
|
from flowmaticdb.query.enums import ChainEnum, JoinEnum
|
|
11
|
-
from flowmaticdb.query.expressions import SqlABC
|
|
11
|
+
from flowmaticdb.query.expressions import SqlABC, Identifier
|
|
12
12
|
|
|
13
13
|
if TYPE_CHECKING:
|
|
14
14
|
from flowmaticdb.query._select import SelectQuery
|
|
@@ -264,9 +264,9 @@ class Join(ConditionMixin):
|
|
|
264
264
|
return self
|
|
265
265
|
|
|
266
266
|
def on(self, left: list[str], right: list[str]) -> Self:
|
|
267
|
-
self._equals(self.conditions, left, right
|
|
267
|
+
self._equals(self.conditions, left, Identifier(right))
|
|
268
268
|
return self
|
|
269
269
|
|
|
270
270
|
def or_on(self, left: list[str], right: list[str]) -> Self:
|
|
271
|
-
self._equals(self.conditions, left, right,
|
|
271
|
+
self._equals(self.conditions, left, Identifier(right), chain=ChainEnum.OR)
|
|
272
272
|
return self
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Self
|
|
5
|
+
|
|
6
|
+
from flowmaticdb.query._join import Join
|
|
7
|
+
from flowmaticdb.query.enums import JoinEnum
|
|
8
|
+
from flowmaticdb.query.expressions import Alias, SqlABC, SubQuery
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from flowmaticdb.query._select import SelectQuery
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class JoinsMixin:
|
|
15
|
+
joins: list[Any]
|
|
16
|
+
|
|
17
|
+
def left_join(self, table: str | list[str] | SqlABC, on: Callable[[Join], Join | None] | None = None) -> Self:
|
|
18
|
+
return self._add_join(JoinEnum.LEFT_JOIN, table, on)
|
|
19
|
+
|
|
20
|
+
def left_join_table(self, table: str | list[str] | SqlABC, on: Callable[[Join], Join | None] | None = None, alias: str | None = None) -> Self:
|
|
21
|
+
return self.left_join(Alias(table, alias) if alias else table, on)
|
|
22
|
+
|
|
23
|
+
def left_join_sub_query(self, query: SelectQuery, alias: str, on: Callable[[Join], Join | None] | None = None) -> Self:
|
|
24
|
+
return self.left_join(SubQuery(query, alias), on)
|
|
25
|
+
|
|
26
|
+
def left_join_lateral(self, table: str | list[str] | SqlABC, on: Callable[[Join], Join | None] | None = None) -> Self:
|
|
27
|
+
return self._add_join(JoinEnum.LEFT_JOIN_LATERAL, table, on)
|
|
28
|
+
|
|
29
|
+
def left_join_lateral_table(self, table: str | list[str] | SqlABC, on: Callable[[Join], Join | None] | None = None, alias: str | None = None) -> Self:
|
|
30
|
+
return self.left_join_lateral(Alias(table, alias) if alias else table, on)
|
|
31
|
+
|
|
32
|
+
def left_join_lateral_sub_query(self, query: SelectQuery, alias: str, on: Callable[[Join], Join | None] | None = None) -> Self:
|
|
33
|
+
return self.left_join_lateral(SubQuery(query, alias), on)
|
|
34
|
+
|
|
35
|
+
def inner_join(self, table: str | list[str] | SqlABC, on: Callable[[Join], Join | None] | None = None) -> Self:
|
|
36
|
+
return self._add_join(JoinEnum.INNER_JOIN, table, on)
|
|
37
|
+
|
|
38
|
+
def inner_join_table(self, table: str | list[str] | SqlABC, on: Callable[[Join], Join | None] | None = None, alias: str | None = None) -> Self:
|
|
39
|
+
return self.inner_join(Alias(table, alias) if alias else table, on)
|
|
40
|
+
|
|
41
|
+
def inner_join_sub_query(self, query: SelectQuery, alias: str, on: Callable[[Join], Join | None] | None = None) -> Self:
|
|
42
|
+
return self.inner_join(SubQuery(query, alias), on)
|
|
43
|
+
|
|
44
|
+
def inner_join_lateral(self, table: str | list[str] | SqlABC, on: Callable[[Join], Join | None] | None = None) -> Self:
|
|
45
|
+
return self._add_join(JoinEnum.INNER_JOIN_LATERAL, table, on)
|
|
46
|
+
|
|
47
|
+
def inner_join_lateral_table(self, table: str | list[str] | SqlABC, on: Callable[[Join], Join | None] | None = None, alias: str | None = None) -> Self:
|
|
48
|
+
return self.inner_join_lateral(Alias(table, alias) if alias else table, on)
|
|
49
|
+
|
|
50
|
+
def inner_join_lateral_sub_query(self, query: SelectQuery, alias: str, on: Callable[[Join], Join | None] | None = None) -> Self:
|
|
51
|
+
return self.inner_join_lateral(SubQuery(query, alias), on)
|
|
52
|
+
|
|
53
|
+
def cross_join(self, table: str | list[str] | SqlABC) -> Self:
|
|
54
|
+
return self._add_join(JoinEnum.CROSS_JOIN, table, None)
|
|
55
|
+
|
|
56
|
+
def cross_join_table(self, table: str | list[str] | SqlABC, alias: str | None = None) -> Self:
|
|
57
|
+
return self.cross_join(Alias(table, alias) if alias else table)
|
|
58
|
+
|
|
59
|
+
def cross_join_sub_query(self, query: SelectQuery, alias: str) -> Self:
|
|
60
|
+
return self.cross_join(SubQuery(query, alias))
|
|
61
|
+
|
|
62
|
+
def cross_join_lateral(self, table: str | list[str] | SqlABC, on: Callable[[Join], Join | None] | None = None) -> Self:
|
|
63
|
+
return self._add_join(JoinEnum.CROSS_JOIN_LATERAL, table, on)
|
|
64
|
+
|
|
65
|
+
def cross_join_lateral_table(self, table: str | list[str] | SqlABC, on: Callable[[Join], Join | None] | None = None, alias: str | None = None) -> Self:
|
|
66
|
+
return self.cross_join_lateral(Alias(table, alias) if alias else table, on)
|
|
67
|
+
|
|
68
|
+
def cross_join_lateral_sub_query(self, query: SelectQuery, alias: str, on: Callable[[Join], Join | None] | None = None) -> Self:
|
|
69
|
+
return self.cross_join_lateral(SubQuery(query, alias), on)
|
|
70
|
+
|
|
71
|
+
def outer_apply(self, table: str | list[str] | SqlABC, on: Callable[[Join], Join | None] | None = None) -> Self:
|
|
72
|
+
return self._add_join(JoinEnum.LEFT_JOIN_LATERAL, table, on)
|
|
73
|
+
|
|
74
|
+
def outer_apply_table(self, table: str | list[str] | SqlABC, on: Callable[[Join], Join | None] | None = None, alias: str | None = None) -> Self:
|
|
75
|
+
return self.outer_apply(Alias(table, alias) if alias else table, on)
|
|
76
|
+
|
|
77
|
+
def outer_apply_sub_query(self, query: SelectQuery, alias: str, on: Callable[[Join], Join | None] | None = None) -> Self:
|
|
78
|
+
return self.outer_apply(SubQuery(query, alias), on)
|
|
79
|
+
|
|
80
|
+
def cross_apply(self, table: str | list[str] | SqlABC, on: Callable[[Join], Join | None] | None = None) -> Self:
|
|
81
|
+
return self._add_join(JoinEnum.INNER_JOIN_LATERAL, table, on)
|
|
82
|
+
|
|
83
|
+
def cross_apply_table(self, table: str | list[str] | SqlABC, on: Callable[[Join], Join | None] | None = None, alias: str | None = None) -> Self:
|
|
84
|
+
return self.cross_apply(Alias(table, alias) if alias else table, on)
|
|
85
|
+
|
|
86
|
+
def cross_apply_sub_query(self, query: SelectQuery, alias: str, on: Callable[[Join], Join | None] | None = None) -> Self:
|
|
87
|
+
return self.cross_apply(SubQuery(query, alias), on)
|
|
88
|
+
|
|
89
|
+
def join(self, sql: Any) -> Self:
|
|
90
|
+
self.joins.append(sql)
|
|
91
|
+
return self
|
|
92
|
+
|
|
93
|
+
def _add_join(self, join_type: JoinEnum, table: str | list[str] | SqlABC, on: Callable[[Join], Join | None] | None) -> Self:
|
|
94
|
+
join: Any = Join(join=join_type, table=table)
|
|
95
|
+
|
|
96
|
+
if on:
|
|
97
|
+
returned = on(join)
|
|
98
|
+
if returned is not None:
|
|
99
|
+
join = returned
|
|
100
|
+
|
|
101
|
+
if not isinstance(join, Join):
|
|
102
|
+
return self
|
|
103
|
+
|
|
104
|
+
self.joins.append(join)
|
|
105
|
+
|
|
106
|
+
return self
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flowmaticdb
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.2
|
|
4
4
|
Summary: A Python database abstraction layer supporting PostgreSQL, SQLite, and MySQL.
|
|
5
5
|
Author: Flowmatic, UniForceMusic
|
|
6
6
|
License: MIT
|
|
@@ -29,6 +29,10 @@ Provides-Extra: dev
|
|
|
29
29
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
30
30
|
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
31
31
|
Requires-Dist: ruff>=0.1; extra == "dev"
|
|
32
|
+
Provides-Extra: all
|
|
33
|
+
Requires-Dist: asyncpg>=0.29; extra == "all"
|
|
34
|
+
Requires-Dist: mysql-connector-python>=9.0; extra == "all"
|
|
35
|
+
Requires-Dist: psycopg[binary]>=3.1; extra == "all"
|
|
32
36
|
|
|
33
37
|
# Transparency
|
|
34
38
|
This package is a port of [PHP Sentience Database](https://github.com/Sentience-Framework/database)
|
|
@@ -433,21 +437,26 @@ from flowmaticdb import raw, identifier
|
|
|
433
437
|
|
|
434
438
|
query = db.select("users").columns(["users.id", "posts.title"])
|
|
435
439
|
|
|
436
|
-
# INNER JOIN with ON conditions
|
|
437
|
-
join
|
|
438
|
-
|
|
439
|
-
|
|
440
|
+
# INNER JOIN with ON conditions — the callback receives the Join,
|
|
441
|
+
# every join method returns the query so you can keep chaining
|
|
442
|
+
query.inner_join_table(
|
|
443
|
+
"posts",
|
|
444
|
+
lambda join: join
|
|
445
|
+
.on(["users", "id"], ["p", "user_id"]) # ON users.id = p.user_id
|
|
446
|
+
.or_on(["p", "status"], ["'published'"]), # OR p.status = 'published'
|
|
447
|
+
"p",
|
|
448
|
+
)
|
|
440
449
|
|
|
441
450
|
# LEFT JOIN
|
|
442
|
-
query.
|
|
451
|
+
query.left_join_table("comments", lambda join: join.on(["p", "id"], ["c", "post_id"]), "c")
|
|
443
452
|
|
|
444
|
-
# CROSS JOIN
|
|
453
|
+
# CROSS JOIN (never takes ON conditions)
|
|
445
454
|
query.cross_join("sessions")
|
|
446
455
|
|
|
447
456
|
# LATERAL joins
|
|
448
|
-
query.
|
|
449
|
-
query.
|
|
450
|
-
query.
|
|
457
|
+
query.left_join_lateral_sub_query(sub_query, "sq")
|
|
458
|
+
query.inner_join_lateral_sub_query(sub_query, "sq")
|
|
459
|
+
query.cross_join_lateral_sub_query(sub_query, "sq")
|
|
451
460
|
|
|
452
461
|
# Raw join SQL (e.g. for aggregates)
|
|
453
462
|
query.join(raw("LEFT JOIN (SELECT user_id, count(*) AS cnt FROM orders GROUP BY user_id) AS o ON o.user_id = users.id"))
|
|
@@ -458,9 +467,12 @@ query.join(raw("LEFT JOIN (SELECT user_id, count(*) AS cnt FROM orders GROUP BY
|
|
|
458
467
|
`Join` objects support all the same condition methods as WHERE:
|
|
459
468
|
|
|
460
469
|
```python
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
join
|
|
470
|
+
query.inner_join(
|
|
471
|
+
"orders",
|
|
472
|
+
lambda join: join
|
|
473
|
+
.where_equals(["orders", "user_id"], ["users", "id"])
|
|
474
|
+
.where_greater_than("orders.total", 100),
|
|
475
|
+
)
|
|
464
476
|
```
|
|
465
477
|
|
|
466
478
|
---
|
|
@@ -148,42 +148,45 @@ def test_join_or_where_not_empty_chains_with_or_not_and() -> None:
|
|
|
148
148
|
|
|
149
149
|
def test_select_has_full_join_api(sql_dialect: SQLDialect, mock_db) -> None:
|
|
150
150
|
q = SelectQuery(sql_dialect, "users", database=mock_db)
|
|
151
|
-
assert
|
|
152
|
-
assert
|
|
153
|
-
assert
|
|
154
|
-
assert
|
|
155
|
-
assert
|
|
156
|
-
assert
|
|
157
|
-
assert
|
|
151
|
+
assert q.inner_join_table("posts") is q
|
|
152
|
+
assert q.cross_join_table("posts") is q
|
|
153
|
+
assert q.left_join_lateral("posts") is q
|
|
154
|
+
assert q.inner_join_lateral("posts") is q
|
|
155
|
+
assert q.cross_join_lateral("posts") is q
|
|
156
|
+
assert q.outer_apply("posts") is q
|
|
157
|
+
assert q.cross_apply("posts") is q
|
|
158
|
+
assert len(q.joins) == 7
|
|
159
|
+
assert all(isinstance(j, Join) for j in q.joins)
|
|
158
160
|
|
|
159
161
|
|
|
160
162
|
def test_outer_apply_maps_to_left_join_lateral(sql_dialect: SQLDialect, mock_db) -> None:
|
|
161
163
|
q = SelectQuery(sql_dialect, "users", database=mock_db)
|
|
162
|
-
|
|
163
|
-
assert
|
|
164
|
+
q.outer_apply("posts")
|
|
165
|
+
assert q.joins[0].join == JoinEnum.LEFT_JOIN_LATERAL
|
|
164
166
|
|
|
165
167
|
|
|
166
168
|
def test_cross_apply_maps_to_inner_join_lateral(sql_dialect: SQLDialect, mock_db) -> None:
|
|
167
169
|
q = SelectQuery(sql_dialect, "users", database=mock_db)
|
|
168
|
-
|
|
169
|
-
assert
|
|
170
|
+
q.cross_apply("posts")
|
|
171
|
+
assert q.joins[0].join == JoinEnum.INNER_JOIN_LATERAL
|
|
170
172
|
|
|
171
173
|
|
|
172
174
|
def test_left_join_lateral_takes_raw_table_not_subquery_only(sql_dialect: SQLDialect, mock_db) -> None:
|
|
173
175
|
q = SelectQuery(sql_dialect, "users", database=mock_db)
|
|
174
|
-
|
|
175
|
-
assert
|
|
176
|
+
q.left_join_lateral_table("posts", alias="p")
|
|
177
|
+
assert q.joins[0].join == JoinEnum.LEFT_JOIN_LATERAL
|
|
176
178
|
|
|
177
179
|
|
|
178
180
|
def test_left_join_lateral_sub_query_still_available(sql_dialect: SQLDialect, mock_db) -> None:
|
|
179
181
|
inner = SelectQuery(sql_dialect, "posts", database=mock_db)
|
|
180
182
|
q = SelectQuery(sql_dialect, "users", database=mock_db)
|
|
181
|
-
|
|
182
|
-
assert
|
|
183
|
+
q.left_join_lateral_sub_query(inner, "p")
|
|
184
|
+
assert q.joins[0].join == JoinEnum.LEFT_JOIN_LATERAL
|
|
183
185
|
|
|
184
186
|
|
|
185
187
|
def test_inner_join_sub_query_and_cross_join_sub_query_exist(sql_dialect: SQLDialect, mock_db) -> None:
|
|
186
188
|
inner = SelectQuery(sql_dialect, "posts", database=mock_db)
|
|
187
189
|
q = SelectQuery(sql_dialect, "users", database=mock_db)
|
|
188
|
-
assert
|
|
189
|
-
assert
|
|
190
|
+
assert q.inner_join_sub_query(inner, "p") is q
|
|
191
|
+
assert q.cross_join_sub_query(inner, "p") is q
|
|
192
|
+
assert [j.join for j in q.joins] == [JoinEnum.INNER_JOIN, JoinEnum.CROSS_JOIN]
|
|
@@ -118,7 +118,7 @@ def test_pg_type_mapping(pg_dialect: PostgresqlDialect) -> None:
|
|
|
118
118
|
assert pg_dialect.type(TypeEnum.INT) == "INTEGER"
|
|
119
119
|
assert pg_dialect.type(TypeEnum.FLOAT) == "REAL"
|
|
120
120
|
assert pg_dialect.type(TypeEnum.FLOAT, 64) == "DOUBLE PRECISION"
|
|
121
|
-
assert pg_dialect.type(TypeEnum.DATETIME) == "
|
|
121
|
+
assert pg_dialect.type(TypeEnum.DATETIME) == "TIMESTAMPTZ"
|
|
122
122
|
|
|
123
123
|
|
|
124
124
|
def test_pg_like_case_sensitive_by_default(pg_dialect: PostgresqlDialect) -> None:
|
|
@@ -395,8 +395,7 @@ def test_mysql_joins(
|
|
|
395
395
|
|
|
396
396
|
q: SelectQuery = SelectQuery(dialect, "users", database=db)
|
|
397
397
|
q.columns(["name", "title"])
|
|
398
|
-
|
|
399
|
-
j.on(["users", "id"], ["posts", "user_id"])
|
|
398
|
+
q.inner_join("posts", lambda join: join.on(["users", "id"], ["posts", "user_id"]))
|
|
400
399
|
q.order_by_asc("title")
|
|
401
400
|
|
|
402
401
|
qwp: QueryWithParams = q.to_query_with_params()
|
|
@@ -418,8 +417,7 @@ def test_mysql_joins(
|
|
|
418
417
|
|
|
419
418
|
q2: SelectQuery = SelectQuery(dialect, "users", database=db)
|
|
420
419
|
q2.columns(["name", "title"])
|
|
421
|
-
|
|
422
|
-
lj.on(["users", "id"], ["posts", "user_id"])
|
|
420
|
+
q2.left_join("posts", lambda join: join.on(["users", "id"], ["posts", "user_id"]))
|
|
423
421
|
q2.order_by_asc("name")
|
|
424
422
|
|
|
425
423
|
qwp2: QueryWithParams = q2.to_query_with_params()
|
|
@@ -445,8 +443,8 @@ def test_mysql_joins(
|
|
|
445
443
|
|
|
446
444
|
q4: SelectQuery = SelectQuery(dialect, "users", database=db)
|
|
447
445
|
q4.columns(["name", "title"])
|
|
448
|
-
|
|
449
|
-
assert
|
|
446
|
+
q4.cross_join("posts")
|
|
447
|
+
assert q4.joins[0].conditions == []
|
|
450
448
|
|
|
451
449
|
qwp4: QueryWithParams = q4.to_query_with_params()
|
|
452
450
|
result4: ResultABC = adapter.query_with_params(dialect, qwp4)
|
|
@@ -987,8 +985,7 @@ def test_mysql_giant_select(
|
|
|
987
985
|
]
|
|
988
986
|
)
|
|
989
987
|
|
|
990
|
-
|
|
991
|
-
inner.on(["users", "id"], ["posts", "user_id"])
|
|
988
|
+
q.inner_join("posts", lambda join: join.on(["users", "id"], ["posts", "user_id"]))
|
|
992
989
|
|
|
993
990
|
q.where_greater_than(["users", "age"], 18)
|
|
994
991
|
q.where_in(["users", "id"], [1, 2, 3])
|
|
@@ -271,14 +271,14 @@ def test_postgres_joins(pg_adapter: PsycopgAdapter, pg_dialect: PostgresqlDialec
|
|
|
271
271
|
|
|
272
272
|
q = SelectQuery(dialect, "j_users", database=db)
|
|
273
273
|
q.columns([identifier(["j_users", "name"]), identifier(["j_posts", "title"])])
|
|
274
|
-
q.inner_join("j_posts"
|
|
274
|
+
q.inner_join("j_posts", lambda join: join.on(["j_users", "id"], ["j_posts", "user_id"]))
|
|
275
275
|
rows: list[dict[str, Any]] = adapter.query_with_params(dialect, q.to_query_with_params()).fetch_dicts()
|
|
276
276
|
assert len(rows) == 2
|
|
277
277
|
assert all(r["name"] == "Alice" for r in rows)
|
|
278
278
|
|
|
279
279
|
q = SelectQuery(dialect, "j_users", database=db)
|
|
280
280
|
q.columns([identifier(["j_users", "name"]), identifier(["j_posts", "title"])])
|
|
281
|
-
q.left_join("j_posts"
|
|
281
|
+
q.left_join("j_posts", lambda join: join.on(["j_users", "id"], ["j_posts", "user_id"]))
|
|
282
282
|
rows = adapter.query_with_params(dialect, q.to_query_with_params()).fetch_dicts()
|
|
283
283
|
names: dict[str, Any] = {r["name"]: r["title"] for r in rows}
|
|
284
284
|
assert names["Bob"] is None
|
|
@@ -663,7 +663,7 @@ def test_postgres_giant_select(pg_adapter: PsycopgAdapter, pg_dialect: Postgresq
|
|
|
663
663
|
|
|
664
664
|
q = SelectQuery(dialect, Alias("g_users", "u"), database=db)
|
|
665
665
|
q.columns([identifier(["u", "name"]), expression('count("p"."id") AS post_count')])
|
|
666
|
-
q.
|
|
666
|
+
q.inner_join_table("g_posts", lambda join: join.on(["u", "id"], ["p", "user_id"]), "p")
|
|
667
667
|
q.where_greater_than_or_equals("age", 25)
|
|
668
668
|
q.where_in("name", ["Alice", "Bob"])
|
|
669
669
|
q.where_between("age", 20, 40)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from flowmaticdb.dialects import SQLDialect
|
|
6
|
+
from flowmaticdb.query import Join, SelectQuery
|
|
7
|
+
from flowmaticdb.query.enums import ChainEnum, JoinEnum
|
|
8
|
+
from flowmaticdb.query.expressions import Alias
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_join_creation() -> None:
|
|
12
|
+
j = Join(join=JoinEnum.LEFT_JOIN, table="posts")
|
|
13
|
+
assert j.join == JoinEnum.LEFT_JOIN
|
|
14
|
+
assert j.table == "posts"
|
|
15
|
+
assert j.conditions == []
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_join_on_condition() -> None:
|
|
19
|
+
j = Join(join=JoinEnum.INNER_JOIN, table="posts")
|
|
20
|
+
j.on("users.id", "posts.user_id")
|
|
21
|
+
assert len(j.conditions) == 1
|
|
22
|
+
assert j.conditions[0].chain == ChainEnum.AND
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_join_or_on_condition() -> None:
|
|
26
|
+
j = Join(join=JoinEnum.LEFT_JOIN, table="posts")
|
|
27
|
+
j.or_on("a", "b")
|
|
28
|
+
assert j.conditions[0].chain == ChainEnum.OR
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_multiple_join_conditions() -> None:
|
|
32
|
+
j = Join(join=JoinEnum.LEFT_JOIN, table="posts")
|
|
33
|
+
j.on("users.id", "posts.user_id")
|
|
34
|
+
j.on("users.deleted", 0)
|
|
35
|
+
assert len(j.conditions) == 2
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_all_join_types() -> None:
|
|
39
|
+
for join_type in JoinEnum:
|
|
40
|
+
j = Join(join=join_type, table="t")
|
|
41
|
+
assert j.join == join_type
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_join_returns_query_for_chaining(sql_dialect: SQLDialect, mock_db) -> None:
|
|
45
|
+
q = SelectQuery(sql_dialect, "users", database=mock_db)
|
|
46
|
+
returned = q.inner_join("posts").left_join("comments").where_equals("active", 1)
|
|
47
|
+
assert returned is q
|
|
48
|
+
assert [j.join for j in q.joins] == [JoinEnum.INNER_JOIN, JoinEnum.LEFT_JOIN]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_join_callback_receives_join(sql_dialect: SQLDialect, mock_db) -> None:
|
|
52
|
+
q = SelectQuery(sql_dialect, "users", database=mock_db)
|
|
53
|
+
q.inner_join("posts", lambda join: join.on(["users", "id"], ["posts", "user_id"]))
|
|
54
|
+
assert len(q.joins) == 1
|
|
55
|
+
assert len(q.joins[0].conditions) == 1
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_join_callback_returning_none_keeps_join(sql_dialect: SQLDialect, mock_db) -> None:
|
|
59
|
+
def on(join: Join) -> None:
|
|
60
|
+
join.on(["users", "id"], ["posts", "user_id"])
|
|
61
|
+
|
|
62
|
+
q = SelectQuery(sql_dialect, "users", database=mock_db)
|
|
63
|
+
q.inner_join("posts", on)
|
|
64
|
+
assert len(q.joins) == 1
|
|
65
|
+
assert len(q.joins[0].conditions) == 1
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_join_callback_returning_non_join_cancels_join(sql_dialect: SQLDialect, mock_db) -> None:
|
|
69
|
+
def on(join: Join) -> Any:
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
q = SelectQuery(sql_dialect, "users", database=mock_db)
|
|
73
|
+
q.inner_join("posts", on)
|
|
74
|
+
assert q.joins == []
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_join_table_alias_wraps_table(sql_dialect: SQLDialect, mock_db) -> None:
|
|
78
|
+
q = SelectQuery(sql_dialect, "users", database=mock_db)
|
|
79
|
+
q.left_join_table("posts", None, "p")
|
|
80
|
+
assert isinstance(q.joins[0].table, Alias)
|
|
@@ -107,7 +107,6 @@ def test_select_where_group(sql_dialect: SQLDialect, mock_db) -> None:
|
|
|
107
107
|
|
|
108
108
|
def test_select_join(sql_dialect: SQLDialect, mock_db) -> None:
|
|
109
109
|
q: SelectQuery = SelectQuery(sql_dialect, "users", database=mock_db)
|
|
110
|
-
|
|
111
|
-
j.on("users.id", "posts.user_id")
|
|
110
|
+
q.left_join("posts", lambda join: join.on("users.id", "posts.user_id"))
|
|
112
111
|
qwp: QueryWithParams = q.to_query_with_params()
|
|
113
112
|
assert "LEFT JOIN" in qwp.query
|