[tryton-debian-vcs] python-sql branch upstream created. 119d7a2fbbf42dfb537697aebfbebb6aa590cb89
Mathias Behrle
tryton-debian-vcs at alioth.debian.org
Wed Nov 27 16:48:17 UTC 2013
The following commit has been merged in the upstream branch:
https://alioth.debian.org/plugins/scmgit/cgi-bin/gitweb.cgi/?p=tryton/python-sql.git;a=commitdiff;h=119d7a2fbbf42dfb537697aebfbebb6aa590cb89
commit 119d7a2fbbf42dfb537697aebfbebb6aa590cb89
Author: Mathias Behrle <mathiasb at m9s.biz>
Date: Mon Oct 7 14:43:55 2013 +0200
Adding upstream version 0.2.
diff --git a/CHANGELOG b/CHANGELOG
new file mode 100644
index 0000000..2780393
--- /dev/null
+++ b/CHANGELOG
@@ -0,0 +1,6 @@
+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
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..f188446
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,2 @@
+include README
+include CHANGELOG
diff --git a/PKG-INFO b/PKG-INFO
new file mode 100644
index 0000000..2a7162e
--- /dev/null
+++ b/PKG-INFO
@@ -0,0 +1,189 @@
+Metadata-Version: 1.1
+Name: python-sql
+Version: 0.2
+Summary: Library to write SQL queries
+Home-page: http://code.google.com/p/python-sql/
+Author: B2CK
+Author-email: info at b2ck.com
+License: BSD
+Description: 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.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,))
+
+ Insert query with default values::
+
+ >>> tuple(user.insert())
+ ('INSERT INTO "user" DEFAULT VALUES', ())
+
+ Insert query with values::
+
+ >>> tuple(user.insert(columns=[user.name, user.login],
+ ... values=[['Foo', 'foo']]))
+ ('INSERT INTO "user" ("name", "login") VALUES (%s, %s)', ('Foo', 'foo'))
+ >>> tuple(user.insert(columns=[user.name, user.login],
+ ... values=[['Foo', 'foo'], ['Bar', 'bar']]))
+ ('INSERT INTO "user" ("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" SELECT "a"."login", "a"."passwd" FROM "passwd" AS "a"', ())
+
+ Update query with values::
+
+ >>> tuple(user.update(columns=[user.active], values=[True]))
+ ('UPDATE "user" SET "active" = %s', (True,))
+ >>> tuple(invoice.update(columns=[invoice.total], values=[invoice.amount + invoice.tax]))
+ ('UPDATE "invoice" SET "total" = ("invoice"."amount" + "invoice"."tax")', ())
+
+ Update query with where condition::
+
+ >>> tuple(user.update(columns=[user.active], values=[True], where=(
+ ... user.active == False)))
+ ('UPDATE "user" SET "active" = %s WHERE ("user"."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', ())
+ >>> Flavor.set(Flavor(paramstyle='qmark'))
+ >>> select = user.select()
+ >>> select.where = (user.name == 'foo')
+ >>> tuple(select)
+ ('SELECT * FROM "user" AS "a" WHERE ("a"."name" = ?)', ('foo',))
+
+Platform: UNKNOWN
+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 :: 2.6
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Topic :: Database
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
diff --git a/README b/README
new file mode 100644
index 0000000..57f08ee
--- /dev/null
+++ b/README
@@ -0,0 +1,170 @@
+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.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,))
+
+Insert query with default values::
+
+ >>> tuple(user.insert())
+ ('INSERT INTO "user" DEFAULT VALUES', ())
+
+Insert query with values::
+
+ >>> tuple(user.insert(columns=[user.name, user.login],
+ ... values=[['Foo', 'foo']]))
+ ('INSERT INTO "user" ("name", "login") VALUES (%s, %s)', ('Foo', 'foo'))
+ >>> tuple(user.insert(columns=[user.name, user.login],
+ ... values=[['Foo', 'foo'], ['Bar', 'bar']]))
+ ('INSERT INTO "user" ("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" SELECT "a"."login", "a"."passwd" FROM "passwd" AS "a"', ())
+
+Update query with values::
+
+ >>> tuple(user.update(columns=[user.active], values=[True]))
+ ('UPDATE "user" SET "active" = %s', (True,))
+ >>> tuple(invoice.update(columns=[invoice.total], values=[invoice.amount + invoice.tax]))
+ ('UPDATE "invoice" SET "total" = ("invoice"."amount" + "invoice"."tax")', ())
+
+Update query with where condition::
+
+ >>> tuple(user.update(columns=[user.active], values=[True], where=(
+ ... user.active == False)))
+ ('UPDATE "user" SET "active" = %s WHERE ("user"."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', ())
+ >>> Flavor.set(Flavor(paramstyle='qmark'))
+ >>> select = user.select()
+ >>> select.where = (user.name == 'foo')
+ >>> tuple(select)
+ ('SELECT * FROM "user" AS "a" WHERE ("a"."name" = ?)', ('foo',))
diff --git a/python_sql.egg-info/PKG-INFO b/python_sql.egg-info/PKG-INFO
new file mode 100644
index 0000000..2a7162e
--- /dev/null
+++ b/python_sql.egg-info/PKG-INFO
@@ -0,0 +1,189 @@
+Metadata-Version: 1.1
+Name: python-sql
+Version: 0.2
+Summary: Library to write SQL queries
+Home-page: http://code.google.com/p/python-sql/
+Author: B2CK
+Author-email: info at b2ck.com
+License: BSD
+Description: 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.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,))
+
+ Insert query with default values::
+
+ >>> tuple(user.insert())
+ ('INSERT INTO "user" DEFAULT VALUES', ())
+
+ Insert query with values::
+
+ >>> tuple(user.insert(columns=[user.name, user.login],
+ ... values=[['Foo', 'foo']]))
+ ('INSERT INTO "user" ("name", "login") VALUES (%s, %s)', ('Foo', 'foo'))
+ >>> tuple(user.insert(columns=[user.name, user.login],
+ ... values=[['Foo', 'foo'], ['Bar', 'bar']]))
+ ('INSERT INTO "user" ("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" SELECT "a"."login", "a"."passwd" FROM "passwd" AS "a"', ())
+
+ Update query with values::
+
+ >>> tuple(user.update(columns=[user.active], values=[True]))
+ ('UPDATE "user" SET "active" = %s', (True,))
+ >>> tuple(invoice.update(columns=[invoice.total], values=[invoice.amount + invoice.tax]))
+ ('UPDATE "invoice" SET "total" = ("invoice"."amount" + "invoice"."tax")', ())
+
+ Update query with where condition::
+
+ >>> tuple(user.update(columns=[user.active], values=[True], where=(
+ ... user.active == False)))
+ ('UPDATE "user" SET "active" = %s WHERE ("user"."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', ())
+ >>> Flavor.set(Flavor(paramstyle='qmark'))
+ >>> select = user.select()
+ >>> select.where = (user.name == 'foo')
+ >>> tuple(select)
+ ('SELECT * FROM "user" AS "a" WHERE ("a"."name" = ?)', ('foo',))
+
+Platform: UNKNOWN
+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 :: 2.6
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Topic :: Database
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
diff --git a/python_sql.egg-info/SOURCES.txt b/python_sql.egg-info/SOURCES.txt
new file mode 100644
index 0000000..7c24f7a
--- /dev/null
+++ b/python_sql.egg-info/SOURCES.txt
@@ -0,0 +1,30 @@
+CHANGELOG
+MANIFEST.in
+README
+setup.py
+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_column.py
+sql/tests/test_conditionals.py
+sql/tests/test_delete.py
+sql/tests/test_for.py
+sql/tests/test_functions.py
+sql/tests/test_insert.py
+sql/tests/test_join.py
+sql/tests/test_literal.py
+sql/tests/test_operators.py
+sql/tests/test_order.py
+sql/tests/test_select.py
+sql/tests/test_update.py
\ No newline at end of file
diff --git a/python_sql.egg-info/dependency_links.txt b/python_sql.egg-info/dependency_links.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/python_sql.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/python_sql.egg-info/top_level.txt b/python_sql.egg-info/top_level.txt
new file mode 100644
index 0000000..d5ce2f2
--- /dev/null
+++ b/python_sql.egg-info/top_level.txt
@@ -0,0 +1 @@
+sql
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..861a9f5
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,5 @@
+[egg_info]
+tag_build =
+tag_date = 0
+tag_svn_revision = 0
+
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..28fe987
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,65 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2011-2013, Cédric Krier
+# Copyright (c) 2011-2013, 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 <organization> 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 <COPYRIGHT HOLDER> 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.
+
+from setuptools import setup, find_packages
+import os
+import re
+
+
+def read(fname):
+ return open(os.path.join(os.path.dirname(__file__), fname)).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'),
+ author='B2CK',
+ author_email='info at b2ck.com',
+ url='http://code.google.com/p/python-sql/',
+ packages=find_packages(),
+ classifiers=[
+ 'Development Status :: 5 - Production/Stable',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: BSD License',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python :: 2.6',
+ 'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: 3',
+ 'Topic :: Database',
+ 'Topic :: Software Development :: Libraries :: Python Modules',
+ ],
+ license='BSD',
+ test_suite='sql.tests',
+ use_2to3=True,
+ )
diff --git a/sql/__init__.py b/sql/__init__.py
new file mode 100644
index 0000000..2c65f6b
--- /dev/null
+++ b/sql/__init__.py
@@ -0,0 +1,1075 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2011-2013, Cédric Krier
+# Copyright (c) 2013, Nicolas Évrard
+# Copyright (c) 2011-2013, 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 <organization> 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 <COPYRIGHT HOLDER> 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.
+
+from __future__ import division
+
+__version__ = '0.2'
+__all__ = ['Flavor', 'Table', 'Literal', 'Column', 'Join', 'Asc', 'Desc']
+
+import string
+from functools import partial
+from threading import local, currentThread
+from collections import defaultdict
+
+
+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:
+ 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
+ function_mapping - dictionary with Function to replace
+ '''
+
+ def __init__(self, max_limit=None, paramstyle='format', ilike=False,
+ function_mapping=None):
+ self.max_limit = max_limit
+ self.paramstyle = paramstyle
+ self.ilike = ilike
+ self.function_mapping = function_mapping or {}
+
+ @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.'''
+ currentThread().__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 currentThread().__sql_flavor__
+ except AttributeError:
+ flavor = Flavor()
+ currentThread().__sql_flavor__ = flavor
+ return flavor
+
+
+class AliasManager(object):
+ '''
+ Context Manager for unique alias generation
+ '''
+ __slots__ = 'local'
+
+ local = local()
+ local.alias = None
+ local.nested = 0
+
+ @classmethod
+ def __enter__(cls):
+ if getattr(cls.local, 'alias', None) is None:
+ cls.local.alias = defaultdict(cls.alias_factory)
+ cls.local.nested = 0
+ cls.local.nested += 1
+
+ @classmethod
+ def __exit__(cls, type, value, traceback):
+ cls.local.nested -= 1
+ if not cls.local.nested:
+ cls.local.alias = None
+
+ @classmethod
+ def get(cls, from_):
+ if getattr(cls.local, 'alias', None) is None:
+ return ''
+ return cls.local.alias[from_]
+
+ @classmethod
+ def set(cls, from_, alias):
+ assert cls.local.alias.get(from_) is None
+ cls.local.alias[from_] = alias
+
+ @classmethod
+ def alias_factory(cls):
+ i = len(cls.local.alias)
+ return alias(i)
+
+
+class Query(object):
+ __slots__ = ()
+
+ @property
+ def params(self):
+ return ()
+
+ def __iter__(self):
+ yield str(self)
+ yield self.params
+
+
+class FromItem(object):
+ __slots__ = ()
+
+ @property
+ def alias(self):
+ return AliasManager.get(self)
+
+ def __getattr__(self, name):
+ return Column(self, name)
+
+ def __add__(self, other):
+ assert isinstance(other, FromItem)
+ 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)
+
+
+class _SelectQueryMixin(object):
+ __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(_SelectQueryMixin, 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]
+ assert all(isinstance(col, Expression) for col in 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:
+ assert isinstance(value, (int, long))
+ self.__limit = value
+
+ @property
+ def _limit_str(self):
+ limit = ''
+ if self.limit is not None:
+ limit = ' LIMIT %s' % self.limit
+ elif self.offset:
+ max_limit = Flavor.get().max_limit
+ if max_limit:
+ limit = ' LIMIT %s' % max_limit
+ return limit
+
+ @property
+ def offset(self):
+ return self.__offset
+
+ @offset.setter
+ def offset(self, value):
+ if value is not None:
+ assert isinstance(value, (int, long))
+ self.__offset = value
+
+ @property
+ def _offset_str(self):
+ offset = ''
+ if self.offset:
+ offset = ' OFFSET %s' % self.offset
+ return offset
+
+
+class Select(Query, FromItem, _SelectQueryMixin):
+ __slots__ = ('__columns', '__where', '__group_by', '__having', '__for_',
+ 'from_')
+
+ def __init__(self, columns, from_=None, where=None, group_by=None,
+ having=None, for_=None, **kwargs):
+ self.__columns = None
+ self.__where = None
+ self.__group_by = None
+ self.__having = None
+ self.__for_ = None
+ super(Select, self).__init__(**kwargs)
+ # TODO ALL|DISTINCT
+ self.columns = columns
+ self.from_ = from_
+ self.where = where
+ self.group_by = group_by
+ self.having = having
+ self.for_ = for_
+
+ @property
+ def columns(self):
+ return self.__columns
+
+ @columns.setter
+ def columns(self, value):
+ assert all(isinstance(col, Expression) for col in 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:
+ assert isinstance(value, (Expression, And, Or))
+ 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]
+ assert all(isinstance(col, Expression) for col in 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:
+ assert isinstance(value, (Expression, And, Or))
+ 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]
+ assert isinstance(all(isinstance(f, For) for f in value))
+ self.__for_ = value
+
+ @staticmethod
+ def _format_column(column):
+ if isinstance(column, As):
+ return '%s AS %s' % (column.expression, column)
+ else:
+ return str(column)
+
+ def __str__(self):
+ with AliasManager():
+ from_ = str(self.from_)
+ 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(self.for_)
+ return ('SELECT %s FROM %s' % (columns, from_) + where + group_by
+ + having + self._order_by_str + self._limit_str +
+ self._offset_str + for_)
+
+ @property
+ def params(self):
+ p = []
+ for column in self.columns:
+ if isinstance(column, As):
+ p.extend(column.expression.params)
+ p.extend(column.params)
+ 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.having:
+ p.extend(self.having.params)
+ if self.order_by:
+ for expression in self.order_by:
+ p.extend(expression.params)
+ return tuple(p)
+
+ def __or__(self, other):
+ return Union(self, other)
+
+ def __and__(self, other):
+ return Interesect(self, other)
+
+ def __sub__(self, other):
+ return Except(self, other)
+
+
+class Insert(Query):
+ __slots__ = ('__table', '__columns', '__values', '__returning')
+
+ def __init__(self, table, columns=None, values=None, returning=None):
+ self.__table = None
+ self.__columns = None
+ self.__values = None
+ self.__returning = None
+ self.table = table
+ self.columns = columns
+ self.values = values
+ self.returning = returning
+
+ @property
+ def table(self):
+ return self.__table
+
+ @table.setter
+ def table(self, value):
+ assert isinstance(value, Table)
+ self.__table = value
+
+ @property
+ def columns(self):
+ return self.__columns
+
+ @columns.setter
+ def columns(self, value):
+ if value is not None:
+ assert all(isinstance(col, Column) for col in value)
+ assert all(col.table == self.table for col in value)
+ self.__columns = value
+
+ @property
+ def values(self):
+ return self.__values
+
+ @values.setter
+ def values(self, value):
+ if value is not None:
+ assert isinstance(value, (list, Select))
+ self.__values = value
+
+ @property
+ def returning(self):
+ return self.__returning
+
+ @returning.setter
+ def returning(self, value):
+ if value is not None:
+ assert isinstance(value, list)
+ 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)
+ columns = ' (' + ', '.join(map(str, self.columns)) + ')'
+ if isinstance(self.values, list):
+ format_ = partial(self._format, param=Flavor.get().param)
+ values = ' VALUES ' + ', '.join(
+ '(' + ', '.join(map(format_, v)) + ')'
+ for v in self.values)
+ # TODO manage DEFAULT
+ elif isinstance(self.values, Select):
+ values = ' %s' % str(self.values)
+ elif self.values is None:
+ values = ' DEFAULT VALUES'
+ returning = ''
+ if self.returning:
+ returning = ' RETURNING ' + ', '.join(map(str, self.returning))
+ return 'INSERT INTO %s' % self.table + columns + values + returning
+
+ @property
+ def params(self):
+ if isinstance(self.values, list):
+ p = []
+ for values in self.values:
+ for value in values:
+ if isinstance(value, (Expression, Select)):
+ p.extend(value.params)
+ else:
+ p.append(value)
+ return tuple(p)
+ elif isinstance(self.values, Select):
+ return self.values.params
+ else:
+ return ()
+
+
+class Update(Insert):
+ __slots__ = ('__where', '__values', 'from_')
+
+ def __init__(self, table, columns, values, from_=None, where=None):
+ super(Update, self).__init__(table, columns=columns, values=values)
+ 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]
+ assert isinstance(value, list)
+ 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:
+ assert isinstance(value, (Expression, And, Or))
+ self.__where = value
+
+ def __str__(self):
+ assert all(col.table == self.table for col in self.columns)
+ # Get columns without alias
+ columns = map(str, self.columns)
+
+ with AliasManager():
+ from_ = ''
+ if self.from_:
+ table = From([self.table])
+ from_ = ' FROM %s' % str(self.from_)
+ else:
+ table = self.table
+ AliasManager.set(table, str(table)[1:-1])
+ 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)
+ return 'UPDATE %s SET ' % table + values + from_ + where
+
+ @property
+ def params(self):
+ p = []
+ 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)
+ return tuple(p)
+
+
+class Delete(Query):
+ __slots__ = ('__table', '__where', '__returning', 'only')
+
+ def __init__(self, table, only=False, using=None, where=None,
+ returning=None):
+ 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
+
+ @property
+ def table(self):
+ return self.__table
+
+ @table.setter
+ def table(self, value):
+ assert isinstance(value, Table)
+ 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:
+ assert isinstance(value, (Expression, And, Or))
+ self.__where = value
+
+ @property
+ def returning(self):
+ return self.__returning
+
+ @returning.setter
+ def returning(self, value):
+ if value is not None:
+ assert isinstance(value, list)
+ self.__returning = value
+
+ def __str__(self):
+ only = ' ONLY' if self.only else ''
+ where = ''
+ if self.where:
+ where = ' WHERE ' + str(self.where)
+ returning = ''
+ if self.returning:
+ returning = ' RETURNING ' + ', '.join(map(str, self.returning))
+ return 'DELETE FROM%s %s' % (only, self.table) + where + returning
+
+ @property
+ def params(self):
+ return self.where.params if self.where else ()
+
+
+class CombiningQuery(Query, FromItem, _SelectQueryMixin):
+ __slots__ = ('queries', 'all_')
+ _operator = ''
+
+ def __init__(self, *queries, **kwargs):
+ assert all(isinstance(q, _SelectQueryMixin) for q in 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 (operator.join(map(str, self.queries)) + self._order_by_str
+ + self._limit_str + self._offset_str)
+
+ @property
+ def params(self):
+ p = []
+ 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 Interesect(CombiningQuery):
+ __slots__ = ()
+ _operator = 'INTERSECT'
+
+
+class Except(CombiningQuery):
+ __slots__ = ()
+ _operator = 'EXCEPT'
+
+
+class Table(FromItem):
+ __slots__ = '__name'
+
+ def __init__(self, name):
+ super(Table, self).__init__()
+ self.__name = name
+
+ def __str__(self):
+ return '"%s"' % self.__name
+
+ @property
+ def params(self):
+ return ()
+
+ def insert(self, columns=None, values=None, returning=None):
+ return Insert(self, columns=columns, values=values,
+ returning=returning)
+
+ def update(self, columns, values, from_=None, where=None):
+ return Update(self, columns=columns, values=values, from_=from_,
+ where=where)
+
+ def delete(self, only=False, using=None, where=None, returning=None):
+ return Delete(self, only=only, using=using, where=where,
+ returning=returning)
+
+
+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):
+ assert isinstance(value, FromItem)
+ self.__left = value
+
+ @property
+ def right(self):
+ return self.__right
+
+ @right.setter
+ def right(self, value):
+ assert isinstance(value, FromItem)
+ 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:
+ assert isinstance(value, (Expression, And, Or))
+ self.__condition = value
+
+ @property
+ def type_(self):
+ return self.__type_
+
+ @type_.setter
+ def type_(self, value):
+ value = value.upper()
+ assert value in ('INNER', 'LEFT', 'LEFT OUTER',
+ 'RIGHT', 'RIGHT OUTER', 'FULL', 'FULL OUTER', 'CROSS')
+ 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):
+ if hasattr(item, 'params'):
+ p.extend(item.params)
+ if hasattr(self.condition, 'params'):
+ p.extend(self.condition.params)
+ return tuple(p)
+
+ @property
+ def 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_):
+ if isinstance(from_, Query):
+ return '(%s) AS "%s"' % (from_, from_.alias)
+ else:
+ alias = getattr(from_, 'alias', None)
+ if alias:
+ return '%s AS "%s"' % (from_, alias)
+ else:
+ return str(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):
+ assert isinstance(other, (Join, Select))
+ return From(super(From, self).__add__([other]))
+
+
+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)
+
+ 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)
+
+ @property
+ def asc(self):
+ return Asc(self)
+
+ @property
+ def desc(self):
+ return Desc(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):
+ return Flavor.get().param
+
+ @property
+ def params(self):
+ return (self.__value,)
+
+
+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
+
+ def __str__(self):
+ if self.__name == '*':
+ t = '%s'
+ else:
+ t = '"%s"'
+ alias = self.__from.alias
+ if alias:
+ t = '"%s".' + t
+ return t % (alias, self.__name)
+ else:
+ return t % self.__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"' % self.output_name
+
+ @property
+ def params(self):
+ return ()
+
+
+class Cast(Expression):
+ __slots__ = ('expression', 'typename')
+
+ def __init__(self, expression, typename):
+ super(Expression, 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 Order(Expression):
+ __slots__ = ('expression')
+ _sql = ''
+
+ def __init__(self, expression):
+ super(Order, self).__init__()
+ self.expression = expression
+ # TODO USING
+
+ def __str__(self):
+ 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 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 = list(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()
+ assert value in ('UPDATE', 'SHARE')
+ 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
diff --git a/sql/aggregate.py b/sql/aggregate.py
new file mode 100644
index 0000000..0835031
--- /dev/null
+++ b/sql/aggregate.py
@@ -0,0 +1,110 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2011-2013, Cédric Krier
+# Copyright (c) 2011-2013, 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 <organization> 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 <COPYRIGHT HOLDER> 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.
+
+
+from sql import Expression
+
+__all__ = ['Avg', 'BitAnd', 'BitOr', 'BoolAnd', 'BoolOr', 'Count', 'Every',
+ 'Max', 'Min', 'Stddev', 'Sum', 'Variance']
+
+
+class Aggregate(Expression):
+ __slots__ = ('expression')
+ _sql = ''
+
+ def __init__(self, expression):
+ super(Aggregate, self).__init__()
+ self.expression = expression
+
+ def __str__(self):
+ return '%s(%s)' % (self._sql, self.expression)
+
+ @property
+ def params(self):
+ return self.expression.params
+
+
+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'
+
+
+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'
diff --git a/sql/conditionals.py b/sql/conditionals.py
new file mode 100644
index 0000000..766a49e
--- /dev/null
+++ b/sql/conditionals.py
@@ -0,0 +1,119 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2011-2013, Cédric Krier
+# Copyright (c) 2011-2013, 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 <organization> 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 <COPYRIGHT HOLDER> 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.
+
+from sql import Expression, Flavor
+
+__all__ = ['Case', 'Coalesce', 'NullIf', 'Greatest', 'Least']
+
+
+class Conditional(Expression):
+ __slots__ = ()
+ _sql = ''
+ table = ''
+ name = ''
+
+ @staticmethod
+ def _format(value):
+ if isinstance(value, Expression):
+ return str(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):
+ p.extend(cond.params)
+ else:
+ p.append(cond)
+ if isinstance(result, Expression):
+ p.extend(result.params)
+ else:
+ p.append(result)
+ if self.else_ is not None:
+ if isinstance(self.else_, Expression):
+ 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):
+ 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'
diff --git a/sql/functions.py b/sql/functions.py
new file mode 100644
index 0000000..58fb127
--- /dev/null
+++ b/sql/functions.py
@@ -0,0 +1,465 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2011-2013, Cédric Krier
+# Copyright (c) 2011-2013, 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 <organization> 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 <COPYRIGHT HOLDER> 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.
+
+from itertools import chain
+
+from sql import Expression, Flavor, FromItem
+
+__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', 'CurrentTime', 'CurrentTimestamp', 'DatePart',
+ 'DateTrunc', 'Extract', 'Isfinite', 'JustifyDays', 'JustifyHours',
+ 'JustifyInterval', 'Localtime', 'Localtimestamp', 'Now',
+ 'StatementTimestamp', 'Timeofday', 'TransactionTimestamp',
+ 'AtTimeZone']
+
+# Mathematical
+
+
+class Function(Expression, FromItem):
+ __slots__ = ('args',)
+ table = ''
+ name = ''
+ _function = ''
+
+ def __init__(self, *args):
+ self.args = args
+
+ @staticmethod
+ def _format(value):
+ if isinstance(value, Expression):
+ return str(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):
+ 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=' '):
+ assert position.upper() in ('LEADING', 'TRAILING', 'BOTH')
+ 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, basestring):
+ 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.args).params
+ p = []
+ for arg in (self.characters, self.string):
+ if isinstance(arg, basestring):
+ p.append(arg)
+ elif hasattr(arg, 'params'):
+ 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 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 = 'DateTrunc'
+
+
+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.args))
+ param = flavor.param
+ return '%s AT TIME ZONE %s' % (str(self.field), param)
+
+ @property
+ def params(self):
+ Mapping = Flavor.get().function_mapping.get(self.__class__)
+ if Mapping:
+ return Mapping(self.field, self.zone).params
+ return self.field.params + (self.zone,)
diff --git a/sql/operators.py b/sql/operators.py
new file mode 100644
index 0000000..87587b3
--- /dev/null
+++ b/sql/operators.py
@@ -0,0 +1,397 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2011-2013, Cédric Krier
+# Copyright (c) 2011-2013, 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 <organization> 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 <COPYRIGHT HOLDER> 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.
+import warnings
+from array import array
+
+from sql import Expression, Select, CombiningQuery, Flavor
+
+__all__ = ['And', 'Or', 'Not', 'Less', 'Greater', 'LessEqual', 'GreaterEqual',
+ 'Equal', 'NotEqual', '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 NotImplemented
+
+ 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.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):
+ return '(%s %s %s)' % (self._format(self.left), self._operator,
+ self._format(self.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(str, 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 None:
+ return (self.right,)
+ elif self.right is None:
+ return (self.left,)
+ return super(Equal, self)._operands
+
+ def __str__(self):
+ if self.left is None:
+ return '(%s IS NULL)' % self.right
+ elif self.right is None:
+ return '(%s IS NULL)' % self.left
+ return super(Equal, self).__str__()
+
+
+class NotEqual(Equal):
+ __slots__ = ()
+ _operator = '!='
+
+ def __str__(self):
+ if self.left is None:
+ return '(%s IS NOT NULL)' % self.right
+ elif self.right is None:
+ return '(%s IS NOT NULL)' % self.left
+ return super(Equal, self).__str__()
+
+
+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__ = ()
+ _operator = '%'
+
+
+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__ = ()
+ _operator = 'LIKE'
+
+
+class NotLike(BinaryOperator):
+ __slots__ = ()
+ _operator = 'NOT LIKE'
+
+
+class ILike(BinaryOperator):
+ __slots__ = ()
+
+ @property
+ def _operator(self):
+ if Flavor.get().ilike:
+ return 'ILIKE'
+ else:
+ return 'LIKE'
+
+
+class NotILike(BinaryOperator):
+ __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,
+ Like: NotLike,
+ NotLike: Like,
+ ILike: NotILike,
+ NotILike: ILike,
+ In: NotIn,
+ NotIn: In,
+ }
diff --git a/sql/tests/__init__.py b/sql/tests/__init__.py
new file mode 100644
index 0000000..c262fba
--- /dev/null
+++ b/sql/tests/__init__.py
@@ -0,0 +1,70 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2011-2013, Cédric Krier
+# Copyright (c) 2011-2013, 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 <organization> 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 <COPYRIGHT HOLDER> 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.
+
+import sys
+import os
+import unittest
+import doctest
+
+import sql
+
+here = os.path.dirname(__file__)
+readme = os.path.normpath(os.path.join(here, '..', '..', 'README'))
+
+
+def test_suite():
+ suite = additional_tests()
+ loader = unittest.TestLoader()
+ for fn in os.listdir(here):
+ if fn.startswith('test') and fn.endswith('.py'):
+ modname = 'sql.tests.' + fn[:-3]
+ __import__(modname)
+ module = sys.modules[modname]
+ suite.addTests(loader.loadTestsFromModule(module))
+ return suite
+
+
+def additional_tests():
+ suite = unittest.TestSuite()
+ for mod in (sql,):
+ suite.addTest(doctest.DocTestSuite(mod))
+ if os.path.isfile(readme):
+ suite.addTest(doctest.DocFileSuite(readme, module_relative=False,
+ tearDown=lambda t: sql.Flavor.set(sql.Flavor())))
+ return suite
+
+
+def main():
+ suite = test_suite()
+ runner = unittest.TextTestRunner()
+ runner.run(suite)
+
+if __name__ == '__main__':
+ sys.path.insert(0, os.path.dirname(os.path.dirname(
+ os.path.dirname(os.path.abspath(__file__)))))
+ main()
diff --git a/sql/tests/test_aggregate.py b/sql/tests/test_aggregate.py
new file mode 100644
index 0000000..db59c67
--- /dev/null
+++ b/sql/tests/test_aggregate.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2011-2013, Cédric Krier
+# Copyright (c) 2011-2013, 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 <organization> 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 <COPYRIGHT HOLDER> 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.
+
+import unittest
+
+from sql import Table
+from sql.aggregate import Avg
+
+
+class TestAggregate(unittest.TestCase):
+ table = Table('t')
+
+ 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"))')
diff --git a/sql/tests/test_alias.py b/sql/tests/test_alias.py
new file mode 100644
index 0000000..fcfed30
--- /dev/null
+++ b/sql/tests/test_alias.py
@@ -0,0 +1,89 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2011-2013, Cédric Krier
+# Copyright (c) 2011-2013, 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 <organization> 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 <COPYRIGHT HOLDER> 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.
+
+import unittest
+import threading
+
+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():
+ 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():
+ 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()
diff --git a/sql/tests/test_as.py b/sql/tests/test_as.py
new file mode 100644
index 0000000..8e18df9
--- /dev/null
+++ b/sql/tests/test_as.py
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2011-2013, Cédric Krier
+# Copyright (c) 2011-2013, 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 <organization> 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 <COPYRIGHT HOLDER> 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.
+
+import unittest
+
+from sql import As, Column, Table
+
+
+class TestOrder(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(query.params, ())
diff --git a/sql/tests/test_cast.py b/sql/tests/test_cast.py
new file mode 100644
index 0000000..a76b648
--- /dev/null
+++ b/sql/tests/test_cast.py
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2011-2013, Cédric Krier
+# Copyright (c) 2011-2013, 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 <organization> 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 <COPYRIGHT HOLDER> 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.
+
+import unittest
+
+from sql import Cast, Column, Table
+
+
+class TestCast(unittest.TestCase):
+ column = Column(Table('t'), 'c')
+
+ def test_cast(self):
+ self.assertEqual(str(Cast(self.column, 'int')),
+ 'CAST("c" AS int)')
+
+ 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,))
diff --git a/sql/tests/test_column.py b/sql/tests/test_column.py
new file mode 100644
index 0000000..6be3168
--- /dev/null
+++ b/sql/tests/test_column.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2011-2013, Cédric Krier
+# Copyright (c) 2011-2013, 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 <organization> 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 <COPYRIGHT HOLDER> 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.
+
+import unittest
+
+from sql import Column, Table, AliasManager
+
+
+class TestColumn(unittest.TestCase):
+ def test_column(self):
+ column = Column(Table('t'), 'c')
+ self.assertEqual(str(column), '"c"')
+
+ with AliasManager():
+ self.assertEqual(str(column), '"a"."c"')
diff --git a/sql/tests/test_conditionals.py b/sql/tests/test_conditionals.py
new file mode 100644
index 0000000..501d99d
--- /dev/null
+++ b/sql/tests/test_conditionals.py
@@ -0,0 +1,75 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2011-2013, Cédric Krier
+# Copyright (c) 2011-2013, 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 <organization> 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 <COPYRIGHT HOLDER> 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.
+
+import unittest
+
+from sql import Table
+from sql.conditionals import Case, Coalesce, NullIf, Greatest, Least
+
+
+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_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_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',))
diff --git a/sql/tests/test_delete.py b/sql/tests/test_delete.py
new file mode 100644
index 0000000..2ba1474
--- /dev/null
+++ b/sql/tests/test_delete.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2011-2013, Cédric Krier
+# Copyright (c) 2011-2013, 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 <organization> 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 <COPYRIGHT HOLDER> 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.
+
+import unittest
+
+from sql import Table
+
+
+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, ())
diff --git a/sql/tests/test_for.py b/sql/tests/test_for.py
new file mode 100644
index 0000000..cf0b940
--- /dev/null
+++ b/sql/tests/test_for.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2011-2013, Cédric Krier
+# Copyright (c) 2011-2013, 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 <organization> 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 <COPYRIGHT HOLDER> 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.
+
+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')
diff --git a/sql/tests/test_functions.py b/sql/tests/test_functions.py
new file mode 100644
index 0000000..3872187
--- /dev/null
+++ b/sql/tests/test_functions.py
@@ -0,0 +1,80 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2011-2013, Cédric Krier
+# Copyright (c) 2011-2013, 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 <organization> 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 <COPYRIGHT HOLDER> 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.
+
+import unittest
+
+from sql import Table, Flavor
+from sql.functions import Function, Abs, Overlay, Trim, AtTimeZone
+
+
+class TestFunctions(unittest.TestCase):
+ table = Table('t')
+
+ 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',)
+
+ abs_ = Abs(self.table.c1)
+ flavor = Flavor(function_mapping={
+ Abs: MyAbs,
+ })
+ Flavor.set(flavor)
+ try:
+ self.assertEqual(str(abs_), 'MY_ABS("c1")')
+ self.assertEqual(abs_.params, ('test',))
+ finally:
+ Flavor.set(Flavor())
+
+ 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 ',))
+
+ 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',))
diff --git a/sql/tests/test_insert.py b/sql/tests/test_insert.py
new file mode 100644
index 0000000..92954a2
--- /dev/null
+++ b/sql/tests/test_insert.py
@@ -0,0 +1,72 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2011-2013, Cédric Krier
+# Copyright (c) 2013, Nicolas Évrard
+# Copyright (c) 2011-2013, 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 <organization> 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 <COPYRIGHT HOLDER> 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.
+
+import unittest
+
+from sql import Table
+from sql.functions import Abs
+
+
+class TestInsert(unittest.TestCase):
+ table = Table('t')
+
+ def test_insert_default(self):
+ query = self.table.insert()
+ self.assertEqual(str(query), 'INSERT INTO "t" DEFAULT VALUES')
+ self.assertEqual(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(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(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(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(query.params, (-1,))
diff --git a/sql/tests/test_join.py b/sql/tests/test_join.py
new file mode 100644
index 0000000..76fe6dd
--- /dev/null
+++ b/sql/tests/test_join.py
@@ -0,0 +1,66 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2011-2013, Cédric Krier
+# Copyright (c) 2011-2013, 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 <organization> 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 <COPYRIGHT HOLDER> 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.
+
+import unittest
+
+from sql import Join, Table, AliasManager
+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(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_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(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(join.params, ())
diff --git a/sql/tests/test_literal.py b/sql/tests/test_literal.py
new file mode 100644
index 0000000..7c8f3b4
--- /dev/null
+++ b/sql/tests/test_literal.py
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2011-2013, Cédric Krier
+# Copyright (c) 2011-2013, 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 <organization> 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 <COPYRIGHT HOLDER> 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.
+
+import unittest
+
+from sql import Literal
+
+
+class TestLiteral(unittest.TestCase):
+ def test_literal(self):
+ literal = Literal(1)
+ self.assertEqual(str(literal), '%s')
+ self.assertEqual(literal.params, (1,))
diff --git a/sql/tests/test_operators.py b/sql/tests/test_operators.py
new file mode 100644
index 0000000..d726655
--- /dev/null
+++ b/sql/tests/test_operators.py
@@ -0,0 +1,134 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2011-2013, Cédric Krier
+# Copyright (c) 2011-2013, 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 <organization> 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 <COPYRIGHT HOLDER> 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.
+
+import unittest
+import warnings
+from array import array
+
+from sql import Table, Literal
+from sql.operators import And, Not, Less, Equal, NotEqual, In, FloorDiv
+
+
+class TestOperators(unittest.TestCase):
+ table = Table('t')
+
+ def test_and(self):
+ and_ = And((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,))
+
+ def test_not(self):
+ not_ = Not(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_less(self):
+ less = Less(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_equal(self):
+ equal = Equal(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, None)
+ self.assertEqual(str(equal), '("c1" IS NULL)')
+ self.assertEqual(equal.params, ())
+
+ equal = Equal(Literal('test'), None)
+ self.assertEqual(str(equal), '(%s IS NULL)')
+ self.assertEqual(equal.params, ('test',))
+
+ equal = Equal(None, self.table.c1)
+ self.assertEqual(str(equal), '("c1" IS NULL)')
+ self.assertEqual(equal.params, ())
+
+ equal = Equal(None, Literal('test'))
+ self.assertEqual(str(equal), '(%s IS NULL)')
+ self.assertEqual(equal.params, ('test',))
+
+ def test_not_equal(self):
+ equal = NotEqual(self.table.c1, self.table.c2)
+ self.assertEqual(str(equal), '("c1" != "c2")')
+ self.assertEqual(equal.params, ())
+
+ equal = NotEqual(self.table.c1, None)
+ self.assertEqual(str(equal), '("c1" IS NOT NULL)')
+ self.assertEqual(equal.params, ())
+
+ equal = NotEqual(None, self.table.c1)
+ self.assertEqual(str(equal), '("c1" IS NOT NULL)')
+ self.assertEqual(equal.params, ())
+
+ def test_in(self):
+ in_ = In(self.table.c1, [self.table.c2, 1, None])
+ 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', 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_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))
+ self.assertIn('FloorDiv operator is deprecated, use Div function',
+ str(w[-1].message))
diff --git a/sql/tests/test_order.py b/sql/tests/test_order.py
new file mode 100644
index 0000000..6d859cd
--- /dev/null
+++ b/sql/tests/test_order.py
@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2011-2013, Cédric Krier
+# Copyright (c) 2011-2013, 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 <organization> 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 <COPYRIGHT HOLDER> 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.
+
+import unittest
+
+from sql import Asc, Desc, Column, Table
+
+
+class TestOrder(unittest.TestCase):
+ column = Column(Table('t'), 'c')
+
+ def test_asc(self):
+ self.assertEqual(str(Asc(self.column)), '"c" ASC')
+
+ def test_desc(self):
+ self.assertEqual(str(Desc(self.column)), '"c" DESC')
diff --git a/sql/tests/test_select.py b/sql/tests/test_select.py
new file mode 100644
index 0000000..b17142f
--- /dev/null
+++ b/sql/tests/test_select.py
@@ -0,0 +1,186 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2011-2013, Cédric Krier
+# Copyright (c) 2013, Nicolas Évrard
+# Copyright (c) 2011-2013, 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 <organization> 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 <COPYRIGHT HOLDER> 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.
+
+import unittest
+
+from sql import Table, Join, Union, Literal, Flavor
+from sql.functions import Now
+from sql.aggregate import Min
+
+
+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(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(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(query.params, ('foo',))
+
+ 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(union.params, ('foo',))
+
+ union = Union(query1)
+ self.assertEqual(str(union), str(query1))
+ self.assertEqual(union.params, 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(union.params, (1,))
+
+ 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(select.params, ())
+
+ def test_select_function(self):
+ query = Now().select()
+ self.assertEqual(str(query), 'SELECT * FROM NOW() AS "a"')
+ self.assertEqual(query.params, ())
+
+ 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(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(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(query.params, ('foo', '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(query.params, (3,))
+
+ 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(query.params, (1,))
+
+ def test_select_limit_offset(self):
+ query = self.table.select(limit=50, offset=10)
+ self.assertEqual(str(query),
+ 'SELECT * FROM "t" AS "a" LIMIT 50 OFFSET 10')
+ self.assertEqual(query.params, ())
+
+ query.limit = None
+ self.assertEqual(str(query),
+ 'SELECT * FROM "t" AS "a" OFFSET 10')
+ self.assertEqual(query.params, ())
+
+ query.offset = 0
+ self.assertEqual(str(query),
+ 'SELECT * FROM "t" AS "a"')
+ self.assertEqual(query.params, ())
+
+ flavor = Flavor(max_limit=-1)
+ Flavor.set(flavor)
+ try:
+ query.offset = None
+ self.assertEqual(str(query),
+ 'SELECT * FROM "t" AS "a"')
+ self.assertEqual(query.params, ())
+
+ query.offset = 0
+ self.assertEqual(str(query),
+ 'SELECT * FROM "t" AS "a"')
+ self.assertEqual(query.params, ())
+
+ query.offset = 10
+ self.assertEqual(str(query),
+ 'SELECT * FROM "t" AS "a" LIMIT -1 OFFSET 10')
+ self.assertEqual(query.params, ())
+ finally:
+ Flavor.set(Flavor())
diff --git a/sql/tests/test_update.py b/sql/tests/test_update.py
new file mode 100644
index 0000000..c0e0219
--- /dev/null
+++ b/sql/tests/test_update.py
@@ -0,0 +1,63 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2011-2013, Cédric Krier
+# Copyright (c) 2011-2013, 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 <organization> 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 <COPYRIGHT HOLDER> 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.
+
+import unittest
+
+from sql import Table
+
+
+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" SET "c" = %s')
+ self.assertEqual(query.params, ('foo',))
+
+ query.where = (self.table.b == True)
+ self.assertEqual(str(query),
+ 'UPDATE "t" SET "c" = %s WHERE ("t"."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_subselect(self):
+ t1 = Table('t1')
+ t2 = Table('t2')
+ query = t1.update([t1.c], [t2.select(t2.c, where=t2.i == t1.i)])
+ self.assertEqual(str(query),
+ 'UPDATE "t1" SET "c" = ('
+ 'SELECT "b"."c" FROM "t2" AS "b" WHERE ("b"."i" = "t1"."i"))')
+ self.assertEqual(query.params, ())
--
python-sql
More information about the tryton-debian-vcs
mailing list