Skip to content

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.

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" >

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.

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.

  • defaultIdMethod sets the default id method to use for auto-increment columns.
  • package specifies the “package” for the generated classes. Classes are created in subdirectories according to the package value.
  • schema specifies the default SQL schema containing the tables. Ignored on RDBMS not supporting database schemas.
  • namespace specifies the default namespace that generated model classes will use. This attribute can be completed or overridden at the table level.
  • baseClass allows you to specify a default base class that all generated Propulsion objects should extend (in place of Propulsion\OM\BaseObject).
  • defaultPhpNamingMethod the default naming method to use for tables of this database. Defaults to underscore, which transforms table names into CamelCase phpNames.
  • heavyIndexing adds indexes for each component of the primary key (when using composite primary keys).
  • tablePrefix adds a prefix to all the SQL table names.

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.

  • idMethod sets the id method to use for auto-increment columns.
  • phpName specifies the object model class name. By default, Propulsion uses a CamelCase version of the table name as phpName.
  • package specifies the “package” (or subdirectory) in which model classes get generated.
  • schema specifies the default SQL schema containing the table. Ignored on RDBMS not supporting database schemas.
  • namespace specifies 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.
  • skipSql instructs Propulsion not to generate DDL SQL for the specified table. This can be used together with readOnly for supporting VIEWS.
  • abstract whether the generated stub class will be abstract (e.g. if you’re using inheritance).
  • phpNamingMethod the naming method to use. Defaults to underscore, which transforms the table name into a CamelCase phpName.
  • baseClass allows you to specify a class that the generated Propulsion objects should extend (in place of Propulsion\OM\BaseObject).
  • heavyIndexing adds indexes for each component of the primary key (when using composite primary keys).
  • readOnly suppresses the mutator/setter methods, save() and delete() methods.
  • treeMode indicates that this table is part of a node tree. The only supported value is MaterializedPath (deprecated). NestedSet was removed in 3.0 — build a node tree with the nested_set behavior instead, which has been the supported way since 1.5; a table still declaring treeMode = "NestedSet" is refused at build time.
  • reloadOnInsert indicates 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.
  • reloadOnUpdate indicates 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.
  • allowPkInsert can be used if you want to define the primary key of a new object being inserted. By default, if idMethod is native, Propulsion throws an exception. However, this is sometimes useful — e.g. replicating data in a master-master environment. Defaults to false. On MSSQL this requires SET IDENTITY_INSERT bracketing, which Propulsion emits automatically — see Supported databases.
  • inheritsFrom names a parent table, emitting CREATE TABLE child (...) INHERITS (parent). The child still needs its own primary key declared explicitly — Propulsion requires every table to have one, and PostgreSQL’s INHERITS propagates 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.

  • nativeSequence emits a real CREATE SEQUENCE <name> START WITH 1 INCREMENT BY 1 object (MariaDB 10.3+) for the sequence named in the table’s <id-method-parameter>, and a matching DROP 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’s nextval('my_seq') or MSSQL’s NEXT VALUE FOR my_seq. Such a table has no reason to also declare an autoIncrement column.

    <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 nativeUuid is: MysqlPlatform serves 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 reject CREATE SEQUENCE outright. Setting this only when the target really is MariaDB 10.3+ is the schema author’s responsibility.

  • primaryKeyClustered (default true) — set to false to emit an explicit NONCLUSTERED primary key, so clustering can be moved to a different index or unique constraint via clustered="true". SQL Server allows only one clustered object per table; declaring more than one is rejected at DDL-execution time, not validated here.
  • temporal turns the table into a system-versioned temporal table (SQL Server 2016+ / Azure SQL): a PERIOD FOR SYSTEM_TIME (start, end) clause plus WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = ...)). The table must declare exactly one periodRowStart and one periodRowEnd column, or the build throws an EngineException.
  • historyTable names the history table for a temporal table. Defaults to <table>_History. Either way the name is auto-qualified with a schema (dbo when 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
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>
  • type the 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’s UUID_BINARY type is not present; see UUID and binary columns.
  • sqlType the SQL type to be used in CREATE and ALTER statements (overriding the mapping between Propulsion types and RDBMS types).
  • defaultValue the default value that the object will have for this column in the PHP instance after creating a new Object(). This value is always interpreted as a string.
  • defaultExpr the 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.
  • valueSet the list of enumerated values accepted on an ENUM or SET column. The list contains 255 values at most, separated by commas. Derived automatically from the enum’s own case values when enumClass is set, so the two can’t drift.
  • enumClass names a backed PHP enum for an ENUM column. The generated property, getter, setter, hydration, and buildCriteria() all work with the enum instance directly rather than the raw label string. Storage is unchanged (still the emulated integer index unless nativeEnum is also set). Independent of nativeEnum — either, neither, or both.
  • nativeEnum opts an ENUM column into the platform’s real enum mechanism instead of the emulated integer index. See Enum columns.
  • identity opts an auto-increment primary key into the standard GENERATED BY DEFAULT AS IDENTITY syntax. Honored on PostgreSQL (PG10+, in place of the serial/bigserial pseudo-types) and Oracle (12c+). Requires autoIncrement="true" and no explicit <id-method-parameter> named sequence. Opt-in rather than the default because countless existing schemas assert the exact serial/bigserial DDL; the implicit sequence naming is identical either way, so nothing downstream changes. Note that converting an existing column’s identity-ness via ALTER is not attempted — that’s a structurally different statement, and re-declaring it is an error on Oracle.
  • generatedAs makes 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 a DEFAULT.
  • generatedTypeVIRTUAL (the default, computed on read) or STORED (computed on write and stored). On MSSQL, STORED emits PERSISTED; T-SQL only allows NOT NULL alongside PERSISTED and never allows a bare NULL keyword on a computed column, which is stricter than SQLite’s grammar.
  • lazyLoad a 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).
  • primaryString a column defined as primary string serves as the default value for a __toString() method in the generated Propulsion object.
  • nativeArray stores an ARRAY column as a real PostgreSQL TEXT[] 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.
  • tsvectorFrom auto-populates a TSVECTOR column from a comma-separated list of source columns, via a real GENERATED ALWAYS AS (to_tsvector(...)) STORED column (PG12+) rather than a trigger. Mutually exclusive with the column’s own DEFAULT.
  • tsvectorConfig the text-search configuration used by tsvectorFrom. Defaults to english.
  • unsigned appends UNSIGNED after a numeric column’s type. Silently ignored on a non-numeric column.
  • zerofill appends UNSIGNED ZEROFILL. ZEROFILL implies UNSIGNED in MySQL even without unsigned also set, and Propulsion matches that.
  • nativeUuid emits MariaDB 10.7+‘s real native UUID column type for a UUID column, instead of the CHAR(36) emulation every platform otherwise uses. Ignored on a column of any other type.

    Opt-in rather than the default because MysqlPlatform serves 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 runtime DBMySQL::isMariaDb() probe that gates RETURNING support 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.

  • rowVersion emits the real ROWVERSION type 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 the optimistic_lock behavior instead.
  • periodRowStart / periodRowEnd mark the two system-time boundary columns of a temporal table, emitting col DATETIME2 GENERATED ALWAYS AS ROW START|END NOT NULL. You declare them explicitly like any other column rather than having Propulsion synthesize hidden ones.

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>
  • skipSql instructs 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.
  • defaultJoin affects the default join type used in the generated joinXXX() 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. Criteria here is Propulsion\Query\Criteria.

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>
  • size only for MySQL databases, where your RDBMS may require an index prefix length.
  • expression on <index-column> makes that entry a raw SQL expression instead of a column name, e.g. expression="lower(title)". Give either name or expression, not both. Honored on PostgreSQL and SQLite; emitted verbatim, wrapped in parentheses.
  • opclass on <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’s hnsw and ivfflat access methods refuse to build without one, and expression is 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.
  • indexType names the index access method or kind. On PostgreSQL it becomes USING <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. A FULLTEXT/SPATIAL index can’t also be UNIQUE, so indexType takes priority over uniqueness when both are set.
  • where adds a trailing WHERE predicate, 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 trailing WITH (...), e.g. storageParameters="fillfactor=70". Deliberately not parsed into key/value pairs, since PostgreSQL has dozens of access-method-specific parameters.
  • concurrently (PostgreSQL) emits CREATE INDEX CONCURRENTLY.
  • clustered (MSSQL) splices CLUSTERED or NONCLUSTERED into the statement. Unset (the default) reproduces the prior DDL unchanged. See primaryKeyClustered for moving clustering off the primary key.

