API Documentation¶
Note that most of the SQL shown is informative, not literal. This library never directly inserts user provided values in to the SQL.
Connections¶
-
class
dqo.
Database
(sync_src=None, async_src=None, sync_dialect=None, async_dialect=None)¶ Parameters: - src – A function returning a database connection, or a connection pool.
- dialect – The database
Dilect
to speak (optional).
The
Database
controls connections to your database. The src parameter is required. For example:db = dqo.Database( src=lambda: psycopg2.connect("dbname='mydb'"), )
If you’re connecting with the async library
asyncpg
:db = dqo.Database( src=lambda: asyncpg.connect(database='mydb') )
If you’re doing
asyncpg
connection pooling:db = dqo.Database( src=asyncpg.create_pool(database='mydb') )
If you don’t pass in the dialect it will be auto-detected by opening and closing a single connection.
You typically assign a database one of three places…
To either (or both) of the global default databases:
dqo.SYNC_DB = ... dqo.ASYNC_DB = ...
As the default for a table:
@dqo.Table(sync_db=[...], async_db=[...]) class User: [...]
Or bound to a given query:
User.ALL.bind(sync_db=db).first()
-
dialect
¶ This property returns the dialect associated with this database. (It auto-switches between sync and async depending on context.)
-
class
dqo.
Dialect
¶ Parameters: - version – The database version. Example: 10, ‘9.2.6’, etc.
- lib – The library used. Example: psycopg2, asyncpg, etc.
All parameters are optional, even calling it as function is optional. Examples:
dqo.Dialect.POSTGRES dqo.Dialect.POSTGRES(10) dqo.Dialect.POSTGRES(version='9.2.6') dqo.Dialect.POSTGRES(lib='psycopg2') dqo.Dialect.POSTGRES(10, lib=asyncpg)
Querying¶
-
class
dqo.query.
Query
(tbl)¶ You don’t instantiate this directly, rather every table will have a reference like
User.ALL
.All query objects are immutable. Methods are either builder methods (that return a new query) or terminal (that execute SQL on the database).
Builder Methods
The following methods all return a new immutable query.
-
bind
(db_or_tx)¶ Parameters: db_or_tx – Either a database or a transaction object. Specifies what database or connection transaction to use for this query. For example:
db = dqo.Database(src=...) q = User.ALL.where(...).bind(db) for user in q: # do something
-
limit
(n)¶ Parameters: int – The max number of rows to return. Example:
User.ALL.limit(10)
Equivalent to
.top(10)
.
-
order_by
(*columns)¶ Specifies the ordering for the query. Example:
User.ALL.order_by(User.last_name, User.first_name)
To control the order:
User.ALL.order_by(User.name.asc) User.ALL.order_by(User.name.desc)
Every call clobbers any previous calls to
order_by
.Only effects
SELECT
queries. Other query types accept but ignore order by directives. They intentionally don’t throw an error, as it’s common to change a select into something else as part of a workflow. Example:to_delete = User.ALL.where(name='John').order_by(User.kind) preview(to_delete) # display who's to be deleted to_delete.delete()
-
select
(*columns)¶ Parameters: columns – One or more columns to select. By default all columns on a table are selected. You can customize this by calling
select()
with one or more columns which should be selected in the query. For instance, to select only:User.ALL.select(User.id, User.name, User.email)
To select all columns except the user’s email, prepend the negative operator:
User.ALL.select(-User.email)
To add a column (or something else, like a function) for selection, use the positive operator:
User.ALL.select(+dqo.sql.LOWER(User.first_name))
If you send in an explicit list (no +/-) you will replace the existing selected columns. If you pass in +/- columns, you will modify the existing selected columns. You cannot do both in the came call.
-
set
(**kwargs)¶ Sets values in preperation for an update. Example:
User.ALL.where(id=1).set(name='John').update()
-
top
(int)¶ Parameters: int – The max number of rows to return. Example:
User.ALL.top(10)
Equivalent to
.limit(10)
.
-
-
top
(n)
-
where
(*conditions, **kwargs)¶ Adds conditions to a query. For example:
User.ALL.where(User.email == 'someone@somewhere.com')
Keyword arguments will be evaluated to fields on the bound table. For example, the above statement could also be written as:
User.ALL.where(email='someone@somewhere.com')
Multiple calls to
where()
will result in a SQLAND
. For example:User.ALL.where(name='John').where(email='me@here.com')
Would result in:
select * from users where name='John' and email='me@here.com'
All of the following would also result in the same SQL as above:
User.ALL.where(name='John', email='me@here.com') User.ALL.where(User.name=='John', User.email=='me@here.com') User.ALL.where( (User.name=='John') & (User.email=='me@here.com') )
An
OR
statment can be created with the bitwise or operator:User.ALL.where( (User.name=='John') | (User.email=='me@here.com') )
When using the
&
and|
operators, make sure you wrap the condition in parentheses as they have lower precedence than others like==
.
Terminal Methods
The following methods all execute SQL and return data, or if executed in an
async
environment acoroutine
.-
__aiter__
()¶ All queries are async iterable. For example:
async for user in User.ALL: # do something asynchronously
-
__iter__
()¶ All queries are iterable. For example:
for user in User.ALL: # do something
If in a transaction or a with block defining the scope of the connection, the results will stream. If a query has to open its own connection it will load all records before streaming. This is because there is no guarantee an iterator will complete, and waiting for the garbage collector is a fool’s game.
-
count
()¶ Returns: The number of rows matching the query. does a
count(1)
of the existing query. Example:>>> User.ALL.count() 42
-
count_by
(*columns)¶ Returns: A dict
where the keys are the db values of the columns selected and the values are their counts.>>> User.ALL.count_by(User.name) {'John':1, 'Paul':2}
If multiple columns are passed, the keys will be tuples.
>>> User.ALL.count_by(User.first_name, User.last_name) {('John','Smith'):1, ('Paul','Anderson'):2}
-
delete
()¶ Returns: The number of rows deleted. Deletes the records matched by a given query. Example:
>>> User.ALL.where(id=1).delete() 1
-
first
()¶ Returns: An instance of the selected type or None
if not found.Adds a
.limit(1)
to a given query and returns the first result (if any). Example:user = User.ALL.first()
In async code:
user = await User.ALL.first()
-
insert
(*args, **data)¶ Inserts one or more rows. If keyword arguments are passed, a single row is inserted returning the primary key (if defined). For example:
>>> User.ALL.insert(name='John', email='me@here.org') 42
If you have a dict of values:
user = User.ALL.insert(**{'name':'John', 'email':'me@here.org'})
If a list of
dicts
are passed, multiple rows are inserted efficiently in a single query, and a list of their primary keys are returned:>>> User.ALL.insert( {'name':'John', 'email':'me@here.org'}, {'name':'Paul', 'email':'paul@here.org'}, ) [42, 43]
-
plus
(*foreign_keys)¶ - This is very useful syntax sugar for SQL joins. Takes a list of foreign keys to define the paths of the object graph you want
- to query. For instance, let’s assume you have:
If I want to query all people with their employer:
for person in Person.ALL.plus(Person.employer): print(person, person.employer)
If I want to query all employers with their employees:
for company in Company.ALL.plus(Person.employer): print(company) for person in company.employees: print('\t', person)
If I want to know the industry everyone works in:
q = Person.ALL.plus(Person.employer, Company.industry) for person in q: print(person, person.employer.industry)
If you want the company with its employees and industry:
q = Company.ALL \ .plus(Person.employer) \ .plus(Company.industry) for company in q: print(company, 'in', company.industry) for person in company.employees: print('\t', person)
How about all industries with companies and all their employees:
q = Industry.ALL.plus(Company.industry, Person.employer) for industry in q: print(industry) for company in industry.companies: print('\t', industry.company) for person in company.employees: print('\t\t', person)
Every call to this method represents a path in the object graph to be queried, and they can be as long as you wish. If some paths overlap, the individual legs will not be duplicated in the resulting query.
None of these calls will generate O(n) database lookups.
-
update
()¶ Returns: The number of rows updated. Executes an update for previously set values.
User.ALL.where(id=1).set(name='John').update()
-
Columns and Conditions¶
-
class
dqo.column.
BaseColumn
¶ All columns obey normal python operators.
Syntax SQL User.name == 'John'
name = 'john'
User.age < 5
age < 5
User.age <= 5
age <= 5
User.age > 5
age > 5
User.age >= 5
age >= 5
User.name != 'John'
name <> 'john'
(User.name=='John') & (User.age < 5)
name='john' and age=5
(User.age < 2) | (User.age >= 65)
age<2 or age>=65
In addition, the following condition generators are supported:
-
frm
(s)¶ Returns this column bound to a given table alias. Example:
A.ALL.left_join(B.as_('b')).where(B.id.frm('b').is_not_null)
-
in_
(something)¶ Returns a condition where this column is in a list or inner query.
-
is_not_null
¶ Returns condition where this column is not null.
-
is_null
¶ Returns condition where this column is null.
-
not_in
(something)¶ Returns a condition where this column is in a list or inner query.
-
SQL Functions and Literals¶
-
dqo.
sql
(literal)¶ dqo.sql
is a special object that generates functions and other SQL literals for use in your queries. Literals are inserted verbatim into the expressed SQL, so make sure you never use untrusted data!Code here assumes you’ve:
from dqo import sql
For example, while:
User.name == 'John'
would generate:
name = ?
and
'John'
would be passed in as an argument to your database library, this:User.name == sql.JOHN
would generate:
name = JOHN
which probably would not make sense to your database. A more likely example:
User.name == sql.CONCAT( User.first_name, ' ', User.last_name )
would generate:
name = CONCAT(first_name, ' ', last_name)
If your literal isn’t a valid Python identifier, pass it in as a parameter:
# generates COUNT(*) sql.COUNT(sql('*'))
The above illistrates the syntax, but is actually unecessary for
COUNT()
, which has special checks forsql.count('*')
andsql.count(1)
since they’re such common calls.A common operation on a query might be:
# TODO User.ALL.select( +sql.COALESCE(User.name, 'Unknown').as(User.name) )
To provide a query-specific default for the user’s name.
Schema Definition¶
To define a table, add the @dqo.Table
decorator to a class.
-
dqo.
Table
(name=None, db=None, aka=None)¶ Parameters: - name – The name of the table in the database.
- db – The database to use for regular Python code. If
None
defaults todqo.DB
if defined. - aka – A string or list of strings with previous names of this table, used for renaming.
A decorator used to turn a
class
into a dqo database table. For example:@dqo.Table() class Product: name = dqo.Column(str) keywords = dqo.Column([str])
-
class
dqo.
Column
(kind, name=None, null=True, default=None, index=False, unique=False, primary_key=False, tz=None, aka=None)¶ Parameters: - kind – A Python type to be mapped to a database column type. Or a single-element list (containing a type) representing an array column.
- name – The database name of the column.
- null – If the column can be null or not.
- default – The default for the column. If a constant, stored in the database, else (like a lambda) will be calculated on object creation.
- index – If a single column index (w/ the database’s default type, ie. BTREE) should be created for this column.
- unique – If a single column UNIQUE index should be created for this column.
- primary_key – If this column should be the primary key for this table.
- aka – A string or list of strings with previous names of this column, used for renaming.
- tz – If a datatime column, can specify if it’s a timezone aware column.
You can create
ARRAY
columns with subtypes by passing in a list of a single type. Example:@dqo.Table class Product: name = dqo.Column(str) keywords = dqo.Column([str])
-
asc
¶ The ascending form of the column, used in
order_by()
.
-
desc
¶ The descending form of the column, used in
order_by()
.
-
class
dqo.
PrimaryKey
(*columns)¶ Parameters: columns – The columns this primary key is composed of. To identify a single column as a primary key, add
primary_key=True
to its column definiton. For example:@dqo.Table() class A: id = dqo.Column(int, primary_key=True)
This will make the
id
column the primary key for the table.To create a multi-column primary key, use the
PrimaryKey
class, passing in the component columns. For example:@dqo.Table() class A: part1 = dqo.Column(int) part2 = dqo.Column(int) _pk = dqo.PrimaryKey(part1, part2)
Calling it
_pk
isn’t necessary or special, just convention. The name of the variable isn’t used, just make sure it doesn’t conflict with any column names.
-
class
dqo.
ForeignKey
(*to_columns, fake=False, null=True)¶ Parameters: - to_columns – The columns this foreign key refers to.
- fake – A “fake” foreign key will give you the syntax sugar without actually creating the restriction in the database. This is sometimes important for performance reasons.
- null – If nulls are allowed.
Defines a foreign key relationship from this table to another. For example:
@dqo.Table() class A: id = dqo.Column(int, primary_key=True) @dqo.Table() class B: id = dqo.Column(int, primary_key=True) a = dqo.ForeignKey(A.id)
IF you want to create a multi-column foreign key, pass in multiple columns:
@dqo.Table() class B: id = dqo.Column(int, primary_key=True) my_a = dqo.ForeignKey(A.part1, A.part2)
A foreign key creates one database column for every referenced column, named by a combination of the foreign key name and the columns it’s referencing. So the above example would have columns
my_a_part1
andmy_a_part2
on tableb
as shown:alter table b add foreign key (my_a_part1,my_a_part2) references a (part1,part2)
-
class
dqo.
Index
(*columns, unique=False, method=None, include=None, name=None)¶ Parameters: - columns – The columns this index is composed of.
- unique – If this is a unique contraint or not.
- method – The indexing method (
btree
,hash
,gist
,spgist
,gin
, orbrin
on PosrgreSQL). - include – A list of non-indexed columns to store in the index. Postgres 11 feature - see [https://www.postgresql.org/docs/current/static/sql-createindex.html]
- name – The name of the index.
Creates an index on the current table. For example:
@dqo.Table() class Something: col1 = dqo.Column(int) col2 = dqo.Column(int) _idx1 = dqo.Index(col1, col2, unique=True)
Would generate:
create table "a" (col1 integer, col2 integer) create unique index on "a" (col1,col2)
Columns have convenience parameters
index
andunique
to simplify making single column indexs. Example:@dqo.Table() class Something: col1 = dqo.Column(int, index=True) col2 = dqo.Column(int, unique=True)