psycodict.validation¶
What psycodict will put into a statement.
Almost all of psycodict’s SQL is composed from bound placeholders and quoted
identifiers, which are safe whatever the values are. The exceptions are the
things PostgreSQL has no placeholder for – a column type above all – which
have to be interpolated as SQL text. This module is where those are checked,
in one place, so that the rule is the same wherever the value came from: a
caller, the header of a data file, or a row of a meta_* table that
something else edited. Alongside them sit the identifier rules: what names
psycodict will create, and how it derives the name of a companion table or a
_tmp index from one it was given.
Nothing here touches a connection: every function takes a value and returns the spelling to emit, or raises.
- exception psycodict.validation.InvalidColumnTypeError[source]¶
Bases:
ValueError,RuntimeErrorRaised for a column type psycodict will not put into a statement.
A
ValueError, since an unusable type is a bad argument, and also aRuntimeError, which is what psycodict raised for an invalid type before 1.0.0 and what existing callers may catch.
- psycodict.validation.validate_column_type(typ)[source]¶
Check that
typis a PostgreSQL column type psycodict is willing to create, and return the spelling that callers must put into DDL.Validation is centralized here because a column type is interpolated into
CREATE TABLEandALTER TABLEstatements as SQL text rather than bound as a value: PostgreSQL has no placeholder for a type. Callers must emit the returned spelling and never the string they passed in, since the two are equal only for input that needed no normalization.INPUT:
typ– a string, e.g.'bigint','numeric(10, 2)','varchar(16)[]'or'text COLLATE "C"'. Surrounding whitespace is ignored.
OUTPUT:
A pair
(sql_spelling, storage_cost).storage_costis the width of the type in bytes, or -1 if it is variable (which every array is), and is used to order columns when creating a table.The declaration is read in the order PostgreSQL writes it: a scalar type, then any array brackets, then a collation. Each layer is validated and the spelling is rebuilt from the validated pieces, so no part of the caller’s string reaches the DDL unchecked.
Raises
InvalidColumnTypeError(aValueError) on anything else, including a type that merely starts with a valid type.EXAMPLES:
>>> from psycodict.validation import validate_column_type >>> validate_column_type("bigint") ('bigint', 8) >>> validate_column_type(" TEXT ") ('text', -1) >>> validate_column_type('varchar(16)[] COLLATE "C"') ('varchar(16)[] COLLATE "C"', -1) >>> validate_column_type("text; DROP TABLE students; --") Traceback (most recent call last): ... psycodict.validation.InvalidColumnTypeError: 'text; DROP TABLE students; --' is not a valid type: it contains the character ';'
- psycodict.validation.column_type_sql(typ)[source]¶
The SQL fragment for a column type, validated by
validate_column_type().INPUT:
typ– a string giving a PostgreSQL column type
OUTPUT:
A
psycopg.sql.SQLfragment naming the type, ready to be interpolated into aCREATE TABLEorALTER TABLEstatement.EXAMPLES:
>>> from psycodict.validation import column_type_sql >>> column_type_sql("numeric(10, 2)").as_string() 'numeric(10, 2)'
- psycodict.validation.normalize_storage_param(key, value, access_method)[source]¶
Check one index storage parameter and return the value to emit.
INPUT:
key– the parameter namevalue– what it was given, from a caller or frommeta_indexesaccess_method– the index type, whose parameterskeymust be one of
OUTPUT:
The normalized value, which is what both the
WITHclause and the metadata row get: a boolean spelled"on"is stored asTrue, so a definition means the same thing however it was written.
- exception psycodict.validation.InvalidDefinitionError[source]¶
Bases:
ValueErrorRaised for an index or constraint definition psycodict will not build DDL from, whether it came from a caller, a metadata file or a
meta_*row.
- psycodict.validation.validate_persistent_name(name, kind='Relation')[source]¶
Check a name that is stored as the key of a relation.
A persistent name – an index name in
meta_indexes, a constraint name inmeta_constraints, the table name they refer to – has to be the catalog name of the relation it describes. PostgreSQL truncates a name over its limit rather than refusing it, so a longer string cannot be one: the metadata would be naming a relation that does not exist.This is the distinction
derived_identifier()must not blur. A physical name psycodict builds by appending_tmpmay be shortened, because psycodict computes it the same way everywhere; a name the metadata stores may not, because it is what the relation is called.
- psycodict.validation.validate_relation_name(name, kind='Relation', max_length=None)[source]¶
Check that
namecan be used as a PostgreSQL identifier.INPUT:
name– the name of a table, index, constraint or columnkind– what the name names, used in the error messagemax_length– a byte length to hold the name to, for a name psycodict is being asked to record. Not applied by default: psycodict derives the names it puts in DDL from an existing one (seederived_identifier()), and a name already in a database is a fact rather than a proposal.
OUTPUT:
nameitself.A quoted identifier can hold anything but a NUL, and psycodict quotes every name with
Identifier, so this checks what would actually make a name unusable rather than what it looks like:x-y,index with spacesandidx_éare all legal PostgreSQL names, and a name that looks like SQL is inert once quoted. Control characters are refused as a psycodict policy – they would make its logs and export files unreadable – not because PostgreSQL minds.
- psycodict.validation.identifier_bytes(name)[source]¶
The length of an identifier as PostgreSQL measures it: UTF-8 bytes, not Python characters.
- psycodict.validation.utf8_prefix(name, max_bytes)[source]¶
The longest prefix of
namethat fits inmax_bytesUTF-8 bytes, cut on a character boundary.
- psycodict.validation.validate_schema_name(name)[source]¶
Check a PostgreSQL schema name psycodict will operate in.
A schema name is an identifier like any other, and it is quoted wherever psycodict emits it – including in
search_path, which it is composed into as oneIdentifierrather than written as text – so the rules are the identifier rules: not empty, no control characters, and short enough that the server will not truncate it into a different schema. It is checked once, in the constructor, rather than at each use.The one name a quoted identifier cannot express is
$user: PostgreSQL unquotes eachsearch_pathelement before looking for that token, so"$user"is replaced by the connecting role’s name and never selects a schema actually called$user. psycodict refuses it here rather than silently operating somewhere else. Any other spelling,$USERincluded, is an ordinary identifier.EXAMPLES:
>>> from psycodict.validation import validate_schema_name >>> validate_schema_name("my schema, other") 'my schema, other' >>> validate_schema_name("$user") Traceback (most recent call last): ... psycodict.validation.InvalidDefinitionError: Schema name '$user' ...
- psycodict.validation.catalog_identifier(name)[source]¶
The spelling the catalog holds
nameunder.PostgreSQL truncates an over-long identifier as it parses the statement, so a relation created as
<57 bytes>_countsis inpg_classunder the first 63 bytes of that name. SQL keeps reaching it by the long spelling – the same truncation happens every time, which is whyphysical_table_name()concatenates rather than shortens – and a comparison the server makes is fine too, sincepg_class.relnameis of typenameand a bound value is coerced to it.What is not fine is comparing the long spelling to a catalog name in Python, where nothing truncates anything. Put a name through here first.
- psycodict.validation.derived_identifier(base, suffix='', max_length=63)[source]¶
The name of a relation psycodict derives from another one.
INPUT:
base– the name it is derived fromsuffix– what psycodict appends:"_tmp","_old1","_dep0","_pkey"and so onmax_length– the server’s identifier limit, in bytes
OUTPUT:
base + suffixwhen that fits, which is the overwhelmingly common case and what psycodict has always produced. When it does not fit, the base is cut on a UTF-8 character boundary and a short digest of the whole base is inserted before the suffix, so that two names differing only in the part that was cut do not collide.Every path that creates, drops, renames or looks for a derived relation must call this, and none may build
name + suffixitself: PostgreSQL truncates a name it is given, so a path that computed the name differently would look for a relation that another path had created under a different name.
- psycodict.validation.physical_table_name(logical_name, component='search', lifecycle='')[source]¶
The name of one relation of a search table’s family.
INPUT:
logical_name– the search table’s name, asmeta_tablesrecords itcomponent–"search","counts"or"stats"lifecycle–""for the live relation, or the suffix of a copy psycodict swaps through:"_tmp","_old3"
OUTPUT:
The relation’s name, as every path must compute it.
Unlike
derived_identifier(), this does not shorten a name that does not fit, and deliberately so. PostgreSQL truncates an over-long identifier consistently – a legacy table whose counts table was created as 67 bytes has it in the catalog under the first 63, and a reference by the 67-byte name still reaches it – so concatenation is what addresses the relations an existing database already has. Shortening them here would make psycodict look for relations that do not exist.What truncation does break is distinctness:
<63 bytes>_old1and<63 bytes>_old2are the same relation. So a relation psycodict is about to create goes throughcheck_new_table_name()first, which refuses the name rather than letting two of them become one. A name withinMAX_SEARCH_TABLE_NAME_LENGTH– which every table created since that limit is – can never reach either case.
- psycodict.validation.check_new_table_name(name, kind='Table')[source]¶
Refuse a relation psycodict is about to create under a name PostgreSQL would truncate.
Only reachable for a table whose name predates
MAX_SEARCH_TABLE_NAME_LENGTH; the point is that it stops rather than creating a second relation that turns out to be the first one.
- psycodict.validation.parse_check_function(name, valid_check_functions=())[source]¶
The identifier components of an approved CHECK function.
INPUT:
name– the function as recorded inmeta_constraints, either"function"or"schema.function"valid_check_functions– the approved names
OUTPUT:
A tuple of components, to be emitted as
Identifier(*components). A qualified name is two identifiers and must not be quoted as one: PostgreSQL reads"schema.function"as a single function whose name contains a dot, which is not the function that was approved.
- psycodict.validation.validate_column_name(name)[source]¶
Check a column name an index or constraint definition refers to.
Columns are quoted with
Identifierwherever they are used, and a column that exists is a column whatever it is called – the LMFDB has one called2adic_index– so this checks only that the name is a string psycodict can put in a statement at all.
- psycodict.validation.search_table_family(name)[source]¶
The relations a search table owns: itself and its companions.
The spellings
physical_table_name()produces, which are the ones psycodict puts into DDL. For the spellings the catalog holds them under, put each throughcatalog_identifier().
- psycodict.validation.validate_search_table_registry(names, relations)[source]¶
Check the recorded search-table names as a set, against what the database actually has.
INPUT:
names– the names recorded inmeta_tablesrelations– the names of the relations in psycodict’s schema
OUTPUT:
names, unchanged.Validating one name at a time is not enough: what makes a name a search table is that it owns a whole family of relations – itself, its counts table and its stats table – and that nothing else owns any of them. A row naming
foo_countswhenfoois a search table would otherwise putfoo’s counts table into the registry as a table in its own right, and every write to one would go to the other.That is a rule about ownership rather than about spelling, which is why a name ending in
_statsis not refused out of hand: the LMFDB has a search table calledhgcwa_per_group_stats, and it ownshgcwa_per_group_stats,hgcwa_per_group_stats_countsandhgcwa_per_group_stats_stats– a complete family that overlaps nothing.relationsholds catalog spellings, so the family is put throughcatalog_identifier()before being looked up in it. A legacy table whose name leaves fewer than seven bytes for_countshas that relation in the catalog under a truncated name, and asking for the untruncated one here would report a table the database plainly has as missing – whichrefresh_tablesruns before building anything, so it would refuse to connect at all.
- psycodict.validation.validate_search_table_name(name, reserved=(), strict=True)[source]¶
Check that
namecan be used as the name of a search table.INPUT:
name– the proposed or recorded namereserved– names that are taken for another purpose on the database object (its attributes and methods), which a new table may not shadowstrict– whether to apply the conventions a new name must follow, as opposed to the rules that keep an existing one safe to use
OUTPUT:
nameitself.A search table’s name is used in more places than a relation name: as an identifier, as the key of a table object on the database, and as the stem of the files
copy_togenerates. A name containing a path separator or a..component would send an export outside the directory it was asked for, so that much is checked of every name, however it arrives.The rest – lowercase spelling, and staying clear of the suffixes psycodict appends to a search table’s own name – is a convention for names psycodict is being asked to create. It is not applied to a name a database already has, since that database is a fact: the LMFDB, for one, has a search table called
hgcwa_per_group_stats, and refusing to connect to a database on account of a name that has worked for years would be a worse failure than the one being prevented.
- psycodict.validation.validate_index_predicate(predicate)[source]¶
Check the predicate of a partial index.
The predicate is administrative raw SQL: psycodict does not parse it, and
create_indexdocuments that it is trusted input. What this rules out is a predicate that does not stay a predicate – one that ends theCREATE INDEXstatement it is appended to, or comments out the rest of it – so that a poisonedmeta_indexesrow cannot turn a restore into two statements.INPUT:
predicate– a string giving theWHEREclause of a partial index
OUTPUT:
The predicate, stripped of surrounding whitespace.
This is deliberately conservative: a predicate that needs a semicolon, a comment or a dollar-quoted string is rejected rather than analyzed.
- psycodict.validation.index_modifier_sql(modifier, type)[source]¶
The SQL for one modifier of one index column.
INPUT:
modifier– a modifier normalized byvalidate_index_definition()type– the access method of the index
OUTPUT:
A fixed
SQLconstant for a direction or null placement, and a quoted identifier for an operator class. Nothing here is built by formatting the stored string into SQL text.
- class psycodict.validation.IndexDefinition(name, table, access_method, columns, modifiers, storage_params, whereclause)¶
Bases:
tuple- access_method¶
Alias for field number 2
- columns¶
Alias for field number 3
- modifiers¶
Alias for field number 4
- name¶
Alias for field number 0
- storage_params¶
Alias for field number 5
- table¶
Alias for field number 1
- whereclause¶
Alias for field number 6
- class psycodict.validation.ConstraintDefinition(name, table, constraint_type, columns, check_func)¶
Bases:
tuple- check_func¶
Alias for field number 4
- columns¶
Alias for field number 3
- constraint_type¶
Alias for field number 2
- name¶
Alias for field number 0
- table¶
Alias for field number 1
- psycodict.validation.validate_index_definition(name, table, type, columns, modifiers, storage_params, whereclause=None, valid_columns=None)[source]¶
Check an index definition and return it normalized.
INPUT:
name,table– the names of the index and of the relation it is built on.namemay be None when the caller has not generated it yet (create_indexderives it from the columns it is validating here).type– the access method, one of the keys of_operator_classescolumns– a nonempty list of column namesmodifiers– a list, of the same length ascolumns, of lists of modifiers for each column: an operator class valid fortype, a direction and a null placementstorage_params– a dictionary of storage parameters valid fortypewhereclause– the predicate of a partial index, or Nonevalid_columns– the columns of the relation, if known; when given, every column of the index must be one of them
OUTPUT:
An
IndexDefinition. Itsmodifiersare canonicalized to the spellings in_operator_classesand_index_modifiersand sorted into the order PostgreSQL expects, so the statement builder never emits a string that came out of the metadata.
- psycodict.validation.validate_constraint_definition(name, table, type, columns, check_func, valid_columns=None, valid_check_functions=())[source]¶
Check a constraint definition and return it normalized.
INPUT:
name,table– the names of the constraint and of the relation it applies totype–"UNIQUE","CHECK"or"NOT NULL"columns– a nonempty list of column names;NOT NULLtakes onecheck_func– for a CHECK constraint, the name of the function it calls, which must be one ofvalid_check_functions; None otherwisevalid_columns– the columns of the relation, if knownvalid_check_functions– the approved check functions, normallyPostgresTable._valid_check_functions
OUTPUT:
A
ConstraintDefinition.