All of USING, WHERE, INCLUDE, WITH, and CONCURRENTLY compose freely on PostgreSQL.

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.

  • size only for MySQL databases.
  • clustered (MSSQL) — as on <index>.

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="&amp;&amp;" />
</exclusion>
</table>
CONSTRAINT reservation_no_overlap EXCLUDE USING gist ("room_id" WITH =, "during" WITH &&)
  • name the constraint name.
  • indexType the access method. Defaults to gist, which has the widest operator-class support and is what PostgreSQL’s own documentation uses for every exclusion-constraint example.
  • where a predicate, making it a partial exclusion constraint.
  • operator on <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.

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>

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.

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.

Propulsion typeDescExample Default DB Type (PostgreSQL)Default PHP Native Type
CHARFixed-length character dataCHARstring
VARCHARVariable-length character dataVARCHARstring
LONGVARCHARLong variable-length character dataTEXTstring
CLOBCharacter LOB (locator object)TEXTstring

LONGVARCHAR and CLOB need no declared size, and allow for very large strings.

Propulsion typeDescExample Default DB Type (PostgreSQL)Default PHP Native Type
NUMERICNumeric dataNUMERICstring (PHP int is limited)
DECIMALDecimal dataDECIMALstring (PHP int is limited)
TINYINTTiny integerINT2int
SMALLINTSmall integerINT2int
INTEGERIntegerINTEGERint
BIGINTLarge integerINT8int
REALReal numberFLOATdouble
FLOATFloating point numberDOUBLE PRECISIONdouble
DOUBLEFloating point numberDOUBLE PRECISIONdouble
Propulsion typeDescExample Default DB Type (PostgreSQL)Default PHP Native Type
BINARYFixed-length binary dataBYTEAstring
VARBINARYVariable-length binary dataBYTEAstream or string
LONGVARBINARYLong variable-length binary dataBYTEAstream or string
BLOBBinary LOB (locator object)BYTEAstream or string
Propulsion typeDescExample Default DB Type (PostgreSQL)Default PHP Native Type
DATEDate (e.g. YYYY-MM-DD)DATEDateTime object
TIMETime (e.g. HH:MM:SS)TIMEDateTime object
TIMESTAMPDate + time (e.g. YYYY-MM-DD HH:MM:SS)TIMESTAMPDateTime object

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 arithmetic

