Scene 01
The primary key
Every table declares its primary key with the pkey argument of table. pkey names one column. In the invoice table the key is id. The recipe does not declare id as a column. The sys_fields option adds it, together with other system columns. Lesson 25 covers sys_fields. A list of names separated by commas is not accepted. AsqueelDb raises a ValueError that says the pkey is not a column or a compositeColumn. A key made of several columns is declared through a compositeColumn, which the next scene shows. How asqueel generates key values on insert is the subject of lesson 20.
Example & screen notes
Slide title "pkey". Code from the course model:
tbl = pkg.table('invoice', pkey='id', name_long='Invoice', caption_field='inv_number')
tbl.options().sys_fields()
Three bullets:
pkeynames one column, or onecompositeColumnpkey='product_id,year'raisesValueError- How key values are generated: lesson 20
Scene 02
compositeColumn
A compositeColumn packs several physical columns of the same table under one name. It does not create a column in the database. Here product_year_key packs product_id and year. With unique True, migration creates one UNIQUE constraint over both columns. A price can exist once per product and year. The name product_year_key is also a key that a relation can point to, and a name that pkey can use. When pkey names a compositeColumn, the primary key of the table is made of its member columns.
Example & screen notes
Slide title "A key over two columns". Code, an addition to the course model copied from examples/test_invoice_pg/invc.py:
def price_year(pkg):
tbl = pkg.table('price_year', pkey='id', name_long='Price year')
tbl.options().sys_fields()
tbl.column('product_id', size='22').relation(
'product.id', relation_name='price_years', onDelete='cascade', foreign_key=True)
tbl.column('year', dtype='L')
tbl.column('unit_price', dtype='money')
tbl.compositeColumn('product_year_key', columns='product_id,year', unique=True)
Highlight the compositeColumn line. Below, the DDL line it produces:
CONSTRAINT "cst_8b8d4128" UNIQUE ("product_id", "year")
Scene 03
Table constraints
A named table constraint goes in the constraints collection of the table. You call constraints, then constraint. The constraint_type is UNIQUE or CHECK. Any other value raises a validation error. A UNIQUE constraint needs columns, a list of names separated by commas. A CHECK constraint needs check_clause, an SQL expression. A missing columns or check_clause raises when the model is built. Unlike column, constraint is not routed for you. Calling constraint directly on the table raises an error that asks for the constraints collection.
Example & screen notes
Slide title "constraints().constraint(...)". Code:
tbl.constraints().constraint('price_year_positive', constraint_type='CHECK',
check_clause='(unit_price >= (0)::numeric)')
note = pkg.table('invoice_note', pkey='id')
note.constraints().constraint('invoice_note_one_type', constraint_type='UNIQUE',
columns='invoice_id,note_type')
DDL produced:
CONSTRAINT "price_year_positive" CHECK (unit_price >= (0)::numeric)
ALTER TABLE "invc"."invc_invoice_note"
ADD CONSTRAINT "invoice_note_one_type" UNIQUE ("invoice_id", "note_type");
Scene 04
Write clauses in the form PostgreSQL stores
Migration compares the clause in the recipe with the clause that PostgreSQL stores. PostgreSQL rewrites a clause into its own normal form. For example, unit_price greater or equal to zero is stored with brackets and a cast to numeric. If the recipe keeps the short text, every later migration sees a difference. It drops the constraint and creates it again. Then migrate raises MigrationError, because differences remain after applying. Write the clause in the form PostgreSQL stores. You can read that form with psql, in the output of backslash d on the table. The same rule applies to the where clause of a partial index.
Example & screen notes
Slide title "Clause text and migration". Two rows:
check_clause |
Second migration_plan() |
|---|---|
'unit_price >= 0' |
drops and re-creates the constraint |
'(unit_price >= (0)::numeric)' |
empty |
Scene 05
Indexes
An index is declared with index on the table. columns is a string of names, a list, or a dictionary. The dictionary maps each name to None or to DESC, for a descending column. name is the index name. Without a name, migration generates one. where makes a partial index. unique makes a unique index. method chooses the access method, and the default is btree. The second index on the slide is descending and partial. The third is unique over two columns of the note table.
Example & screen notes
Slide title "index(...)". Code:
tbl.index(columns='year,product_id')
tbl.index(columns={'year': 'DESC'}, name='price_year_recent',
where='(unit_price IS NOT NULL)')
note.index(columns='invoice_id,priority', unique=True, name='invoice_note_priority')
DDL produced:
CREATE INDEX "idx_2bff00ca" ON "invc"."invc_price_year" USING btree ("year", "product_id");
CREATE INDEX "price_year_recent" ON "invc"."invc_price_year" USING btree ("year" DESC)
WHERE unit_price IS NOT NULL;
CREATE UNIQUE INDEX "invoice_note_priority" ON "invc"."invc_invoice_note"
USING btree ("invoice_id", "priority");
Scene 06
Indexes declared on a column
A column can ask for its own index with indexed. True creates a plain index. A dictionary carries options. sorting sets the direction. unique without where makes the column unique and creates no index. A column that carries a relation always receives an index. The DDL of the course model shows it, for example on customer_id of the invoice table. Two dictionary options do not work at column level in this release. A where or a method makes migration raise UnsupportedFeatureError. For a partial index or another method, use index on the table, as in the previous scene.
Example & screen notes
Slide title "column(indexed=...)". Table:
| Declaration | Result |
|---|---|
indexed=True |
CREATE INDEX ... ("email") |
indexed=dict(sorting='DESC') |
CREATE INDEX ... ("email" DESC) |
indexed=dict(unique=True) |
UNIQUE ("email") constraint |
.relation(...) on the column |
an index on the column |
indexed=dict(where=...) or dict(method=...) |
UnsupportedFeatureError at migration |
Scene 07
Extensions
A PostgreSQL extension is declared on the database. You call extensions, then extension with the extension name. Migration creates it with CREATE EXTENSION IF NOT EXISTS. Migration never drops an extension. The extension must be available on the server. Declaring it only asks PostgreSQL to install it in this database.
Example & screen notes
Slide title "extensions().extension(...)". Code:
db.extensions().extension('pg_trgm')
DDL produced:
CREATE EXTENSION IF NOT EXISTS "pg_trgm";
Scene 08
Recap
The primary key names one column or one compositeColumn. Composite columns, constraints and indexes become DDL through migration. Clauses must be written in the form PostgreSQL stores, or every migration re-creates them. The next lesson covers relations.
Example & screen notes
Three bullets:
pkeynames one column or onecompositeColumncompositeColumn(unique=True),constraints().constraint(...)andindex(...)produce constraints and indexes- Write CHECK and WHERE clauses in the form PostgreSQL stores