././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1727729038.9927337 python_sql-1.5.2/0000755000175000017500000000000014676606617012052 5ustar00cedced././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1726938994.0 python_sql-1.5.2/.flake80000644000175000017500000000005114673577562013224 0ustar00cedced[flake8] ignore=E123,E124,E126,E128,W503 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1727728859.0 python_sql-1.5.2/.gitlab-ci.yml0000644000175000017500000000252014676606333014500 0ustar00cedcedworkflow: rules: - if: $CI_COMMIT_BRANCH =~ /^topic\/.*/ && $CI_PIPELINE_SOURCE == "push" when: never - when: always stages: - check - test .check: stage: check image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/tryton/ci check-flake8: extends: .check script: - flake8 check-isort: extends: .check script: - isort -m VERTICAL_GRID -c . check-dist: extends: .check before_script: - pip install twine script: - python setup.py sdist - twine check dist/* .test: stage: test .test-tox: extends: .test variables: PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" cache: paths: - .cache/pip before_script: - pip install tox coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/' artifacts: reports: junit: junit.xml coverage_report: coverage_format: cobertura path: coverage.xml test-tox-python: extends: .test-tox image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/python:${PYTHON_VERSION} script: - tox -e "py${PYTHON_VERSION/./}" -- -v --output-file junit.xml parallel: matrix: - PYTHON_VERSION: ["3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] test-tox-pypy: extends: .test-tox image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/pypy:3 script: - tox -e pypy3 -- -v --output-file junit.xml ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1727729020.0 python_sql-1.5.2/.hgtags0000644000175000017500000000177014676606574013337 0ustar00cedced2543c23b577ce86260954129b1fb002628ffa80c 0.1 e8322fa5b1148647989c52214f07c55a8ce457bb 0.2 00e4b65c6a964f1fb1d42c9f5e65fe0deda0de57 0.3 45063df05605c7ca496de0c5f4adddc0b135bd4f 0.4 aca607de1cf8b18388aac9e5aa11da9e922ab976 0.5 43793e923bac2c7c293340dfa595736ecf6d92a4 0.6 cc2ba29c02bc0647f1feb3ff2f49df27af3dd9d6 0.7 5ef77ab47a7bdaaf568ae1c5b3f1b0698ee2418c 0.8 e3bdeb99dd975024e30d8af18c324a0a7f860e63 0.9 7459778aa23150aa6ac39356621c29d368ae1f36 1.0.0 194182e5b0e2dc6486a32778860f29c80c0672f9 1.1.0 de68c850bc6a809b0c88ddbe2fa99b02df07bee3 1.2.0 b2bcc0f71f6881316c11330c07de34113f088888 1.2.1 1c38ffeacbb82a9ff6ae3568cdc017dbbeddff5d 1.2.2 edc03ee84f0ac96d403d8f984d59fffa3274cd2f 1.3.0 a317c40a4d60089ba9e465fbd64b78df24f9e890 1.4.0 e71bbae3398cb6a0e72f97a0cada9fcdee2bddea 1.4.1 fcb64787b51db2068061eb4aa13825abc1134916 1.4.2 111e3e86865360f83a65c04fa48c55f3d2957ee3 1.4.3 6f9066b83fe3a8c4699a8555ad1bc406f18974ff 1.5.0 79a69b0bbbd35a8d95e1b754ed3feb03df23fb70 1.5.1 41b0aaa68f5e5bab3889fa1ef57ef44c6c21cacf 1.5.2 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1639788479.0 python_sql-1.5.2/.isort.cfg0000644000175000017500000000003714157227677013750 0ustar00cedced[settings] multi_line_output=4 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1727728915.0 python_sql-1.5.2/CHANGELOG0000644000175000017500000000646014676606423013265 0ustar00cedcedVersion 1.5.2 - 2024-09-30 * Use parameter for unary operator * Support default values when inserting not matched merge * Replace assert by ValueError Version 1.5.1 - 2024-05-28 * Use parameter for start and end of WINDOW FRAME * Use parameter for limit and offset Version 1.5.0 - 2024-05-13 * Skip alias on INSERT without ON CONFLICT or RETURNING * Add MERGE * Support UPSERT * Remove default escape char on LIKE and ILIKE * Add GROUPING SETS, CUBE, and ROLLUP Version 1.4.3 - 2023-12-30 * Render common table expression in combining query * Add support for Python 3.12 Version 1.4.2 - 2023-06-25 * Restore usage of alias in returning expression Version 1.4.1 - 2023-06-16 * Do not use alias in returning expression Version 1.4.0 - 2022-05-02 * Use unittest discover * Use only column name for INSERT and UPDATE * Add escape to Like operators * Add default literal '*' expression to Count * Add support for Python 3.10 Version 1.3.0 - 2021-09-14 * Add GROUPS frame to Window * Add exclude to Window * Add method for each type of join * Remove support for Python older than 3.5 * Support Select queries in conditionals and functions Version 1.2.2 - 2021-05-16 * Escape table and column names containing double quote * Use alias on the main table used in UPDATE and INSERT * Add support for Python 3.9 Version 1.2.1 - 2020-10-08 * Fix AttributeError in Select.params with windows Version 1.2.0 - 2020-10-05 * Add explicit Windows to Select * Fix missing Windows definitions in nested expressions Version 1.1.0 - 2020-01-25 * Add ORDER BY clause to aggregate functions * Add support for Python 3.8 * Add distinct on Select Version 1.0.0 - 2018-09-30 * Add Flavor filter_ to fallback to case expression * Allow to use expression in AtTimeZone * Fix Select query in returning * Add comparison predicates * Add COLLATE Version 0.9 - 2017-04-24 * Add distinct_on on Select * Allow to use Select as Column of Select query * Support Select without from clause Version 0.8 - 2015-09-19 * Add DISTINCT qualifier to aggregate expressions * Allow to order on select queries * Add NULL ordering * Use UPPER to simulate missing ILIKE * Add CURRENT_DATE function * Fix DateTrunc function name * Add no_boolean Flavor * Add converter format2numeric * Add rownum limit style * Add no_as Flavor Version 0.7 - 2015-05-19 * Fix WINDOW and HAVING params order in Select * Add window functions * Add filter and within group to aggregate * Add limitstyle with 'offset' and 'limit' * Add Lateral Version 0.6 - 2015-02-05 * Fix Delete query under multithread * Add missing quote for with query name Version 0.5 - 2015-01-29 * Add schema * Add Common Table Expression * Escape Mod operator '%' with format paramstyle * Deprecate Interesect in favor of Intersect * Add Values Version 0.4 - 2014-08-03 * Fix Mapping in Trim function * Add __truediv__ alias of __div__ for Python3 * Fix For tables setter when tables is not a list * Allow to add Table to existing From list * Fix Select with for_ clause * Add Null alias for None * Add missing format of operand in Unary operator Version 0.3 - 2014-01-31 * Add returning to Update * Add missing params for returning * Add columns definitions to Function * Fix AtTimeZone mapping Version 0.2 - 2013-09-18 * Fix usage mixture between Div operator and function * Add array support in operators Version 0.1 - 2013-08-28 * Initial release ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1727728981.0 python_sql-1.5.2/COPYRIGHT0000644000175000017500000000305014676606525013341 0ustar00cedcedCopyright (c) 2011-2024, Cédric Krier Copyright (c) 2013-2023, Nicolas Évrard Copyright (c) 2011-2024, B2CK All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677791559.0 python_sql-1.5.2/MANIFEST.in0000644000175000017500000000006714400210507013561 0ustar00cedcedinclude COPYRIGHT include README.rst include CHANGELOG ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1727729038.9927337 python_sql-1.5.2/PKG-INFO0000644000175000017500000002050214676606617013146 0ustar00cedcedMetadata-Version: 2.1 Name: python-sql Version: 1.5.2 Summary: Library to write SQL queries Home-page: https://pypi.org/project/python-sql/ Download-URL: https://downloads.tryton.org/python-sql/ Author: Tryton Author-email: foundation@tryton.org License: BSD Project-URL: Bug Tracker, https://bugs.tryton.org/python-sql Project-URL: Forum, https://discuss.tryton.org/tags/python-sql Project-URL: Source Code, https://code.tryton.org/python-sql Keywords: SQL database query Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Topic :: Database Classifier: Topic :: Software Development :: Libraries :: Python Modules Requires-Python: >=3.5 python-sql ========== python-sql is a library to write SQL queries in a pythonic way. Nutshell -------- Import:: >>> from sql import * >>> from sql.aggregate import * >>> from sql.conditionals import * Simple selects:: >>> user = Table('user') >>> select = user.select() >>> tuple(select) ('SELECT * FROM "user" AS "a"', ()) >>> select = user.select(user.name) >>> tuple(select) ('SELECT "a"."name" FROM "user" AS "a"', ()) >>> select = user.select(Count(Literal(1))) >>> tuple(select) ('SELECT COUNT(%s) FROM "user" AS "a"', (1,)) >>> select = user.select(user.name, distinct=True) >>> tuple(select) ('SELECT DISTINCT "a"."name" FROM "user" AS "a"', ()) >>> select = user.select(user.id, user.name) >>> tuple(select) ('SELECT "a"."id", "a"."name" FROM "user" AS "a"', ()) Select with where condition:: >>> select.where = user.name == 'foo' >>> tuple(select) ('SELECT "a"."id", "a"."name" FROM "user" AS "a" WHERE ("a"."name" = %s)', ('foo',)) >>> select.where = (user.name == 'foo') & (user.active == True) >>> tuple(select) ('SELECT "a"."id", "a"."name" FROM "user" AS "a" WHERE (("a"."name" = %s) AND ("a"."active" = %s))', ('foo', True)) >>> select.where = user.name == user.login >>> tuple(select) ('SELECT "a"."id", "a"."name" FROM "user" AS "a" WHERE ("a"."name" = "a"."login")', ()) Select with join:: >>> join = user.join(Table('user_group')) >>> join.condition = join.right.user == user.id >>> select = join.select(user.name, join.right.group) >>> tuple(select) ('SELECT "a"."name", "b"."group" FROM "user" AS "a" INNER JOIN "user_group" AS "b" ON ("b"."user" = "a"."id")', ()) Select with multiple joins:: >>> join1 = user.join(Table('user')) >>> join2 = join1.join(Table('user')) >>> select = join2.select(user.id, join1.right.id, join2.right.id) >>> tuple(select) ('SELECT "a"."id", "b"."id", "c"."id" FROM "user" AS "a" INNER JOIN "user" AS "b" INNER JOIN "user" AS "c"', ()) Select with group_by:: >>> invoice = Table('invoice') >>> select = invoice.select(Sum(invoice.amount), invoice.currency, ... group_by=invoice.currency) >>> tuple(select) ('SELECT SUM("a"."amount"), "a"."currency" FROM "invoice" AS "a" GROUP BY "a"."currency"', ()) Select with output name:: >>> tuple(user.select(user.name.as_('First Name'))) ('SELECT "a"."name" AS "First Name" FROM "user" AS "a"', ()) Select with order_by:: >>> tuple(user.select(order_by=user.date)) ('SELECT * FROM "user" AS "a" ORDER BY "a"."date"', ()) >>> tuple(user.select(order_by=Asc(user.date))) ('SELECT * FROM "user" AS "a" ORDER BY "a"."date" ASC', ()) >>> tuple(user.select(order_by=(user.date.asc, user.id.desc))) ('SELECT * FROM "user" AS "a" ORDER BY "a"."date" ASC, "a"."id" DESC', ()) Select with sub-select:: >>> user_group = Table('user_group') >>> subselect = user_group.select(user_group.user, ... where=user_group.active == True) >>> user = Table('user') >>> tuple(user.select(user.id, where=user.id.in_(subselect))) ('SELECT "a"."id" FROM "user" AS "a" WHERE ("a"."id" IN (SELECT "b"."user" FROM "user_group" AS "b" WHERE ("b"."active" = %s)))', (True,)) >>> tuple(subselect.select(subselect.user)) ('SELECT "a"."user" FROM (SELECT "b"."user" FROM "user_group" AS "b" WHERE ("b"."active" = %s)) AS "a"', (True,)) Select on other schema:: >>> other_table = Table('user', 'myschema') >>> tuple(other_table.select()) ('SELECT * FROM "myschema"."user" AS "a"', ()) Insert query with default values:: >>> tuple(user.insert()) ('INSERT INTO "user" AS "a" DEFAULT VALUES', ()) Insert query with values:: >>> tuple(user.insert(columns=[user.name, user.login], ... values=[['Foo', 'foo']])) ('INSERT INTO "user" AS "a" ("name", "login") VALUES (%s, %s)', ('Foo', 'foo')) >>> tuple(user.insert(columns=[user.name, user.login], ... values=[['Foo', 'foo'], ['Bar', 'bar']])) ('INSERT INTO "user" AS "a" ("name", "login") VALUES (%s, %s), (%s, %s)', ('Foo', 'foo', 'Bar', 'bar')) Insert query with query:: >>> passwd = Table('passwd') >>> select = passwd.select(passwd.login, passwd.passwd) >>> tuple(user.insert(values=select)) ('INSERT INTO "user" AS "b" SELECT "a"."login", "a"."passwd" FROM "passwd" AS "a"', ()) Update query with values:: >>> tuple(user.update(columns=[user.active], values=[True])) ('UPDATE "user" AS "a" SET "active" = %s', (True,)) >>> tuple(invoice.update(columns=[invoice.total], values=[invoice.amount + invoice.tax])) ('UPDATE "invoice" AS "a" SET "total" = ("a"."amount" + "a"."tax")', ()) Update query with where condition:: >>> tuple(user.update(columns=[user.active], values=[True], ... where=user.active == False)) ('UPDATE "user" AS "a" SET "active" = %s WHERE ("a"."active" = %s)', (True, False)) Update query with from list:: >>> group = Table('user_group') >>> tuple(user.update(columns=[user.active], values=[group.active], ... from_=[group], where=user.id == group.user)) ('UPDATE "user" AS "b" SET "active" = "a"."active" FROM "user_group" AS "a" WHERE ("b"."id" = "a"."user")', ()) Delete query:: >>> tuple(user.delete()) ('DELETE FROM "user"', ()) Delete query with where condition:: >>> tuple(user.delete(where=user.name == 'foo')) ('DELETE FROM "user" WHERE ("name" = %s)', ('foo',)) Delete query with sub-query:: >>> tuple(user.delete( ... where=user.id.in_(user_group.select(user_group.user)))) ('DELETE FROM "user" WHERE ("id" IN (SELECT "a"."user" FROM "user_group" AS "a"))', ()) Flavors:: >>> select = user.select() >>> select.offset = 10 >>> Flavor.set(Flavor()) >>> tuple(select) ('SELECT * FROM "user" AS "a" OFFSET 10', ()) >>> Flavor.set(Flavor(max_limit=18446744073709551615)) >>> tuple(select) ('SELECT * FROM "user" AS "a" LIMIT 18446744073709551615 OFFSET 10', ()) >>> Flavor.set(Flavor(max_limit=-1)) >>> tuple(select) ('SELECT * FROM "user" AS "a" LIMIT -1 OFFSET 10', ()) Limit style:: >>> select = user.select(limit=10, offset=20) >>> Flavor.set(Flavor(limitstyle='limit')) >>> tuple(select) ('SELECT * FROM "user" AS "a" LIMIT 10 OFFSET 20', ()) >>> Flavor.set(Flavor(limitstyle='fetch')) >>> tuple(select) ('SELECT * FROM "user" AS "a" OFFSET (20) ROWS FETCH FIRST (10) ROWS ONLY', ()) >>> Flavor.set(Flavor(limitstyle='rownum')) >>> tuple(select) ('SELECT "a".* FROM (SELECT "b".*, ROWNUM AS "rnum" FROM (SELECT * FROM "user" AS "c") AS "b" WHERE (ROWNUM <= %s)) AS "a" WHERE ("rnum" > %s)', (30, 20)) qmark style:: >>> Flavor.set(Flavor(paramstyle='qmark')) >>> select = user.select() >>> select.where = user.name == 'foo' >>> tuple(select) ('SELECT * FROM "user" AS "a" WHERE ("a"."name" = ?)', ('foo',)) numeric style:: >>> Flavor.set(Flavor(paramstyle='format')) >>> select = user.select() >>> select.where = user.name == 'foo' >>> format2numeric(*select) ('SELECT * FROM "user" AS "a" WHERE ("a"."name" = :0)', ('foo',)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1631274613.0 python_sql-1.5.2/README.rst0000644000175000017500000001614114116643165013530 0ustar00cedcedpython-sql ========== python-sql is a library to write SQL queries in a pythonic way. Nutshell -------- Import:: >>> from sql import * >>> from sql.aggregate import * >>> from sql.conditionals import * Simple selects:: >>> user = Table('user') >>> select = user.select() >>> tuple(select) ('SELECT * FROM "user" AS "a"', ()) >>> select = user.select(user.name) >>> tuple(select) ('SELECT "a"."name" FROM "user" AS "a"', ()) >>> select = user.select(Count(Literal(1))) >>> tuple(select) ('SELECT COUNT(%s) FROM "user" AS "a"', (1,)) >>> select = user.select(user.name, distinct=True) >>> tuple(select) ('SELECT DISTINCT "a"."name" FROM "user" AS "a"', ()) >>> select = user.select(user.id, user.name) >>> tuple(select) ('SELECT "a"."id", "a"."name" FROM "user" AS "a"', ()) Select with where condition:: >>> select.where = user.name == 'foo' >>> tuple(select) ('SELECT "a"."id", "a"."name" FROM "user" AS "a" WHERE ("a"."name" = %s)', ('foo',)) >>> select.where = (user.name == 'foo') & (user.active == True) >>> tuple(select) ('SELECT "a"."id", "a"."name" FROM "user" AS "a" WHERE (("a"."name" = %s) AND ("a"."active" = %s))', ('foo', True)) >>> select.where = user.name == user.login >>> tuple(select) ('SELECT "a"."id", "a"."name" FROM "user" AS "a" WHERE ("a"."name" = "a"."login")', ()) Select with join:: >>> join = user.join(Table('user_group')) >>> join.condition = join.right.user == user.id >>> select = join.select(user.name, join.right.group) >>> tuple(select) ('SELECT "a"."name", "b"."group" FROM "user" AS "a" INNER JOIN "user_group" AS "b" ON ("b"."user" = "a"."id")', ()) Select with multiple joins:: >>> join1 = user.join(Table('user')) >>> join2 = join1.join(Table('user')) >>> select = join2.select(user.id, join1.right.id, join2.right.id) >>> tuple(select) ('SELECT "a"."id", "b"."id", "c"."id" FROM "user" AS "a" INNER JOIN "user" AS "b" INNER JOIN "user" AS "c"', ()) Select with group_by:: >>> invoice = Table('invoice') >>> select = invoice.select(Sum(invoice.amount), invoice.currency, ... group_by=invoice.currency) >>> tuple(select) ('SELECT SUM("a"."amount"), "a"."currency" FROM "invoice" AS "a" GROUP BY "a"."currency"', ()) Select with output name:: >>> tuple(user.select(user.name.as_('First Name'))) ('SELECT "a"."name" AS "First Name" FROM "user" AS "a"', ()) Select with order_by:: >>> tuple(user.select(order_by=user.date)) ('SELECT * FROM "user" AS "a" ORDER BY "a"."date"', ()) >>> tuple(user.select(order_by=Asc(user.date))) ('SELECT * FROM "user" AS "a" ORDER BY "a"."date" ASC', ()) >>> tuple(user.select(order_by=(user.date.asc, user.id.desc))) ('SELECT * FROM "user" AS "a" ORDER BY "a"."date" ASC, "a"."id" DESC', ()) Select with sub-select:: >>> user_group = Table('user_group') >>> subselect = user_group.select(user_group.user, ... where=user_group.active == True) >>> user = Table('user') >>> tuple(user.select(user.id, where=user.id.in_(subselect))) ('SELECT "a"."id" FROM "user" AS "a" WHERE ("a"."id" IN (SELECT "b"."user" FROM "user_group" AS "b" WHERE ("b"."active" = %s)))', (True,)) >>> tuple(subselect.select(subselect.user)) ('SELECT "a"."user" FROM (SELECT "b"."user" FROM "user_group" AS "b" WHERE ("b"."active" = %s)) AS "a"', (True,)) Select on other schema:: >>> other_table = Table('user', 'myschema') >>> tuple(other_table.select()) ('SELECT * FROM "myschema"."user" AS "a"', ()) Insert query with default values:: >>> tuple(user.insert()) ('INSERT INTO "user" AS "a" DEFAULT VALUES', ()) Insert query with values:: >>> tuple(user.insert(columns=[user.name, user.login], ... values=[['Foo', 'foo']])) ('INSERT INTO "user" AS "a" ("name", "login") VALUES (%s, %s)', ('Foo', 'foo')) >>> tuple(user.insert(columns=[user.name, user.login], ... values=[['Foo', 'foo'], ['Bar', 'bar']])) ('INSERT INTO "user" AS "a" ("name", "login") VALUES (%s, %s), (%s, %s)', ('Foo', 'foo', 'Bar', 'bar')) Insert query with query:: >>> passwd = Table('passwd') >>> select = passwd.select(passwd.login, passwd.passwd) >>> tuple(user.insert(values=select)) ('INSERT INTO "user" AS "b" SELECT "a"."login", "a"."passwd" FROM "passwd" AS "a"', ()) Update query with values:: >>> tuple(user.update(columns=[user.active], values=[True])) ('UPDATE "user" AS "a" SET "active" = %s', (True,)) >>> tuple(invoice.update(columns=[invoice.total], values=[invoice.amount + invoice.tax])) ('UPDATE "invoice" AS "a" SET "total" = ("a"."amount" + "a"."tax")', ()) Update query with where condition:: >>> tuple(user.update(columns=[user.active], values=[True], ... where=user.active == False)) ('UPDATE "user" AS "a" SET "active" = %s WHERE ("a"."active" = %s)', (True, False)) Update query with from list:: >>> group = Table('user_group') >>> tuple(user.update(columns=[user.active], values=[group.active], ... from_=[group], where=user.id == group.user)) ('UPDATE "user" AS "b" SET "active" = "a"."active" FROM "user_group" AS "a" WHERE ("b"."id" = "a"."user")', ()) Delete query:: >>> tuple(user.delete()) ('DELETE FROM "user"', ()) Delete query with where condition:: >>> tuple(user.delete(where=user.name == 'foo')) ('DELETE FROM "user" WHERE ("name" = %s)', ('foo',)) Delete query with sub-query:: >>> tuple(user.delete( ... where=user.id.in_(user_group.select(user_group.user)))) ('DELETE FROM "user" WHERE ("id" IN (SELECT "a"."user" FROM "user_group" AS "a"))', ()) Flavors:: >>> select = user.select() >>> select.offset = 10 >>> Flavor.set(Flavor()) >>> tuple(select) ('SELECT * FROM "user" AS "a" OFFSET 10', ()) >>> Flavor.set(Flavor(max_limit=18446744073709551615)) >>> tuple(select) ('SELECT * FROM "user" AS "a" LIMIT 18446744073709551615 OFFSET 10', ()) >>> Flavor.set(Flavor(max_limit=-1)) >>> tuple(select) ('SELECT * FROM "user" AS "a" LIMIT -1 OFFSET 10', ()) Limit style:: >>> select = user.select(limit=10, offset=20) >>> Flavor.set(Flavor(limitstyle='limit')) >>> tuple(select) ('SELECT * FROM "user" AS "a" LIMIT 10 OFFSET 20', ()) >>> Flavor.set(Flavor(limitstyle='fetch')) >>> tuple(select) ('SELECT * FROM "user" AS "a" OFFSET (20) ROWS FETCH FIRST (10) ROWS ONLY', ()) >>> Flavor.set(Flavor(limitstyle='rownum')) >>> tuple(select) ('SELECT "a".* FROM (SELECT "b".*, ROWNUM AS "rnum" FROM (SELECT * FROM "user" AS "c") AS "b" WHERE (ROWNUM <= %s)) AS "a" WHERE ("rnum" > %s)', (30, 20)) qmark style:: >>> Flavor.set(Flavor(paramstyle='qmark')) >>> select = user.select() >>> select.where = user.name == 'foo' >>> tuple(select) ('SELECT * FROM "user" AS "a" WHERE ("a"."name" = ?)', ('foo',)) numeric style:: >>> Flavor.set(Flavor(paramstyle='format')) >>> select = user.select() >>> select.where = user.name == 'foo' >>> format2numeric(*select) ('SELECT * FROM "user" AS "a" WHERE ("a"."name" = :0)', ('foo',)) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1727729038.9927337 python_sql-1.5.2/python_sql.egg-info/0000755000175000017500000000000014676606617015744 5ustar00cedced././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1727729038.0 python_sql-1.5.2/python_sql.egg-info/PKG-INFO0000644000175000017500000002050214676606616017037 0ustar00cedcedMetadata-Version: 2.1 Name: python-sql Version: 1.5.2 Summary: Library to write SQL queries Home-page: https://pypi.org/project/python-sql/ Download-URL: https://downloads.tryton.org/python-sql/ Author: Tryton Author-email: foundation@tryton.org License: BSD Project-URL: Bug Tracker, https://bugs.tryton.org/python-sql Project-URL: Forum, https://discuss.tryton.org/tags/python-sql Project-URL: Source Code, https://code.tryton.org/python-sql Keywords: SQL database query Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Topic :: Database Classifier: Topic :: Software Development :: Libraries :: Python Modules Requires-Python: >=3.5 python-sql ========== python-sql is a library to write SQL queries in a pythonic way. Nutshell -------- Import:: >>> from sql import * >>> from sql.aggregate import * >>> from sql.conditionals import * Simple selects:: >>> user = Table('user') >>> select = user.select() >>> tuple(select) ('SELECT * FROM "user" AS "a"', ()) >>> select = user.select(user.name) >>> tuple(select) ('SELECT "a"."name" FROM "user" AS "a"', ()) >>> select = user.select(Count(Literal(1))) >>> tuple(select) ('SELECT COUNT(%s) FROM "user" AS "a"', (1,)) >>> select = user.select(user.name, distinct=True) >>> tuple(select) ('SELECT DISTINCT "a"."name" FROM "user" AS "a"', ()) >>> select = user.select(user.id, user.name) >>> tuple(select) ('SELECT "a"."id", "a"."name" FROM "user" AS "a"', ()) Select with where condition:: >>> select.where = user.name == 'foo' >>> tuple(select) ('SELECT "a"."id", "a"."name" FROM "user" AS "a" WHERE ("a"."name" = %s)', ('foo',)) >>> select.where = (user.name == 'foo') & (user.active == True) >>> tuple(select) ('SELECT "a"."id", "a"."name" FROM "user" AS "a" WHERE (("a"."name" = %s) AND ("a"."active" = %s))', ('foo', True)) >>> select.where = user.name == user.login >>> tuple(select) ('SELECT "a"."id", "a"."name" FROM "user" AS "a" WHERE ("a"."name" = "a"."login")', ()) Select with join:: >>> join = user.join(Table('user_group')) >>> join.condition = join.right.user == user.id >>> select = join.select(user.name, join.right.group) >>> tuple(select) ('SELECT "a"."name", "b"."group" FROM "user" AS "a" INNER JOIN "user_group" AS "b" ON ("b"."user" = "a"."id")', ()) Select with multiple joins:: >>> join1 = user.join(Table('user')) >>> join2 = join1.join(Table('user')) >>> select = join2.select(user.id, join1.right.id, join2.right.id) >>> tuple(select) ('SELECT "a"."id", "b"."id", "c"."id" FROM "user" AS "a" INNER JOIN "user" AS "b" INNER JOIN "user" AS "c"', ()) Select with group_by:: >>> invoice = Table('invoice') >>> select = invoice.select(Sum(invoice.amount), invoice.currency, ... group_by=invoice.currency) >>> tuple(select) ('SELECT SUM("a"."amount"), "a"."currency" FROM "invoice" AS "a" GROUP BY "a"."currency"', ()) Select with output name:: >>> tuple(user.select(user.name.as_('First Name'))) ('SELECT "a"."name" AS "First Name" FROM "user" AS "a"', ()) Select with order_by:: >>> tuple(user.select(order_by=user.date)) ('SELECT * FROM "user" AS "a" ORDER BY "a"."date"', ()) >>> tuple(user.select(order_by=Asc(user.date))) ('SELECT * FROM "user" AS "a" ORDER BY "a"."date" ASC', ()) >>> tuple(user.select(order_by=(user.date.asc, user.id.desc))) ('SELECT * FROM "user" AS "a" ORDER BY "a"."date" ASC, "a"."id" DESC', ()) Select with sub-select:: >>> user_group = Table('user_group') >>> subselect = user_group.select(user_group.user, ... where=user_group.active == True) >>> user = Table('user') >>> tuple(user.select(user.id, where=user.id.in_(subselect))) ('SELECT "a"."id" FROM "user" AS "a" WHERE ("a"."id" IN (SELECT "b"."user" FROM "user_group" AS "b" WHERE ("b"."active" = %s)))', (True,)) >>> tuple(subselect.select(subselect.user)) ('SELECT "a"."user" FROM (SELECT "b"."user" FROM "user_group" AS "b" WHERE ("b"."active" = %s)) AS "a"', (True,)) Select on other schema:: >>> other_table = Table('user', 'myschema') >>> tuple(other_table.select()) ('SELECT * FROM "myschema"."user" AS "a"', ()) Insert query with default values:: >>> tuple(user.insert()) ('INSERT INTO "user" AS "a" DEFAULT VALUES', ()) Insert query with values:: >>> tuple(user.insert(columns=[user.name, user.login], ... values=[['Foo', 'foo']])) ('INSERT INTO "user" AS "a" ("name", "login") VALUES (%s, %s)', ('Foo', 'foo')) >>> tuple(user.insert(columns=[user.name, user.login], ... values=[['Foo', 'foo'], ['Bar', 'bar']])) ('INSERT INTO "user" AS "a" ("name", "login") VALUES (%s, %s), (%s, %s)', ('Foo', 'foo', 'Bar', 'bar')) Insert query with query:: >>> passwd = Table('passwd') >>> select = passwd.select(passwd.login, passwd.passwd) >>> tuple(user.insert(values=select)) ('INSERT INTO "user" AS "b" SELECT "a"."login", "a"."passwd" FROM "passwd" AS "a"', ()) Update query with values:: >>> tuple(user.update(columns=[user.active], values=[True])) ('UPDATE "user" AS "a" SET "active" = %s', (True,)) >>> tuple(invoice.update(columns=[invoice.total], values=[invoice.amount + invoice.tax])) ('UPDATE "invoice" AS "a" SET "total" = ("a"."amount" + "a"."tax")', ()) Update query with where condition:: >>> tuple(user.update(columns=[user.active], values=[True], ... where=user.active == False)) ('UPDATE "user" AS "a" SET "active" = %s WHERE ("a"."active" = %s)', (True, False)) Update query with from list:: >>> group = Table('user_group') >>> tuple(user.update(columns=[user.active], values=[group.active], ... from_=[group], where=user.id == group.user)) ('UPDATE "user" AS "b" SET "active" = "a"."active" FROM "user_group" AS "a" WHERE ("b"."id" = "a"."user")', ()) Delete query:: >>> tuple(user.delete()) ('DELETE FROM "user"', ()) Delete query with where condition:: >>> tuple(user.delete(where=user.name == 'foo')) ('DELETE FROM "user" WHERE ("name" = %s)', ('foo',)) Delete query with sub-query:: >>> tuple(user.delete( ... where=user.id.in_(user_group.select(user_group.user)))) ('DELETE FROM "user" WHERE ("id" IN (SELECT "a"."user" FROM "user_group" AS "a"))', ()) Flavors:: >>> select = user.select() >>> select.offset = 10 >>> Flavor.set(Flavor()) >>> tuple(select) ('SELECT * FROM "user" AS "a" OFFSET 10', ()) >>> Flavor.set(Flavor(max_limit=18446744073709551615)) >>> tuple(select) ('SELECT * FROM "user" AS "a" LIMIT 18446744073709551615 OFFSET 10', ()) >>> Flavor.set(Flavor(max_limit=-1)) >>> tuple(select) ('SELECT * FROM "user" AS "a" LIMIT -1 OFFSET 10', ()) Limit style:: >>> select = user.select(limit=10, offset=20) >>> Flavor.set(Flavor(limitstyle='limit')) >>> tuple(select) ('SELECT * FROM "user" AS "a" LIMIT 10 OFFSET 20', ()) >>> Flavor.set(Flavor(limitstyle='fetch')) >>> tuple(select) ('SELECT * FROM "user" AS "a" OFFSET (20) ROWS FETCH FIRST (10) ROWS ONLY', ()) >>> Flavor.set(Flavor(limitstyle='rownum')) >>> tuple(select) ('SELECT "a".* FROM (SELECT "b".*, ROWNUM AS "rnum" FROM (SELECT * FROM "user" AS "c") AS "b" WHERE (ROWNUM <= %s)) AS "a" WHERE ("rnum" > %s)', (30, 20)) qmark style:: >>> Flavor.set(Flavor(paramstyle='qmark')) >>> select = user.select() >>> select.where = user.name == 'foo' >>> tuple(select) ('SELECT * FROM "user" AS "a" WHERE ("a"."name" = ?)', ('foo',)) numeric style:: >>> Flavor.set(Flavor(paramstyle='format')) >>> select = user.select() >>> select.where = user.name == 'foo' >>> format2numeric(*select) ('SELECT * FROM "user" AS "a" WHERE ("a"."name" = :0)', ('foo',)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1727729038.0 python_sql-1.5.2/python_sql.egg-info/SOURCES.txt0000644000175000017500000000216414676606616017632 0ustar00cedced.flake8 .gitlab-ci.yml .hgtags .isort.cfg CHANGELOG COPYRIGHT MANIFEST.in README.rst setup.py tox.ini python_sql.egg-info/PKG-INFO python_sql.egg-info/SOURCES.txt python_sql.egg-info/dependency_links.txt python_sql.egg-info/top_level.txt sql/__init__.py sql/aggregate.py sql/conditionals.py sql/functions.py sql/operators.py sql/tests/__init__.py sql/tests/test_aggregate.py sql/tests/test_alias.py sql/tests/test_as.py sql/tests/test_cast.py sql/tests/test_collate.py sql/tests/test_column.py sql/tests/test_combining_query.py sql/tests/test_conditionals.py sql/tests/test_delete.py sql/tests/test_excluded.py sql/tests/test_expression.py sql/tests/test_flavor.py sql/tests/test_for.py sql/tests/test_from.py sql/tests/test_from_item.py sql/tests/test_functions.py sql/tests/test_grouping.py sql/tests/test_insert.py sql/tests/test_join.py sql/tests/test_lateral.py sql/tests/test_literal.py sql/tests/test_merge.py sql/tests/test_operators.py sql/tests/test_order.py sql/tests/test_rollup.py sql/tests/test_select.py sql/tests/test_table.py sql/tests/test_update.py sql/tests/test_values.py sql/tests/test_window.py sql/tests/test_with.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1727729038.0 python_sql-1.5.2/python_sql.egg-info/dependency_links.txt0000644000175000017500000000000114676606616022011 0ustar00cedced ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1727729038.0 python_sql-1.5.2/python_sql.egg-info/top_level.txt0000644000175000017500000000000414676606616020467 0ustar00cedcedsql ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1727729038.996067 python_sql-1.5.2/setup.cfg0000644000175000017500000000004614676606617013673 0ustar00cedced[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1727728859.0 python_sql-1.5.2/setup.py0000644000175000017500000000360214676606333013560 0ustar00cedced#!/usr/bin/env python # This file is part of python-sql. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import codecs import os import re from setuptools import find_packages, setup def read(fname): return codecs.open( os.path.join(os.path.dirname(__file__), fname), 'r', 'utf-8').read() def get_version(): init = read(os.path.join('sql', '__init__.py')) return re.search("__version__ = '([0-9.]*)'", init).group(1) setup(name='python-sql', version=get_version(), description='Library to write SQL queries', long_description=read('README.rst'), author='Tryton', author_email='foundation@tryton.org', url='https://pypi.org/project/python-sql/', download_url='https://downloads.tryton.org/python-sql/', project_urls={ "Bug Tracker": 'https://bugs.tryton.org/python-sql', "Forum": 'https://discuss.tryton.org/tags/python-sql', "Source Code": 'https://code.tryton.org/python-sql', }, keywords='SQL database query', packages=find_packages(), python_requires='>=3.5', classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Topic :: Database', 'Topic :: Software Development :: Libraries :: Python Modules', ], license='BSD', ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1727729038.9827335 python_sql-1.5.2/sql/0000755000175000017500000000000014676606617012651 5ustar00cedced././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1727728859.0 python_sql-1.5.2/sql/__init__.py0000644000175000017500000017554614676606333014777 0ustar00cedced# This file is part of python-sql. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import numbers import string import warnings from collections import defaultdict from itertools import chain from threading import current_thread, local __version__ = '1.5.2' __all__ = [ 'Flavor', 'Table', 'Values', 'Literal', 'Column', 'Grouping', 'Conflict', 'Matched', 'MatchedUpdate', 'MatchedDelete', 'NotMatched', 'NotMatchedInsert', 'Rollup', 'Cube', 'Excluded', 'Join', 'Asc', 'Desc', 'NullsFirst', 'NullsLast', 'format2numeric'] def _escape_identifier(name): return '"%s"' % name.replace('"', '""') def alias(i, letters=string.ascii_lowercase): ''' Generate a unique alias based on integer >>> [alias(n) for n in range(6)] ['a', 'b', 'c', 'd', 'e', 'f'] >>> [alias(n) for n in range(26, 30)] ['ba', 'bb', 'bc', 'bd'] >>> [alias(26**n) for n in range(5)] ['b', 'ba', 'baa', 'baaa', 'baaaa'] ''' s = '' length = len(letters) while True: r = i % length s = letters[r] + s i //= length if i == 0: break return s class Flavor(object): ''' Contains the flavor of SQL Contains: limitstyle - state the type of pagination max_limit - limit to use if there is no limit but an offset paramstyle - state the type of parameter marker formatting ilike - support ilike extension no_as - doesn't support AS keyword for column and table no_boolean - doesn't support boolean type null_ordering - support NULL ordering function_mapping - dictionary with Function to replace filter_ - support filter on aggregate functions escape_empty - support empty escape ''' def __init__(self, limitstyle='limit', max_limit=None, paramstyle='format', ilike=False, no_as=False, no_boolean=False, null_ordering=True, function_mapping=None, filter_=False, escape_empty=False): if limitstyle not in {'fetch', 'limit', 'rownum'}: raise ValueError("unsupported limitstyle: %r" % limitstyle) self.limitstyle = limitstyle if (max_limit is not None and not isinstance(max_limit, numbers.Integral)): raise ValueError("unsupported max_limit: %r" % max_limit) self.max_limit = max_limit if paramstyle not in {'format', 'qmark'}: raise ValueError("unsupported paramstyle: %r" % paramstyle) self.paramstyle = paramstyle self.ilike = bool(ilike) self.no_as = bool(no_as) self.no_boolean = bool(no_boolean) self.null_ordering = bool(null_ordering) self.function_mapping = dict(function_mapping or {}) self.filter_ = bool(filter_) self.escape_empty = bool(escape_empty) @property def param(self): if self.paramstyle == 'format': return '%s' elif self.paramstyle == 'qmark': return '?' @staticmethod def set(flavor): '''Set this thread's flavor to flavor.''' current_thread().__sql_flavor__ = flavor @staticmethod def get(): ''' Return this thread's flavor. If this thread does not yet have a flavor, returns a new flavor and sets this thread's flavor. ''' try: return current_thread().__sql_flavor__ except AttributeError: flavor = Flavor() current_thread().__sql_flavor__ = flavor return flavor class AliasManager(object): ''' Context Manager for unique alias generation ''' __slots__ = () local = local() local.alias = None local.nested = 0 local.exclude = None def __init__(self, exclude=None): if exclude: if getattr(self.local, 'exclude', None) is None: self.local.exclude = [] self.local.exclude.extend(exclude) @classmethod def __enter__(cls): if getattr(cls.local, 'alias', None) is None: cls.local.alias = defaultdict(cls.alias_factory) cls.local.nested = 0 if getattr(cls.local, 'exclude', None) is None: cls.local.exclude = [] cls.local.nested += 1 @classmethod def __exit__(cls, type, value, traceback): cls.local.nested -= 1 if not cls.local.nested: cls.local.alias = None cls.local.exclude = None @classmethod def get(cls, from_): if getattr(cls.local, 'alias', None) is None: return '' if from_ in cls.local.exclude: return '' return cls.local.alias[id(from_)] @classmethod def contains(cls, from_): if getattr(cls.local, 'alias', None) is None: return False if from_ in cls.local.exclude: return False return id(from_) in cls.local.alias @classmethod def set(cls, from_, alias): assert cls.local.alias.get(from_) is None cls.local.alias[id(from_)] = alias @classmethod def alias_factory(cls): i = len(cls.local.alias) return alias(i) def format2numeric(query, params): ''' Convert format paramstyle query to numeric paramstyle >>> format2numeric('SELECT * FROM table WHERE col = %s', ('foo',)) ('SELECT * FROM table WHERE col = :0', ('foo',)) >>> format2numeric('SELECT * FROM table WHERE col1 = %s AND col2 = %s', ... ('foo', 'bar')) ('SELECT * FROM table WHERE col1 = :0 AND col2 = :1', ('foo', 'bar')) ''' return (query % tuple(':%i' % i for i, _ in enumerate(params)), params) class Query(object): __slots__ = () @property def params(self): return () def __iter__(self): yield str(self) yield self.params def __or__(self, other): return Union(self, other) def __and__(self, other): return Intersect(self, other) def __sub__(self, other): return Except(self, other) class WithQuery(Query): __slots__ = ('_with',) def __init__(self, **kwargs): self._with = None self.with_ = kwargs.pop('with_', None) super(Query, self).__init__(**kwargs) @property def with_(self): return self._with @with_.setter def with_(self, value): if value is not None: if isinstance(value, With): value = [value] if any(not isinstance(w, With) for w in value): raise ValueError("invalid with: %r" % value) self._with = value def _with_str(self): if not self.with_: return '' recursive = (' RECURSIVE' if any(w.recursive for w in self.with_) else '') with_ = ('WITH%s ' % recursive + ', '.join(w.statement() for w in self.with_) + ' ') return with_ def _with_params(self): if not self.with_: return () params = [] for w in self.with_: params.extend(w.statement_params()) return tuple(params) class FromItem(object): __slots__ = () @property def alias(self): return AliasManager.get(self) @property def has_alias(self): return AliasManager.contains(self) def __getattr__(self, name): if name.startswith('__'): raise AttributeError return Column(self, name) def __add__(self, other): if not isinstance(other, FromItem): return NotImplemented return From((self, other)) def select(self, *args, **kwargs): return From((self,)).select(*args, **kwargs) def join(self, right, type_='INNER', condition=None): return Join(self, right, type_=type_, condition=condition) def left_join(self, right, condition=None): return self.join(right, type_='LEFT', condition=condition) def left_outer_join(self, right, condition=None): return self.join(right, type_='LEFT OUTER', condition=condition) def right_join(self, right, condition=None): return self.join(right, type_='RIGHT', condition=condition) def right_outer_join(self, right, condition=None): return self.join(right, type_='RIGHT OUTER', condition=condition) def full_join(self, right, condition=None): return self.join(right, type_='FULL', condition=condition) def full_outer_join(self, right, condition=None): return self.join(right, type_='FULL OUTER', condition=condition) def cross_join(self, right, condition=None): return self.join(right, type_='CROSS', condition=condition) def lateral(self): return Lateral(self) class Lateral(FromItem): __slots__ = ('_from_item',) def __init__(self, from_item): self._from_item = from_item def __str__(self): template = '%s' if isinstance(self._from_item, Query): template = '(%s)' return 'LATERAL ' + template % self._from_item def __getattr__(self, name): return getattr(self._from_item, name) class With(FromItem): __slots__ = ('columns', 'query', 'recursive') def __init__(self, *columns, **kwargs): self.recursive = kwargs.pop('recursive', False) self.columns = columns self.query = kwargs.pop('query', None) super(With, self).__init__(**kwargs) def statement(self): columns = (' (%s)' % ', '.join('"%s"' % c for c in self.columns) if self.columns else '') return '"%s"%s AS (%s)' % (self.alias, columns, self.query) def statement_params(self): return self.query.params def __str__(self): return '"%s"' % self.alias @property def params(self): return tuple() class SelectQuery(WithQuery): __slots__ = ('_order_by', '_limit', '_offset') def __init__(self, *args, **kwargs): self._order_by = None self._limit = None self._offset = None self.order_by = kwargs.pop('order_by', None) self.limit = kwargs.pop('limit', None) self.offset = kwargs.pop('offset', None) super(SelectQuery, self).__init__(*args, **kwargs) @property def order_by(self): return self._order_by @order_by.setter def order_by(self, value): if value is not None: if isinstance(value, Expression): value = [value] if any(not isinstance(col, Expression) for col in value): raise ValueError("invalid order by: %r" % value) self._order_by = value @property def _order_by_str(self): order_by = '' if self.order_by: order_by = ' ORDER BY ' + ', '.join(map(str, self.order_by)) return order_by @property def limit(self): return self._limit @limit.setter def limit(self, value): if value is not None: if not isinstance(value, numbers.Integral): raise ValueError("invalid limit: %r" % value) self._limit = value @property def offset(self): return self._offset @offset.setter def offset(self, value): if value is not None: if not isinstance(value, numbers.Integral): raise ValueError("invalid offset: %r" % value) self._offset = value @property def _limit_offset_str(self): param = Flavor.get().param if Flavor.get().limitstyle == 'limit': offset = '' if self.offset: offset = ' OFFSET %s' % param limit = '' if self.limit is not None: limit = ' LIMIT %s' % param elif self.offset: max_limit = Flavor.get().max_limit if max_limit: limit = ' LIMIT %s' % max_limit return limit + offset else: offset = '' if self.offset: offset = ' OFFSET (%s) ROWS' % param fetch = '' if self.limit is not None: fetch = ' FETCH FIRST (%s) ROWS ONLY' % param return offset + fetch @property def _limit_offset_params(self): p = [] if Flavor.get().limitstyle == 'limit': if self.limit is not None: p.append(self.limit) if self.offset: p.append(self.offset) else: if self.offset: p.append(self.offset) if self.limit is not None: p.append(self.limit) return tuple(p) def as_(self, output_name): return As(self, output_name) class Select(FromItem, SelectQuery): __slots__ = ('_columns', '_where', '_group_by', '_having', '_for_', 'from_', '_distinct', '_distinct_on', '_windows') def __init__(self, columns, from_=None, where=None, group_by=None, having=None, for_=None, distinct=False, distinct_on=None, windows=None, **kwargs): self._distinct = False self._distinct_on = [] self._columns = None self._where = None self._group_by = None self._having = None self._for_ = None self._windows = [] super(Select, self).__init__(**kwargs) self.distinct = distinct self.distinct_on = distinct_on self.columns = columns self.from_ = from_ self.where = where self.group_by = group_by self.having = having self.for_ = for_ self.windows = windows @property def distinct(self): return bool(self._distinct or self._distinct_on) @distinct.setter def distinct(self, value): self._distinct = bool(value) @property def distinct_on(self): return self._distinct_on @distinct_on.setter def distinct_on(self, value): if value is not None: if isinstance(value, Expression): value = [value] if any(not isinstance(col, Expression) for col in value): raise ValueError("invalid distinct on: %r" % value) self._distinct_on = value @property def columns(self): return self._columns @columns.setter def columns(self, value): if any( not isinstance(col, (Expression, SelectQuery)) for col in value): raise ValueError("invalid columns: %r" % value) self._columns = tuple(value) @property def where(self): return self._where @where.setter def where(self, value): from sql.operators import And, Or if value is not None: if not isinstance(value, (Expression, And, Or)): raise ValueError("invalid where: %r" % value) self._where = value @property def group_by(self): return self._group_by @group_by.setter def group_by(self, value): if value is not None: if isinstance(value, Expression): value = [value] if any(not isinstance(col, Expression) for col in value): raise ValueError("invalid group by: %r" % value) self._group_by = value @property def having(self): return self._having @having.setter def having(self, value): from sql.operators import And, Or if value is not None: if not isinstance(value, (Expression, And, Or)): raise ValueError("invalid having: %r" % value) self._having = value @property def for_(self): return self._for_ @for_.setter def for_(self, value): if value is not None: if isinstance(value, For): value = [value] if any(not isinstance(f, For) for f in value): raise ValueError("invalid for: %r" % value) self._for_ = value @property def windows(self): from sql.aggregate import Aggregate from sql.functions import WindowFunction windows = set() if self._windows is not None: for window in self._windows: windows.add(window) yield window for column in self.columns: window_function = None if isinstance(column, (WindowFunction, Aggregate)): window_function = column elif (isinstance(column, As) and isinstance(column.expression, (WindowFunction, Aggregate))): window_function = column.expression if (window_function and window_function.window and window_function.window not in windows): windows.add(window_function.window) yield window_function.window @windows.setter def windows(self, value): if value is not None: if any(not isinstance(w, Window) for w in value): raise ValueError("invalid windows: %r" % value) self._windows = value @staticmethod def _format_column(column): if isinstance(column, As): if isinstance(column.expression, Select): expression = '(%s)' % column.expression else: expression = column.expression if Flavor.get().no_as: return '%s %s' % (expression, column) else: return '%s AS %s' % (expression, column) else: if isinstance(column, Select): return '(%s)' % column else: return str(column) def _rownum(self, func): aliases = [c.output_name if isinstance(c, As) else None for c in self.columns] def columns(table): if aliases and all(aliases): return [Column(table, alias) for alias in aliases] else: return [Column(table, '*')] limitselect = self.select(*columns(self)) if self.limit is not None: max_row = self.limit if self.offset is not None: max_row += self.offset limitselect.where = _rownum <= max_row if self.offset is not None: rnum = _rownum.as_('rnum') limitselect.columns += (rnum,) offsetselect = limitselect.select(*columns(limitselect), where=rnum > self.offset) query = offsetselect else: query = limitselect self.limit, limit = None, self.limit self.offset, offset = None, self.offset query.for_, self.for_ = self.for_, None try: value = func(query) finally: self.limit = limit self.offset = offset self.for_ = query.for_ return value def __str__(self): if (Flavor.get().limitstyle == 'rownum' and (self.limit is not None or self.offset is not None)): return self._rownum(str) with AliasManager(): if self.from_ is not None: from_ = ' FROM %s' % self.from_ else: from_ = '' # format window before expressions to set alias window = ', '.join( '"%s" AS (%s)' % (w.alias, w) for w in self.windows) if window: window = ' WINDOW ' + window if self.distinct: distinct = 'DISTINCT ' if self.distinct_on: distinct += ('ON (%s) ' % ', '.join(map(str, self.distinct_on))) else: distinct = '' if self.columns: columns = ', '.join(map(self._format_column, self.columns)) else: columns = '*' where = '' if self.where: where = ' WHERE ' + str(self.where) group_by = '' if self.group_by: group_by = ' GROUP BY ' + ', '.join(map(str, self.group_by)) having = '' if self.having: having = ' HAVING ' + str(self.having) for_ = '' if self.for_ is not None: for_ = ' ' + ' '.join(map(str, self.for_)) return (self._with_str() + 'SELECT %s%s%s' % (distinct, columns, from_) + where + group_by + having + window + self._order_by_str + self._limit_offset_str + for_) @property def params(self): if (Flavor.get().limitstyle == 'rownum' and (self.limit is not None or self.offset is not None)): return self._rownum(lambda q: q.params) p = [] with AliasManager(): # Set alias to window function to skip their params for window in self.windows: window.alias p.extend(self._with_params()) for column in chain(self.distinct_on or (), self.columns): if isinstance(column, As): p.extend(column.expression.params) p.extend(column.params) if self.from_ is not None: p.extend(self.from_.params) if self.where: p.extend(self.where.params) if self.group_by: for expression in self.group_by: p.extend(expression.params) if self.order_by: for expression in self.order_by: p.extend(expression.params) if self.having: p.extend(self.having.params) for window in self.windows: p.extend(window.params) p.extend(self._limit_offset_params) return tuple(p) class Insert(WithQuery): __slots__ = ('_table', '_columns', '_values', '_on_conflict', '_returning') def __init__( self, table, columns=None, values=None, returning=None, on_conflict=None, **kwargs): self._table = None self._columns = None self._values = None self._on_conflict = None self._returning = None self.table = table self.columns = columns self.values = values self.on_conflict = on_conflict self.returning = returning super(Insert, self).__init__(**kwargs) @property def table(self): return self._table @table.setter def table(self, value): if not isinstance(value, Table): raise ValueError("invalid table: %r" % value) self._table = value @property def columns(self): return self._columns @columns.setter def columns(self, value): if value is not None: if any( not isinstance(col, Column) or col.table != self.table for col in value): raise ValueError("invalid columns: %r" % value) self._columns = value @property def values(self): return self._values @values.setter def values(self, value): if value is not None: if not isinstance(value, (list, Select)): raise ValueError("invalid values: %r" % value) if isinstance(value, list): value = Values(value) self._values = value @property def on_conflict(self): return self._on_conflict @on_conflict.setter def on_conflict(self, value): if value is not None: if not isinstance(value, Conflict) or value.table != self.table: raise ValueError("invalid on conflict: %r" % value) self._on_conflict = value @property def returning(self): return self._returning @returning.setter def returning(self, value): if value is not None: if not isinstance(value, list): raise ValueError("invalid returning: %r" % value) self._returning = value @staticmethod def _format(value, param=None): if param is None: param = Flavor.get().param if isinstance(value, Expression): return str(value) elif isinstance(value, Select): return '(%s)' % value else: return param def __str__(self): columns = '' if self.columns: assert all(col.table == self.table for col in self.columns) # Get columns without alias columns = ', '.join(c.column_name for c in self.columns) columns = ' (' + columns + ')' with AliasManager(): if isinstance(self.values, Query): values = ' %s' % str(self.values) # TODO manage DEFAULT elif self.values is None: values = ' DEFAULT VALUES' on_conflict = '' if self.on_conflict: on_conflict = ' %s' % self.on_conflict returning = '' if self.returning: returning = ' RETURNING ' + ', '.join( map(self._format, self.returning)) if on_conflict or returning: table = '%s AS "%s"' % (self.table, self.table.alias) else: table = str(self.table) return (self._with_str() + 'INSERT INTO %s' % table + columns + values + on_conflict + returning) @property def params(self): p = [] p.extend(self._with_params()) if isinstance(self.values, Query): p.extend(self.values.params) if self.on_conflict: p.extend(self.on_conflict.params) if self.returning: for exp in self.returning: p.extend(exp.params) return tuple(p) class Conflict(object): __slots__ = ( '_table', '_indexed_columns', '_index_where', '_columns', '_values', '_where') def __init__( self, table, indexed_columns=None, index_where=None, columns=None, values=None, where=None): self._table = None self._indexed_columns = None self._index_where = None self._columns = None self._values = None self._where = None self.table = table self.indexed_columns = indexed_columns self.index_where = index_where self.columns = columns self.values = values self.where = where @property def table(self): return self._table @table.setter def table(self, value): if not isinstance(value, Table): raise ValueError("invalid table: %r" % value) self._table = value @property def indexed_columns(self): return self._indexed_columns @indexed_columns.setter def indexed_columns(self, value): if value is not None: if any( not isinstance(col, Column) or col.table != self.table for col in value): raise ValueError("invalid indexed columns: %r" % value) self._indexed_columns = value @property def index_where(self): return self._index_where @index_where.setter def index_where(self, value): from sql.operators import And, Or if value is not None: if not isinstance(value, (Expression, And, Or)): raise ValueError("invalid index where: %r" % value) self._index_where = value @property def columns(self): return self._columns @columns.setter def columns(self, value): if value is not None: if any( not isinstance(col, Column) or col.table != self.table for col in value): raise ValueError("invalid columns: %r" % value) self._columns = value @property def values(self): return self._values @values.setter def values(self, value): if value is not None: if not isinstance(value, (list, Select)): raise ValueError("invalid values: %r" % value) if isinstance(value, list): value = Values([value]) self._values = value @property def where(self): return self._where @where.setter def where(self, value): from sql.operators import And, Or if value is not None: if not isinstance(value, (Expression, And, Or)): raise ValueError("invalid where: %r" % value) self._where = value def __str__(self): indexed_columns = '' if self.indexed_columns: assert all(c.table == self.table for c in self.indexed_columns) # Get columns without alias indexed_columns = ', '.join( c.column_name for c in self.indexed_columns) indexed_columns = ' (' + indexed_columns + ')' if self.index_where: indexed_columns += ' WHERE ' + str(self.index_where) else: assert not self.index_where do = '' if not self.columns: assert not self.values assert not self.where do = 'NOTHING' else: assert all(c.table == self.table for c in self.columns) # Get columns without alias do = ', '.join(c.column_name for c in self.columns) # TODO manage DEFAULT values = str(self.values) if values.startswith('VALUES'): values = values[len('VALUES'):] else: values = ' (' + values + ')' if len(self.columns) == 1: # PostgreSQL would require ROW expression # with single column with parenthesis do = 'UPDATE SET ' + do + ' =' + values else: do = 'UPDATE SET (' + do + ') =' + values if self.where: do += ' WHERE %s' % self.where return 'ON CONFLICT' + indexed_columns + ' DO ' + do @property def params(self): p = [] if self.index_where: p.extend(self.index_where.params) if self.values: p.extend(self.values.params) if self.where: p.extend(self.where.params) return p class Update(Insert): __slots__ = ('_where', '_values', 'from_') def __init__(self, table, columns, values, from_=None, where=None, returning=None, **kwargs): super(Update, self).__init__(table, columns=columns, values=values, returning=returning, **kwargs) self._where = None self.from_ = From(from_) if from_ else None self.where = where @property def values(self): return self._values @values.setter def values(self, value): if isinstance(value, Select): value = [value] if not isinstance(value, list): raise ValueError("invalid values: %r" % value) self._values = value @property def where(self): return self._where @where.setter def where(self, value): from sql.operators import And, Or if value is not None: if not isinstance(value, (Expression, And, Or)): raise ValueError("invalid where: %r" % value) self._where = value @staticmethod def _format_column(value): return Select._format_column(value) def __str__(self): assert all(col.table == self.table for col in self.columns) # Get columns without alias columns = [c.column_name for c in self.columns] with AliasManager(): from_ = '' if self.from_: from_ = ' FROM %s' % str(self.from_) values = ', '.join('%s = %s' % (c, self._format(v)) for c, v in zip(columns, self.values)) where = '' if self.where: where = ' WHERE ' + str(self.where) returning = '' if self.returning: returning = ' RETURNING ' + ', '.join( map(self._format_column, self.returning)) return (self._with_str() + 'UPDATE %s AS "%s" SET ' % (self.table, self.table.alias) + values + from_ + where + returning) @property def params(self): p = [] p.extend(self._with_params()) for value in self.values: if isinstance(value, (Expression, Select)): p.extend(value.params) else: p.append(value) if self.from_: p.extend(self.from_.params) if self.where: p.extend(self.where.params) if self.returning: for exp in self.returning: p.extend(exp.params) return tuple(p) class Delete(WithQuery): __slots__ = ('_table', '_where', '_returning', 'only') def __init__(self, table, only=False, using=None, where=None, returning=None, **kwargs): self._table = None self._where = None self._returning = None self.table = table self.only = only # TODO using (not standard) self.where = where self.returning = returning super(Delete, self).__init__(**kwargs) @property def table(self): return self._table @table.setter def table(self, value): if not isinstance(value, Table): raise ValueError("invalid table: %r" % value) self._table = value @property def where(self): return self._where @where.setter def where(self, value): from sql.operators import And, Or if value is not None: if not isinstance(value, (Expression, And, Or)): raise ValueError("invalid where: %r" % value) self._where = value @property def returning(self): return self._returning @returning.setter def returning(self, value): if value is not None: if any( not isinstance(col, (Expression, SelectQuery)) for col in value): raise ValueError("invalid returning: %r" % value) self._returning = value @staticmethod def _format(value): return Select._format_column(value) def __str__(self): with AliasManager(exclude=[self.table]): only = ' ONLY' if self.only else '' where = '' if self.where: where = ' WHERE ' + str(self.where) returning = '' if self.returning: returning = ' RETURNING ' + ', '.join( map(self._format, self.returning)) return (self._with_str() + 'DELETE FROM%s %s' % (only, self.table) + where + returning) @property def params(self): p = [] p.extend(self._with_params()) if self.where: p.extend(self.where.params) if self.returning: for exp in self.returning: p.extend(exp.params) return tuple(p) class Merge(WithQuery): __slots__ = ('_target', '_source', '_condition', '_whens') def __init__(self, target, source, condition, *whens, **kwargs): self._target = None self._source = None self._condition = None self._whens = None self.target = target self.source = source self.condition = condition self.whens = whens super().__init__(**kwargs) @property def target(self): return self._target @target.setter def target(self, value): if not isinstance(value, Table): raise ValueError("invalid target: %r" % value) self._target = value @property def source(self): return self._source @source.setter def source(self, value): if not isinstance(value, (Table, SelectQuery, Values)): raise ValueError("invalid source: %r" % value) self._source = value @property def condition(self): return self._condition @condition.setter def condition(self, value): if not isinstance(value, Expression): raise ValueError("invalid condition: %r" % value) self._condition = value @property def whens(self): return self._whens @whens.setter def whens(self, value): if any(not isinstance(w, Matched) for w in value): raise ValueError("invalid whens: %r" % value) self._whens = tuple(value) def __str__(self): with AliasManager(): if isinstance(self.source, (Select, Values)): source = '(%s)' % self.source else: source = self.source condition = 'ON %s' % self.condition return (self._with_str() + 'MERGE INTO %s AS "%s" ' % (self.target, self.target.alias) + 'USING %s AS "%s" ' % (source, self.source.alias) + condition + ' ' + ' '.join(map(str, self.whens))) @property def params(self): p = [] p.extend(self._with_params()) if isinstance(self.source, (SelectQuery, Values)): p.extend(self.source.params) if self.condition: p.extend(self.condition.params) for match in self.whens: p.extend(match.params) return tuple(p) class Matched(object): __slots__ = ('_condition',) _when = 'MATCHED' def __init__(self, condition=None): self._condition = None self.condition = condition @property def condition(self): return self._condition @condition.setter def condition(self, value): if value is not None: if not isinstance(value, Expression): raise ValueError("invalid condition: %r" % value) self._condition = value def _then_str(self): return 'DO NOTHING' def __str__(self): if self.condition is not None: condition = ' AND ' + str(self.condition) else: condition = '' return 'WHEN ' + self._when + condition + ' THEN ' + self._then_str() @property def params(self): p = [] if self.condition: p.extend(self.condition.params) return tuple(p) class _MatchedValues(Matched): __slots__ = ('_columns', '_values') def __init__(self, columns, values, **kwargs): self._columns = columns self._values = values self.columns = columns self.values = values super().__init__(**kwargs) @property def columns(self): return self._columns @columns.setter def columns(self, value): if any(not isinstance(col, Column) for col in value): raise ValueError("invalid columns: %r" % value) self._columns = value class MatchedUpdate(_MatchedValues, Matched): __slots__ = () @property def values(self): return self._values @values.setter def values(self, value): self._values = value def _then_str(self): columns = [c.column_name for c in self.columns] return 'UPDATE SET ' + ', '.join( '%s = %s' % (c, Update._format(v)) for c, v in zip(columns, self.values)) @property def params(self): p = list(super().params) for value in self.values: if isinstance(value, (Expression, Select)): p.extend(value.params) else: p.append(value) return tuple(p) class MatchedDelete(Matched): __slots__ = () def _then_str(self): return 'DELETE' class NotMatched(Matched): __slots__ = () _when = 'NOT MATCHED' class NotMatchedInsert(_MatchedValues, NotMatched): __slots__ = () @property def values(self): return self._values @values.setter def values(self, value): if value is not None: value = Values([value]) self._values = value def _then_str(self): columns = ', '.join(c.column_name for c in self.columns) columns = '(' + columns + ')' if self.values is None: values = ' DEFAULT VALUES' else: values = ' ' + str(self.values) return 'INSERT ' + columns + values @property def params(self): p = list(super().params) if self.values: p.extend(self.values.params) return tuple(p) class CombiningQuery(FromItem, SelectQuery): __slots__ = ('queries', 'all_') _operator = '' def __init__(self, *queries, **kwargs): if any(not isinstance(q, Query) for q in queries): raise ValueError("invalid queries: %r" % (queries,)) self.queries = queries self.all_ = kwargs.pop('all_', False) super(CombiningQuery, self).__init__(**kwargs) def __str__(self): with AliasManager(): operator = ' %s %s' % (self._operator, 'ALL ' if self.all_ else '') return ( self._with_str() + operator.join(map(str, self.queries)) + self._order_by_str + self._limit_offset_str) @property def params(self): p = [] with AliasManager(): p.extend(self._with_params()) for q in self.queries: p.extend(q.params) if self.order_by: for expression in self.order_by: p.extend(expression.params) return tuple(p) class Union(CombiningQuery): __slots__ = () _operator = 'UNION' class Intersect(CombiningQuery): __slots__ = () _operator = 'INTERSECT' class Interesect(Intersect): def __init__(self, *args, **kwargs): warnings.warn('Interesect query is deprecated, use Intersect', DeprecationWarning, stacklevel=2) super(Interesect, self).__init__(*args, **kwargs) class Except(CombiningQuery): __slots__ = () _operator = 'EXCEPT' class Table(FromItem): __slots__ = ('_name', '_schema', '_database') def __init__(self, name, schema=None, database=None): super(Table, self).__init__() self._name = name self._schema = schema self._database = database def __str__(self): return '.'.join(map(_escape_identifier, filter(None, (self._database, self._schema, self._name)))) @property def params(self): return () def insert( self, columns=None, values=None, returning=None, with_=None, on_conflict=None): return Insert(self, columns=columns, values=values, on_conflict=on_conflict, returning=returning, with_=with_) def update(self, columns, values, from_=None, where=None, returning=None, with_=None): return Update(self, columns=columns, values=values, from_=from_, where=where, returning=returning, with_=with_) def delete(self, only=False, using=None, where=None, returning=None, with_=None): return Delete(self, only=only, using=using, where=where, returning=returning, with_=with_) def merge(self, source, condition, *whens, with_=None): return Merge(self, source, condition, *whens, with_=with_) class _Excluded(Table): def __init__(self): super().__init__('EXCLUDED') @property def alias(self): return 'EXCLUDED' @property def has_alias(self): return False Excluded = _Excluded() class Join(FromItem): __slots__ = ('_left', '_right', '_condition', '_type_') def __init__(self, left, right, type_='INNER', condition=None): super(Join, self).__init__() self._left, self._right = None, None self._condition = None self._type_ = None self.left = left self.right = right self.condition = condition self.type_ = type_ @property def left(self): return self._left @left.setter def left(self, value): if not isinstance(value, FromItem): raise ValueError("invalid left: %r" % value) self._left = value @property def right(self): return self._right @right.setter def right(self, value): if not isinstance(value, FromItem): raise ValueError("invalid right: %r" % value) self._right = value @property def condition(self): return self._condition @condition.setter def condition(self, value): from sql.operators import And, Or if value is not None: if not isinstance(value, (Expression, And, Or)): raise ValueError("invalid condition: %r" % value) self._condition = value @property def type_(self): return self._type_ @type_.setter def type_(self, value): value = value.upper() if value not in { 'INNER', 'LEFT', 'LEFT OUTER', 'RIGHT', 'RIGHT OUTER', 'FULL', 'FULL OUTER', 'CROSS'}: raise ValueError("invalid type: %r" % value) self._type_ = value def __str__(self): join = '%s %s JOIN %s' % (From([self.left]), self.type_, From([self.right])) if self.condition: condition = ' ON %s' % self.condition else: condition = '' return join + condition @property def params(self): p = [] for item in (self.left, self.right): p.extend(item.params) if self.condition: p.extend(self.condition.params) return tuple(p) @property def alias(self): raise AttributeError @property def has_alias(self): raise AttributeError def __getattr__(self, name): raise AttributeError def select(self, *args, **kwargs): return super(Join, self).select(*args, **kwargs) class From(list): __slots__ = () def select(self, *args, **kwargs): return Select(args, from_=self, **kwargs) def __str__(self): def format(from_): template = '%s' if isinstance(from_, Query): template = '(%s)' alias = getattr(from_, 'alias', None) # TODO column_alias columns_definitions = getattr(from_, 'columns_definitions', None) if Flavor.get().no_as: alias_template = ' "%s"' else: alias_template = ' AS "%s"' # XXX find a better test for __getattr__ which returns Column if (alias and columns_definitions and not isinstance(columns_definitions, Column)): return (template + alias_template + ' (%s)') % (from_, alias, columns_definitions) elif alias: return (template + alias_template) % (from_, alias) else: return template % from_ return ', '.join(map(format, self)) @property def params(self): p = [] for from_ in self: p.extend(from_.params) return tuple(p) def __add__(self, other): if not isinstance(other, FromItem): return NotImplemented elif isinstance(other, CombiningQuery): return NotImplemented return From(super(From, self).__add__([other])) class Values(list, Query, FromItem): __slots__ = () # TODO order, fetch def __str__(self): param = Flavor.get().param def format_(value): if isinstance(value, Expression): return str(value) else: return param return 'VALUES ' + ', '.join( '(%s)' % ', '.join(map(format_, v)) for v in self) @property def params(self): p = list(super().params) for values in self: for value in values: if isinstance(value, Expression): p.extend(value.params) else: p.append(value) return tuple(p) class Expression(object): __slots__ = () def __str__(self): raise NotImplementedError @property def params(self): raise NotImplementedError def __and__(self, other): from sql.operators import And return And((self, other)) def __or__(self, other): from sql.operators import Or return Or((self, other)) def __invert__(self): from sql.operators import Not return Not(self) def __add__(self, other): from sql.operators import Add return Add(self, other) def __sub__(self, other): from sql.operators import Sub return Sub(self, other) def __mul__(self, other): from sql.operators import Mul return Mul(self, other) def __div__(self, other): from sql.operators import Div return Div(self, other) __truediv__ = __div__ def __floordiv__(self, other): from sql.functions import Div return Div(self, other) def __mod__(self, other): from sql.operators import Mod return Mod(self, other) def __pow__(self, other): from sql.operators import Pow return Pow(self, other) def __neg__(self): from sql.operators import Neg return Neg(self) def __pos__(self): from sql.operators import Pos return Pos(self) def __abs__(self): from sql.operators import Abs return Abs(self) def __lshift__(self, other): from sql.operators import LShift return LShift(self, other) def __rshift__(self, other): from sql.operators import RShift return RShift(self, other) def __lt__(self, other): from sql.operators import Less return Less(self, other) def __le__(self, other): from sql.operators import LessEqual return LessEqual(self, other) def __eq__(self, other): from sql.operators import Equal return Equal(self, other) # When overriding __eq__, __hash__ is implicitly set to None __hash__ = object.__hash__ def __ne__(self, other): from sql.operators import NotEqual return NotEqual(self, other) def __gt__(self, other): from sql.operators import Greater return Greater(self, other) def __ge__(self, other): from sql.operators import GreaterEqual return GreaterEqual(self, other) def in_(self, values): from sql.operators import In return In(self, values) def like(self, test): from sql.operators import Like return Like(self, test) def ilike(self, test): from sql.operators import ILike return ILike(self, test) def as_(self, output_name): return As(self, output_name) def cast(self, typename): return Cast(self, typename) def collate(self, collation): return Collate(self, collation) @property def asc(self): return Asc(self) @property def desc(self): return Desc(self) @property def nulls_first(self): return NullsFirst(self) @property def nulls_last(self): return NullsLast(self) class Literal(Expression): __slots__ = ('_value') def __init__(self, value): super(Literal, self).__init__() self._value = value @property def value(self): return self._value def __str__(self): flavor = Flavor.get() if flavor.no_boolean: if self._value is True: return '(1 = 1)' elif self._value is False: return '(1 != 1)' return flavor.param @property def params(self): if Flavor.get().no_boolean: if self._value is True or self._value is False: return () return (self._value,) Null = None class _Rownum(Expression): def __str__(self): return 'ROWNUM' @property def params(self): return () _rownum = _Rownum() class Column(Expression): __slots__ = ('_from', '_name') def __init__(self, from_, name): super(Column, self).__init__() self._from = from_ self._name = name @property def table(self): return self._from @property def name(self): return self._name @property def column_name(self): return ( self._name if self._name == '*' else _escape_identifier(self._name)) def __str__(self): alias = self._from.alias if alias: return '%s.%s' % (_escape_identifier(alias), self.column_name) else: return self.column_name @property def params(self): return () class As(Expression): __slots__ = ('expression', 'output_name') def __init__(self, expression, output_name): super(As, self).__init__() self.expression = expression self.output_name = output_name def __str__(self): return '%s' % _escape_identifier(self.output_name) @property def params(self): return () class Cast(Expression): __slots__ = ('expression', 'typename') def __init__(self, expression, typename): super(Cast, self).__init__() self.expression = expression self.typename = typename def __str__(self): if isinstance(self.expression, Expression): value = self.expression else: value = Flavor.get().param return 'CAST(%s AS %s)' % (value, self.typename) @property def params(self): if isinstance(self.expression, Expression): return self.expression.params else: return (self.expression,) class Collate(Expression): __slots__ = ('_expression', '_collation') def __init__(self, expression, collation): super(Collate, self).__init__() self.expression = expression self.collation = collation @property def expression(self): return self._expression @expression.setter def expression(self, value): self._expression = value @property def collation(self): return self._collation @collation.setter def collation(self, value): self._collation = value def __str__(self): if isinstance(self.expression, Expression): value = self.expression else: value = Flavor.get().param return '%s COLLATE %s' % (value, _escape_identifier(self.collation)) @property def params(self): if isinstance(self.expression, Expression): return self.expression.params else: return (self.expression,) class Grouping(Expression): __slots__ = ('_sets',) def __init__(self, *sets): super().__init__() self.sets = sets @property def sets(self): return self._sets @sets.setter def sets(self, value): if any( not isinstance(col, Expression) for cols in value for col in cols): raise ValueError("invalid sets: %r" % value) self._sets = tuple(tuple(cols) for cols in value) def __str__(self): return 'GROUPING SETS (%s)' % ( ', '.join( '(%s)' % ', '.join(str(col) for col in cols) for cols in self.sets)) @property def params(self): return sum((col.params for cols in self.sets for col in cols), ()) class Rollup(Expression): __slots__ = ('_expressions',) def __init__(self, *expressions): super().__init__() self.expressions = expressions @property def expressions(self): return self._expressions @expressions.setter def expressions(self, value): if not all( isinstance(col, Expression) or all(isinstance(c, Expression) for c in col) for col in value): raise ValueError("invalid expressions: %r" % value) self._expressions = tuple(value) def __str__(self): def format(col): if isinstance(col, Expression): return str(col) else: return '(%s)' % ', '.join(str(c) for c in col) return '%s (%s)' % ( self.__class__.__name__.upper(), ', '.join(format(col) for col in self.expressions)) @property def params(self): p = [] for col in self.expressions: if isinstance(col, Expression): p.extend(col.params) else: for c in col: p.extend(c.params) return tuple(p) class Cube(Rollup): pass class Window(object): __slots__ = ( '_partition', '_order_by', '_frame', '_start', '_end', '_exclude') def __init__(self, partition, order_by=None, frame=None, start=None, end=0, exclude=None): super(Window, self).__init__() self._partition = None self._order_by = None self._frame = None self._start = None self._end = None self.partition = partition self.order_by = order_by self.frame = frame self.start = start self.end = end self.exclude = exclude @property def partition(self): return self._partition @partition.setter def partition(self, value): if any(not isinstance(e, Expression) for e in value): raise ValueError("invalid partition: %r" % value) self._partition = value @property def order_by(self): return self._order_by @order_by.setter def order_by(self, value): if value is not None: if isinstance(value, Expression): value = [value] if any(not isinstance(col, Expression) for col in value): raise ValueError("invalid order by: %r" % value) self._order_by = value @property def frame(self): return self._frame @frame.setter def frame(self, value): if value: if value not in {'RANGE', 'ROWS', 'GROUPS'}: raise ValueError("invalid frame: %r" % value) self._frame = value @property def start(self): return self._start @start.setter def start(self, value): if value: if not isinstance(value, numbers.Integral): raise ValueError("invalid start: %r" % value) self._start = value @property def end(self): return self._end @end.setter def end(self, value): if value: if not isinstance(value, numbers.Integral): raise ValueError("invalid end: %r" % value) self._end = value @property def exclude(self): return self._exclude @exclude.setter def exclude(self, value): if value: if value not in {'CURRENT ROW', 'GROUP', 'TIES'}: raise ValueError("invalid exclude: %r" % value) self._exclude = value @property def alias(self): return AliasManager.get(self) @property def has_alias(self): return AliasManager.contains(self) def __str__(self): param = Flavor.get().param partition = '' if self.partition: partition = 'PARTITION BY ' + ', '.join(map(str, self.partition)) order_by = '' if self.order_by: order_by = ' ORDER BY ' + ', '.join(map(str, self.order_by)) def format(frame, direction): if frame is None: return 'UNBOUNDED %s' % direction elif not frame: return 'CURRENT ROW' elif frame < 0: return '%s PRECEDING' % param elif frame > 0: return '%s FOLLOWING' % param frame = '' if self.frame: start = format(self.start, 'PRECEDING') end = format(self.end, 'FOLLOWING') frame = ' %s BETWEEN %s AND %s' % (self.frame, start, end) exclude = '' if self.exclude: exclude = ' EXCLUDE %s' % self.exclude return partition + order_by + frame + exclude @property def params(self): p = [] if self.partition: for expression in self.partition: p.extend(expression.params) if self.order_by: for expression in self.order_by: p.extend(expression.params) if self.frame: if self.start: p.append(abs(self.start)) if self.end: p.append(abs(self.end)) return tuple(p) class Order(Expression): __slots__ = ('_expression') _sql = '' def __init__(self, expression): super(Order, self).__init__() self._expression = None self.expression = expression # TODO USING @property def expression(self): return self._expression @expression.setter def expression(self, value): if not isinstance(value, (Expression, SelectQuery)): raise ValueError("invalid expression: %r" % value) self._expression = value def __str__(self): if isinstance(self.expression, SelectQuery): return '(%s) %s' % (self.expression, self._sql) return '%s %s' % (self.expression, self._sql) @property def params(self): return self.expression.params class Asc(Order): __slots__ = () _sql = 'ASC' class Desc(Order): __slots__ = () _sql = 'DESC' class NullOrder(Expression): __slots__ = ('expression') _sql = '' def __init__(self, expression): super(NullOrder, self).__init__() self.expression = expression def __str__(self): if not Flavor.get().null_ordering: return '%s, %s' % (self._case, self.expression) return '%s NULLS %s' % (self.expression, self._sql) @property def params(self): p = [] if not Flavor.get().null_ordering: p.extend(self.expression.params) p.extend(self._case_values()) p.extend(self.expression.params) return tuple(p) @property def _case(self): from .conditionals import Case values = self._case_values() if isinstance(self.expression, Order): expression = self.expression.expression else: expression = self.expression return Asc(Case((expression == Null, values[0]), else_=values[1])) def _case_values(self): raise NotImplementedError class NullsFirst(NullOrder): __slots__ = () _sql = 'FIRST' def _case_values(self): return (0, 1) class NullsLast(NullOrder): __slots__ = () _sql = 'LAST' def _case_values(self): return (1, 0) class For(object): __slots__ = ('_tables', '_type_', 'nowait') def __init__(self, type_, *tables, **kwargs): self._tables = None self._type_ = None self.tables = list(tables) self.type_ = type_ self.nowait = kwargs.get('nowait') @property def tables(self): return self._tables @tables.setter def tables(self, value): if not isinstance(value, list): value = [value] all(isinstance(table, Table) for table in value) self._tables = value @property def type_(self): return self._type_ @type_.setter def type_(self, value): value = value.upper() if value not in {'UPDATE', 'SHARE'}: raise ValueError("invalid type: %r" % value) self._type_ = value def __str__(self): tables = '' if self.tables: tables = ' OF ' + ', '.join(map(str, self.tables)) nowait = '' if self.nowait: nowait = ' NOWAIT' return ('FOR %s' % self.type_) + tables + nowait ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1726940161.0 python_sql-1.5.2/sql/aggregate.py0000644000175000017500000001331214673602001015125 0ustar00cedced# This file is part of python-sql. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. from sql import Expression, Flavor, Literal, Window __all__ = ['Avg', 'BitAnd', 'BitOr', 'BoolAnd', 'BoolOr', 'Count', 'Every', 'Max', 'Min', 'Stddev', 'Sum', 'Variance'] _sentinel = object() class Aggregate(Expression): __slots__ = ('_expression', '_distinct', '_order_by', '_within', '_filter', '_window') _sql = '' def __init__(self, expression, distinct=False, order_by=None, within=None, filter_=None, window=None): super(Aggregate, self).__init__() self.expression = expression self.distinct = distinct self.order_by = order_by self.within = within self.filter_ = filter_ self.window = window @property def expression(self): return self._expression @expression.setter def expression(self, value): if not isinstance(value, Expression): raise ValueError("invalid expression: %r" % value) self._expression = value @property def distinct(self): return self._distinct @distinct.setter def distinct(self, value): if not isinstance(value, bool): raise ValueError("invalid distinct: %r" % value) self._distinct = value @property def order_by(self): return self._order_by @order_by.setter def order_by(self, value): if value is not None: if isinstance(value, Expression): value = [value] if any(not isinstance(col, Expression) for col in value): raise ValueError("invalid order by: %r" % value) self._order_by = value @property def within(self): return self._within @within.setter def within(self, value): if value is not None: if isinstance(value, Expression): value = [value] if any(not isinstance(col, Expression) for col in value): raise ValueError("invalid within: %r" % value) self._within = value @property def filter_(self): return self._filter @filter_.setter def filter_(self, value): from sql.operators import And, Or if value is not None: if not isinstance(value, (Expression, And, Or)): raise ValueError("invalid filter: %r" % value) self._filter = value @property def window(self): return self._window @window.setter def window(self, value): if value: if not isinstance(value, Window): raise ValueError("invalid window: %r" % value) self._window = value @property def _case_expression(self): return self.expression def __str__(self): quantifier = 'DISTINCT ' if self.distinct else '' has_filter = Flavor.get().filter_ expression = self.expression if self.filter_ and not has_filter: from sql.conditionals import Case expression = Case((self.filter_, self._case_expression)) order_by = '' if self.order_by: order_by = ' ORDER BY %s' % ', '.join(map(str, self.order_by)) aggregate = '%s(%s%s%s)' % ( self._sql, quantifier, expression, order_by) within = '' if self.within: within = (' WITHIN GROUP (ORDER BY %s)' % ', '.join(map(str, self.within))) filter_ = '' if self.filter_ and has_filter: filter_ = ' FILTER (WHERE %s)' % self.filter_ window = '' if self.window: if self.window.has_alias: window = ' OVER "%s"' % self.window.alias else: window = ' OVER (%s)' % self.window return aggregate + within + filter_ + window @property def params(self): has_filter = Flavor.get().filter_ p = [] if self.filter_ and not has_filter: p.extend(self.filter_.params) p.extend(self._case_expression.params) else: p.extend(self.expression.params) if self.order_by: for expression in self.order_by: p.extend(expression.params) if self.within: for expression in self.within: p.extend(expression.params) if self.filter_ and has_filter: p.extend(self.filter_.params) if self.window and not self.window.has_alias: p.extend(self.window.params) return tuple(p) class Avg(Aggregate): __slots__ = () _sql = 'AVG' class BitAnd(Aggregate): __slots__ = () _sql = 'BIT_AND' class BitOr(Aggregate): __slots__ = () _sql = 'BIT_OR' class BoolAnd(Aggregate): __slots__ = () _sql = 'BOOL_AND' class BoolOr(Aggregate): __slots__ = () _sql = 'BOOL_OR' class Count(Aggregate): __slots__ = () _sql = 'COUNT' def __init__(self, expression=_sentinel, **kwargs): if expression is _sentinel: expression = Literal('*') super().__init__(expression, **kwargs) @property def _case_expression(self): expression = super(Count, self)._case_expression if (isinstance(self.expression, Literal) and expression.value == '*'): expression = Literal(1) return expression class Every(Aggregate): __slots__ = () _sql = 'EVERY' class Max(Aggregate): __slots__ = () _sql = 'MAX' class Min(Aggregate): __slots__ = () _sql = 'MIN' class Stddev(Aggregate): __slots__ = () _sql = 'Stddev' class Sum(Aggregate): __slots__ = () _sql = 'SUM' class Variance(Aggregate): __slots__ = () _sql = 'VARIANCE' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1639788499.0 python_sql-1.5.2/sql/conditionals.py0000644000175000017500000000475514157227723015713 0ustar00cedced# This file is part of python-sql. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. from sql import CombiningQuery, Expression, Flavor, Select __all__ = ['Case', 'Coalesce', 'NullIf', 'Greatest', 'Least'] class Conditional(Expression): __slots__ = () _sql = '' table = '' name = '' @staticmethod def _format(value): if isinstance(value, Expression): return str(value) elif isinstance(value, (Select, CombiningQuery)): return '(%s)' % value else: return Flavor().get().param class Case(Conditional): __slots__ = ('whens', 'else_') def __init__(self, *whens, **kwargs): self.whens = whens self.else_ = kwargs.get('else_') def __str__(self): case = 'CASE ' for cond, result in self.whens: case += 'WHEN %s THEN %s ' % ( self._format(cond), self._format(result)) if self.else_ is not None: case += 'ELSE %s ' % self._format(self.else_) case += 'END' return case @property def params(self): p = [] for cond, result in self.whens: if isinstance(cond, (Expression, Select, CombiningQuery)): p.extend(cond.params) else: p.append(cond) if isinstance(result, (Expression, Select, CombiningQuery)): p.extend(result.params) else: p.append(result) if self.else_ is not None: if isinstance(self.else_, (Expression, Select, CombiningQuery)): p.extend(self.else_.params) else: p.append(self.else_) return tuple(p) class Coalesce(Conditional): __slots__ = ('values') _conditional = 'COALESCE' def __init__(self, *values): self.values = values def __str__(self): return (self._conditional + '(' + ', '.join(map(self._format, self.values)) + ')') @property def params(self): p = [] for value in self.values: if isinstance(value, (Expression, Select, CombiningQuery)): p.extend(value.params) else: p.append(value) return tuple(p) class NullIf(Coalesce): __slots__ = () _conditional = 'NULLIF' class Greatest(Coalesce): __slots__ = () _conditional = 'GREATEST' class Least(Coalesce): __slots__ = () _conditional = 'LEAST' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1727728859.0 python_sql-1.5.2/sql/functions.py0000644000175000017500000003040614676606333015231 0ustar00cedced# This file is part of python-sql. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. from itertools import chain from sql import CombiningQuery, Expression, Flavor, FromItem, Select, Window __all__ = ['Abs', 'Cbrt', 'Ceil', 'Degrees', 'Div', 'Exp', 'Floor', 'Ln', 'Log', 'Mod', 'Pi', 'Power', 'Radians', 'Random', 'Round', 'SetSeed', 'Sign', 'Sqrt', 'Trunc', 'WidthBucket', 'Acos', 'Asin', 'Atan', 'Atan2', 'Cos', 'Cot', 'Sin', 'Tan', 'BitLength', 'CharLength', 'Overlay', 'Position', 'Substring', 'Trim', 'Upper', 'ToChar', 'ToDate', 'ToNumber', 'ToTimestamp', 'Age', 'ClockTimestamp', 'CurrentDate', 'CurrentTime', 'CurrentTimestamp', 'DatePart', 'DateTrunc', 'Extract', 'Isfinite', 'JustifyDays', 'JustifyHours', 'JustifyInterval', 'Localtime', 'Localtimestamp', 'Now', 'StatementTimestamp', 'Timeofday', 'TransactionTimestamp', 'AtTimeZone', 'RowNumber', 'Rank', 'DenseRank', 'PercentRank', 'CumeDist', 'Ntile', 'Lag', 'Lead', 'FirstValue', 'LastValue', 'NthValue'] # Mathematical class Function(Expression, FromItem): __slots__ = ('args', '_columns_definitions') table = '' name = '' _function = '' def __init__(self, *args, **kwargs): self.args = args self.columns_definitions = kwargs.get('columns_definitions', []) @property def columns_definitions(self): return ', '.join('"%s" %s' % (c, d) for c, d in self._columns_definitions) @columns_definitions.setter def columns_definitions(self, value): if not isinstance(value, list): raise ValueError("invalid columns definitions: %r" % value) self._columns_definitions = value @staticmethod def _format(value): if isinstance(value, Expression): return str(value) elif isinstance(value, (Select, CombiningQuery)): return '(%s)' % value else: return Flavor().get().param def __str__(self): Mapping = Flavor.get().function_mapping.get(self.__class__) if Mapping: return str(Mapping(*self.args)) return self._function + '(' + ', '.join( map(self._format, self.args)) + ')' @property def params(self): Mapping = Flavor.get().function_mapping.get(self.__class__) if Mapping: return Mapping(*self.args).params p = [] for arg in self.args: if isinstance(arg, (Expression, Select, CombiningQuery)): p.extend(arg.params) else: p.append(arg) return tuple(p) class FunctionKeyword(Function): __slots__ = () _function = '' _keywords = () def __str__(self): Mapping = Flavor.get().function_mapping.get(self.__class__) if Mapping: return str(Mapping(*self.args)) return (self._function + '(' + ' '.join(chain(*zip( self._keywords, map(self._format, self.args))))[1:] + ')') class FunctionNotCallable(Function): __slots__ = () _function = '' def __str__(self): Mapping = Flavor.get().function_mapping.get(self.__class__) if Mapping: return str(Mapping(*self.args)) return self._function class Abs(Function): __slots__ = () _function = 'ABS' class Cbrt(Function): __slots__ = () _function = 'CBRT' class Ceil(Function): __slots__ = () _function = 'CEIL' class Degrees(Function): __slots__ = () _function = 'DEGREES' class Div(Function): __slots__ = () _function = 'DIV' class Exp(Function): __slots__ = () _function = 'EXP' class Floor(Function): __slots__ = () _function = 'FLOOR' class Ln(Function): __slots__ = () _function = 'LN' class Log(Function): __slots__ = () _function = 'LOG' class Mod(Function): __slots__ = () _function = 'MOD' class Pi(Function): __slots__ = () _function = 'PI' class Power(Function): __slots__ = () _function = 'POWER' class Radians(Function): __slots__ = () _function = 'RADIANS' class Random(Function): __slots__ = () _function = 'RANDOM' class Round(Function): __slots__ = () _function = 'ROUND' class SetSeed(Function): __slots__ = () _function = 'SETSEED' class Sign(Function): __slots__ = () _function = 'SIGN' class Sqrt(Function): __slots__ = () _function = 'SQRT' class Trunc(Function): __slots__ = () _function = 'TRUNC' class WidthBucket(Function): __slots__ = () _function = 'WIDTH_BUCKET' # Trigonometric class Acos(Function): __slots__ = () _function = 'ACOS' class Asin(Function): __slots__ = () _function = 'ASIN' class Atan(Function): __slots__ = () _function = 'ATAN' class Atan2(Function): __slots__ = () _function = 'ATAN2' class Cos(Function): __slots__ = () _function = 'Cos' class Cot(Function): __slots__ = () _function = 'COT' class Sin(Function): __slots__ = () _function = 'SIN' class Tan(Function): __slots__ = () _function = 'TAN' # String class BitLength(Function): __slots__ = () _function = 'BIT_LENGTH' class CharLength(Function): __slots__ = () _function = 'CHAR_LENGTH' class Lower(Function): __slots__ = () _function = 'LOWER' class OctetLength(Function): __slots__ = () _function = 'OCTET_LENGTH' class Overlay(FunctionKeyword): __slots__ = () _function = 'OVERLAY' _keywords = ('', 'PLACING', 'FROM', 'FOR') class Position(FunctionKeyword): __slots__ = () _function = 'POSITION' _keywords = ('', 'IN') class Substring(FunctionKeyword): __slots__ = () _function = 'SUBSTRING' _keywords = ('', 'FROM', 'FOR') class Trim(Function): __slots__ = ('position', 'characters', 'string') _function = 'TRIM' def __init__(self, string, position='BOTH', characters=' '): if position.upper() not in {'LEADING', 'TRAILING', 'BOTH'}: raise ValueError("invalid position: %r" % position) self.position = position.upper() self.characters = characters self.string = string def __str__(self): flavor = Flavor.get() Mapping = flavor.function_mapping.get(self.__class__) if Mapping: return str(Mapping(self.string, self.position, self.characters)) param = flavor.param def format(arg): if isinstance(arg, str): return param else: return str(arg) return self._function + '(%s %s FROM %s)' % ( self.position, format(self.characters), format(self.string)) @property def params(self): Mapping = Flavor.get().function_mapping.get(self.__class__) if Mapping: return Mapping(self.string, self.position, self.characters).params p = [] for arg in (self.characters, self.string): if isinstance(arg, str): p.append(arg) else: p.extend(arg.params) return tuple(p) class Upper(Function): __slots__ = () _function = 'UPPER' class ToChar(Function): __slots__ = () _function = 'TO_CHAR' class ToDate(Function): __slots__ = () _function = 'TO_DATE' class ToNumber(Function): __slots__ = () _function = 'TO_NUMBER' class ToTimestamp(Function): __slots__ = () _function = 'TO_TIMESTAMP' class Age(Function): __slots__ = () _function = 'AGE' class ClockTimestamp(Function): __slots__ = () _function = 'CLOCK_TIMESTAMP' class CurrentDate(FunctionNotCallable): __slots__ = () _function = 'CURRENT_DATE' class CurrentTime(FunctionNotCallable): __slots__ = () _function = 'CURRENT_TIME' class CurrentTimestamp(FunctionNotCallable): __slots__ = () _function = 'CURRENT_TIMESTAMP' class DatePart(Function): __slots__ = () _function = 'DATE_PART' class DateTrunc(Function): __slots__ = () _function = 'DATE_TRUNC' class Extract(FunctionKeyword): __slots__ = () _function = 'EXTRACT' _keywords = ('', 'FROM') class Isfinite(Function): __slots__ = () _function = 'ISFINITE' class JustifyDays(Function): __slots__ = () _function = 'JUSTIFY_DAYS' class JustifyHours(Function): __slots__ = () _function = 'JUSTIFY_HOURS' class JustifyInterval(Function): __slots__ = () _function = 'JUSTIFY_INTERVAL' class Localtime(FunctionNotCallable): __slots__ = () _function = 'LOCALTIME' class Localtimestamp(FunctionNotCallable): __slots__ = () _function = 'LOCALTIMESTAMP' class Now(Function): __slots__ = () _function = 'NOW' class StatementTimestamp(Function): __slots__ = () _function = 'STATEMENT_TIMESTAMP' class Timeofday(Function): __slots__ = () _function = 'TIMEOFDAY' class TransactionTimestamp(Function): __slots__ = () _function = 'TRANSACTION_TIMESTAMP' class AtTimeZone(Function): __slots__ = ('field', 'zone') def __init__(self, field, zone): self.field = field self.zone = zone def __str__(self): flavor = Flavor.get() Mapping = flavor.function_mapping.get(self.__class__) if Mapping: return str(Mapping(self.field, self.zone)) if isinstance(self.zone, Expression): zone = str(self.zone) elif isinstance(self.zone, (Select, CombiningQuery)): zone = '(%s)' % self.zone else: zone = flavor.param return '%s AT TIME ZONE %s' % (str(self.field), zone) @property def params(self): Mapping = Flavor.get().function_mapping.get(self.__class__) if Mapping: return Mapping(self.field, self.zone).params if isinstance(self.zone, (Expression, Select, CombiningQuery)): return self.field.params + self.zone.params else: return self.field.params + (self.zone,) class WindowFunction(Function): __slots__ = ('_filter', '_window') def __init__(self, *args, **kwargs): self.filter_ = kwargs.pop('filter_', None) self.window = kwargs['window'] super(WindowFunction, self).__init__(*args, **kwargs) @property def filter_(self): return self._filter @filter_.setter def filter_(self, value): from sql.operators import And, Or if value is not None: if not isinstance(value, (Expression, And, Or)): raise ValueError("invalid filter: %r" % value) self._filter = value @property def window(self): return self._window @window.setter def window(self, value): if value: if not isinstance(value, Window): raise ValueError("invalid window: %r" % value) self._window = value def __str__(self): function = super(WindowFunction, self).__str__() filter_ = '' if self.filter_: filter_ = ' FILTER (WHERE %s)' % self.filter_ if self.window.has_alias: over = ' OVER "%s"' % self.window.alias else: over = ' OVER (%s)' % self.window return function + filter_ + over @property def params(self): p = list(super(WindowFunction, self).params) if self.filter_: p.extend(self.filter_.params) if not self.window.has_alias: p.extend(self.window.params) return tuple(p) class RowNumber(WindowFunction): __slots__ = () _function = 'ROW_NUMBER' class Rank(WindowFunction): __slots__ = () _function = 'RANK' class DenseRank(WindowFunction): __slots__ = () _function = 'DENSE_RANK' class PercentRank(WindowFunction): __slots__ = () _function = 'PERCENT_RANK' class CumeDist(WindowFunction): __slots__ = () _function = 'CUME_DIST' class Ntile(WindowFunction): __slots__ = () _function = 'NTILE' class Lag(WindowFunction): __slots__ = () _function = 'LAG' class Lead(WindowFunction): __slots__ = () _function = 'LEAD' class FirstValue(WindowFunction): __slots__ = () _function = 'FIRST_VALUE' class LastValue(WindowFunction): __slots__ = () _function = 'LAST_VALUE' class NthValue(WindowFunction): __slots__ = () _function = 'NTH_VALUE' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1727728886.0 python_sql-1.5.2/sql/operators.py0000644000175000017500000002525314676606366015251 0ustar00cedced# This file is part of python-sql. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import warnings from array import array from sql import CombiningQuery, Expression, Flavor, Null, Select __all__ = ['And', 'Or', 'Not', 'Less', 'Greater', 'LessEqual', 'GreaterEqual', 'Equal', 'NotEqual', 'Between', 'NotBetween', 'IsDistinct', 'IsNotDistinct', 'Is', 'IsNot', 'Add', 'Sub', 'Mul', 'Div', 'FloorDiv', 'Mod', 'Pow', 'SquareRoot', 'CubeRoot', 'Factorial', 'Abs', 'BAnd', 'BOr', 'BXor', 'BNot', 'LShift', 'RShift', 'Concat', 'Like', 'NotLike', 'ILike', 'NotILike', 'In', 'NotIn', 'Exists', 'Any', 'Some', 'All'] class Operator(Expression): __slots__ = () @property def table(self): return '' @property def name(self): return '' @property def _operands(self): return () @property def params(self): def convert(operands): params = [] for operand in operands: if isinstance(operand, (Expression, Select, CombiningQuery)): params.extend(operand.params) elif isinstance(operand, (list, tuple)): params.extend(convert(operand)) elif isinstance(operand, array): params.extend(operand) else: params.append(operand) return params return tuple(convert(self._operands)) def _format(self, operand, param=None): if param is None: param = Flavor.get().param if isinstance(operand, Expression): return str(operand) elif isinstance(operand, (Select, CombiningQuery)): return '(%s)' % operand elif isinstance(operand, (list, tuple)): return '(' + ', '.join(self._format(o, param) for o in operand) + ')' elif isinstance(operand, array): return '(' + ', '.join((param,) * len(operand)) + ')' else: return param def __str__(self): raise NotImplementedError def __and__(self, other): if isinstance(other, And): return And([self] + other) else: return And((self, other)) def __or__(self, other): if isinstance(other, Or): return Or([self] + other) else: return Or((self, other)) class UnaryOperator(Operator): __slots__ = 'operand' _operator = '' def __init__(self, operand): self.operand = operand @property def _operands(self): return (self.operand,) def __str__(self): return '(%s %s)' % (self._operator, self._format(self.operand)) class BinaryOperator(Operator): __slots__ = ('left', 'right') _operator = '' def __init__(self, left, right): self.left = left self.right = right @property def _operands(self): return (self.left, self.right) def __str__(self): left, right = self._operands return '(%s %s %s)' % (self._format(left), self._operator, self._format(right)) def __invert__(self): return _INVERT[self.__class__](self.left, self.right) class NaryOperator(list, Operator): __slots__ = () _operator = '' @property def _operands(self): return self def __str__(self): return '(' + (' %s ' % self._operator).join( map(self._format, self)) + ')' class And(NaryOperator): __slots__ = () _operator = 'AND' class Or(NaryOperator): __slots__ = () _operator = 'OR' class Not(UnaryOperator): __slots__ = () _operator = 'NOT' class Neg(UnaryOperator): __slots__ = () _operator = '-' class Pos(UnaryOperator): __slots__ = () _operator = '+' class Less(BinaryOperator): __slots__ = () _operator = '<' class Greater(BinaryOperator): __slots__ = () _operator = '>' class LessEqual(BinaryOperator): __slots__ = () _operator = '<=' class GreaterEqual(BinaryOperator): __slots__ = () _operator = '>=' class Equal(BinaryOperator): __slots__ = () _operator = '=' @property def _operands(self): if self.left is Null: return (self.right,) elif self.right is Null: return (self.left,) return super(Equal, self)._operands def __str__(self): if self.left is Null: return '(%s IS NULL)' % self.right elif self.right is Null: return '(%s IS NULL)' % self.left return super(Equal, self).__str__() class NotEqual(Equal): __slots__ = () _operator = '!=' def __str__(self): if self.left is Null: return '(%s IS NOT NULL)' % self.right elif self.right is Null: return '(%s IS NOT NULL)' % self.left return super(Equal, self).__str__() class Between(Operator): __slots__ = ('operand', 'left', 'right', 'symmetric') _operator = 'BETWEEN' def __init__(self, operand, left, right, symmetric=False): self.operand = operand self.left = left self.right = right self.symmetric = symmetric @property def _operands(self): return (self.operand, self.left, self.right) def __str__(self): operator = self._operator if self.symmetric: operator += ' SYMMETRIC' return '(%s %s %s AND %s)' % ( self._format(self.operand), operator, self._format(self.left), self._format(self.right)) def __invert__(self): return _INVERT[self.__class__]( self.operand, self.left, self.right, self.symmetric) class NotBetween(Between): __slots__ = () _operator = 'NOT BETWEEN' class IsDistinct(BinaryOperator): __slots__ = () _operator = 'IS DISTINCT FROM' class IsNotDistinct(IsDistinct): __slots__ = () _operator = 'IS NOT DISTINCT FROM' class Is(BinaryOperator): __slots__ = () _operator = 'IS' def __init__(self, left, right): if right not in {None, True, False}: raise ValueError("invalid right: %r" % right) super(Is, self).__init__(left, right) @property def _operands(self): return (self.left,) def __str__(self): if self.right is None: return '(%s %s UNKNOWN)' % ( self._format(self.left), self._operator) elif self.right is True: return '(%s %s TRUE)' % (self._format(self.left), self._operator) elif self.right is False: return '(%s %s FALSE)' % (self._format(self.left), self._operator) class IsNot(Is): __slots__ = () _operator = 'IS NOT' class Add(BinaryOperator): __slots__ = () _operator = '+' class Sub(BinaryOperator): __slots__ = () _operator = '-' class Mul(BinaryOperator): __slots__ = () _operator = '*' class Div(BinaryOperator): __slots__ = () _operator = '/' # For backward compatibility class FloorDiv(BinaryOperator): __slots__ = () _operator = '/' def __init__(self, left, right): warnings.warn('FloorDiv operator is deprecated, use Div function', DeprecationWarning, stacklevel=2) super(FloorDiv, self).__init__(left, right) class Mod(BinaryOperator): __slots__ = () @property def _operator(self): # '%' must be escaped with format paramstyle if Flavor.get().paramstyle == 'format': return '%%' else: return '%' class Pow(BinaryOperator): __slots__ = () _operator = '^' class SquareRoot(UnaryOperator): __slots__ = () _operator = '|/' class CubeRoot(UnaryOperator): __slots__ = () _operator = '||/' class Factorial(UnaryOperator): __slots__ = () _operator = '!!' class Abs(UnaryOperator): __slots__ = () _operator = '@' class BAnd(BinaryOperator): __slots__ = () _operator = '&' class BOr(BinaryOperator): __slots__ = () _operator = '|' class BXor(BinaryOperator): __slots__ = () _operator = '#' class BNot(UnaryOperator): __slots__ = () _operator = '~' class LShift(BinaryOperator): __slots__ = () _operator = '<<' class RShift(BinaryOperator): __slots__ = () _operator = '>>' class Concat(BinaryOperator): __slots__ = () _operator = '||' class Like(BinaryOperator): __slots__ = 'escape' _operator = 'LIKE' def __init__(self, left, right, escape=None): super().__init__(left, right) if escape and len(escape) != 1: raise ValueError("invalid escape: %r" % escape) self.escape = escape @property def params(self): params = super().params if self.escape or Flavor().get().escape_empty: params += (self.escape or '',) return params def __str__(self): left, right = self._operands if self.escape or Flavor().get().escape_empty: return '(%s %s %s ESCAPE %s)' % ( self._format(left), self._operator, self._format(right), self._format(self.escape or '')) else: return '(%s %s %s)' % ( self._format(left), self._operator, self._format(right)) def __invert__(self): return _INVERT[self.__class__](self.left, self.right, self.escape) class NotLike(Like): __slots__ = () _operator = 'NOT LIKE' class ILike(Like): __slots__ = () @property def _operator(self): if Flavor.get().ilike: return 'ILIKE' else: return 'LIKE' @property def _operands(self): operands = super(ILike, self)._operands if not Flavor.get().ilike: from .functions import Upper operands = tuple(Upper(o) for o in operands) return operands class NotILike(ILike): __slots__ = () @property def _operator(self): if Flavor.get().ilike: return 'NOT ILIKE' else: return 'NOT LIKE' # TODO SIMILAR class In(BinaryOperator): __slots__ = () _operator = 'IN' class NotIn(BinaryOperator): __slots__ = () _operator = 'NOT IN' class Exists(UnaryOperator): __slots__ = () _operator = 'EXISTS' class Any(UnaryOperator): __slots__ = () _operator = 'ANY' Some = Any class All(UnaryOperator): __slots__ = () _operator = 'ALL' _INVERT = { Less: GreaterEqual, Greater: LessEqual, LessEqual: Greater, GreaterEqual: Less, Equal: NotEqual, NotEqual: Equal, Between: NotBetween, NotBetween: Between, IsDistinct: IsNotDistinct, IsNotDistinct: IsDistinct, Is: IsNot, IsNot: Is, Like: NotLike, NotLike: Like, ILike: NotILike, NotILike: ILike, In: NotIn, NotIn: In, } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1727729038.9927337 python_sql-1.5.2/sql/tests/0000755000175000017500000000000014676606617014013 5ustar00cedced././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1651077714.0 python_sql-1.5.2/sql/tests/__init__.py0000644000175000017500000000124314232271122016075 0ustar00cedced# This file is part of python-sql. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import doctest import os import sql here = os.path.dirname(__file__) readme = os.path.normpath(os.path.join(here, '..', '..', 'README')) def load_tests(loader, tests, pattern): tests.addTests(loader.discover(start_dir=here, pattern=pattern)) for mod in (sql,): tests.addTest(doctest.DocTestSuite(mod)) if os.path.isfile(readme): tests.addTest(doctest.DocFileSuite( readme, module_relative=False, tearDown=lambda t: sql.Flavor.set(sql.Flavor()))) return tests ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1726940181.0 python_sql-1.5.2/sql/tests/test_aggregate.py0000644000175000017500000000646014673602025017342 0ustar00cedced# This file is part of python-sql. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import unittest from sql import AliasManager, Flavor, Literal, Table, Window from sql.aggregate import Aggregate, Avg, Count class TestAggregate(unittest.TestCase): table = Table('t') def test_invalid_expression(self): with self.assertRaises(ValueError): Aggregate('foo') def test_invalid_distinct(self): with self.assertRaises(ValueError): Aggregate(self.table.c, distinct='foo') def test_invalid_order(self): with self.assertRaises(ValueError): Aggregate(self.table.c, order_by=['foo']) def test_invalid_within(self): with self.assertRaises(ValueError): Aggregate(self.table.c, within=['foo']) def test_invalid_filter(self): with self.assertRaises(ValueError): Aggregate(self.table.c, filter_='foo') def test_invalid_window(self): with self.assertRaises(ValueError): Aggregate(self.table.c, window='foo') def test_avg(self): avg = Avg(self.table.c) self.assertEqual(str(avg), 'AVG("c")') avg = Avg(self.table.a + self.table.b) self.assertEqual(str(avg), 'AVG(("a" + "b"))') def test_count_without_expression(self): count = Count() self.assertEqual(str(count), 'COUNT(%s)') self.assertEqual(count.params, ('*',)) def test_order_by_one_column(self): avg = Avg(self.table.a, order_by=self.table.b) self.assertEqual(str(avg), 'AVG("a" ORDER BY "b")') self.assertEqual(avg.params, ()) def test_order_by_multiple_columns(self): avg = Avg( self.table.a, order_by=[self.table.b.asc, self.table.c.desc]) self.assertEqual(str(avg), 'AVG("a" ORDER BY "b" ASC, "c" DESC)') self.assertEqual(avg.params, ()) def test_within(self): avg = Avg(self.table.a, within=self.table.b) self.assertEqual(str(avg), 'AVG("a") WITHIN GROUP (ORDER BY "b")') self.assertEqual(avg.params, ()) def test_filter(self): flavor = Flavor(filter_=True) Flavor.set(flavor) try: avg = Avg(self.table.a + 1, filter_=self.table.a > 0) self.assertEqual( str(avg), 'AVG(("a" + %s)) FILTER (WHERE ("a" > %s))') self.assertEqual(avg.params, (1, 0)) finally: Flavor.set(Flavor()) def test_filter_case(self): avg = Avg(self.table.a + 1, filter_=self.table.a > 0) self.assertEqual( str(avg), 'AVG(CASE WHEN ("a" > %s) THEN ("a" + %s) END)') self.assertEqual(avg.params, (0, 1)) def test_filter_case_count_star(self): count = Count(Literal('*'), filter_=self.table.a > 0) self.assertEqual( str(count), 'COUNT(CASE WHEN ("a" > %s) THEN %s END)') self.assertEqual(count.params, (0, 1)) def test_window(self): avg = Avg(self.table.c, window=Window([])) with AliasManager(): self.assertEqual(str(avg), 'AVG("a"."c") OVER ()') self.assertEqual(avg.params, ()) def test_distinct(self): avg = Avg(self.table.c, distinct=True) self.assertEqual(str(avg), 'AVG(DISTINCT "c")') self.assertEqual(avg.params, ()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717020245.0 python_sql-1.5.2/sql/tests/test_alias.py0000644000175000017500000000455014625723125016505 0ustar00cedced# This file is part of python-sql. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import threading import unittest from sql import AliasManager, Table class TestAliasManager(unittest.TestCase): def setUp(self): self.synchro = threading.Event() self.succeed1 = threading.Event() self.succeed2 = threading.Event() self.finish1 = threading.Event() self.finish2 = threading.Event() self.t1 = Table('t1') self.t2 = Table('t2') def func1(self): try: with AliasManager(exclude=[self.t2]): a1 = AliasManager.get(self.t1) a2 = AliasManager.get(self.t2) self.synchro.wait() self.assertEqual(a1, AliasManager.get(self.t1)) self.assertEqual(a2, AliasManager.get(self.t2)) self.succeed1.set() return except Exception: pass finally: self.finish1.set() def func2(self): try: with AliasManager(exclude=[self.t2]): a2 = AliasManager.get(self.t2) a1 = AliasManager.get(self.t1) self.synchro.set() self.assertEqual(a1, AliasManager.get(self.t1)) self.assertEqual(a2, AliasManager.get(self.t2)) self.succeed2.set() return except Exception: pass finally: self.synchro.set() self.finish2.set() def test_threading(self): th1 = threading.Thread(target=self.func1) th2 = threading.Thread(target=self.func2) th1.start() th2.start() self.finish1.wait() self.finish2.wait() if not self.succeed1.is_set() or not self.succeed2.is_set(): self.fail() def test_contains(self): with AliasManager(): AliasManager.get(self.t1) self.assertTrue(AliasManager.contains(self.t1)) def test_contains_exclude(self): with AliasManager(exclude=[self.t1]): self.assertEqual(AliasManager.get(self.t1), '') self.assertFalse(AliasManager.contains(self.t1)) def test_set(self): with AliasManager(): AliasManager.set(self.t1, 'foo') self.assertEqual(AliasManager.get(self.t1), 'foo') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1639788499.0 python_sql-1.5.2/sql/tests/test_as.py0000644000175000017500000000163614157227723016024 0ustar00cedced# This file is part of python-sql. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import unittest from sql import As, Column, Flavor, Table class TestAs(unittest.TestCase): table = Table('t') column = Column(table, 'c') def test_as(self): self.assertEqual(str(As(self.column, 'foo')), '"foo"') def test_as_select(self): query = self.table.select(self.column.as_('foo')) self.assertEqual(str(query), 'SELECT "a"."c" AS "foo" FROM "t" AS "a"') self.assertEqual(tuple(query.params), ()) def test_no_as(self): query = self.table.select(self.column.as_('foo')) try: Flavor.set(Flavor(no_as=True)) self.assertEqual(str(query), 'SELECT "a"."c" "foo" FROM "t" "a"') self.assertEqual(tuple(query.params), ()) finally: Flavor.set(Flavor()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1631604725.0 python_sql-1.5.2/sql/tests/test_cast.py0000644000175000017500000000121514120047765016340 0ustar00cedced# This file is part of python-sql. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import unittest from sql import Cast, Column, Table class TestCast(unittest.TestCase): column = Column(Table('t'), 'c') def test_cast(self): for cast in [Cast(self.column, 'int'), self.column.cast('int')]: self.assertEqual(str(cast), 'CAST("c" AS int)') self.assertEqual(cast.params, ()) def test_cast_no_expression(self): cast = Cast(1.1, 'int') self.assertEqual(str(cast), 'CAST(%s AS int)') self.assertEqual(cast.params, (1.1,)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717000483.0 python_sql-1.5.2/sql/tests/test_collate.py0000644000175000017500000000126014625654443017040 0ustar00cedced# This file is part of python-sql. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import unittest from sql import Collate, Column, Table class TestCollate(unittest.TestCase): column = Column(Table('t'), 'c') def test_collate(self): for collate in [Collate(self.column, 'C'), self.column.collate('C')]: self.assertEqual(str(collate), '"c" COLLATE "C"') self.assertEqual(collate.params, ()) def test_collate_no_expression(self): collate = Collate("foo", 'C') self.assertEqual(str(collate), '%s COLLATE "C"') self.assertEqual(collate.params, ("foo",)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1651077714.0 python_sql-1.5.2/sql/tests/test_column.py0000644000175000017500000000154214232271122016674 0ustar00cedced# This file is part of python-sql. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import unittest from sql import AliasManager, Column, Table class TestColumn(unittest.TestCase): def test_column(self): column = Column(Table('t'), 'c') self.assertEqual(str(column), '"c"') self.assertEqual(column.name, 'c') self.assertEqual(column.column_name, '"c"') with AliasManager(): self.assertEqual(str(column), '"a"."c"') def test_quote_in_column(self): column = Column(Table('t'), 'b "c"') self.assertEqual(str(column), '"b ""c"""') self.assertEqual(column.name, 'b "c"') self.assertEqual(column.column_name, '"b ""c"""') with AliasManager(): self.assertEqual(str(column), '"a"."b ""c"""') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716993510.0 python_sql-1.5.2/sql/tests/test_combining_query.py0000644000175000017500000000455714625636746020631 0ustar00cedced# This file is part of python-sql. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import unittest from sql import CombiningQuery, Table, Union, With class TestUnion(unittest.TestCase): q1 = Table('t1').select() q2 = Table('t2').select() q3 = Table('t3').select() def test_invalid_queries(self): with self.assertRaises(ValueError): CombiningQuery('foo', 'bar') def test_union2(self): query = Union(self.q1, self.q2) self.assertEqual(str(query), 'SELECT * FROM "t1" AS "a" UNION SELECT * FROM "t2" AS "b"') self.assertEqual(tuple(query.params), ()) query = self.q1 | self.q2 self.assertEqual(str(query), 'SELECT * FROM "t1" AS "a" UNION SELECT * FROM "t2" AS "b"') self.assertEqual(tuple(query.params), ()) def test_union_with(self): table = Table('t') with_ = With() with_.query = table.select(table.id, where=table.id == 1) query = Union(self.q1, self.q2, with_=with_) self.assertEqual(str(query), 'WITH "a" AS (' 'SELECT "b"."id" FROM "t" AS "b" WHERE ("b"."id" = %s)) ' 'SELECT * FROM "t1" AS "c" UNION SELECT * FROM "t2" AS "d"') self.assertEqual(tuple(query.params), (1,)) def test_union3(self): query = Union(self.q1, self.q2, self.q3) self.assertEqual(str(query), 'SELECT * FROM "t1" AS "a" UNION SELECT * FROM "t2" AS "b" ' 'UNION SELECT * FROM "t3" AS "c"') self.assertEqual(tuple(query.params), ()) query = Union(Union(self.q1, self.q2), self.q3) self.assertEqual(str(query), 'SELECT * FROM "t1" AS "a" UNION SELECT * FROM "t2" AS "b" ' 'UNION SELECT * FROM "t3" AS "c"') self.assertEqual(tuple(query.params), ()) query = Union(self.q1, Union(self.q2, self.q3)) self.assertEqual(str(query), 'SELECT * FROM "t1" AS "a" UNION SELECT * FROM "t2" AS "b" ' 'UNION SELECT * FROM "t3" AS "c"') self.assertEqual(tuple(query.params), ()) query = self.q1 | self.q2 | self.q3 self.assertEqual(str(query), 'SELECT * FROM "t1" AS "a" UNION SELECT * FROM "t2" AS "b" ' 'UNION SELECT * FROM "t3" AS "c"') self.assertEqual(tuple(query.params), ()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1639788499.0 python_sql-1.5.2/sql/tests/test_conditionals.py0000644000175000017500000000522114157227723020101 0ustar00cedced# This file is part of python-sql. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import unittest from sql import Table from sql.conditionals import Case, Coalesce, Greatest, Least, NullIf class TestConditionals(unittest.TestCase): table = Table('t') def test_case(self): case = Case((self.table.c1, 'foo'), (self.table.c2, 'bar'), else_=self.table.c3) self.assertEqual(str(case), 'CASE WHEN "c1" THEN %s ' 'WHEN "c2" THEN %s ' 'ELSE "c3" END') self.assertEqual(case.params, ('foo', 'bar')) def test_case_no_expression(self): case = Case((True, self.table.c1), (self.table.c2, False), else_=False) self.assertEqual(str(case), 'CASE WHEN %s THEN "c1" ' 'WHEN "c2" THEN %s ' 'ELSE %s END') self.assertEqual(case.params, (True, False, False)) def test_case_sql(self): case = Case( (self.table.select(self.table.bool, where=self.table.c2 == 'bar'), self.table.c1), else_=self.table.select(self.table.c1, where=self.table.c2 == 'foo')) self.assertEqual(str(case), 'CASE WHEN ' '(SELECT "a"."bool" FROM "t" AS "a" WHERE ("a"."c2" = %s)) ' 'THEN "c1" ' 'ELSE (SELECT "a"."c1" FROM "t" AS "a" WHERE ("a"."c2" = %s)) END') self.assertEqual(case.params, ('bar', 'foo')) def test_coalesce(self): coalesce = Coalesce(self.table.c1, self.table.c2, 'foo') self.assertEqual(str(coalesce), 'COALESCE("c1", "c2", %s)') self.assertEqual(coalesce.params, ('foo',)) def test_coalesce_sql(self): coalesce = Coalesce( self.table.select(self.table.c1, where=self.table.c2 == 'bar'), self.table.c2) self.assertEqual(str(coalesce), 'COALESCE(' '(SELECT "a"."c1" FROM "t" AS "a" WHERE ("a"."c2" = %s)), "c2")') self.assertEqual(coalesce.params, ('bar',)) def test_nullif(self): nullif = NullIf(self.table.c1, 'foo') self.assertEqual(str(nullif), 'NULLIF("c1", %s)') self.assertEqual(nullif.params, ('foo',)) def test_greatest(self): greatest = Greatest(self.table.c1, self.table.c2, 'foo') self.assertEqual(str(greatest), 'GREATEST("c1", "c2", %s)') self.assertEqual(greatest.params, ('foo',)) def test_least(self): least = Least(self.table.c1, self.table.c2, 'foo') self.assertEqual(str(least), 'LEAST("c1", "c2", %s)') self.assertEqual(least.params, ('foo',)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717017105.0 python_sql-1.5.2/sql/tests/test_delete.py0000644000175000017500000000430314625715021016646 0ustar00cedced# This file is part of python-sql. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import unittest from sql import Delete, Table, With class TestDelete(unittest.TestCase): table = Table('t') def test_delete1(self): query = self.table.delete() self.assertEqual(str(query), 'DELETE FROM "t"') self.assertEqual(query.params, ()) def test_delete2(self): query = self.table.delete(where=(self.table.c == 'foo')) self.assertEqual(str(query), 'DELETE FROM "t" WHERE ("c" = %s)') self.assertEqual(query.params, ('foo',)) def test_delete3(self): t1 = Table('t1') t2 = Table('t2') query = t1.delete(where=(t1.c.in_(t2.select(t2.c)))) self.assertEqual(str(query), 'DELETE FROM "t1" WHERE ("c" IN (' 'SELECT "a"."c" FROM "t2" AS "a"))') self.assertEqual(query.params, ()) def test_delete_invalid_table(self): with self.assertRaises(ValueError): Delete('foo') def test_delete_invalid_where(self): with self.assertRaises(ValueError): self.table.delete(where='foo') def test_delete_returning(self): query = self.table.delete(returning=[self.table.c]) self.assertEqual(str(query), 'DELETE FROM "t" RETURNING "c"') self.assertEqual(query.params, ()) def test_delet_returning_select(self): query = self.table.delete(returning=[self.table.select()]) self.assertEqual( str(query), 'DELETE FROM "t" RETURNING (SELECT * FROM "t")') self.assertEqual(query.params, ()) def test_delete_invalid_returning(self): with self.assertRaises(ValueError): self.table.delete(returning='foo') def test_with(self): t1 = Table('t1') w = With(query=t1.select(t1.c1)) query = self.table.delete(with_=[w], where=self.table.c2.in_(w.select(w.c3))) self.assertEqual(str(query), 'WITH "a" AS (SELECT "b"."c1" FROM "t1" AS "b") ' 'DELETE FROM "t" WHERE ' '("c2" IN (SELECT "a"."c3" FROM "a" AS "a"))') self.assertEqual(query.params, ()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717020245.0 python_sql-1.5.2/sql/tests/test_excluded.py0000644000175000017500000000060414625723125017205 0ustar00cedced# This file is part of python-sql. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import unittest from sql import Excluded class TestExcluded(unittest.TestCase): def test_alias(self): self.assertEqual(Excluded.alias, 'EXCLUDED') def test_has_alias(self): self.assertFalse(Excluded.has_alias) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717020245.0 python_sql-1.5.2/sql/tests/test_expression.py0000644000175000017500000000071114625723125017606 0ustar00cedced# This file is part of python-sql. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import unittest from sql import Expression class TestExpression(unittest.TestCase): def test_str(self): with self.assertRaises(NotImplementedError): str(Expression()) def test_params(self): with self.assertRaises(NotImplementedError): Expression().params ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716912410.0 python_sql-1.5.2/sql/tests/test_flavor.py0000644000175000017500000000233014625400432016670 0ustar00cedced# This file is part of python-sql. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import unittest from sql import Flavor class TestFlavor(unittest.TestCase): def test(self): Flavor() def test_limitstyle(self): flavor = Flavor(limitstyle='rownum') self.assertEqual(flavor.limitstyle, 'rownum') def test_invalid_limitstyle(self): with self.assertRaises(ValueError): Flavor(limitstyle='foo') def test_max_limit(self): flavor = Flavor(max_limit=42) self.assertEqual(flavor.max_limit, 42) def test_invalid_max_limit(self): with self.assertRaises(ValueError): Flavor(max_limit='foo') def test_paramstyle_format(self): flavor = Flavor(paramstyle='format') self.assertEqual(flavor.paramstyle, 'format') self.assertEqual(flavor.param, '%s') def test_paramstyle_qmark(self): flavor = Flavor(paramstyle='qmark') self.assertEqual(flavor.paramstyle, 'qmark') self.assertEqual(flavor.param, '?') def test_invalid_paramstyle(self): with self.assertRaises(ValueError): Flavor(paramstyle='foo') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717001152.0 python_sql-1.5.2/sql/tests/test_for.py0000644000175000017500000000121314625655700016176 0ustar00cedced# This file is part of python-sql. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import unittest from sql import For, Table class TestFor(unittest.TestCase): def test_for(self): for_ = For('UPDATE', Table('t1'), Table('t2'), nowait=True) self.assertEqual(str(for_), 'FOR UPDATE OF "t1", "t2" NOWAIT') def test_for_single_table(self): for_ = For('UPDATE') for_.tables = Table('t1') self.assertEqual(str(for_), 'FOR UPDATE OF "t1"') def test_invalid_type(self): with self.assertRaises(ValueError): For('foo') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717000255.0 python_sql-1.5.2/sql/tests/test_from.py0000644000175000017500000000131114625654077016360 0ustar00cedced# This file is part of python-sql. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import unittest from sql import CombiningQuery, From, Table class TestFrom(unittest.TestCase): def test_add(self): t1 = Table('t1') t2 = Table('t2') from_ = From([t1]) + t2 self.assertEqual(from_, [t1, t2]) def test_invalid_add(self): with self.assertRaises(TypeError): From([Table('t')]) + 'foo' def test_invalid_add_combining_query(self): with self.assertRaises(TypeError): From([Table('t')]) + CombiningQuery( Table('t1').select(), Table('t2').select()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716914475.0 python_sql-1.5.2/sql/tests/test_from_item.py0000644000175000017500000000223114625404453017367 0ustar00cedced# This file is part of python-sql. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import unittest from sql import AliasManager, Column, From, FromItem class TestFromItem(unittest.TestCase): def test_from_item(self): from_item = FromItem() with AliasManager(): self.assertFalse(from_item.has_alias) from_item.alias self.assertTrue(from_item.has_alias) def test_get_column(self): from_item = FromItem() foo = from_item.foo self.assertIsInstance(foo, Column) self.assertEqual(foo.name, 'foo') def test_get_invalid_column(self): from_item = FromItem() with self.assertRaises(AttributeError): from_item.__foo__ def test_add(self): from_item1 = FromItem() from_item2 = FromItem() from_ = from_item1 + from_item2 self.assertIsInstance(from_, From) self.assertEqual(from_, [from_item1, from_item2]) def test_invalid_add(self): from_item = FromItem() with self.assertRaises(TypeError): from_item + 'foo' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717001957.0 python_sql-1.5.2/sql/tests/test_functions.py0000644000175000017500000001343314625657345017436 0ustar00cedced# This file is part of python-sql. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import unittest from sql import AliasManager, Flavor, Table, Window from sql.functions import ( Abs, AtTimeZone, CurrentTime, Div, Function, FunctionKeyword, FunctionNotCallable, Overlay, Rank, Trim, WindowFunction) class TestFunctions(unittest.TestCase): table = Table('t') def test_invalid_columns_definitions(self): with self.assertRaises(ValueError): Function(columns_definitions='foo') def test_abs(self): abs_ = Abs(self.table.c1) self.assertEqual(str(abs_), 'ABS("c1")') self.assertEqual(abs_.params, ()) abs_ = Abs(-12) self.assertEqual(str(abs_), 'ABS(%s)') self.assertEqual(abs_.params, (-12,)) def test_mapping(self): class MyAbs(Function): _function = 'MY_ABS' params = ('test',) class MyOverlay(FunctionKeyword): _function = 'MY_OVERLAY' _keywords = ('', 'PLACING', 'FROM', 'FOR') class MyCurrentTime(FunctionNotCallable): _function = 'MY_CURRENT_TIME' class MyTrim(Trim): _function = 'MY_TRIM' abs_ = Abs(self.table.c1) overlay = Overlay(self.table.c1, 'test', 2) current_time = CurrentTime() trim = Trim(' test ') flavor = Flavor(function_mapping={ Abs: MyAbs, Overlay: MyOverlay, CurrentTime: MyCurrentTime, Trim: MyTrim, }) Flavor.set(flavor) try: self.assertEqual(str(abs_), 'MY_ABS("c1")') self.assertEqual(abs_.params, ('test',)) self.assertEqual(str(overlay), 'MY_OVERLAY("c1" PLACING %s FROM %s)') self.assertEqual(overlay.params, ('test', 2)) self.assertEqual(str(current_time), 'MY_CURRENT_TIME') self.assertEqual(current_time.params, ()) self.assertEqual(str(trim), 'MY_TRIM(BOTH %s FROM %s)') self.assertEqual(trim.params, (' ', ' test ',)) finally: Flavor.set(Flavor()) def test_sql(self): abs_ = Abs(self.table.select(self.table.c1, where=self.table.c2 == 'foo')) self.assertEqual(str(abs_), 'ABS((SELECT "a"."c1" FROM "t" AS "a" WHERE ("a"."c2" = %s)))') self.assertEqual(abs_.params, ('foo',)) def test_overlay(self): overlay = Overlay(self.table.c1, 'test', 3) self.assertEqual(str(overlay), 'OVERLAY("c1" PLACING %s FROM %s)') self.assertEqual(overlay.params, ('test', 3)) overlay = Overlay(self.table.c1, 'test', 3, 7) self.assertEqual(str(overlay), 'OVERLAY("c1" PLACING %s FROM %s FOR %s)') self.assertEqual(overlay.params, ('test', 3, 7)) def test_trim(self): trim = Trim(' test ') self.assertEqual(str(trim), 'TRIM(BOTH %s FROM %s)') self.assertEqual(trim.params, (' ', ' test ',)) trim = Trim(self.table.c1) self.assertEqual(str(trim), 'TRIM(BOTH %s FROM "c1")') self.assertEqual(trim.params, (' ',)) def test_trim_invalid_position(self): with self.assertRaises(ValueError): Trim('test', 'foo') def test_at_time_zone(self): time_zone = AtTimeZone(self.table.c1, 'UTC') self.assertEqual(str(time_zone), '"c1" AT TIME ZONE %s') self.assertEqual(time_zone.params, ('UTC',)) def test_at_time_zone_expression(self): time_zone = AtTimeZone(self.table.c1, self.table.zone) self.assertEqual(str(time_zone), '"c1" AT TIME ZONE "zone"') self.assertEqual(time_zone.params, ()) def test_at_time_zone_sql(self): time_zone = AtTimeZone(self.table.c1, self.table.select(self.table.tz, where=self.table.c1 == 'foo')) self.assertEqual(str(time_zone), '"c1" AT TIME ZONE ' '(SELECT "a"."tz" FROM "t" AS "a" WHERE ("a"."c1" = %s))') self.assertEqual(time_zone.params, ('foo',)) def test_at_time_zone_mapping(self): class MyAtTimeZone(Function): _function = 'MY_TIMEZONE' time_zone = AtTimeZone(self.table.c1, 'UTC') flavor = Flavor(function_mapping={ AtTimeZone: MyAtTimeZone, }) Flavor.set(flavor) try: self.assertEqual(str(time_zone), 'MY_TIMEZONE("c1", %s)') self.assertEqual(time_zone.params, ('UTC',)) finally: Flavor.set(Flavor()) def test_div(self): for div in [Div(self.table.c1, self.table.c2), self.table.c1 // self.table.c2]: self.assertEqual(str(div), 'DIV("c1", "c2")') self.assertEqual(div.params, ()) def test_current_time(self): current_time = CurrentTime() self.assertEqual(str(current_time), 'CURRENT_TIME') self.assertEqual(current_time.params, ()) class TestWindowFunction(unittest.TestCase): def test_window(self): t = Table('t') function = Rank(t.c, window=Window([])) with AliasManager(): self.assertEqual(str(function), 'RANK("a"."c") OVER ()') self.assertEqual(function.params, ()) def test_invalid_window(self): with self.assertRaises(ValueError): WindowFunction(window='foo') def test_filter(self): t = Table('t') function = Rank(t.c, filter_=t.c > 0, window=Window([])) with AliasManager(): self.assertEqual(str(function), 'RANK("a"."c") FILTER (WHERE ("a"."c" > %s)) OVER ()') self.assertEqual(function.params, (0,)) def test_invalid_filter(self): with self.assertRaises(ValueError): WindowFunction(filter_='foo', window=Window([])) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717000605.0 python_sql-1.5.2/sql/tests/test_grouping.py0000644000175000017500000000052214625654635017252 0ustar00cedced# This file is part of python-sql. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import unittest from sql import Grouping class TestGrouping(unittest.TestCase): def test_invalid_sets(self): with self.assertRaises(ValueError): Grouping('foo') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717002787.0 python_sql-1.5.2/sql/tests/test_insert.py0000644000175000017500000002222714625661043016721 0ustar00cedced# This file is part of python-sql. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import unittest from sql import Conflict, Excluded, Insert, Table, With from sql.functions import Abs class TestInsert(unittest.TestCase): table = Table('t') def test_insert_invalid_table(self): with self.assertRaises(ValueError): Insert('foo') def test_insert_invalid_columns(self): with self.assertRaises(ValueError): self.table.insert(['foo'], [['foo']]) def test_insert_invalid_values(self): with self.assertRaises(ValueError): self.table.insert([self.table.c], 'foo') def test_insert_default(self): query = self.table.insert() self.assertEqual(str(query), 'INSERT INTO "t" DEFAULT VALUES') self.assertEqual(tuple(query.params), ()) def test_insert_values(self): query = self.table.insert([self.table.c1, self.table.c2], [['foo', 'bar']]) self.assertEqual(str(query), 'INSERT INTO "t" ("c1", "c2") VALUES (%s, %s)') self.assertEqual(tuple(query.params), ('foo', 'bar')) def test_insert_many_values(self): query = self.table.insert([self.table.c1, self.table.c2], [['foo', 'bar'], ['spam', 'eggs']]) self.assertEqual(str(query), 'INSERT INTO "t" ("c1", "c2") VALUES (%s, %s), (%s, %s)') self.assertEqual(tuple(query.params), ('foo', 'bar', 'spam', 'eggs')) def test_insert_subselect(self): t1 = Table('t1') t2 = Table('t2') subquery = t2.select(t2.c1, t2.c2) query = t1.insert([t1.c1, t1.c2], subquery) self.assertEqual(str(query), 'INSERT INTO "t1" ("c1", "c2") ' 'SELECT "a"."c1", "a"."c2" FROM "t2" AS "a"') self.assertEqual(tuple(query.params), ()) def test_insert_function(self): query = self.table.insert([self.table.c], [[Abs(-1)]]) self.assertEqual(str(query), 'INSERT INTO "t" ("c") VALUES (ABS(%s))') self.assertEqual(tuple(query.params), (-1,)) def test_insert_returning(self): query = self.table.insert([self.table.c1, self.table.c2], [['foo', 'bar']], returning=[self.table.c1, self.table.c2]) self.assertEqual(str(query), 'INSERT INTO "t" AS "a" ("c1", "c2") VALUES (%s, %s) ' 'RETURNING "a"."c1", "a"."c2"') self.assertEqual(tuple(query.params), ('foo', 'bar')) def test_insert_returning_select(self): t1 = Table('t1') t2 = Table('t2') query = t1.insert([t1.c], [['foo']], returning=[ t2.select(t2.c, where=(t2.c1 == t1.c) & (t2.c2 == 'bar'))]) self.assertEqual(str(query), 'INSERT INTO "t1" AS "b" ("c") VALUES (%s) ' 'RETURNING (SELECT "a"."c" FROM "t2" AS "a" ' 'WHERE (("a"."c1" = "b"."c") AND ("a"."c2" = %s)))') self.assertEqual(tuple(query.params), ('foo', 'bar')) def test_insert_invalid_returning(self): with self.assertRaises(ValueError): self.table.insert(returning='foo') def test_with(self): t1 = Table('t1') w = With(query=t1.select()) query = self.table.insert( [self.table.c1], with_=[w], values=w.select()) self.assertEqual(str(query), 'WITH "a" AS (SELECT * FROM "t1" AS "b") ' 'INSERT INTO "t" ("c1") SELECT * FROM "a" AS "a"') self.assertEqual(tuple(query.params), ()) def test_insert_in_with(self): t1 = Table('t1') w = With(query=self.table.insert( [self.table.c1], values=[['foo']], returning=[self.table.id])) query = t1.update( [t1.c], [w.id], from_=[w], with_=[w]) self.assertEqual(str(query), 'WITH "a" AS (' 'INSERT INTO "t" AS "b" ("c1") VALUES (%s) ' 'RETURNING "b"."id") ' 'UPDATE "t1" AS "c" SET "c" = "a"."id" FROM "a" AS "a"') self.assertEqual(tuple(query.params), ('foo',)) def test_schema(self): t1 = Table('t1', 'default') query = t1.insert([t1.c1], [['foo']]) self.assertEqual(str(query), 'INSERT INTO "default"."t1" ("c1") VALUES (%s)') self.assertEqual(tuple(query.params), ('foo',)) def test_upsert_invalid_on_conflict(self): with self.assertRaises(ValueError): self.table.insert(on_conflict='foo') def test_upsert_invalid_table_on_conflict(self): with self.assertRaises(ValueError): self.table.insert(on_conflict=Conflict(Table('t1'))) def test_upsert_nothing(self): query = self.table.insert( [self.table.c1], [['foo']], on_conflict=Conflict(self.table)) self.assertEqual(str(query), 'INSERT INTO "t" AS "a" ("c1") VALUES (%s) ' 'ON CONFLICT DO NOTHING') self.assertEqual(tuple(query.params), ('foo',)) def test_upsert_indexed_column(self): query = self.table.insert( [self.table.c1], [['foo']], on_conflict=Conflict( self.table, indexed_columns=[self.table.c1, self.table.c2])) self.assertEqual(str(query), 'INSERT INTO "t" AS "a" ("c1") VALUES (%s) ' 'ON CONFLICT ("c1", "c2") DO NOTHING') self.assertEqual(tuple(query.params), ('foo',)) def test_upsert_indexed_column_index_where(self): query = self.table.insert( [self.table.c1], [['foo']], on_conflict=Conflict( self.table, indexed_columns=[self.table.c1], index_where=self.table.c2 == 'bar')) self.assertEqual(str(query), 'INSERT INTO "t" AS "a" ("c1") VALUES (%s) ' 'ON CONFLICT ("c1") WHERE ("a"."c2" = %s) DO NOTHING') self.assertEqual(tuple(query.params), ('foo', 'bar')) def test_upsert_update(self): query = self.table.insert( [self.table.c1], [['baz']], on_conflict=Conflict( self.table, columns=[self.table.c1, self.table.c2], values=['foo', 'bar'])) self.assertEqual(str(query), 'INSERT INTO "t" AS "a" ("c1") VALUES (%s) ' 'ON CONFLICT DO UPDATE SET ("c1", "c2") = (%s, %s)') self.assertEqual(tuple(query.params), ('baz', 'foo', 'bar')) def test_upsert_update_where(self): query = self.table.insert( [self.table.c1], [['baz']], on_conflict=Conflict( self.table, columns=[self.table.c1], values=['foo'], where=self.table.c2 == 'bar')) self.assertEqual(str(query), 'INSERT INTO "t" AS "a" ("c1") VALUES (%s) ' 'ON CONFLICT DO UPDATE SET "c1" = (%s) ' 'WHERE ("a"."c2" = %s)') self.assertEqual(tuple(query.params), ('baz', 'foo', 'bar')) def test_upsert_update_subquery(self): t1 = Table('t1') t2 = Table('t2') subquery = t2.select(t2.c1, t2.c2) query = t1.insert( [t1.c1], [['baz']], on_conflict=Conflict( t1, columns=[t1.c1, t1.c2], values=subquery)) self.assertEqual(str(query), 'INSERT INTO "t1" AS "b" ("c1") VALUES (%s) ' 'ON CONFLICT DO UPDATE SET ("c1", "c2") = ' '(SELECT "a"."c1", "a"."c2" FROM "t2" AS "a")') self.assertEqual(tuple(query.params), ('baz',)) def test_upsert_update_excluded(self): query = self.table.insert( [self.table.c1], [[1]], on_conflict=Conflict( self.table, columns=[self.table.c1], values=[Excluded.c1 + 2])) self.assertEqual(str(query), 'INSERT INTO "t" AS "a" ("c1") VALUES (%s) ' 'ON CONFLICT DO UPDATE SET "c1" = (("EXCLUDED"."c1" + %s))') self.assertEqual(tuple(query.params), (1, 2)) def test_conflict_invalid_table(self): with self.assertRaises(ValueError): Conflict('foo') def test_conflict_invalid_indexed_columns(self): with self.assertRaises(ValueError): Conflict(self.table, indexed_columns=['foo']) def test_conflict_indexed_columns_invalid_table(self): with self.assertRaises(ValueError): Conflict(self.table, indexed_columns=[Table('t').c]) def test_conflict_invalid_index_where(self): with self.assertRaises(ValueError): Conflict(self.table, index_where='foo') def test_conflict_invalid_columns(self): with self.assertRaises(ValueError): Conflict(self.table, columns=['foo']) def test_conflict_columns_invalid_table(self): with self.assertRaises(ValueError): Conflict(self.table, columns=[Table('t').c]) def test_conflict_invalid_values(self): with self.assertRaises(ValueError): Conflict(self.table, values='foo') def test_conflict_invalid_where(self): with self.assertRaises(ValueError): Conflict(self.table, where='foo') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717020245.0 python_sql-1.5.2/sql/tests/test_join.py0000644000175000017500000000512314625723125016350 0ustar00cedced# This file is part of python-sql. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import unittest from sql import AliasManager, Join, Table from sql.functions import Now class TestJoin(unittest.TestCase): def test_join(self): t1 = Table('t1') t2 = Table('t2') join = Join(t1, t2) with AliasManager(): self.assertEqual(str(join), '"t1" AS "a" INNER JOIN "t2" AS "b"') self.assertEqual(tuple(join.params), ()) join.condition = t1.c == t2.c with AliasManager(): self.assertEqual(str(join), '"t1" AS "a" INNER JOIN "t2" AS "b" ON ("a"."c" = "b"."c")') def test_join_invalid_left(self): with self.assertRaises(ValueError): Join('foo', Table('t1')) def test_join_invalid_right(self): with self.assertRaises(ValueError): Join(Table('t1'), 'foo') def test_join_invalid_condition(self): with self.assertRaises(ValueError): Join(Table('t1'), Table('t2'), condition='foo') def test_join_invalid_type(self): with self.assertRaises(ValueError): Join(Table('t1'), Table('t2'), type_='foo') def test_join_subselect(self): t1 = Table('t1') t2 = Table('t2') select = t2.select() join = Join(t1, select) join.condition = t1.c == select.c with AliasManager(): self.assertEqual(str(join), '"t1" AS "a" INNER JOIN (SELECT * FROM "t2" AS "c") AS "b" ' 'ON ("a"."c" = "b"."c")') self.assertEqual(tuple(join.params), ()) def test_join_function(self): t1 = Table('t1') join = Join(t1, Now()) with AliasManager(): self.assertEqual(str(join), '"t1" AS "a" INNER JOIN NOW() AS "b"') self.assertEqual(tuple(join.params), ()) def test_join_methods(self): t1 = Table('t1') t2 = Table('t2') for method in [ 'left_join', 'left_outer_join', 'right_join', 'right_outer_join', 'full_join', 'full_outer_join', 'cross_join']: with self.subTest(method=method): join = getattr(t1, method)(t2) type_ = method[:-len('_join')].replace('_', ' ').upper() self.assertEqual(join.type_, type_) def test_join_alias(self): join = Join(Table('t1'), Table('t2')) with self.assertRaises(AttributeError): join.alias with self.assertRaises(AttributeError): join.has_alias ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717020245.0 python_sql-1.5.2/sql/tests/test_lateral.py0000644000175000017500000000202714625723125017035 0ustar00cedced# This file is part of python-sql. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import unittest from sql import From, Lateral, Table from sql.functions import Function class TestLateral(unittest.TestCase): def test_lateral_select(self): t1 = Table('t1') t2 = Table('t2') lateral = t2.select(where=t2.id == t1.t2).lateral() query = From([t1, lateral]).select() self.assertEqual(str(query), 'SELECT * FROM "t1" AS "a", LATERAL ' '(SELECT * FROM "t2" AS "c" WHERE ("c"."id" = "a"."t2")) AS "b"') self.assertEqual(tuple(query.params), ()) def test_lateral_function(self): class Func(Function): _function = 'FUNC' t = Table('t') lateral = Lateral(Func(t.a)) query = From([t, lateral]).select() self.assertEqual(str(query), 'SELECT * FROM "t" AS "a", LATERAL FUNC("a"."a") AS "b"') self.assertEqual(tuple(query.params), ()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1639788499.0 python_sql-1.5.2/sql/tests/test_literal.py0000644000175000017500000000176214157227723017055 0ustar00cedced# This file is part of python-sql. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import unittest from sql import Flavor, Literal class TestLiteral(unittest.TestCase): def test_literal(self): literal = Literal(1) self.assertEqual(str(literal), '%s') self.assertEqual(literal.params, (1,)) self.assertEqual(literal.value, 1) def test_no_boolean(self): true = Literal(True) false = Literal(False) self.assertEqual(str(true), '%s') self.assertEqual(true.params, (True,)) self.assertEqual(str(false), '%s') self.assertEqual(false.params, (False,)) try: Flavor.set(Flavor(no_boolean=True)) self.assertEqual(str(true), '(1 = 1)') self.assertEqual(str(false), '(1 != 1)') self.assertEqual(true.params, ()) self.assertEqual(false.params, ()) finally: Flavor.set(Flavor()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717020245.0 python_sql-1.5.2/sql/tests/test_merge.py0000644000175000017500000001252414625723125016513 0ustar00cedced# This file is part of python-sql. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import unittest from sql import ( Literal, Matched, MatchedDelete, MatchedUpdate, Merge, NotMatched, NotMatchedInsert, Table, With) class TestMerge(unittest.TestCase): target = Table('t') source = Table('s') def test_merge(self): query = self.target.merge( self.source, self.target.c1 == self.source.c2, Matched()) self.assertEqual( str(query), 'MERGE INTO "t" AS "a" USING "s" AS "b" ' 'ON ("a"."c1" = "b"."c2") ' 'WHEN MATCHED THEN DO NOTHING') self.assertEqual(query.params, ()) def test_merge_invalid_target(self): with self.assertRaises(ValueError): Merge('foo', self.source, Literal(True)) def test_merge_invalid_source(self): with self.assertRaises(ValueError): self.target.merge('foo', Literal(True)) def test_merge_invalid_condition(self): with self.assertRaises(ValueError): self.target.merge(self.source, 'foo') def test_merge_invalid_whens(self): with self.assertRaises(ValueError): self.target.merge(self.source, Literal(True), 'foo') def test_condition(self): query = self.target.merge( self.source, (self.target.c1 == self.source.c2) & (self.target.c3 == 42), Matched()) self.assertEqual( str(query), 'MERGE INTO "t" AS "a" USING "s" AS "b" ' 'ON (("a"."c1" = "b"."c2") AND ("a"."c3" = %s)) ' 'WHEN MATCHED THEN DO NOTHING') self.assertEqual(query.params, (42,)) def test_matched(self): query = self.target.merge( self.source, self.target.c1 == self.source.c2, Matched((self.source.c3 == 42) & (self.target.c4 == self.source.c5))) self.assertEqual( str(query), 'MERGE INTO "t" AS "a" USING "s" AS "b" ' 'ON ("a"."c1" = "b"."c2") ' 'WHEN MATCHED ' 'AND (("b"."c3" = %s) AND ("a"."c4" = "b"."c5")) ' 'THEN DO NOTHING') self.assertEqual(query.params, (42,)) def test_matched_update(self): query = self.target.merge( self.source, self.target.c1 == self.source.c2, MatchedUpdate( [self.target.c1, self.target.c2], [self.target.c1 + self.source.c2, 42])) self.assertEqual( str(query), 'MERGE INTO "t" AS "a" USING "s" AS "b" ' 'ON ("a"."c1" = "b"."c2") ' 'WHEN MATCHED THEN ' 'UPDATE SET "c1" = ("a"."c1" + "b"."c2"), "c2" = %s') self.assertEqual(query.params, (42,)) def test_matched_delete(self): query = self.target.merge( self.source, self.target.c1 == self.source.c2, MatchedDelete()) self.assertEqual( str(query), 'MERGE INTO "t" AS "a" USING "s" AS "b" ' 'ON ("a"."c1" = "b"."c2") ' 'WHEN MATCHED THEN DELETE') self.assertEqual(query.params, ()) def test_not_matched(self): query = self.target.merge( self.source, self.target.c1 == self.source.c2, NotMatched()) self.assertEqual( str(query), 'MERGE INTO "t" AS "a" USING "s" AS "b" ' 'ON ("a"."c1" = "b"."c2") ' 'WHEN NOT MATCHED THEN DO NOTHING') self.assertEqual(query.params, ()) def test_not_matched_insert(self): query = self.target.merge( self.source, self.target.c1 == self.source.c2, NotMatchedInsert( [self.target.c1, self.target.c2], [self.source.c3, self.source.c4])) self.assertEqual( str(query), 'MERGE INTO "t" AS "a" USING "s" AS "b" ' 'ON ("a"."c1" = "b"."c2") ' 'WHEN NOT MATCHED THEN ' 'INSERT ("c1", "c2") VALUES ("b"."c3", "b"."c4")') self.assertEqual(query.params, ()) def test_not_matched_insert_default(self): query = self.target.merge( self.source, self.target.c1 == self.source.c2, NotMatchedInsert([self.target.c1, self.target.c2], None)) self.assertEqual( str(query), 'MERGE INTO "t" AS "a" USING "s" AS "b" ' 'ON ("a"."c1" = "b"."c2") ' 'WHEN NOT MATCHED THEN ' 'INSERT ("c1", "c2") DEFAULT VALUES') self.assertEqual(query.params, ()) def test_matched_invalid_condition(self): with self.assertRaises(ValueError): Matched('foo') def test_matched_values_invalid_columns(self): with self.assertRaises(ValueError): MatchedUpdate('foo', []) def test_with(self): t1 = Table('t1') w = With(query=t1.select(where=t1.c2 == 42)) source = w.select() query = self.target.merge( source, self.target.c1 == source.c2, Matched(), with_=[w]) self.assertEqual( str(query), 'WITH "a" AS (SELECT * FROM "t1" AS "d" WHERE ("d"."c2" = %s)) ' 'MERGE INTO "t" AS "b" ' 'USING (SELECT * FROM "a" AS "a") AS "c" ' 'ON ("b"."c1" = "c"."c2") ' 'WHEN MATCHED THEN DO NOTHING') self.assertEqual(query.params, (42,)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1727728886.0 python_sql-1.5.2/sql/tests/test_operators.py0000644000175000017500000003744014676606366017453 0ustar00cedced# This file is part of python-sql. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import unittest import warnings from array import array from sql import Flavor, Literal, Null, Table from sql.operators import ( Abs, And, Between, Div, Equal, Exists, FloorDiv, Greater, GreaterEqual, ILike, In, Is, IsDistinct, IsNot, IsNotDistinct, Less, LessEqual, Like, LShift, Mod, Mul, Neg, Not, NotBetween, NotEqual, NotILike, NotIn, NotLike, Operator, Or, Pos, Pow, RShift, Sub) class TestOperators(unittest.TestCase): table = Table('t') def test_operator_operands(self): self.assertEqual(Operator()._operands, ()) def test_operator_str(self): with self.assertRaises(NotImplementedError): str(Operator()) def test_and(self): for and_ in [And((self.table.c1, self.table.c2)), self.table.c1 & self.table.c2]: self.assertEqual(str(and_), '("c1" AND "c2")') self.assertEqual(and_.params, ()) and_ = And((Literal(True), self.table.c2)) self.assertEqual(str(and_), '(%s AND "c2")') self.assertEqual(and_.params, (True,)) and_ = And((Literal(True), 'foo')) self.assertEqual(str(and_), '(%s AND %s)') self.assertEqual(and_.params, (True, 'foo')) def test_operator_operators(self): and_ = And((Literal(True), self.table.c1)) and2 = and_ & And((Literal(True), self.table.c2)) self.assertEqual(str(and2), '((%s AND "c1") AND %s AND "c2")') self.assertEqual(and2.params, (True, True)) and3 = and_ & Literal(True) self.assertEqual(str(and3), '((%s AND "c1") AND %s)') self.assertEqual(and3.params, (True, True)) or_ = Or((Literal(True), self.table.c1)) or2 = or_ | Or((Literal(True), self.table.c2)) self.assertEqual(str(or2), '((%s OR "c1") OR %s OR "c2")') self.assertEqual(or2.params, (True, True)) or3 = or_ | Literal(True) self.assertEqual(str(or3), '((%s OR "c1") OR %s)') self.assertEqual(or3.params, (True, True)) def test_operator_compat_column(self): and_ = And((self.table.c1, self.table.c2)) self.assertEqual(and_.table, '') self.assertEqual(and_.name, '') def test_or(self): for or_ in [Or((self.table.c1, self.table.c2)), self.table.c1 | self.table.c2]: self.assertEqual(str(or_), '("c1" OR "c2")') self.assertEqual(or_.params, ()) def test_not(self): for not_ in [Not(self.table.c), ~self.table.c]: self.assertEqual(str(not_), '(NOT "c")') self.assertEqual(not_.params, ()) not_ = Not(Literal(False)) self.assertEqual(str(not_), '(NOT %s)') self.assertEqual(not_.params, (False,)) def test_neg(self): for neg in [Neg(self.table.c1), -self.table.c1]: self.assertEqual(str(neg), '(- "c1")') self.assertEqual(neg.params, ()) def test_pos(self): for pos in [Pos(self.table.c1), +self.table.c1]: self.assertEqual(str(pos), '(+ "c1")') self.assertEqual(pos.params, ()) def test_less(self): for less in [Less(self.table.c1, self.table.c2), self.table.c1 < self.table.c2, ~GreaterEqual(self.table.c1, self.table.c2)]: self.assertEqual(str(less), '("c1" < "c2")') self.assertEqual(less.params, ()) less = Less(Literal(0), self.table.c2) self.assertEqual(str(less), '(%s < "c2")') self.assertEqual(less.params, (0,)) def test_greater(self): for greater in [Greater(self.table.c1, self.table.c2), self.table.c1 > self.table.c2, ~LessEqual(self.table.c1, self.table.c2)]: self.assertEqual(str(greater), '("c1" > "c2")') self.assertEqual(greater.params, ()) def test_less_equal(self): for less in [LessEqual(self.table.c1, self.table.c2), self.table.c1 <= self.table.c2, ~Greater(self.table.c1, self.table.c2)]: self.assertEqual(str(less), '("c1" <= "c2")') self.assertEqual(less.params, ()) def test_greater_equal(self): for greater in [GreaterEqual(self.table.c1, self.table.c2), self.table.c1 >= self.table.c2, ~Less(self.table.c1, self.table.c2)]: self.assertEqual(str(greater), '("c1" >= "c2")') self.assertEqual(greater.params, ()) def test_equal(self): for equal in [Equal(self.table.c1, self.table.c2), self.table.c1 == self.table.c2, ~NotEqual(self.table.c1, self.table.c2)]: self.assertEqual(str(equal), '("c1" = "c2")') self.assertEqual(equal.params, ()) equal = Equal(Literal('foo'), Literal('bar')) self.assertEqual(str(equal), '(%s = %s)') self.assertEqual(equal.params, ('foo', 'bar')) equal = Equal(self.table.c1, Null) self.assertEqual(str(equal), '("c1" IS NULL)') self.assertEqual(equal.params, ()) equal = Equal(Literal('test'), Null) self.assertEqual(str(equal), '(%s IS NULL)') self.assertEqual(equal.params, ('test',)) equal = Equal(Null, self.table.c1) self.assertEqual(str(equal), '("c1" IS NULL)') self.assertEqual(equal.params, ()) equal = Equal(Null, Literal('test')) self.assertEqual(str(equal), '(%s IS NULL)') self.assertEqual(equal.params, ('test',)) def test_not_equal(self): for equal in [NotEqual(self.table.c1, self.table.c2), self.table.c1 != self.table.c2, ~Equal(self.table.c1, self.table.c2)]: self.assertEqual(str(equal), '("c1" != "c2")') self.assertEqual(equal.params, ()) equal = NotEqual(self.table.c1, Null) self.assertEqual(str(equal), '("c1" IS NOT NULL)') self.assertEqual(equal.params, ()) equal = NotEqual(Null, self.table.c1) self.assertEqual(str(equal), '("c1" IS NOT NULL)') self.assertEqual(equal.params, ()) def test_between(self): for between in [Between(self.table.c1, 1, 2), ~NotBetween(self.table.c1, 1, 2)]: self.assertEqual(str(between), '("c1" BETWEEN %s AND %s)') self.assertEqual(between.params, (1, 2)) between = Between( self.table.c1, self.table.c2, self.table.c3, symmetric=True) self.assertEqual( str(between), '("c1" BETWEEN SYMMETRIC "c2" AND "c3")') self.assertEqual(between.params, ()) def test_not_between(self): for between in [NotBetween(self.table.c1, 1, 2), ~Between(self.table.c1, 1, 2)]: self.assertEqual(str(between), '("c1" NOT BETWEEN %s AND %s)') self.assertEqual(between.params, (1, 2)) between = NotBetween( self.table.c1, self.table.c2, self.table.c3, symmetric=True) self.assertEqual( str(between), '("c1" NOT BETWEEN SYMMETRIC "c2" AND "c3")') self.assertEqual(between.params, ()) def test_is_distinct(self): for distinct in [IsDistinct(self.table.c1, self.table.c2), ~IsNotDistinct(self.table.c1, self.table.c2)]: self.assertEqual(str(distinct), '("c1" IS DISTINCT FROM "c2")') self.assertEqual(distinct.params, ()) def test_is_not_distinct(self): for distinct in [IsNotDistinct(self.table.c1, self.table.c2), ~IsDistinct(self.table.c1, self.table.c2)]: self.assertEqual(str(distinct), '("c1" IS NOT DISTINCT FROM "c2")') self.assertEqual(distinct.params, ()) def test_is(self): for is_ in [Is(self.table.c1, None), ~IsNot(self.table.c1, None)]: self.assertEqual(str(is_), '("c1" IS UNKNOWN)') self.assertEqual(is_.params, ()) for is_ in [Is(self.table.c1, True), ~IsNot(self.table.c1, True)]: self.assertEqual(str(is_), '("c1" IS TRUE)') self.assertEqual(is_.params, ()) for is_ in [Is(self.table.c1, False), ~IsNot(self.table.c1, False)]: self.assertEqual(str(is_), '("c1" IS FALSE)') self.assertEqual(is_.params, ()) def test_is_invalid_right(self): with self.assertRaises(ValueError): Is(self.table.c, 'foo') def test_is_not(self): for is_ in [IsNot(self.table.c1, None), ~Is(self.table.c1, None)]: self.assertEqual(str(is_), '("c1" IS NOT UNKNOWN)') self.assertEqual(is_.params, ()) for is_ in [IsNot(self.table.c1, True), ~Is(self.table.c1, True)]: self.assertEqual(str(is_), '("c1" IS NOT TRUE)') self.assertEqual(is_.params, ()) for is_ in [IsNot(self.table.c1, False), ~Is(self.table.c1, False)]: self.assertEqual(str(is_), '("c1" IS NOT FALSE)') self.assertEqual(is_.params, ()) def test_sub(self): for sub in [Sub(self.table.c1, self.table.c2), self.table.c1 - self.table.c2]: self.assertEqual(str(sub), '("c1" - "c2")') self.assertEqual(sub.params, ()) def test_mul(self): for mul in [Mul(self.table.c1, self.table.c2), self.table.c1 * self.table.c2]: self.assertEqual(str(mul), '("c1" * "c2")') self.assertEqual(mul.params, ()) def test_div(self): for div in [Div(self.table.c1, self.table.c2), self.table.c1 / self.table.c2]: self.assertEqual(str(div), '("c1" / "c2")') self.assertEqual(div.params, ()) def test_mod(self): for mod in [Mod(self.table.c1, self.table.c2), self.table.c1 % self.table.c2]: self.assertEqual(str(mod), '("c1" %% "c2")') self.assertEqual(mod.params, ()) def test_mod_paramstyle(self): flavor = Flavor(paramstyle='format') Flavor.set(flavor) try: mod = Mod(self.table.c1, self.table.c2) self.assertEqual(str(mod), '("c1" %% "c2")') self.assertEqual(mod.params, ()) finally: Flavor.set(Flavor()) flavor = Flavor(paramstyle='qmark') Flavor.set(flavor) try: mod = Mod(self.table.c1, self.table.c2) self.assertEqual(str(mod), '("c1" % "c2")') self.assertEqual(mod.params, ()) finally: Flavor.set(Flavor()) def test_pow(self): for pow_ in [Pow(self.table.c1, self.table.c2), self.table.c1 ** self.table.c2]: self.assertEqual(str(pow_), '("c1" ^ "c2")') self.assertEqual(pow_.params, ()) def test_abs(self): for abs_ in [Abs(self.table.c1), abs(self.table.c1)]: self.assertEqual(str(abs_), '(@ "c1")') self.assertEqual(abs_.params, ()) def test_lshift(self): for lshift in [LShift(self.table.c1, 2), self.table.c1 << 2]: self.assertEqual(str(lshift), '("c1" << %s)') self.assertEqual(lshift.params, (2,)) def test_rshift(self): for rshift in [RShift(self.table.c1, 2), self.table.c1 >> 2]: self.assertEqual(str(rshift), '("c1" >> %s)') self.assertEqual(rshift.params, (2,)) def test_like(self): for like in [Like(self.table.c1, 'foo'), self.table.c1.like('foo'), ~NotLike(self.table.c1, 'foo'), ~~Like(self.table.c1, 'foo')]: self.assertEqual(str(like), '("c1" LIKE %s)') self.assertEqual(like.params, ('foo',)) def test_like_escape(self): like = Like(self.table.c1, 'foo', escape='$') self.assertEqual(str(like), '("c1" LIKE %s ESCAPE %s)') self.assertEqual(like.params, ('foo', '$')) def test_like_escape_empty_false(self): flavor = Flavor(escape_empty=False) Flavor.set(flavor) try: like = Like(self.table.c1, 'foo') self.assertEqual(str(like), '("c1" LIKE %s)') self.assertEqual(like.params, ('foo',)) finally: Flavor.set(Flavor()) def test_like_escape_empty_true(self): flavor = Flavor(escape_empty=True) Flavor.set(flavor) try: like = Like(self.table.c1, 'foo') self.assertEqual(str(like), '("c1" LIKE %s ESCAPE %s)') self.assertEqual(like.params, ('foo', '')) finally: Flavor.set(Flavor()) def test_like_invalid_escape(self): with self.assertRaises(ValueError): Like(self.table.c, 'test', escape='fo') def test_ilike(self): flavor = Flavor(ilike=True) Flavor.set(flavor) try: for like in [ILike(self.table.c1, 'foo'), self.table.c1.ilike('foo'), ~NotILike(self.table.c1, 'foo')]: self.assertEqual(str(like), '("c1" ILIKE %s)') self.assertEqual(like.params, ('foo',)) finally: Flavor.set(Flavor()) flavor = Flavor(ilike=False) Flavor.set(flavor) try: like = ILike(self.table.c1, 'foo') self.assertEqual( str(like), '(UPPER("c1") LIKE UPPER(%s))') self.assertEqual(like.params, ('foo',)) finally: Flavor.set(Flavor()) def test_not_ilike(self): flavor = Flavor(ilike=True) Flavor.set(flavor) try: for like in [NotILike(self.table.c1, 'foo'), ~self.table.c1.ilike('foo')]: self.assertEqual(str(like), '("c1" NOT ILIKE %s)') self.assertEqual(like.params, ('foo',)) finally: Flavor.set(Flavor()) flavor = Flavor(ilike=False) Flavor.set(flavor) try: like = NotILike(self.table.c1, 'foo') self.assertEqual( str(like), '(UPPER("c1") NOT LIKE UPPER(%s))') self.assertEqual(like.params, ('foo',)) finally: Flavor.set(Flavor()) def test_in(self): for in_ in [In(self.table.c1, [self.table.c2, 1, Null]), ~NotIn(self.table.c1, [self.table.c2, 1, Null]), ~~In(self.table.c1, [self.table.c2, 1, Null])]: self.assertEqual(str(in_), '("c1" IN ("c2", %s, %s))') self.assertEqual(in_.params, (1, None)) t2 = Table('t2') in_ = In(self.table.c1, t2.select(t2.c2)) self.assertEqual(str(in_), '("c1" IN (SELECT "a"."c2" FROM "t2" AS "a"))') self.assertEqual(in_.params, ()) in_ = In(self.table.c1, t2.select(t2.c2) | t2.select(t2.c3)) self.assertEqual(str(in_), '("c1" IN (SELECT "a"."c2" FROM "t2" AS "a" ' 'UNION SELECT "a"."c3" FROM "t2" AS "a"))') self.assertEqual(in_.params, ()) in_ = In(self.table.c1, array('l', list(range(10)))) self.assertEqual(str(in_), '("c1" IN (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s))') self.assertEqual(in_.params, tuple(range(10))) def test_exists(self): exists = Exists(self.table.select(self.table.c1, where=self.table.c1 == 1)) self.assertEqual(str(exists), '(EXISTS (SELECT "a"."c1" FROM "t" AS "a" ' 'WHERE ("a"."c1" = %s)))') self.assertEqual(exists.params, (1,)) def test_floordiv(self): with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") FloorDiv(4, 2) self.assertEqual(len(w), 1) self.assertTrue(issubclass(w[-1].category, DeprecationWarning)) if hasattr(self, 'assertIn'): self.assertIn( 'FloorDiv operator is deprecated, use Div function', str(w[-1].message)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717020245.0 python_sql-1.5.2/sql/tests/test_order.py0000644000175000017500000000434014625723125016524 0ustar00cedced# This file is part of python-sql. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import unittest from sql import ( Asc, Column, Desc, Flavor, Literal, NullOrder, NullsFirst, NullsLast, Order, Table) class TestOrder(unittest.TestCase): column = Column(Table('t'), 'c') def test_asc(self): self.assertEqual(str(self.column.asc), '"c" ASC') def test_desc(self): self.assertEqual(str(self.column.desc), '"c" DESC') def test_nulls_first(self): self.assertEqual(str(self.column.nulls_first), '"c" NULLS FIRST') self.assertEqual(str(Asc(self.column).nulls_first), '"c" ASC NULLS FIRST') def test_nulls_last(self): self.assertEqual(str(self.column.nulls_last), '"c" NULLS LAST') self.assertEqual(str(Asc(self.column).nulls_last), '"c" ASC NULLS LAST') def test_null_order_case_values(self): with self.assertRaises(NotImplementedError): NullOrder(self.column)._case_values() def test_no_null_ordering(self): try: Flavor.set(Flavor(null_ordering=False)) exp = NullsFirst(self.column) self.assertEqual(str(exp), 'CASE WHEN ("c" IS NULL) THEN %s ELSE %s END ASC, "c"') self.assertEqual(exp.params, (0, 1)) exp = NullsFirst(Desc(self.column)) self.assertEqual(str(exp), 'CASE WHEN ("c" IS NULL) THEN %s ELSE %s END ASC, "c" DESC') self.assertEqual(exp.params, (0, 1)) exp = NullsLast(Literal(2)) self.assertEqual(str(exp), 'CASE WHEN (%s IS NULL) THEN %s ELSE %s END ASC, %s') self.assertEqual(exp.params, (2, 1, 0, 2)) finally: Flavor.set(Flavor()) def test_order_query(self): table = Table('t') column = Column(table, 'c') query = table.select(column) self.assertEqual(str(Asc(query)), '(SELECT "a"."c" FROM "t" AS "a") ASC') self.assertEqual(str(Desc(query)), '(SELECT "a"."c" FROM "t" AS "a") DESC') def test_invalid_expression(self): with self.assertRaises(ValueError): Order('foo') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717000661.0 python_sql-1.5.2/sql/tests/test_rollup.py0000644000175000017500000000052314625654725016736 0ustar00cedced# This file is part of python-sql. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import unittest from sql import Rollup class TestRollup(unittest.TestCase): def test_invalid_expressions(self): with self.assertRaises(ValueError): Rollup('foo') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717020245.0 python_sql-1.5.2/sql/tests/test_select.py0000644000175000017500000005237514625723125016703 0ustar00cedced# This file is part of python-sql. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import unittest import warnings from copy import deepcopy from sql import ( Cube, Flavor, For, Grouping, Join, Literal, Rollup, Select, Table, Union, Window, With) from sql.aggregate import Max, Min from sql.functions import DatePart, Function, Now, Rank class TestSelect(unittest.TestCase): table = Table('t') def test_select1(self): query = self.table.select() self.assertEqual(str(query), 'SELECT * FROM "t" AS "a"') self.assertEqual(tuple(query.params), ()) def test_select2(self): query = self.table.select(self.table.c) self.assertEqual(str(query), 'SELECT "a"."c" FROM "t" AS "a"') self.assertEqual(tuple(query.params), ()) query.columns += (self.table.c2,) self.assertEqual(str(query), 'SELECT "a"."c", "a"."c2" FROM "t" AS "a"') def test_select3(self): query = self.table.select(where=(self.table.c == 'foo')) self.assertEqual(str(query), 'SELECT * FROM "t" AS "a" WHERE ("a"."c" = %s)') self.assertEqual(tuple(query.params), ('foo',)) def test_select_iter(self): query = self.table.select() self.assertEqual( tuple(query), ('SELECT * FROM "t" AS "a"', ())) def test_select_without_from(self): query = Select([Literal(1)]) self.assertEqual(str(query), 'SELECT %s') self.assertEqual(tuple(query.params), (1,)) def test_select_select(self): query = Select([Select([Literal(1)])]) self.assertEqual(str(query), 'SELECT (SELECT %s)') self.assertEqual(tuple(query.params), (1,)) def test_select_select_as(self): query = Select([Select([Literal(1)]).as_('foo')]) self.assertEqual(str(query), 'SELECT (SELECT %s) AS "foo"') self.assertEqual(tuple(query.params), (1,)) def test_select_invalid_column(self): with self.assertRaises(ValueError): Select(['foo']) def test_select_invalid_where(self): with self.assertRaises(ValueError): self.table.select(where='foo') def test_select_distinct(self): query = self.table.select(self.table.c, distinct=True) self.assertEqual( str(query), 'SELECT DISTINCT "a"."c" FROM "t" AS "a"') self.assertEqual(tuple(query.params), ()) def test_select_distinct_on(self): query = self.table.select(self.table.c, distinct_on=self.table.c) self.assertEqual( str(query), 'SELECT DISTINCT ON ("a"."c") "a"."c" FROM "t" AS "a"') self.assertEqual(tuple(query.params), ()) query = self.table.select( self.table.c, distinct_on=[self.table.a, self.table.b]) self.assertEqual( str(query), 'SELECT DISTINCT ON ("a"."a", "a"."b") "a"."c" FROM "t" AS "a"') self.assertEqual(tuple(query.params), ()) def test_select_invalid_distinct_on(self): with self.assertRaises(ValueError): self.table.select(self.table.c, distinct_on='foo') def test_select_from_list(self): t2 = Table('t2') t3 = Table('t3') query = (self.table + t2 + t3).select(self.table.c, getattr(t2, '*')) self.assertEqual(str(query), 'SELECT "a"."c", "b".* FROM "t" AS "a", "t2" AS "b", "t3" AS "c"') self.assertEqual(tuple(query.params), ()) def test_select_union(self): query1 = self.table.select() query2 = Table('t2').select() union = query1 | query2 self.assertEqual(str(union), 'SELECT * FROM "t" AS "a" UNION SELECT * FROM "t2" AS "b"') union.all_ = True self.assertEqual(str(union), 'SELECT * FROM "t" AS "a" UNION ALL ' 'SELECT * FROM "t2" AS "b"') self.assertEqual(str(union.select()), 'SELECT * FROM (' 'SELECT * FROM "t" AS "b" UNION ALL ' 'SELECT * FROM "t2" AS "c") AS "a"') query1.where = self.table.c == 'foo' self.assertEqual(str(union), 'SELECT * FROM "t" AS "a" WHERE ("a"."c" = %s) UNION ALL ' 'SELECT * FROM "t2" AS "b"') self.assertEqual(tuple(union.params), ('foo',)) union = Union(query1) self.assertEqual(str(union), str(query1)) self.assertEqual(tuple(union.params), tuple(query1.params)) def test_select_union_order(self): query1 = self.table.select() query2 = Table('t2').select() union = query1 | query2 union.order_by = Literal(1) self.assertEqual(str(union), 'SELECT * FROM "t" AS "a" UNION ' 'SELECT * FROM "t2" AS "b" ' 'ORDER BY %s') self.assertEqual(tuple(union.params), (1,)) def test_select_intersect(self): query1 = self.table.select() query2 = Table('t2').select() intersect = query1 & query2 self.assertEqual(str(intersect), 'SELECT * FROM "t" AS "a" INTERSECT SELECT * FROM "t2" AS "b"') from sql import Interesect, Intersect with warnings.catch_warnings(record=True) as w: warnings.simplefilter('always') interesect = Interesect(query1, query2) self.assertEqual(len(w), 1) self.assertTrue(issubclass(w[-1].category, DeprecationWarning)) if hasattr(self, 'assertIn'): self.assertIn('Interesect query is deprecated, use Intersect', str(w[-1].message)) self.assertTrue(isinstance(interesect, Intersect)) def test_select_except(self): query1 = self.table.select() query2 = Table('t2').select() except_ = query1 - query2 self.assertEqual(str(except_), 'SELECT * FROM "t" AS "a" EXCEPT SELECT * FROM "t2" AS "b"') def test_select_join(self): t1 = Table('t1') t2 = Table('t2') join = Join(t1, t2) self.assertEqual(str(join.select()), 'SELECT * FROM "t1" AS "a" INNER JOIN "t2" AS "b"') self.assertEqual(str(join.select(getattr(t1, '*'))), 'SELECT "a".* FROM "t1" AS "a" INNER JOIN "t2" AS "b"') def test_select_subselect(self): t1 = Table('t1') select = t1.select() self.assertEqual(str(select.select()), 'SELECT * FROM (SELECT * FROM "t1" AS "b") AS "a"') self.assertEqual(tuple(select.params), ()) def test_select_function(self): query = Now().select() self.assertEqual(str(query), 'SELECT * FROM NOW() AS "a"') self.assertEqual(tuple(query.params), ()) def test_select_function_columns_definitions(self): class Crosstab(Function): _function = 'CROSSTAB' query = Crosstab('query1', 'query2', columns_definitions=[ ('c1', 'INT'), ('c2', 'CHAR'), ('c3', 'BOOL')]).select() self.assertEqual(str(query), 'SELECT * FROM CROSSTAB(%s, %s) ' 'AS "a" ("c1" INT, "c2" CHAR, "c3" BOOL)') self.assertEqual(tuple(query.params), ('query1', 'query2')) def test_select_group_by(self): column = self.table.c query = self.table.select(column, group_by=column) self.assertEqual(str(query), 'SELECT "a"."c" FROM "t" AS "a" GROUP BY "a"."c"') self.assertEqual(tuple(query.params), ()) output = column.as_('c1') query = self.table.select(output, group_by=output) self.assertEqual(str(query), 'SELECT "a"."c" AS "c1" FROM "t" AS "a" GROUP BY "c1"') self.assertEqual(tuple(query.params), ()) query = self.table.select(Literal('foo'), group_by=Literal('foo')) self.assertEqual(str(query), 'SELECT %s FROM "t" AS "a" GROUP BY %s') self.assertEqual(tuple(query.params), ('foo', 'foo')) def test_select_group_by_grouping_sets(self): query = self.table.select( Literal('*'), group_by=Grouping((self.table.a, self.table.b), (Literal('foo'),))) self.assertEqual(str(query), 'SELECT %s FROM "t" AS "a" ' 'GROUP BY GROUPING SETS (("a"."a", "a"."b"), (%s))') self.assertEqual(tuple(query.params), ('*', 'foo',)) query = self.table.select( Literal('*'), group_by=[ self.table.a, Grouping((self.table.b,), (self.table.c,))]) self.assertEqual(str(query), 'SELECT %s FROM "t" AS "a" ' 'GROUP BY "a"."a", GROUPING SETS (("a"."b"), ("a"."c"))') self.assertEqual(tuple(query.params), ('*',)) def test_select_group_by_rollup(self): query = self.table.select( Literal('*'), group_by=Rollup(self.table.a, self.table.b, Literal('foo'))) self.assertEqual(str(query), 'SELECT %s FROM "t" AS "a" ' 'GROUP BY ROLLUP ("a"."a", "a"."b", %s)') self.assertEqual(tuple(query.params), ('*', 'foo')) query = self.table.select( Literal('*'), group_by=Rollup((self.table.a, self.table.b), self.table.c)) self.assertEqual(str(query), 'SELECT %s FROM "t" AS "a" ' 'GROUP BY ROLLUP (("a"."a", "a"."b"), "a"."c")') self.assertEqual(tuple(query.params), ('*',)) def test_select_group_by_cube(self): query = self.table.select( Literal('*'), group_by=Cube(self.table.a, self.table.b)) self.assertEqual(str(query), 'SELECT %s FROM "t" AS "a" ' 'GROUP BY CUBE ("a"."a", "a"."b")') self.assertEqual(tuple(query.params), ('*',)) def test_select_invalid_group_by(self): with self.assertRaises(ValueError): self.table.select(group_by=['foo']) def test_select_having(self): col1 = self.table.col1 col2 = self.table.col2 query = self.table.select(col1, Min(col2), having=(Min(col2) > 3)) self.assertEqual(str(query), 'SELECT "a"."col1", MIN("a"."col2") FROM "t" AS "a" ' 'HAVING (MIN("a"."col2") > %s)') self.assertEqual(tuple(query.params), (3,)) def test_select_invalid_having(self): with self.assertRaises(ValueError): self.table.select(having='foo') def test_select_order(self): c = self.table.c query = self.table.select(c, order_by=Literal(1)) self.assertEqual(str(query), 'SELECT "a"."c" FROM "t" AS "a" ORDER BY %s') self.assertEqual(tuple(query.params), (1,)) def test_select_invalid_order(self): with self.assertRaises(ValueError): self.table.select(order_by='foo') def test_select_limit_offset(self): try: Flavor.set(Flavor(limitstyle='limit')) query = self.table.select(limit=50, offset=10) self.assertEqual(str(query), 'SELECT * FROM "t" AS "a" LIMIT %s OFFSET %s') self.assertEqual(tuple(query.params), (50, 10)) query.limit = None self.assertEqual(str(query), 'SELECT * FROM "t" AS "a" OFFSET %s') self.assertEqual(tuple(query.params), (10,)) query.offset = 0 self.assertEqual(str(query), 'SELECT * FROM "t" AS "a"') self.assertEqual(tuple(query.params), ()) Flavor.set(Flavor(limitstyle='limit', max_limit=-1)) query.offset = None self.assertEqual(str(query), 'SELECT * FROM "t" AS "a"') self.assertEqual(tuple(query.params), ()) query.offset = 0 self.assertEqual(str(query), 'SELECT * FROM "t" AS "a"') self.assertEqual(tuple(query.params), ()) query.offset = 10 self.assertEqual(str(query), 'SELECT * FROM "t" AS "a" LIMIT -1 OFFSET %s') self.assertEqual(tuple(query.params), (10,)) finally: Flavor.set(Flavor()) def test_select_invalid_limit(self): with self.assertRaises(ValueError): self.table.select(limit='foo') def test_select_invalid_offset(self): with self.assertRaises(ValueError): self.table.select(offset='foo') def test_select_offset_fetch(self): try: Flavor.set(Flavor(limitstyle='fetch')) query = self.table.select(limit=50, offset=10) self.assertEqual(str(query), 'SELECT * FROM "t" AS "a" ' 'OFFSET (%s) ROWS FETCH FIRST (%s) ROWS ONLY') self.assertEqual(tuple(query.params), (10, 50)) query.limit = None self.assertEqual(str(query), 'SELECT * FROM "t" AS "a" OFFSET (%s) ROWS') self.assertEqual(tuple(query.params), (10,)) query.offset = 0 self.assertEqual(str(query), 'SELECT * FROM "t" AS "a"') self.assertEqual(tuple(query.params), ()) finally: Flavor.set(Flavor()) def test_select_rownum(self): try: Flavor.set(Flavor(limitstyle='rownum')) query = self.table.select(limit=50, offset=10) self.assertEqual(str(query), 'SELECT "a".* FROM (' 'SELECT "b".*, ROWNUM AS "rnum" FROM (' 'SELECT * FROM "t" AS "c") AS "b" ' 'WHERE (ROWNUM <= %s)) AS "a" ' 'WHERE ("rnum" > %s)') self.assertEqual(tuple(query.params), (60, 10)) query = self.table.select( self.table.c1.as_('col1'), self.table.c2.as_('col2'), limit=50, offset=10) self.assertEqual(str(query), 'SELECT "a"."col1", "a"."col2" FROM (' 'SELECT "b"."col1", "b"."col2", ROWNUM AS "rnum" FROM (' 'SELECT "c"."c1" AS "col1", "c"."c2" AS "col2" ' 'FROM "t" AS "c") AS "b" ' 'WHERE (ROWNUM <= %s)) AS "a" ' 'WHERE ("rnum" > %s)') self.assertEqual(tuple(query.params), (60, 10)) subquery = query.select(query.col1, query.col2) self.assertEqual(str(subquery), 'SELECT "a"."col1", "a"."col2" FROM (' 'SELECT "b"."col1", "b"."col2" FROM (' 'SELECT "a"."col1", "a"."col2", ROWNUM AS "rnum" ' 'FROM (' 'SELECT "c"."c1" AS "col1", "c"."c2" AS "col2" ' 'FROM "t" AS "c") AS "a" ' 'WHERE (ROWNUM <= %s)) AS "b" ' 'WHERE ("rnum" > %s)) AS "a"') # XXX alias of query is reused but not a problem # as it is hidden in subquery self.assertEqual(tuple(query.params), (60, 10)) query = self.table.select(limit=50, offset=10, order_by=[self.table.c]) self.assertEqual(str(query), 'SELECT "a".* FROM (' 'SELECT "b".*, ROWNUM AS "rnum" FROM (' 'SELECT * FROM "t" AS "c" ORDER BY "c"."c") AS "b" ' 'WHERE (ROWNUM <= %s)) AS "a" ' 'WHERE ("rnum" > %s)') self.assertEqual(tuple(query.params), (60, 10)) query = self.table.select(limit=50) self.assertEqual(str(query), 'SELECT "a".* FROM (' 'SELECT * FROM "t" AS "b") AS "a" ' 'WHERE (ROWNUM <= %s)') self.assertEqual(tuple(query.params), (50,)) query = self.table.select(offset=10) self.assertEqual(str(query), 'SELECT "a".* FROM (' 'SELECT "b".*, ROWNUM AS "rnum" FROM (' 'SELECT * FROM "t" AS "c") AS "b") AS "a" ' 'WHERE ("rnum" > %s)') self.assertEqual(tuple(query.params), (10,)) query = self.table.select(self.table.c.as_('col'), where=self.table.c >= 20, limit=50, offset=10) self.assertEqual(str(query), 'SELECT "a"."col" FROM (' 'SELECT "b"."col", ROWNUM AS "rnum" FROM (' 'SELECT "c"."c" AS "col" FROM "t" AS "c" ' 'WHERE ("c"."c" >= %s)) AS "b" ' 'WHERE (ROWNUM <= %s)) AS "a" ' 'WHERE ("rnum" > %s)') self.assertEqual(tuple(query.params), (20, 60, 10)) finally: Flavor.set(Flavor()) def test_select_for(self): c = self.table.c query = self.table.select(c, for_=For('UPDATE')) self.assertEqual(str(query), 'SELECT "a"."c" FROM "t" AS "a" FOR UPDATE') self.assertEqual(tuple(query.params), ()) def test_select_invalid_for(self): with self.assertRaises(ValueError): self.table.select(for_=['foo']) def test_copy(self): query = self.table.select() copy_query = deepcopy(query) self.assertNotEqual(query, copy_query) self.assertEqual(str(copy_query), 'SELECT * FROM "t" AS "a"') self.assertEqual(tuple(copy_query.params), ()) def test_with(self): w = With(query=self.table.select(self.table.c1)) query = w.select(with_=[w]) self.assertEqual(str(query), 'WITH "a" AS (SELECT "b"."c1" FROM "t" AS "b") ' 'SELECT * FROM "a" AS "a"') self.assertEqual(tuple(query.params), ()) def test_window(self): query = self.table.select(Min(self.table.c1, window=Window([self.table.c2]))) self.assertEqual(str(query), 'SELECT MIN("a"."c1") OVER "b" FROM "t" AS "a" ' 'WINDOW "b" AS (PARTITION BY "a"."c2")') self.assertEqual(tuple(query.params), ()) query = self.table.select(Rank(window=Window([]))) self.assertEqual(str(query), 'SELECT RANK() OVER "b" FROM "t" AS "a" ' 'WINDOW "b" AS ()') self.assertEqual(tuple(query.params), ()) window = Window([self.table.c1]) query = self.table.select( Rank(filter_=self.table.c1 > 0, window=window), Min(self.table.c1, window=window)) self.assertEqual(str(query), 'SELECT RANK() FILTER (WHERE ("a"."c1" > %s)) OVER "b", ' 'MIN("a"."c1") OVER "b" FROM "t" AS "a" ' 'WINDOW "b" AS (PARTITION BY "a"."c1")') self.assertEqual(tuple(query.params), (0,)) window = Window([DatePart('year', self.table.date_col)]) query = self.table.select( Min(self.table.c1, window=window)) self.assertEqual(str(query), 'SELECT MIN("a"."c1") OVER "b" FROM "t" AS "a" ' 'WINDOW "b" AS (PARTITION BY DATE_PART(%s, "a"."date_col"))') self.assertEqual(tuple(query.params), ('year',)) window = Window([self.table.c2]) query = self.table.select( Max(self.table.c1, window=window) / Min(self.table.c1, window=window)) self.assertEqual(str(query), 'SELECT (MAX("a"."c1") OVER (PARTITION BY "a"."c2") ' '/ MIN("a"."c1") OVER (PARTITION BY "a"."c2")) ' 'FROM "t" AS "a"') self.assertEqual(tuple(query.params), ()) window = Window([Literal(1)]) query = self.table.select( Max(self.table.c1, window=window) / Min(self.table.c1, window=window)) self.assertEqual(str(query), 'SELECT (MAX("a"."c1") OVER (PARTITION BY %s) ' '/ MIN("a"."c1") OVER (PARTITION BY %s)) ' 'FROM "t" AS "a"') self.assertEqual(tuple(query.params), (1, 1)) window1 = Window([self.table.c2]) window2 = Window([Literal(1)]) query = self.table.select( Max(self.table.c1, window=window1) / Min(self.table.c1, window=window2), windows=[window1]) self.assertEqual(str(query), 'SELECT (MAX("a"."c1") OVER "b" ' '/ MIN("a"."c1") OVER (PARTITION BY %s)) ' 'FROM "t" AS "a" ' 'WINDOW "b" AS (PARTITION BY "a"."c2")') self.assertEqual(tuple(query.params), (1,)) def test_window_with_alias(self): query = self.table.select( Min(self.table.c1, window=Window([self.table.c2])).as_('min')) self.assertEqual( str(query), 'SELECT MIN("a"."c1") OVER "b" AS "min" FROM "t" AS "a" ' 'WINDOW "b" AS (PARTITION BY "a"."c2")') self.assertEqual(query.params, ()) def test_select_invalid_window(self): with self.assertRaises(ValueError): self.table.select(windows=['foo']) def test_order_params(self): with_ = With(query=self.table.select(self.table.c, where=(self.table.c > 1))) w = Window([Literal(8)]) query = Select([Literal(2), Min(self.table.c, window=w)], from_=self.table.select(where=self.table.c > 3), with_=with_, where=self.table.c > 4, group_by=[Literal(5)], order_by=[Literal(6)], having=Literal(7)) self.assertEqual(tuple(query.params), (1, 2, 3, 4, 5, 6, 7, 8)) def test_no_as(self): query = self.table.select(self.table.c) try: Flavor.set(Flavor(no_as=True)) self.assertEqual(str(query), 'SELECT "a"."c" FROM "t" "a"') self.assertEqual(tuple(query.params), ()) finally: Flavor.set(Flavor()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1631604868.0 python_sql-1.5.2/sql/tests/test_table.py0000644000175000017500000000136614120050204016462 0ustar00cedced# This file is part of python-sql. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import unittest from sql import Table class TestTable(unittest.TestCase): def test_name(self): t = Table('mytable') self.assertEqual(str(t), '"mytable"') def test_quoted_name(self): t = Table('my "quoted" name') self.assertEqual(str(t), '"my ""quoted"" name"') def test_schema(self): t = Table('mytable', schema='myschema') self.assertEqual(str(t), '"myschema"."mytable"') def test_database(self): t = Table('mytable', database='mydatabase', schema='myschema') self.assertEqual(str(t), '"mydatabase"."myschema"."mytable"') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717002854.0 python_sql-1.5.2/sql/tests/test_update.py0000644000175000017500000000731014625661146016677 0ustar00cedced# This file is part of python-sql. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import unittest from sql import Literal, Table, With class TestUpdate(unittest.TestCase): table = Table('t') def test_update1(self): query = self.table.update([self.table.c], ['foo']) self.assertEqual(str(query), 'UPDATE "t" AS "a" SET "c" = %s') self.assertEqual(query.params, ('foo',)) query.where = (self.table.b == Literal(True)) self.assertEqual(str(query), 'UPDATE "t" AS "a" SET "c" = %s WHERE ("a"."b" = %s)') self.assertEqual(query.params, ('foo', True)) def test_update2(self): t1 = Table('t1') t2 = Table('t2') query = t1.update([t1.c], ['foo'], from_=[t2], where=(t1.c == t2.c)) self.assertEqual(str(query), 'UPDATE "t1" AS "b" SET "c" = %s FROM "t2" AS "a" ' 'WHERE ("b"."c" = "a"."c")') self.assertEqual(query.params, ('foo',)) def test_update_invalid_values(self): with self.assertRaises(ValueError): self.table.update([self.table.c], 'foo') def test_update_invalid_where(self): with self.assertRaises(ValueError): self.table.update([self.table.c], ['foo'], where='foo') def test_update_subselect(self): t1 = Table('t1') t2 = Table('t2') query_list = t1.update([t1.c], [t2.select(t2.c, where=t2.i == t1.i)]) query_nolist = t1.update([t1.c], t2.select(t2.c, where=t2.i == t1.i)) for query in [query_list, query_nolist]: self.assertEqual(str(query), 'UPDATE "t1" AS "b" SET "c" = (' 'SELECT "a"."c" FROM "t2" AS "a" WHERE ("a"."i" = "b"."i"))') self.assertEqual(query.params, ()) def test_update_returning(self): query = self.table.update([self.table.c], ['foo'], returning=[self.table.c]) self.assertEqual(str(query), 'UPDATE "t" AS "a" SET "c" = %s RETURNING "a"."c"') self.assertEqual(query.params, ('foo',)) def test_update_returning_select(self): t1 = Table('t1') t2 = Table('t2') query = t1.update([t1.c], ['foo'], returning=[ t2.select(t2.c, where=(t2.c1 == t1.c) & (t2.c2 == 'bar'))]) self.assertEqual(str(query), 'UPDATE "t1" AS "b" SET "c" = %s ' 'RETURNING (SELECT "a"."c" FROM "t2" AS "a" ' 'WHERE (("a"."c1" = "b"."c") AND ("a"."c2" = %s)))') self.assertEqual(query.params, ('foo', 'bar')) def test_with(self): t1 = Table('t1') w = With(query=t1.select(t1.c1)) query = self.table.update( [self.table.c2], with_=[w], values=[w.select(w.c3, where=w.c4 == 2)]) self.assertEqual(str(query), 'WITH "a" AS (SELECT "b"."c1" FROM "t1" AS "b") ' 'UPDATE "t" AS "c" SET "c2" = (SELECT "a"."c3" FROM "a" AS "a" ' 'WHERE ("a"."c4" = %s))') self.assertEqual(query.params, (2,)) def test_schema(self): t1 = Table('t1', 'default') query = t1.update([t1.c1], ['foo']) self.assertEqual( str(query), 'UPDATE "default"."t1" AS "a" SET "c1" = %s') self.assertEqual(query.params, ('foo',)) def test_schema_subselect(self): t1 = Table('t1', 'default') t2 = Table('t2', 'default') query = t1.update([t1.c1], t2.select(t2.c, where=t2.i == t1.i)) self.assertEqual(str(query), 'UPDATE "default"."t1" AS "b" SET "c1" = (' 'SELECT "a"."c" FROM "default"."t2" AS "a" ' 'WHERE ("a"."i" = "b"."i"))') self.assertEqual(query.params, ()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1631604889.0 python_sql-1.5.2/sql/tests/test_values.py0000644000175000017500000000203514120050231016664 0ustar00cedced# This file is part of python-sql. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import unittest from sql import Values class TestValues(unittest.TestCase): def test_single_values(self): values = Values([[1]]) self.assertEqual(str(values), 'VALUES (%s)') self.assertEqual(values.params, (1,)) def test_many_values(self): values = Values([[1, 2], [3, 4]]) self.assertEqual(str(values), 'VALUES (%s, %s), (%s, %s)') self.assertEqual(values.params, (1, 2, 3, 4)) def test_select(self): values = Values([[1], [2], [3]]) query = values.select() self.assertEqual(str(query), 'SELECT * FROM (VALUES (%s), (%s), (%s)) AS "a"') self.assertEqual(tuple(query.params), (1, 2, 3)) def test_union(self): values = Values([[1]]) values |= Values([[2]]) self.assertEqual(str(values), 'VALUES (%s) UNION VALUES (%s)') self.assertEqual(values.params, (1, 2)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717001051.0 python_sql-1.5.2/sql/tests/test_window.py0000644000175000017500000000615714625655533016737 0ustar00cedced# This file is part of python-sql. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import unittest from sql import Table, Window class TestWindow(unittest.TestCase): def test_window(self): t = Table('t') window = Window([t.c1, t.c2]) self.assertEqual(str(window), 'PARTITION BY "c1", "c2"') self.assertEqual(window.params, ()) def test_window_invalid_partition(self): with self.assertRaises(ValueError): Window(['foo']) def test_window_order(self): t = Table('t') window = Window([t.c], order_by=t.c) self.assertEqual(str(window), 'PARTITION BY "c" ORDER BY "c"') self.assertEqual(window.params, ()) def test_window_invalid_order(self): with self.assertRaises(ValueError): Window([Table('t').c], order_by='foo') def test_window_range(self): t = Table('t') window = Window([t.c], frame='RANGE') self.assertEqual(str(window), 'PARTITION BY "c" RANGE ' 'BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW') self.assertEqual(window.params, ()) window.start = -1 self.assertEqual(str(window), 'PARTITION BY "c" RANGE ' 'BETWEEN %s PRECEDING AND CURRENT ROW') self.assertEqual(window.params, (1,)) window.start = 0 window.end = 1 self.assertEqual(str(window), 'PARTITION BY "c" RANGE ' 'BETWEEN CURRENT ROW AND %s FOLLOWING') self.assertEqual(window.params, (1,)) window.start = 1 window.end = None self.assertEqual(str(window), 'PARTITION BY "c" RANGE ' 'BETWEEN %s FOLLOWING AND UNBOUNDED FOLLOWING') self.assertEqual(window.params, (1,)) def test_window_invalid_frame(self): with self.assertRaises(ValueError): Window([Table('t').c], frame='foo') def test_window_invalid_start(self): with self.assertRaises(ValueError): Window([Table('t').c], start='foo') def test_window_invalid_end(self): with self.assertRaises(ValueError): Window([Table('t').c], end='foo') def test_window_exclude(self): t = Table('t') window = Window([t.c], exclude='TIES') self.assertEqual(str(window), 'PARTITION BY "c" EXCLUDE TIES') self.assertEqual(window.params, ()) def test_window_invalid_exclude(self): with self.assertRaises(ValueError): Window([Table('t').c], exclude='foo') def test_window_rows(self): t = Table('t') window = Window([t.c], frame='ROWS') self.assertEqual(str(window), 'PARTITION BY "c" ROWS ' 'BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW') self.assertEqual(window.params, ()) def test_window_groups(self): t = Table('t') window = Window([t.c], frame='GROUPS') self.assertEqual(str(window), 'PARTITION BY "c" GROUPS ' 'BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW') self.assertEqual(window.params, ()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1726872270.0 python_sql-1.5.2/sql/tests/test_with.py0000644000175000017500000000450314673375316016375 0ustar00cedced# This file is part of python-sql. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import unittest from sql import AliasManager, Literal, Table, Values, With, WithQuery class TestWith(unittest.TestCase): table = Table('t') def test_with(self): with AliasManager(): simple = With(query=self.table.select(self.table.id, where=self.table.id == 1)) self.assertEqual(simple.statement(), '"a" AS (' 'SELECT "b"."id" FROM "t" AS "b" WHERE ("b"."id" = %s)' ')') self.assertEqual(simple.statement_params(), (1,)) def test_with_columns(self): with AliasManager(): second = With('a', query=self.table.select(self.table.a)) self.assertEqual(second.statement(), '"a" ("a") AS (' 'SELECT "b"."a" FROM "t" AS "b"' ')') self.assertEqual(second.statement_params(), ()) def test_with_query(self): with AliasManager(): simple = With() simple.query = self.table.select(self.table.id, where=self.table.id == 1) second = With() second.query = simple.select() wq = WithQuery(with_=[simple, second]) self.assertEqual(wq._with_str(), 'WITH "a" AS (' 'SELECT "b"."id" FROM "t" AS "b" WHERE ("b"."id" = %s)' '), "c" AS (' 'SELECT * FROM "a" AS "a"' ') ') self.assertEqual(wq._with_params(), (1,)) def test_recursive(self): upto10 = With('n', recursive=True) upto10.query = Values([(1,)]) upto10.query |= upto10.select( upto10.n + Literal(1), where=upto10.n < Literal(100)) upto10.query.all_ = True q = upto10.select(with_=[upto10]) self.assertEqual(str(q), 'WITH RECURSIVE "a" ("n") AS (' 'VALUES (%s) ' 'UNION ALL ' 'SELECT ("a"."n" + %s) FROM "a" AS "a" WHERE ("a"."n" < %s)' ') SELECT * FROM "a" AS "a"') self.assertEqual(tuple(q.params), (1, 1, 100)) def test_invalid_with(self): with self.assertRaises(ValueError): WithQuery(with_=['foo']) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1727728859.0 python_sql-1.5.2/tox.ini0000644000175000017500000000112014676606333013352 0ustar00cedced# Tox (http://tox.testrun.org/) is a tool for running tests # in multiple virtualenvs. This configuration file will run the # test suite on all supported python versions. To use it, "pip install tox" # and then run "tox" from this directory. [tox] envlist = py35, py36, py37, py38, py39, py310, py311, py312, pypy3 [testenv] usedevelop = true commands = coverage run --omit=*/tests/*,*/.tox/* -m xmlrunner discover -s sql.tests {posargs} commands_post = coverage report --omit=README.rst coverage xml --omit=README.rst deps = coverage unittest-xml-reporting passenv = *