The 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.

Propulsion typeDescExample Default DB Type (PostgreSQL)Default PHP Native Type
INTERVALA durationINTERVALDateInterval 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.

  • BOOLEAN columns map to a boolean in PHP. Depending on the native support for this type, they are stored in SQL as BOOLEAN or an emulated integer type.
  • OBJECT columns map to PHP objects and are stored as serialize()d text.

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 string

valueSet 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:

PlatformnativeEnum="true" emits
PostgreSQLA real CREATE TYPE <table>_<column>_enum AS ENUM (...) before the table, dropped with it
MySQLInline ENUM('a', 'b', ...)
SQLite, OracleThe emulated text/int domain plus a CHECK (col IN (...)) constraint
MSSQLNothing — 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.

Propulsion typeDescExample Default DB Type (MySQL)Default PHP Native Type
SETAny subset of a fixed label vocabularySET(‘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 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.

Propulsion typeDescExample Default DB Type (PostgreSQL)Default PHP Native Type
JSONJSON data, stored/queried as exact input textJSONmixed
JSONBJSON data, stored as a decomposed binary formJSONBmixed

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.

Propulsion typeDescExample Default DB Type (PostgreSQL)Default PHP Native Type
UUIDA UUID, stored in canonical hyphenated formUUIDstring

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.

Propulsion typeDescExample Default DB Type (PostgreSQL)Default PHP Native Type
INETAn IPv4/IPv6 host addressINETstring
CIDRAn IPv4/IPv6 network specificationCIDRstring
MACADDRA MAC addressMACADDRstring
CITEXTCase-insensitive textCITEXTstring

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.

Propulsion typeDescExample Default DB Type (PostgreSQL)Default PHP Native Type
INT4RANGEA range of 4-byte integersINT4RANGERange object
INT8RANGEA range of 8-byte integersINT8RANGERange object
NUMRANGEA range of numericsNUMRANGERange object
DATERANGEA range of datesDATERANGERange object
TSRANGEA range of timestampsTSRANGERange object
TSTZRANGEA range of timestamps with time zoneTSTZRANGERange 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.

Propulsion typeDescExample Default DB Type (PostgreSQL)Default PHP Native Type
VECTORA fixed-dimension embedding vectorvector(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>

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.

Propulsion typeDescExample Default DB Type (PostgreSQL)Default PHP Native Type
TSVECTORA parsed full-text search documentTSVECTORstring

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.

Propulsion typeDescExample Default DB Type (PostgreSQL)Default PHP Native Type
GEOMETRYA geometry, stored as WKTTEXTstring

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.

The following types are still supported for compatibility, but aren’t needed for new schemas:

Propulsion typeDescExample Default DB Type (PostgreSQL)Default PHP Native Type
BU_DATEPre-/post-epoch date (e.g. 1201-03-02)DATEstring
BU_TIMESTAMPPre-/post-epoch Date + time (e.g. 1201-03-02 12:33:00)TIMESTAMPstring

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"/>

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;

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>

Propulsion supports the following vendor parameters for MySQL:

Name | Example values
-----------------|---------------
// in <table> element
Engine | InnoDB (default), MyISAM, MEMORY, etc.
AutoIncrement | 1234, N, etc
AvgRowLength |
Charset | utf8, latin1, etc.
Checksum | 0, 1
Collate | 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, 1
IndexDirectory | /var/db/foo (for MyISAM storage engine)
InsertMethod | FIRST, LAST (for MERGE storage engine)
KeyBlockSize | 0 (default), 1024, etc
MaxRows | 1000, 4294967295, etc
MinRows | 1000 (for MEMORY storage engine)
PackKeys | 0, 1, DEFAULT
RowFormat | FIXED, DYNAMIC, COMPRESSED, COMPACT, REDUNDANT
Union | (t1,t2) (for MERGE storage engine)
// in <column> element
Charset | utf8, latin1, etc.
Collate | utf8_unicode_ci, latin1_german1_ci, etc.
// in <index> element
Index_type | FULLTEXT

Propulsion supports the following vendor parameters for Oracle:

Name | Example values
-----------------|---------------
// in <table> element
PCTFree | 20
InitTrans | 4
MinExtents | 1
MaxExtents | 99
PCTIncrease | 0
Tablespace | L_128K
PKPCTFree | 20
PKInitTrans | 4
PKMinExtents | 1
PKMaxExtents | 99
PKPCTIncrease | 0
PKTablespace | IL_128K
// in <index> element
PCTFree | 20
InitTrans | 4
MinExtents | 1
MaxExtents | 99
PCTIncrease | 0
Tablespace | L_128K

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_schema

Setting 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.

For overriding the mapping between Propulsion types and native SQL types, you can create your own Platform class and override the mapping.

For example:

<?php
use 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):

build.php
return [
'propulsion.platform.class' => 'CustomPgsqlPlatform',
];