Database schema reference
The schema for schema.xml contains a small number of elements with required and optional attributes. Propulsion’s generator ships an XSD (generator/resources/xsd/database.xsd) that you can point an XML-aware editor at for autocompletion, but note that the build pipeline does not itself validate schema.xml against this XSD — schema.xml is parsed directly by a hand-written reader, so a malformed attribute is only caught if that reader happens to reject it.
This format is a superset of Propel 1’s. The differences called out on this page are the one column type Propulsion dropped (UUID_BINARY), the many column types it adds (JSON, JSONB, UUID, INTERVAL, SET, GEOMETRY, VECTOR, TSVECTOR, the PostgreSQL network and range types), the platform-specific column, table, and index attributes it adds, and class-name renames (Propel* → Propulsion*) where they appear in attribute values.
At-a-glance
Section titled “At-a-glance”The hierarchical tree relationship for the elements is:
<database> <table> <column> <inheritance /> </column> <foreign-key> <reference /> </foreign-key> <index> <index-column /> </index> <unique> <unique-column /> </unique> <exclusion> <exclusion-column /> </exclusion> <id-method-parameter/> <behavior> <parameter /> </behavior> </table> <external-schema /></database>You can find example schemas in Propulsion’s own test fixtures.
<database name="my_connection_name" defaultIdMethod="native" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="vendor/quioteframework/propulsion/generator/resources/xsd/database.xsd" >Detailed reference
Section titled “Detailed reference”First, some conventions:
- Text surrounded by a
/is text that you would provide and is not defined in the language (e.g. a table name). - Optional items are surrounded by
[and]characters. - Items where you have an alternative choice have a
|character between them (e.g.true|false). - Alternative choices may be delimited by
{and}to indicate that this is the default option, if not overridden elsewhere. - … means repeat the previous item.
database element
Section titled “database element”Starting with the <database> element. The attributes and elements available are:
<database name="/DatabaseName/" defaultIdMethod="native|none" [package="/ProjectName/"] [schema="/SQLSchema/"] [namespace="/ClassNamespace/"] [baseClass="/baseClassName/"] [defaultPhpNamingMethod="nochange|{underscore}|phpname|clean"] [heavyIndexing="true|false"] [tablePrefix="/tablePrefix/"]> <table> <external-schema> ...</database>Only the name and the defaultIdMethod attributes are required.
A <database> element may include an <external-schema> element, or multiple <table> elements.
Database attributes
Section titled “Database attributes”defaultIdMethodsets the default id method to use for auto-increment columns.packagespecifies the “package” for the generated classes. Classes are created in subdirectories according to thepackagevalue.schemaspecifies the default SQL schema containing the tables. Ignored on RDBMS not supporting database schemas.namespacespecifies the default namespace that generated model classes will use. This attribute can be completed or overridden at the table level.baseClassallows you to specify a default base class that all generated Propulsion objects should extend (in place ofPropulsion\OM\BaseObject).defaultPhpNamingMethodthe default naming method to use for tables of this database. Defaults tounderscore, which transforms table names into CamelCase phpNames.heavyIndexingadds indexes for each component of the primary key (when using composite primary keys).tablePrefixadds a prefix to all the SQL table names.
table element
Section titled “table element”The <table> element is the most complicated of the usable elements. Its definition looks like this:
<table name = "/TableName/" [idMethod = "native|{none}"] [phpName = "/PhpObjectName/"] [package="/PhpObjectPackage/"] [schema="/SQLSchema/"] [namespace = "/PhpObjectNamespace/"] [skipSql = "true|false"] [abstract = "true|false"] [phpNamingMethod = "nochange|{underscore}|phpname|clean"] [baseClass = "/baseClassName/"] [description="/A text description of the table/"] [heavyIndexing = "true|false"] [readOnly = "true|false"] [treeMode = "MaterializedPath"] [reloadOnInsert = "true|false"] [reloadOnUpdate = "true|false"] [allowPkInsert = "true|false"] [inheritsFrom = "/ParentTableName/"] [nativeSequence = "true|{false}"] [primaryKeyClustered = "{true}|false"] [temporal = "true|{false}"] [historyTable = "/HistoryTableName/"]>
<column> ... <foreign-key> ... <index> ... <unique> ... <exclusion> ... <id-method-parameter> ... <behavior> ...</table>According to the schema, name is the only required attribute. Also, the idMethod, package, schema, namespace, phpNamingMethod, baseClass, and heavyIndexing attributes all default to what is specified by the <database> element.
Table attributes
Section titled “Table attributes”idMethodsets the id method to use for auto-increment columns.phpNamespecifies the object model class name. By default, Propulsion uses a CamelCase version of the table name as phpName.packagespecifies the “package” (or subdirectory) in which model classes get generated.schemaspecifies the default SQL schema containing the table. Ignored on RDBMS not supporting database schemas.namespacespecifies the namespace that the generated model classes will use. If the table namespace starts with a\, it overrides the namespace defined in the<database>tag; otherwise, the actual table namespace is the concatenation of the database namespace and the table namespace.skipSqlinstructs Propulsion not to generate DDL SQL for the specified table. This can be used together withreadOnlyfor supporting VIEWS.abstractwhether the generated stub class will be abstract (e.g. if you’re using inheritance).phpNamingMethodthe naming method to use. Defaults tounderscore, which transforms the table name into a CamelCase phpName.baseClassallows you to specify a class that the generated Propulsion objects should extend (in place ofPropulsion\OM\BaseObject).heavyIndexingadds indexes for each component of the primary key (when using composite primary keys).readOnlysuppresses the mutator/setter methods,save()anddelete()methods.treeModeindicates that this table is part of a node tree. The only supported value isMaterializedPath(deprecated).NestedSetwas removed in 3.0 — build a node tree with thenested_setbehavior instead, which has been the supported way since 1.5; a table still declaringtreeMode = "NestedSet"is refused at build time.reloadOnInsertindicates that the object should be reloaded from the database when an INSERT is performed. Useful if you have triggers (or other server-side functionality like column default expressions) that alters the database row on INSERT.reloadOnUpdateindicates that the object should be reloaded from the database when an UPDATE is performed. Useful if you have triggers (or other server-side functionality) that alters the database row on UPDATE.allowPkInsertcan be used if you want to define the primary key of a new object being inserted. By default, ifidMethodisnative, Propulsion throws an exception. However, this is sometimes useful — e.g. replicating data in a master-master environment. Defaults tofalse. On MSSQL this requiresSET IDENTITY_INSERTbracketing, which Propulsion emits automatically — see Supported databases.
PostgreSQL-only table attributes
Section titled “PostgreSQL-only table attributes”inheritsFromnames a parent table, emittingCREATE TABLE child (...) INHERITS (parent). The child still needs its own primary key declared explicitly — Propulsion requires every table to have one, and PostgreSQL’sINHERITSpropagates the parent’s columns but not its primary-key constraint. If the child redeclares a column the parent also has, getting the declarations compatible is your responsibility; PostgreSQL merges compatible ones, and Propulsion does not validate this. No other supported platform has an equivalent mechanism.
Declarative partitioning (PARTITION BY) is not supported.
MariaDB-only table attributes
Section titled “MariaDB-only table attributes”-
nativeSequenceemits a realCREATE SEQUENCE <name> START WITH 1 INCREMENT BY 1object (MariaDB 10.3+) for the sequence named in the table’s<id-method-parameter>, and a matchingDROP SEQUENCE IF EXISTS. Both are required together: without a named<id-method-parameter>there’s nothing to create, and plain MySQL has no sequence object at any version, so<id-method-parameter>alone is silently ignored on this platform.A column draws its next id from the sequence through the ordinary raw-expression default,
defaultExpr="NEXTVAL(my_seq)"— MariaDB’s own function-call syntax, unlike PostgreSQL’snextval('my_seq')or MSSQL’sNEXT VALUE FOR my_seq. Such a table has no reason to also declare anautoIncrementcolumn.<table name="book" nativeSequence="true"><id-method-parameter value="book_seq" /><column name="id" type="INTEGER" primaryKey="true" defaultExpr="NEXTVAL(book_seq)" /></table>This is opt-in for the same reason
nativeUuidis:MysqlPlatformserves both MySQL and MariaDB, and there’s no live connection at schema-generation time to detect which server the generated DDL will run against — plain MySQL would rejectCREATE SEQUENCEoutright. Setting this only when the target really is MariaDB 10.3+ is the schema author’s responsibility.
MSSQL-only table attributes
Section titled “MSSQL-only table attributes”primaryKeyClustered(defaulttrue) — set tofalseto emit an explicitNONCLUSTEREDprimary key, so clustering can be moved to a different index or unique constraint viaclustered="true". SQL Server allows only one clustered object per table; declaring more than one is rejected at DDL-execution time, not validated here.temporalturns the table into a system-versioned temporal table (SQL Server 2016+ / Azure SQL): aPERIOD FOR SYSTEM_TIME (start, end)clause plusWITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = ...)). The table must declare exactly oneperiodRowStartand oneperiodRowEndcolumn, or the build throws anEngineException.historyTablenames the history table for atemporaltable. Defaults to<table>_History. Either way the name is auto-qualified with a schema (dbowhen the base table has none), because SQL Server rejects a bare single-part name here outright.
Dropping a temporal table runs ALTER TABLE ... SET (SYSTEM_VERSIONING = OFF) first and then drops the history table explicitly — SQL Server refuses to drop a still-versioned table, and doesn’t remove the history table on its own. Propulsion checks the live catalog (sys.tables.temporal_type) rather than just the schema declaration, so this still works against a database left over from a schema that used to be temporal and no longer is.
column element
Section titled “column element”<column name = "/ColumnName/" [phpName = "/PHPColumnName/"] [primaryKey = "true|{false}"] [required = "true|{false}"] [type = "BOOLEAN|TINYINT|SMALLINT|INTEGER|BIGINT|DOUBLE|FLOAT|REAL|DECIMAL|NUMERIC|CHAR|VARCHAR|LONGVARCHAR|DATE|TIME|TIMESTAMP|BLOB|CLOB|OBJECT|ARRAY|ENUM|SET|JSON|JSONB|UUID|INTERVAL|INET|CIDR|MACADDR|CITEXT|INT4RANGE|INT8RANGE|NUMRANGE|DATERANGE|TSRANGE|TSTZRANGE|VECTOR|GEOMETRY|TSVECTOR|BU_DATE|BU_TIMESTAMP|BOOLEAN_EMU|BINARY|VARBINARY|LONGVARBINARY"] [phpType = "boolean|int|integer|double|float|string|/BuiltInClassName/|/UserDefinedClassName/"] [sqlType = "/NativeDatabaseColumnType/"] [size = "/NumericLengthOfColumn/"] [scale = "/DigitsAfterDecimalPlace/"] [defaultValue = "/AnyDefaultValueMatchingType/"] [defaultExpr = "/AnyDefaultExpressionMatchingType/"] [valueSet = "/CommaSeparatedValues/"] [enumClass = "/BackedPhpEnumClassName/"] [nativeEnum = "true|{false}"] [autoIncrement = "true|{false}"] [identity = "true|{false}"] [generatedAs = "/RawSqlExpression/"] [generatedType = "{VIRTUAL}|STORED"] [nativeArray = "true|{false}"] [nativeUuid = "true|{false}"] [tsvectorFrom = "/CommaSeparatedColumnNames/"] [tsvectorConfig = "{english}|/PgTextSearchConfig/"] [unsigned = "true|{false}"] [zerofill = "true|{false}"] [rowVersion = "true|{false}"] [periodRowStart = "true|{false}"] [periodRowEnd = "true|{false}"] [lazyLoad = "true|{false}"] [description = "/Column Description/"] [primaryString = "true|{false}"] [phpNamingMethod = "nochange|underscore|phpname"] [inheritance = "single|{false}"] > [<inheritance key="/KeyName/" class="/ClassName/" [extends="/BaseClassName/"] />]</column>Column attributes
Section titled “Column attributes”typethe database-agnostic column type. Propulsion maps native SQL types to these types depending on the RDBMS. Using Propulsion types guarantees that a column definition is portable. See Column types below. Propel 2’sUUID_BINARYtype is not present; see UUID and binary columns.sqlTypethe SQL type to be used in CREATE and ALTER statements (overriding the mapping between Propulsion types and RDBMS types).defaultValuethe default value that the object will have for this column in the PHP instance after creating anew Object(). This value is always interpreted as a string.defaultExprthe default value for this column as expressed in SQL. This value is used solely for the “sql” target which builds your database from the schema.xml file. The defaultExpr is the SQL expression used as the “default” for the column.valueSetthe list of enumerated values accepted on anENUMorSETcolumn. The list contains 255 values at most, separated by commas. Derived automatically from the enum’s own case values whenenumClassis set, so the two can’t drift.enumClassnames a backed PHPenumfor anENUMcolumn. The generated property, getter, setter, hydration, andbuildCriteria()all work with the enum instance directly rather than the raw label string. Storage is unchanged (still the emulated integer index unlessnativeEnumis also set). Independent ofnativeEnum— either, neither, or both.nativeEnumopts anENUMcolumn into the platform’s real enum mechanism instead of the emulated integer index. See Enum columns.identityopts an auto-increment primary key into the standardGENERATED BY DEFAULT AS IDENTITYsyntax. Honored on PostgreSQL (PG10+, in place of theserial/bigserialpseudo-types) and Oracle (12c+). RequiresautoIncrement="true"and no explicit<id-method-parameter>named sequence. Opt-in rather than the default because countless existing schemas assert the exactserial/bigserialDDL; the implicit sequence naming is identical either way, so nothing downstream changes. Note that converting an existing column’s identity-ness viaALTERis not attempted — that’s a structurally different statement, and re-declaring it is an error on Oracle.generatedAsmakes this a generated/computed column from a raw SQL expression, e.g.generatedAs="price * quantity". Honored on SQLite (3.31+,GENERATED ALWAYS AS (expr) VIRTUAL|STORED) and MSSQL (AS (expr) [PERSISTED]). A generated column cannot also carry aDEFAULT.generatedType—VIRTUAL(the default, computed on read) orSTORED(computed on write and stored). On MSSQL,STOREDemitsPERSISTED; T-SQL only allowsNOT NULLalongsidePERSISTEDand never allows a bareNULLkeyword on a computed column, which is stricter than SQLite’s grammar.lazyLoada lazy-loaded column is not fetched from the database by model queries. Only the generated getter method for such a column issues a query to the database. Useful for large column types (such as CLOB and BLOB).primaryStringa column defined as primary string serves as the default value for a__toString()method in the generated Propulsion object.
PostgreSQL-only column attributes
Section titled “PostgreSQL-only column attributes”nativeArraystores anARRAYcolumn as a real PostgreSQLTEXT[]instead of the emulated" | "-delimited text format. See Array columns for the migration caveat — this is a breaking wire-format change for a column that already has data, which is why it isn’t the default.tsvectorFromauto-populates aTSVECTORcolumn from a comma-separated list of source columns, via a realGENERATED ALWAYS AS (to_tsvector(...)) STOREDcolumn (PG12+) rather than a trigger. Mutually exclusive with the column’s ownDEFAULT.tsvectorConfigthe text-search configuration used bytsvectorFrom. Defaults toenglish.
MySQL-only column attributes
Section titled “MySQL-only column attributes”unsignedappendsUNSIGNEDafter a numeric column’s type. Silently ignored on a non-numeric column.zerofillappendsUNSIGNED ZEROFILL.ZEROFILLimpliesUNSIGNEDin MySQL even withoutunsignedalso set, and Propulsion matches that.
MariaDB-only column attributes
Section titled “MariaDB-only column attributes”-
nativeUuidemits MariaDB 10.7+‘s real nativeUUIDcolumn type for aUUIDcolumn, instead of theCHAR(36)emulation every platform otherwise uses. Ignored on a column of any other type.Opt-in rather than the default because
MysqlPlatformserves both MySQL and MariaDB, and there’s no way at schema-generation time to tell which server the DDL will run against — plain MySQL has no native UUID type at any version. (The runtimeDBMySQL::isMariaDb()probe that gatesRETURNINGsupport needs a live connection, which the generator doesn’t have.) Setting this only when the target really is MariaDB 10.7+ is the schema author’s responsibility.The generated PHP side is unchanged: the setter still validates the canonical 8-4-4-4-12 form and normalizes to lowercase, and the getter still returns a string. See also
nativeSequence.
MSSQL-only column attributes
Section titled “MSSQL-only column attributes”rowVersionemits the realROWVERSIONtype in place of the column’s mapped domain type — a database-maintained concurrency token. This is DDL and type mapping only; runtime hydration and comparison for using it as a concurrency check aren’t implemented. For optimistic concurrency on any platform, use theoptimistic_lockbehavior instead.periodRowStart/periodRowEndmark the two system-time boundary columns of atemporaltable, emittingcol DATETIME2 GENERATED ALWAYS AS ROW START|END NOT NULL. You declare them explicitly like any other column rather than having Propulsion synthesize hidden ones.
foreign-key element
Section titled “foreign-key element”To link a column to another table use the following syntax:
<foreign-key foreignTable = "/TheOtherTableName/" [foreignSchema = "/TheOtherTableSQLSchema/"] [name = "/Name for this foreign key/"] [phpName = "/Name for the foreign object in methods generated in this class/"] [refPhpName = "/Name for this object in methods generated in the foreign class/"] [onDelete = "cascade|setnull|restrict|none"] [onUpdate = "cascade|setnull|restrict|none"] [skipSql = "true|false"] [defaultJoin= "Criteria::INNER_JOIN|Criteria::LEFT_JOIN"]> <reference local="/LocalColumnName/" foreign="/ForeignColumnName/" /></foreign-key>Foreign key attributes
Section titled “Foreign key attributes”skipSqlinstructs Propulsion not to generate DDL SQL for the specified foreign key. This can be used to support relationships in the model without an actual foreign key.defaultJoinaffects the default join type used in the generatedjoinXXX()methods in the model query class. Propulsion uses an INNER JOIN for foreign keys attached to a required column, and a LEFT JOIN for foreign keys attached to a non-required column, but you can override this in the foreign key element.Criteriahere isPropulsion\Query\Criteria.
index element
Section titled “index element”To create an index on one or more columns, use the following syntax:
<index [name="/IndexName/"] [indexType="/AccessMethodOrIndexKind/"] [where="/RawSqlPredicate/"] [include="/CommaSeparatedColumnNames/"] [storageParameters="/RawWithClauseText/"] [concurrently="true|{false}"] [clustered="true|false"]> <index-column [name="/ColumnName/"] [size="/LengthOfIndexColumn/"] [expression="/RawSqlExpression/"] [opclass="/PgOperatorClass/"] /> ...</index>sizeonly for MySQL databases, where your RDBMS may require an index prefix length.expressionon<index-column>makes that entry a raw SQL expression instead of a column name, e.g.expression="lower(title)". Give eithernameorexpression, not both. Honored on PostgreSQL and SQLite; emitted verbatim, wrapped in parentheses.opclasson<index-column>(PostgreSQL) emits an operator class after the column, inside the index’s column list:USING hnsw ("embedding" vector_l2_ops). Not merely ergonomics — pgvector’shnswandivfflataccess methods refuse to build without one, andexpressionis not a substitute, since an expression entry is wrapped in its own parentheses. Emitted verbatim rather than quoted as an identifier, because an operator class name is not one.indexTypenames the index access method or kind. On PostgreSQL it becomesUSING <type>—gin,gist,brin,hash. On MySQL it selects the index kind:indexType="fulltext"or"spatial". This supersedes the older<vendor type="mysql"><parameter name="Index_type" .../></vendor>convention, which still works as a fallback. AFULLTEXT/SPATIALindex can’t also beUNIQUE, soindexTypetakes priority over uniqueness when both are set.whereadds a trailingWHEREpredicate, making it a partial index. Honored on PostgreSQL and SQLite.include(PostgreSQL) adds non-key “covering” columns for index-only scans:INCLUDE (col1, col2).storageParameters(PostgreSQL) is emitted verbatim inside a trailingWITH (...), e.g.storageParameters="fillfactor=70". Deliberately not parsed into key/value pairs, since PostgreSQL has dozens of access-method-specific parameters.concurrently(PostgreSQL) emitsCREATE INDEX CONCURRENTLY.clustered(MSSQL) splicesCLUSTEREDorNONCLUSTEREDinto the statement. Unset (the default) reproduces the prior DDL unchanged. SeeprimaryKeyClusteredfor moving clustering off the primary key.
All of USING, WHERE, INCLUDE, WITH, and CONCURRENTLY compose freely on PostgreSQL.
unique element
Section titled “unique element”To create a unique index on one or more columns, use the following syntax:
<unique [name="/IndexName/"] [clustered="true|false"]> <unique-column name="/ColumnName/" [size="/LengthOfIndexColumn/"] /> ...</unique>In some cases your RDBMS may require you to specify an index size for unique indexes.
sizeonly for MySQL databases.clustered(MSSQL) — as on<index>.
exclusion element
Section titled “exclusion element”PostgreSQL only. An exclusion constraint rejects rows whose values conflict with an existing row under a given operator — the standard way to prevent overlapping ranges. Unlike a unique constraint, each column pairs with its own comparison operator:
<table name="reservation"> <column name="id" required="true" primaryKey="true" autoIncrement="true" type="integer" /> <column name="room_id" required="true" type="integer" /> <column name="during" required="true" type="TSTZRANGE" />
<exclusion name="reservation_no_overlap"> <exclusion-column name="room_id" operator="=" /> <exclusion-column name="during" operator="&&" /> </exclusion></table>CONSTRAINT reservation_no_overlap EXCLUDE USING gist ("room_id" WITH =, "during" WITH &&)namethe constraint name.indexTypethe access method. Defaults togist, which has the widest operator-class support and is what PostgreSQL’s own documentation uses for every exclusion-constraint example.wherea predicate, making it a partial exclusion constraint.operatoron<exclusion-column>is required — the comparison operator for that column.
The constraint is emitted inline inside CREATE TABLE, alongside UNIQUE and PRIMARY KEY. An <exclusion> element is a no-op on every other platform.
id-method-parameter element
Section titled “id-method-parameter element”If you are using a database that uses sequences for auto-increment columns (e.g. PostgreSQL or Oracle), you can customize the name of the sequence using the <id-method-parameter> tag:
<id-method-parameter value="my_custom_sequence_name"/>On MSSQL this same element opts the table into a real CREATE SEQUENCE object (SQL Server 2012+), which is a fully independent mechanism there rather than an alternative name for the same object — SQL Server’s default IDENTITY is a column property with no backing sequence at all. A column that wants its value from the sequence declares it as a raw default expression, and must not also be autoIncrement="true", which would give it two competing value sources:
<table name="invoice"> <id-method-parameter value="invoice_seq"/> <column name="id" required="true" primaryKey="true" type="BIGINT" defaultExpr="NEXT VALUE FOR invoice_seq" /></table>external-schema element
Section titled “external-schema element”The <external-schema> element includes another schema file from the filesystem into the current schema. The format is:
<external-schema filename="/a path to a file/" referenceOnly="{true}|false"/>The filename can be relative or absolute. Beware that the external schema must contain a <database> with the same name as the current element. By default, tables from external schemas are ignored by the SQL build task — that means Propulsion won’t try to insert the external tables. If you want Propulsion to take the tables from an external schema into account in SQL, set the referenceOnly attribute to false.
Column types
Section titled “Column types”Here are the Propulsion column types with some example mappings to native database and PHP types. There are also several ways to customize the mapping between these types.
Text types
Section titled “Text types”| Propulsion type | Desc | Example Default DB Type (PostgreSQL) | Default PHP Native Type |
|---|---|---|---|
| CHAR | Fixed-length character data | CHAR | string |
| VARCHAR | Variable-length character data | VARCHAR | string |
| LONGVARCHAR | Long variable-length character data | TEXT | string |
| CLOB | Character LOB (locator object) | TEXT | string |
LONGVARCHAR and CLOB need no declared size, and allow for very large strings.
Numeric types
Section titled “Numeric types”| Propulsion type | Desc | Example Default DB Type (PostgreSQL) | Default PHP Native Type |
|---|---|---|---|
| NUMERIC | Numeric data | NUMERIC | string (PHP int is limited) |
| DECIMAL | Decimal data | DECIMAL | string (PHP int is limited) |
| TINYINT | Tiny integer | INT2 | int |
| SMALLINT | Small integer | INT2 | int |
| INTEGER | Integer | INTEGER | int |
| BIGINT | Large integer | INT8 | int |
| REAL | Real number | FLOAT | double |
| FLOAT | Floating point number | DOUBLE PRECISION | double |
| DOUBLE | Floating point number | DOUBLE PRECISION | double |
Binary types
Section titled “Binary types”| Propulsion type | Desc | Example Default DB Type (PostgreSQL) | Default PHP Native Type |
|---|---|---|---|
| BINARY | Fixed-length binary data | BYTEA | string |
| VARBINARY | Variable-length binary data | BYTEA | stream or string |
| LONGVARBINARY | Long variable-length binary data | BYTEA | stream or string |
| BLOB | Binary LOB (locator object) | BYTEA | stream or string |
Temporal (date/time) types
Section titled “Temporal (date/time) types”| Propulsion type | Desc | Example Default DB Type (PostgreSQL) | Default PHP Native Type |
|---|---|---|---|
| DATE | Date (e.g. YYYY-MM-DD) | DATE | DateTime object |
| TIME | Time (e.g. HH:MM:SS) | TIME | DateTime object |
| TIMESTAMP | Date + time (e.g. YYYY-MM-DD HH:MM:SS) | TIMESTAMP | DateTime object |
Exact-decimal numerics
Section titled “Exact-decimal numerics”A DECIMAL/NUMERIC column maps to a PHP string by default, since neither int nor float can represent an arbitrary-precision decimal without loss. Opt into PHP 8.4+‘s BcMath\Number value object instead, using the existing generic phpType attribute — no new schema syntax:
<column name="price" type="DECIMAL" size="10" scale="2" phpType="\BcMath\Number" />$product->setPrice('19.99'); // string, int, and float are all accepted$product->setPrice(new \BcMath\Number('19.99'));$total = $product->getPrice() * 3; // ?BcMath\Number, with real decimal arithmeticThe property, getter, and setter are typed ?Number; the setter additionally accepts string|int|float|null and normalizes. Values are cast back to (string) at the database boundary, so storage is unchanged and this is safe to adopt on a column that already has data.
Temporal intervals
Section titled “Temporal intervals”| Propulsion type | Desc | Example Default DB Type (PostgreSQL) | Default PHP Native Type |
|---|---|---|---|
| INTERVAL | A duration | INTERVAL | DateInterval object |
INTERVAL maps to PostgreSQL’s native interval type and to VARCHAR(32) elsewhere, storing an ISO-8601 duration string ("P1DT2H") uniformly on every platform.
PostgreSQL’s default intervalstyle doesn’t output ISO-8601 (it produces "1 day 02:03:04"), so Propulsion sets SET intervalstyle = 'iso_8601' on every new PostgreSQL connection. That makes the wire format identical across all platforms and lets the generated hydration code be a single new DateInterval($v) with no platform branching.
Other types
Section titled “Other types”BOOLEANcolumns map to a boolean in PHP. Depending on the native support for this type, they are stored in SQL asBOOLEANor an emulated integer type.OBJECTcolumns map to PHP objects and are stored asserialize()d text.
Enum columns
Section titled “Enum columns”ENUM columns accept values among a list of predefined ones, declared with the valueSet attribute, separated by commas. Two independent opt-ins change how they behave:
enumClass maps the column to a backed PHP enum instead of the raw label string:
enum Status: string { case Draft = 'draft'; case Published = 'published';}<column name="status" type="ENUM" enumClass="\App\Model\Status" />$book->setStatus(Status::Published);$book->getStatus() === Status::Published; // a real enum instance, not a stringvalueSet is derived from the enum’s own case values at parse time, so you don’t declare the vocabulary twice and the two can’t drift apart. Storage is unchanged.
nativeEnum changes storage, from the emulated integer index to the platform’s real enum mechanism:
| Platform | nativeEnum="true" emits |
|---|---|
| PostgreSQL | A real CREATE TYPE <table>_<column>_enum AS ENUM (...) before the table, dropped with it |
| MySQL | Inline ENUM('a', 'b', ...) |
| SQLite, Oracle | The emulated text/int domain plus a CHECK (col IN (...)) constraint |
| MSSQL | Nothing — SQL Server has no native enum mechanism, so the attribute is a no-op there |
A native-storage column holds the label text itself, not the emulated integer index. The generated code converts between the in-memory representation (the emulated index for a plain ENUM property, or the enum instance when enumClass is set) and the label text at the database boundary, and the column binds as PDO::PARAM_STR rather than PARAM_INT.
enumClass and nativeEnum are orthogonal — use either, neither, or both.
Set columns
Section titled “Set columns”| Propulsion type | Desc | Example Default DB Type (MySQL) | Default PHP Native Type |
|---|---|---|---|
| SET | Any subset of a fixed label vocabulary | SET(‘a’, ‘b’, …) | array<string> |
A SET column holds several labels from its valueSet at once, unlike ENUM’s single selection. MySQL emits a real inline SET('a', 'b', ...); every other platform stores a comma-joined string of labels in its own long-text column (TEXT on PostgreSQL, MEDIUMTEXT on SQLite, VARCHAR(MAX) on MSSQL, CLOB on Oracle).
There’s no opt-in flag here, because unlike ENUM there’s no meaningful alternative representation to opt out of — a multi-select column can’t be a compact integer index. That also means the wire format is identical whether the column is MySQL’s real native SET or another platform’s emulated text: PDO returns and accepts a SET value as a single comma-joined string either way. Hydration therefore needs no platform branching at all, in contrast to ARRAY.
Plural-named SET columns get the same has/add/remove-element convenience methods plural-named ARRAY columns do.
Array columns
Section titled “Array columns”ARRAY columns map to PHP arrays. By default they’re stored as a single " | "-delimited string, which works identically on every platform.
On PostgreSQL, nativeArray="true" stores a real TEXT[] instead:
<column name="tags" type="ARRAY" nativeArray="true" />A plain ARRAY column has no declared element type, so the native form is always TEXT[] rather than a typed int[] — the same “no rich subtype” decision INET/CIDR/MACADDR make. Values are encoded and decoded through Propulsion\Type\PgArray, which handles PostgreSQL’s array-literal syntax ({a,"b,c",NULL}) including quoting and escaping of commas, braces, quotes, backslashes, whitespace, and the empty string, and distinguishes an unquoted NULL (the SQL null) from a quoted "NULL" (the four-character string).
Two things a native array column loses: the CONTAINS_ALL/CONTAINS_SOME/CONTAINS_NONE LIKE-based containment shortcut (which is built entirely on the emulated format), and the singular-named per-element filter method a plural-named ARRAY column otherwise gets (filterByTag() next to filterByTags()), which is skipped entirely so it can’t emit a LIKE comparison against a real text[]. A plain value is still encoded for equality and IN comparisons.
Query-layer array operators (@>, &&, ANY) aren’t supported; use a raw expression for those.
JSON types
Section titled “JSON types”| Propulsion type | Desc | Example Default DB Type (PostgreSQL) | Default PHP Native Type |
|---|---|---|---|
| JSON | JSON data, stored/queried as exact input text | JSON | mixed |
| JSONB | JSON data, stored as a decomposed binary form | JSONB | mixed |
JSON and JSONB are stored as real JSON text via json_encode()/json_decode() — a real behavioral difference from OBJECT/ARRAY, which use serialize()/a custom delimited format. Both map to native JSON/JSONB on PostgreSQL (schema authors pick either), native JSON on MySQL (which has no separate binary variant, so both Propulsion types map to the same native column type there), and a text fallback (CLOB on Oracle, TEXT on SQLite, VARCHAR(MAX) on MSSQL) elsewhere. The generated getter decodes with JSON_THROW_ON_ERROR, raising a PropulsionException that names the offending column on malformed data rather than returning null or corrupting the value. See Working with Advanced Column Types.
UUID type
Section titled “UUID type”| Propulsion type | Desc | Example Default DB Type (PostgreSQL) | Default PHP Native Type |
|---|---|---|---|
| UUID | A UUID, stored in canonical hyphenated form | UUID | string |
UUID maps to PostgreSQL’s native uuid column type, and to CHAR(36) elsewhere (MySQL, SQLite, Oracle, MSSQL) — with one opt-in exception: nativeUuid="true" emits MariaDB 10.7+‘s native UUID type. The generated setter validates the canonical 8-4-4-4-12 hex format and normalizes it to lowercase, raising a PropulsionException on malformed input rather than storing garbage; filtering, hydration, and TableMap metadata need no further generator support, since UUID is a text type for those purposes (see Column::isTextType()). This is unrelated to Propel 2’s UUID_BINARY type (16-byte binary storage) which Propulsion doesn’t implement — see UUID and binary columns for that case.
Propel 2’s UUID_BINARY column type (16-byte binary storage) is not implemented in Propulsion — it isn’t in Propulsion\Generator\Model\PropulsionTypes or the schema XSD’s type enumeration, so model:build rejects it as an invalid type. See UUID and binary columns for what to do instead.
Network and case-insensitive text types
Section titled “Network and case-insensitive text types”| Propulsion type | Desc | Example Default DB Type (PostgreSQL) | Default PHP Native Type |
|---|---|---|---|
| INET | An IPv4/IPv6 host address | INET | string |
| CIDR | An IPv4/IPv6 network specification | CIDR | string |
| MACADDR | A MAC address | MACADDR | string |
| CITEXT | Case-insensitive text | CITEXT | string |
All four are native on PostgreSQL and emulated as a sized VARCHAR elsewhere — VARCHAR(43) for INET/CIDR, VARCHAR(17) for MACADDR, and each platform’s own long-text type for CITEXT (TEXT on MySQL, MEDIUMTEXT on SQLite, VARCHAR(MAX) on MSSQL, NVARCHAR2(2000) on Oracle).
There’s no rich PHP value object for these — they hydrate as plain strings, the same way UUID does, so filtering and hydration work exactly like a VARCHAR column with no restrictions.
citext ships as a PostgreSQL contrib extension rather than a built-in type, so CREATE EXTENSION IF NOT EXISTS citext is emitted before the table — but only when a column actually uses it.
Range types
Section titled “Range types”| Propulsion type | Desc | Example Default DB Type (PostgreSQL) | Default PHP Native Type |
|---|---|---|---|
| INT4RANGE | A range of 4-byte integers | INT4RANGE | Range object |
| INT8RANGE | A range of 8-byte integers | INT8RANGE | Range object |
| NUMRANGE | A range of numerics | NUMRANGE | Range object |
| DATERANGE | A range of dates | DATERANGE | Range object |
| TSRANGE | A range of timestamps | TSRANGE | Range object |
| TSTZRANGE | A range of timestamps with time zone | TSTZRANGE | Range object |
Native on PostgreSQL, emulated as a VARCHAR(64) holding the range literal text ("[1,10)") elsewhere.
All six hydrate to Propulsion\Type\Range, which keeps its bounds as raw strings rather than guessing a subtype-specific PHP type — an INT4RANGE’s bounds are integers, a DATERANGE’s are dates, and there’s no single type covering both:
use Propulsion\Type\Range;
$reservation->setDuring(Range::parse('[2026-08-01,2026-08-08)'));echo $reservation->getDuring(); // '[2026-08-01,2026-08-08)'echo $reservation->getDuring()->getLower(); // '2026-08-01'Range::parse() and __toString() round-trip PostgreSQL’s bracket notation, including (,5] for an unbounded lower end, empty, and the escaped-double-quote form for bound values containing a comma.
Query-layer range operators (&&, @>) aren’t supported; use a raw expression. Exclusion constraints, which are the usual companion feature, are — see the <exclusion> element.
Vector type
Section titled “Vector type”| Propulsion type | Desc | Example Default DB Type (PostgreSQL) | Default PHP Native Type |
|---|---|---|---|
| VECTOR | A fixed-dimension embedding vector | vector(n) | array<float> |
Native vector(n) on PostgreSQL via pgvector (CREATE EXTENSION IF NOT EXISTS vector is emitted when a column uses it), and emulated as unbounded text elsewhere — deliberately not a sized VARCHAR, since an embedding’s text form can be long.
The dimension reuses the ordinary size attribute rather than a new one:
<column name="embedding" type="VECTOR" size="1536" />$doc->setEmbedding([0.021, -0.114, /* ...1534 more */]);$doc->getEmbedding(); // array<float>nativeVector on MySQL and MariaDB
Section titled “nativeVector on MySQL and MariaDB”MariaDB 11.7+ and MySQL 9.0+ have a real VECTOR(n) type, spelled identically on both. It is opt-in per column, like nativeUuid, because switching the storage format under an existing emulated column is the schema author’s decision:
<column name="embedding" type="VECTOR" size="1536" nativeVector="true" />A nativeVector column with no size throws at build time rather than emitting a type the server will reject.
Neither engine accepts a bracketed-JSON string bound straight into a native vector column, so reads and writes are wrapped in the engine’s conversion functions — MariaDB’s VEC_FromText()/VEC_ToText(), MySQL’s STRING_TO_VECTOR()/VECTOR_TO_STRING(). Which pair is used is decided at runtime from the server version, so the same generated schema works against either. A connection the application built itself and handed to setConnection() never goes through initConnection() and so is not probed; it defaults to MariaDB, and DBMySQL::setServerFlavor() declares it in one line. Getting it wrong is a loud “FUNCTION does not exist” from the server, not silent corruption.
Distance queries — nearest-N by L2, cosine, inner product or L1 — go through VectorExpression; see vector distance queries for what each platform supports, and opclass for the operator class pgvector’s HNSW and IVFFlat indexes require.
Full-text search type
Section titled “Full-text search type”| Propulsion type | Desc | Example Default DB Type (PostgreSQL) | Default PHP Native Type |
|---|---|---|---|
| TSVECTOR | A parsed full-text search document | TSVECTOR | string |
Native tsvector on PostgreSQL, emulated as each platform’s own long-text type elsewhere. It hydrates as a plain string with no value object — a tsvector’s internal lexeme/position text isn’t meant to be constructed or read by application code.
The intended way to populate one on PostgreSQL is tsvectorFrom, which builds a real generated column instead of the more common trigger approach:
<table name="book"> <column name="title" type="VARCHAR" size="255" /> <column name="summary" type="LONGVARCHAR" /> <column name="search_doc" type="TSVECTOR" tsvectorFrom="title, summary" tsvectorConfig="english" />
<index name="book_search_idx" indexType="gin"> <index-column name="search_doc" /> </index></table>"search_doc" TSVECTOR GENERATED ALWAYS AS (to_tsvector('english', coalesce("title", '') || ' ' || coalesce("summary", ''))) STORED...CREATE INDEX book_search_idx ON book USING gin ("search_doc");tsvectorFrom is PostgreSQL-only; on every other platform (and on PostgreSQL without it) a TSVECTOR column is an ordinary column you populate yourself. Because it’s a generated column, it can’t also carry a DEFAULT.
Geometry type
Section titled “Geometry type”| Propulsion type | Desc | Example Default DB Type (PostgreSQL) | Default PHP Native Type |
|---|---|---|---|
| GEOMETRY | A geometry, stored as WKT | TEXT | string |
GEOMETRY is stored as plain WKT (“well-known text”, e.g. "POINT(1 2)") on every platform, PostgreSQL and MySQL included — it does not map to PostGIS geometry, MySQL GEOMETRY, MSSQL geometry, or Oracle SDO_GEOMETRY.
That’s a narrower scope than the other types on this page, for a concrete reason: unlike UUID, JSON, vector, or the range types, none of those native geometry types accept or return raw WKT through a plain parameterized bind. Each needs the bound value wrapped in a conversion function (ST_GeomFromText(), STGeomFromText(), SDO_UTIL.FROM_WKTGEOMETRY()) at the statement level to write, and a matching ST_AsText() around the column in the SELECT list to read back. That’s a query-layer change, not a type mapping — and shipping a “native” mapping without it would produce silently broken round-trips.
If you need real native geometry columns and spatial indexes today, declare them with an explicit sqlType override and do the WKT conversion in your own SQL.
Legacy temporal types
Section titled “Legacy temporal types”The following types are still supported for compatibility, but aren’t needed for new schemas:
| Propulsion type | Desc | Example Default DB Type (PostgreSQL) | Default PHP Native Type |
|---|---|---|---|
| BU_DATE | Pre-/post-epoch date (e.g. 1201-03-02) | DATE | string |
| BU_TIMESTAMP | Pre-/post-epoch Date + time (e.g. 1201-03-02 12:33:00) | TIMESTAMP | string |
Customizing mappings
Section titled “Customizing mappings”Specify column attributes
Section titled “Specify column attributes”You can change the way that Propulsion maps its own types to native SQL types or to PHP types by overriding the values for a specific column.
For example:
(Overriding PHP type)
<column name="population_served" type="INTEGER" phpType="string"/>(Overriding SQL type)
<column name="ip_address" type="VARCHAR" sqlType="inet"/>Adding vendor info
Section titled “Adding vendor info”Propulsion supports database-specific elements in the schema (currently for MySQL, Oracle, and — for a single schema parameter — PostgreSQL; the MSSQL, SQLite, and SQL Server (sqlsrv) platforms parse <vendor> info into the model but don’t act on it). These “vendor” parameters affect the generated SQL. To add vendor data, add a <vendor> tag with a type attribute specifying the target database vendor. In the <vendor> tag, add <parameter> tags with a name and a value attribute. For instance:
<table name="book"> <vendor type="mysql"> <parameter name="Engine" value="InnoDB"/> <parameter name="Charset" value="utf8"/> </vendor></table>This will change the generated SQL table creation to look like:
CREATE TABLE book () ENGINE = InnoDB DEFAULT CHARACTER SET utf8;Global vendor info
Section titled “Global vendor info”To set a global <vendor> tag for your whole database, define the <vendor> element at your database level. This will apply the “vendor” parameters to all the tables in the database. But you can still override these global “vendor” parameters in your <table> element.
<database name="bookstore"> <vendor type="mysql"> <parameter name="Engine" value="InnoDB"/> </vendor>
<table name="book"> <!-- ... --> </table></database>MySQL vendor info
Section titled “MySQL vendor info”Propulsion supports the following vendor parameters for MySQL:
Name | Example values-----------------|---------------// in <table> elementEngine | InnoDB (default), MyISAM, MEMORY, etc.AutoIncrement | 1234, N, etcAvgRowLength |Charset | utf8, latin1, etc.Checksum | 0, 1Collate | utf8_unicode_ci, latin1_german1_ci, etc.Connection | mysql://fed_user@remote_host:9306/federated/test_table (for FEDERATED storage engine)DataDirectory | /var/db/foo (for MyISAM storage engine)DelayKeyWrite | 0, 1IndexDirectory | /var/db/foo (for MyISAM storage engine)InsertMethod | FIRST, LAST (for MERGE storage engine)KeyBlockSize | 0 (default), 1024, etcMaxRows | 1000, 4294967295, etcMinRows | 1000 (for MEMORY storage engine)PackKeys | 0, 1, DEFAULTRowFormat | FIXED, DYNAMIC, COMPRESSED, COMPACT, REDUNDANTUnion | (t1,t2) (for MERGE storage engine)// in <column> elementCharset | utf8, latin1, etc.Collate | utf8_unicode_ci, latin1_german1_ci, etc.// in <index> elementIndex_type | FULLTEXTOracle vendor info
Section titled “Oracle vendor info”Propulsion supports the following vendor parameters for Oracle:
Name | Example values-----------------|---------------// in <table> elementPCTFree | 20InitTrans | 4MinExtents | 1MaxExtents | 99PCTIncrease | 0Tablespace | L_128KPKPCTFree | 20PKInitTrans | 4PKMinExtents | 1PKMaxExtents | 99PKPCTIncrease | 0PKTablespace | IL_128K// in <index> elementPCTFree | 20InitTrans | 4MinExtents | 1MaxExtents | 99PCTIncrease | 0Tablespace | L_128KPostgreSQL vendor info
Section titled “PostgreSQL vendor info”Propulsion supports the following vendor parameter for PostgreSQL (vendor type is pgsql):
Name | Example values-----------------|---------------// in <table> element (or <database> element, applying to all tables)schema | my_schemaSetting schema emits a CREATE SCHEMA statement for that schema and qualifies the table name with it, in addition to (or instead of) the <table schema="..."> attribute.
Using a custom platform
Section titled “Using a custom platform”For overriding the mapping between Propulsion types and native SQL types, you can create your own Platform class and override the mapping.
For example:
<?phpuse Propulsion\Generator\Platform\PgsqlPlatform;use Propulsion\Generator\Model\Domain;use Propulsion\Generator\Model\PropulsionTypes;
class CustomPgsqlPlatform extends PgsqlPlatform{ /** * Initializes custom domain mapping. */ protected function initialize() { parent::initialize(); $this->setSchemaDomainMapping(new Domain(PropulsionTypes::LONGVARCHAR, "TEXT")); $this->setSchemaDomainMapping(new Domain(PropulsionTypes::BINARY, "BYTEA")); $this->setSchemaDomainMapping(new Domain(PropulsionTypes::VARBINARY, "BYTEA")); $this->setSchemaDomainMapping(new Domain(PropulsionTypes::LONGVARBINARY, "BYTEA")); $this->setSchemaDomainMapping(new Domain(PropulsionTypes::BLOB, "BYTEA")); $this->setSchemaDomainMapping(new Domain(PropulsionTypes::CLOB, "TEXT")); }}You must then specify that mapping as a build property (see the configuration file reference):
return [ 'propulsion.platform.class' => 'CustomPgsqlPlatform',];