Skip to content

T14719 db enhancements#17029

Merged
niden merged 27 commits into
5.0.xfrom
T14719-db-enhancements
May 17, 2026
Merged

T14719 db enhancements#17029
niden merged 27 commits into
5.0.xfrom
T14719-db-enhancements

Conversation

@niden
Copy link
Copy Markdown
Member

@niden niden commented May 16, 2026

Hello!

In raising this pull request, I confirm the following:

  • I have read and understood the Contributing Guidelines
  • I have checked that another pull request for this purpose does not exist
  • I wrote some tests for this PR
  • I have updated the relevant CHANGELOG
  • I have created a PR for the documentation about this change

Adding new functionality to the Phalcon\Db for new features in MySQL 8+, Postgresql and Sqlite

  • Added CHECK-constraint support via new Phalcon\Db\Check value object and Phalcon\Db\CheckInterface. Check takes a constraint name (string; empty string means an unnamed constraint, in which case the dialect omits the CONSTRAINT <name> prefix) and a definition array containing the required expression key (the boolean SQL predicate). Phalcon\Db\Dialect\Mysql, Phalcon\Db\Dialect\Postgresql, and Phalcon\Db\Dialect\Sqlite all recognize definition["checks"] (array of CheckInterface) inside createTable() and emit an inline [CONSTRAINT "<name>"] CHECK (<expr>) line alongside the column/index/reference lines. New dialect methods addCheck() and dropCheck() emit the equivalent ALTER TABLE ... ADD CONSTRAINT ... CHECK (...) and ALTER TABLE ... DROP CHECK|CONSTRAINT ... SQL for MySQL 8.0.16+ and PostgreSQL; SQLite throws (CHECK constraints can only be declared at CREATE TABLE time in SQLite, the same limitation that already applies to FK constraints). New adapter methods Phalcon\Db\Adapter\AbstractAdapter::addCheck() and Phalcon\Db\Adapter\AbstractAdapter::dropCheck() provide the symmetric one-call ergonomics already available for addForeignKey() / dropForeignKey().
  • Added MySQL 8.0+ INVISIBLE index support to Phalcon\Db\Index. The constructor's second parameter is now backward-compatibly overloaded: passing a plain list of column names continues to work (legacy positional form), while passing an associative array containing a columns key activates the new definition-array form (["columns" => [...], "type" => "...", "invisible" => true]). The third positional type argument is honored only when the second argument is the legacy list form; in definition-array mode type comes from the array. Index gains a matching isInvisible(): bool accessor and throws a Phalcon\Db\Exception if the definition-array path is taken but columns is not itself an array. Phalcon\Db\Dialect\Mysql::addIndex and Phalcon\Db\Dialect\Mysql::createTable emit a trailing INVISIBLE keyword for invisible indexes. Phalcon\Db\Adapter\Pdo\Mysql::describeIndexes reverse-engineers the flag from the MySQL 8.0+ Visible column of SHOW INDEXES (absent on 5.7, which defaults to visible). PostgreSQL and SQLite have no equivalent and their dialects ignore the flag. The new method is declared as a commented @todo v7 stub on Phalcon\Contracts\Db\Index to avoid breaking third-party implementors during the v5 line. This definition-array hook is the path that will be reused by upcoming items Phalcon_Model_Query constructor causes a crash. #8 (descending indexes), --enable-webtools bug on windows #9 (partial indexes), and How to compile under Windows? #10 (functional indexes) so the constructor stays tidy
  • Added MySQL 8.0.23+ INVISIBLE column support to Phalcon\Db\Column. A new boolean definition-array key invisible (default false) is parsed by the constructor; a matching isInvisible(): bool accessor reports the state at runtime. Phalcon\Db\Dialect\Mysql::addColumn, Phalcon\Db\Dialect\Mysql::createTable, and Phalcon\Db\Dialect\Mysql::modifyColumn emit INVISIBLE after the NOT NULL/NULL clause when the flag is set. PostgreSQL and SQLite have no equivalent and their dialects ignore the flag. Phalcon\Db\Adapter\Pdo\Mysql::describeColumns reverse-engineers the flag from the EXTRA column of information_schema.COLUMNS (already in the result set since item view->setParamToView crash the server with complex arrays #1's switch from SHOW FULL COLUMNS) - substring-matched so the flag is still detected when MySQL concatenates it with other extras like INVISIBLE STORED GENERATED. The new method is declared as a commented @todo v7 stub on Phalcon\Contracts\Db\Column to avoid breaking third-party implementors during the v5 line
  • Added PostgreSQL CREATE INDEX CONCURRENTLY support via a new concurrently definition-array key on Phalcon\Db\Index (default false). Phalcon\Db\Index::isConcurrent(): bool exposes the flag at runtime. Phalcon\Db\Dialect\Postgresql::addIndex now emits CONCURRENTLY between the INDEX keyword and the index name when the flag is set (CREATE INDEX CONCURRENTLY "idx_email" ON "schema"."table" (...)), so the index can be built without taking the strong lock that normally blocks writers. MySQL and SQLite have no equivalent concept and their dialects ignore the flag. No reverse engineering is meaningful (the option is creation-time only - once built, the index is indistinguishable from one built non-concurrently). The new method is declared as a commented @todo v7 stub on Phalcon\Contracts\Db\Index to avoid breaking third-party implementors during the v5 line
  • Added PostgreSQL materialized-view support to the Db dialect and adapter layers. Three new methods land on Phalcon\Db\Dialect: createMaterializedView(string $viewName, array $definition, string $schemaName = null): string (definition takes a required sql key, same shape as createView), dropMaterializedView(string $viewName, string $schemaName = null, bool $ifExists = true): string, and refreshMaterializedView(string $viewName, string $schemaName = null, bool $concurrent = false): string (passing $concurrent = true emits REFRESH MATERIALIZED VIEW CONCURRENTLY ... for non-blocking refresh - requires a unique index on the view). Phalcon\Db\Dialect\Postgresql overrides all three to emit the correct SQL; the base implementations throw Phalcon\Db\Exception, which is inherited unchanged by Phalcon\Db\Dialect\Mysql and Phalcon\Db\Dialect\Sqlite (neither engine has a materialized-view concept). Phalcon\Db\Adapter\AbstractAdapter gains three matching createMaterializedView() / dropMaterializedView() / refreshMaterializedView() wrappers that execute the dialect-built SQL and return bool. The new methods are declared as commented @todo v7 stubs on Phalcon\Contracts\Db\Dialect and Phalcon\Contracts\Db\Adapter\Adapter to avoid breaking third-party implementors during the v5 line
  • Added PostgreSQL-specific column types and array-column support to Phalcon\Db\Column and Phalcon\Db\Dialect\Postgresql. Ten new Column::TYPE_* constants are introduced: TYPE_BYTEA (30), TYPE_INET (31), TYPE_CIDR (32), TYPE_MACADDR (33), TYPE_INT4RANGE (34), TYPE_INT8RANGE (35), TYPE_NUMRANGE (36), TYPE_TSRANGE (37), TYPE_TSTZRANGE (38), TYPE_DATERANGE (39). Phalcon\Db\Dialect\Postgresql::getColumnDefinition recognizes the new types and emits the matching keywords (BYTEA, INET, CIDR, MACADDR, INT4RANGE, INT8RANGE, NUMRANGE, TSRANGE, TSTZRANGE, DATERANGE). MySQL and SQLite dialects fall through their existing default branches for these constants - users targeting those engines should pick a portable base type instead. Additionally, a new boolean definition-array key array and a Phalcon\Db\Column::isArray(): bool accessor expose array-column intent; when isArray() is true, the PostgreSQL dialect appends [] to the type (INTEGER[], TEXT[], INET[], etc.). MySQL and SQLite ignore the flag. Phalcon\Db\Adapter\Pdo\Postgresql::describeColumns reverse-engineers the new types by matching the data_type column from information_schema.columns and sets array when data_type reports ARRAY or contains []. The new method is declared as a commented @todo v7 stub on Phalcon\Contracts\Db\Column to avoid breaking third-party implementors during the v5 line
  • Added FOR SHARE shared-lock emission to Phalcon\Db\Dialect\Postgresql::sharedLock(); it previously returned the original query unchanged (silent no-op), so callers had no way to express PostgreSQL's row-level shared lock through the cphalcon dialect API. The method now appends " FOR SHARE" and also accepts the optional string $modifier = "" second argument introduced by item Documentation typos #3, so callers can request FOR SHARE NOWAIT / FOR SHARE SKIP LOCKED via the Phalcon\Contracts\Db\Dialect::LOCK_NOWAIT / LOCK_SKIP_LOCKED constants. The signature change is propagated to Phalcon\Contracts\Db\Dialect::sharedLock, Phalcon\Contracts\Db\Adapter\Adapter::sharedLock, and the SQLite and MySQL impls - SQLite remains a no-op regardless of the modifier (no row-level locking), and MySQL still emits its legacy LOCK IN SHARE MODE and silently ignores any modifier (the legacy syntax does not support NOWAIT / SKIP LOCKED; users on MySQL 8.0+ who need those modifiers can use forUpdate() instead). Phalcon\Db\Adapter\AbstractAdapter::sharedLock passes the modifier through to the dialect
  • Added NOWAIT / SKIP LOCKED row-lock modifiers to forUpdate(). The dialect and adapter forUpdate() methods now accept an optional second string $modifier = "" argument; pass one of the new contract constants Phalcon\Contracts\Db\Dialect::LOCK_NONE (default), Phalcon\Contracts\Db\Dialect::LOCK_NOWAIT, or Phalcon\Contracts\Db\Dialect::LOCK_SKIP_LOCKED to emit SELECT … FOR UPDATE, SELECT … FOR UPDATE NOWAIT, or SELECT … FOR UPDATE SKIP LOCKED respectively. Recognized by MySQL 8.0+ and PostgreSQL 9.5+; SQLite has no row-level locking and silently ignores the modifier. Signature change is propagated to Phalcon\Contracts\Db\Dialect::forUpdate, Phalcon\Contracts\Db\Adapter\Adapter::forUpdate, Phalcon\Db\Dialect::forUpdate (base), Phalcon\Db\Dialect\Sqlite::forUpdate (override remains a no-op), and Phalcon\Db\Adapter\AbstractAdapter::forUpdate (pass-through). Existing single-argument call sites are unaffected - the second parameter defaults to ""
  • Added ON CONFLICT (...) DO UPDATE SET ... upsert support to the Db dialect and adapter layers. New SQL-transformer method Phalcon\Db\Dialect::onConflictUpdate(string $sqlQuery, array $conflictColumns, array $updateColumns): string appends an upsert clause to the supplied INSERT statement using the SQL-standard form recognized by PostgreSQL 9.5+ and SQLite 3.24+: INSERT INTO ... ON CONFLICT ("col") DO UPDATE SET "other" = excluded."other". The base implementation in Phalcon\Db\Dialect::onConflictUpdate provides the standard emission (inherited by Phalcon\Db\Dialect\Postgresql and Phalcon\Db\Dialect\Sqlite); Phalcon\Db\Dialect\Mysql::onConflictUpdate overrides to throw because MySQL's INSERT ... ON DUPLICATE KEY UPDATE shape is incompatible (deferred to parser item Fix uninitialized variable #23). Throws Phalcon\Db\Exception when either the conflictColumns or updateColumns array is empty. Phalcon\Db\Adapter\AbstractAdapter::onConflictUpdate provides the symmetric one-call passthrough. The new method is declared as a commented @todo v7 stub on Phalcon\Contracts\Db\Dialect and Phalcon\Contracts\Db\Adapter\Adapter to avoid breaking third-party implementors during the v5 line
  • Added Phalcon\Contracts\Db namespace housing the canonical contracts for the Db layer: Phalcon\Contracts\Db\Check, Phalcon\Contracts\Db\Column, Phalcon\Contracts\Db\Dialect, Phalcon\Contracts\Db\Index, Phalcon\Contracts\Db\Reference, Phalcon\Contracts\Db\Result, and Phalcon\Contracts\Db\Adapter\Adapter. The legacy interfaces (Phalcon\Db\CheckInterface, Phalcon\Db\ColumnInterface, Phalcon\Db\DialectInterface, Phalcon\Db\IndexInterface, Phalcon\Db\ReferenceInterface, Phalcon\Db\ResultInterface, Phalcon\Db\Adapter\AdapterInterface) are kept as thin extensions of their contract counterparts, marked @deprecated (matching the Phalcon\Support\Collection\CollectionInterface migration pattern), so existing implementors and typehints continue to work in the v5 line.
  • Added RETURNING clause support to the Db dialect and adapter layers. New SQL-transformer method Phalcon\Db\Dialect::returning(string $sqlQuery, array $columns): string appends a RETURNING clause to an INSERT / UPDATE / DELETE statement; pass ["*"] for RETURNING * or a list of column identifiers for RETURNING "col1", "col2". Phalcon\Db\Dialect\Postgresql::returning and Phalcon\Db\Dialect\Sqlite::returning provide the emission (SQLite requires 3.35+). The base implementation in Phalcon\Db\Dialect::returning throws Phalcon\Db\Exception, which is inherited unchanged by Phalcon\Db\Dialect\Mysql since MySQL has no RETURNING construct. An empty columns array throws on PgSQL and SQLite. Phalcon\Db\Adapter\AbstractAdapter::returning provides the symmetric one-call passthrough so users can do $connection->query($connection->returning($sql, ["id"])). The new method is declared as a commented @todo v7 stub on Phalcon\Contracts\Db\Dialect and Phalcon\Contracts\Db\Adapter\Adapter to avoid breaking third-party implementors during the v5 line
  • Added functional/expression index support to Phalcon\Db\Index. Entries inside the columns array can now be Phalcon\Db\RawValue instances; string entries continue to be treated as column identifiers and escaped. The base Phalcon\Db\Dialect::getIndexColumnList() helper detects RawValue entries and per-dialect renders them - MySQL and PostgreSQL wrap each expression in its own parentheses (KEY idx ((LOWER(col))) and CREATE INDEX ON t ((lower(col))) respectively), while SQLite emits the expression verbatim (its grammar accepts expr directly as an indexed-column). The helper gains an optional bool $wrapExpressions = true flag - defaults are tuned per dialect at the call site so users do not need to think about it. Expressions compose with the descending direction (Phalcon_Model_Query constructor causes a crash. #8) and partial-index predicate (--enable-webtools bug on windows #9) without any additional API surface. Reverse-engineering of expressions is deferred (PostgreSQL via pg_get_expr(pg_index.indexprs, ...), SQLite via sqlite_master.sql parsing - same conservative cutoff used in item view->setParamToView crash the server with complex arrays #1). No new accessor method is needed - Index::getColumns() continues to return the entries (now of mixed string / RawValue type)
  • Added generated/computed column support to Phalcon\Db\Column via two new definition-array keys: generated (the SQL expression as a string; null keeps the column non-generated) and generationStored (bool, falseVIRTUAL, trueSTORED; PostgreSQL ignores the flag and always emits STORED). Three new public methods report the state at runtime - getGenerationExpression(): string | null, isGenerated(): bool, isGenerationStored(): bool. The class enforces that a generated column cannot also declare default or autoIncrement. All three dialects (Mysql, Postgresql, Sqlite) emit GENERATED ALWAYS AS (<expr>) VIRTUAL\|STORED from addColumn(), createTable(), and (where supported) modifyColumn(), and skip the DEFAULT / AUTO_INCREMENT / AUTOINCREMENT clauses for generated columns. Reverse-engineering through describeColumns() is also wired up: MySQL switches from SHOW FULL COLUMNS to an equivalent information_schema.COLUMNS query that additionally returns GENERATION_EXPRESSION; PostgreSQL extends its information_schema.columns query with is_generated and generation_expression; SQLite switches from PRAGMA table_info to PRAGMA table_xinfo so the hidden flag (2 → VIRTUAL, 3 → STORED) can populate isGenerated() / isGenerationStored(). SQLite cannot expose the generation expression through any pragma, so getGenerationExpression() round-trips as an empty string for SQLite-introspected generated columns (documented limitation). The new methods are declared as commented @todo v7 stubs on Phalcon\Db\ColumnInterface to avoid breaking third-party implementors during the v5 line
  • Added partial-index support on Phalcon\Db\Index via a new where definition-array key (string). Phalcon\Db\Index::getWhere(): string exposes the configured predicate (empty string when none). Phalcon\Db\Dialect\Postgresql::addIndex and Phalcon\Db\Dialect\Sqlite::addIndex append WHERE <expr> to the emitted CREATE INDEX statement. MySQL has no partial-index feature and its dialect ignores the value. Reverse-engineering of the predicate is deferred for both PostgreSQL (requires pg_get_expr(pg_index.indpred, pg_index.indrelid)) and SQLite (requires sqlite_master.sql parsing) - same conservative cutoff used for SQLite generation expressions in item view->setParamToView crash the server with complex arrays #1. Throws Phalcon\Db\Exception if the definition-array where key is supplied with a non-string value. The new method is declared as a commented @todo v7 stub on Phalcon\Contracts\Db\Index
  • Added per-column sort direction (ASC / DESC) support on Phalcon\Db\Index via a new directions definition-array key. The array is parallel to columns; trailing positions absent from directions default to ASC at emission time. Phalcon\Db\Index::getDirections(): array exposes the configured list (empty array means "no per-column direction declared" - preserves the legacy plain (col1, col2) rendering exactly). A new protected Phalcon\Db\Dialect::getIndexColumnList(IndexInterface) helper centralizes the direction-aware emission and is now used by Phalcon\Db\Dialect\Mysql::addIndex / createTable, Phalcon\Db\Dialect\Postgresql::addIndex / createTable, and Phalcon\Db\Dialect\Sqlite::addIndex / createTable. Phalcon\Db\Adapter\Pdo\Mysql::describeIndexes reverse-engineers directions from the Collation column of SHOW INDEXES (A = ASC, D = DESC, NULL = ASC); the resulting Index only carries a non-empty directions array when at least one column is DESC, so existing introspection workflows that don't expect direction metadata see no diff. PostgreSQL and SQLite reverse-engineering of directions is deferred (pg_index.indoption and sqlite_master.sql parsing respectively - same conservative cutoff used for SQLite generation expressions in item view->setParamToView crash the server with complex arrays #1). The new method is declared as a commented @todo v7 stub on Phalcon\Contracts\Db\Index to avoid breaking third-party implementors during the v5 line
  • Added spatial / geometry column-type support to Phalcon\Db\Column and the MySQL and PostgreSQL dialects. Eight new Column::TYPE_* constants land: TYPE_GEOMETRY (40), TYPE_POINT (41), TYPE_LINESTRING (42), TYPE_POLYGON (43), TYPE_MULTIPOINT (44), TYPE_MULTILINESTRING (45), TYPE_MULTIPOLYGON (46), TYPE_GEOMETRYCOLLECTION (47). Phalcon\Db\Dialect\Mysql::getColumnDefinition and Phalcon\Db\Dialect\Postgresql::getColumnDefinition emit the matching keywords (MySQL recognizes them natively from 5.7; PostgreSQL needs PostGIS installed, which interprets the keywords). SQLite has no native spatial type and its dialect leaves these constants in the default branch. Phalcon\Db\Adapter\Pdo\Mysql::describeColumns reverse-engineers the new types by starts_with-matching the column type - order in the switch was chosen so the longer multi-* / geometrycollection variants are matched before their shorter prefixes (linestring before polygon, etc.). PostgreSQL reverse-engineering for spatial types is deferred because information_schema.data_type does not consistently expose PostGIS type names without joining pg_type - users introspecting PostGIS schemas should query metadata directly until then. Caveat - read-side WKB hydration is not part of this change. A POINT selected directly with SELECT location FROM items still returns raw WKB bytes (cphalcon issues #14769 and #13670); the workaround is to project ST_AsText(location) / ST_AsBinary(location) / ST_AsGeoJSON(location) server-side. The DDL/describe support shipped here is the schema-level prerequisite for any future result-set hydration helper
  • Added support for arbitrary SQL expressions in DDL DEFAULT clauses by recognizing Phalcon\Db\RawValue instances passed as definition["default"]. Previously each dialect quoted any non-numeric, non-CURRENT_TIMESTAMP, non-NULL default as a string literal - preventing legitimate expression defaults like MySQL 8.0.13+ DEFAULT (UUID()), PostgreSQL DEFAULT gen_random_uuid() / DEFAULT nextval('seq'), or SQLite 3.31+ DEFAULT strftime('%s','now'). Phalcon\Db\Dialect\Mysql::addColumn / createTable / modifyColumn, Phalcon\Db\Dialect\Postgresql::castDefault (used by all three of its DDL paths), and Phalcon\Db\Dialect\Sqlite::addColumn / createTable now detect RawValue defaults and emit DEFAULT <raw> verbatim. Plain-scalar and CURRENT_TIMESTAMP / NULL keyword defaults continue to take the existing whitelist path unchanged. Column::hasDefault() already treats a RawValue as a non-null default, so isAutoIncrement() semantics and the generated-column "no default allowed" guard from item view->setParamToView crash the server with complex arrays #1 remain correct
  • Enabled dropColumn() on the SQLite dialect to emit ALTER TABLE ... DROP COLUMN ... instead of throwing unconditionally - SQLite 3.35+ natively supports ALTER TABLE DROP COLUMN, and pre-empting it at the cphalcon dialect level prevented modern users from using the feature. On engines older than 3.35 the server itself rejects the statement at execution time, with a clearer "near 'DROP': syntax error" message than the previous cphalcon-side throw. Phalcon\Db\Adapter\Pdo\Sqlite::dropColumn already passes through the dialect output via AbstractAdapter, so users now get a one-call $connection->dropColumn(...) on SQLite

niden added 24 commits May 15, 2026 14:51
…ce, dialect and adapter add/drop)

Assisted-by: Claude Code
…iles deprecated and extend their canonical contract

Assisted-by: Claude Code
…ate via LOCK_* contract constants

Assisted-by: Claude Code
…EFAULT clauses across all 3 dialects

Assisted-by: Claude Code
…er param propagated to all dialects

Assisted-by: Claude Code
… and Mysql dialect/adapter

Assisted-by: Claude Code
…or now accepts a backward-compatible definition array

Assisted-by: Claude Code
… and dialect index emission

Assisted-by: Claude Code
…SQL and SQLite via Index where key

Assisted-by: Claude Code
…accepts RawValue entries with per-dialect wrapping

Assisted-by: Claude Code
…pter for PostgreSQL and SQLite

Assisted-by: Claude Code
…stgreSQL and SQLite; MySQL throws

Assisted-by: Claude Code
…efresh) to dialect and adapter

Assisted-by: Claude Code
… types) and array-column support to Db Column and PgSQL dialect/adapter

Assisted-by: Claude Code
… (3.35+ supports natively)

Assisted-by: Claude Code
…OLYGON, multi-variants) to Db Column and MySQL/PgSQL dialects

Assisted-by: Claude Code
…ser rejects orphan docblocks between interface methods); class-level @todo retained

Assisted-by: Claude Code
Assisted-by: Claude Code
@niden niden requested a review from Jeckerson May 16, 2026 22:58
@niden niden self-assigned this May 16, 2026
@niden niden added new feature request Planned Feature or New Feature Request 5.0 The issues we want to solve in the 5.0 release labels May 16, 2026
@niden niden merged commit ae83e53 into 5.0.x May 17, 2026
96 checks passed
@niden niden deleted the T14719-db-enhancements branch May 17, 2026 00:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

5.0 The issues we want to solve in the 5.0 release new feature request Planned Feature or New Feature Request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants