T14719 db enhancements#17029
Merged
Merged
Conversation
…dialects Assisted-by: Claude Code
…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
…urrently flag 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
Jeckerson
approved these changes
May 16, 2026
This was referenced May 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Hello!
In raising this pull request, I confirm the following:
Adding new functionality to the
Phalcon\Dbfor new features in MySQL 8+, Postgresql and SqlitePhalcon\Db\Checkvalue object andPhalcon\Db\CheckInterface.Checktakes a constraint name (string; empty string means an unnamed constraint, in which case the dialect omits theCONSTRAINT <name>prefix) and a definition array containing the requiredexpressionkey (the boolean SQL predicate).Phalcon\Db\Dialect\Mysql,Phalcon\Db\Dialect\Postgresql, andPhalcon\Db\Dialect\Sqliteall recognizedefinition["checks"](array ofCheckInterface) insidecreateTable()and emit an inline[CONSTRAINT "<name>"] CHECK (<expr>)line alongside the column/index/reference lines. New dialect methodsaddCheck()anddropCheck()emit the equivalentALTER TABLE ... ADD CONSTRAINT ... CHECK (...)andALTER 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 methodsPhalcon\Db\Adapter\AbstractAdapter::addCheck()andPhalcon\Db\Adapter\AbstractAdapter::dropCheck()provide the symmetric one-call ergonomics already available foraddForeignKey()/dropForeignKey().INVISIBLEindex support toPhalcon\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 acolumnskey activates the new definition-array form (["columns" => [...], "type" => "...", "invisible" => true]). The third positionaltypeargument is honored only when the second argument is the legacy list form; in definition-array modetypecomes from the array.Indexgains a matchingisInvisible(): boolaccessor and throws aPhalcon\Db\Exceptionif the definition-array path is taken butcolumnsis not itself an array.Phalcon\Db\Dialect\Mysql::addIndexandPhalcon\Db\Dialect\Mysql::createTableemit a trailingINVISIBLEkeyword for invisible indexes.Phalcon\Db\Adapter\Pdo\Mysql::describeIndexesreverse-engineers the flag from the MySQL 8.0+Visiblecolumn ofSHOW 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 v7stub onPhalcon\Contracts\Db\Indexto 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 tidyINVISIBLEcolumn support toPhalcon\Db\Column. A new boolean definition-array keyinvisible(defaultfalse) is parsed by the constructor; a matchingisInvisible(): boolaccessor reports the state at runtime.Phalcon\Db\Dialect\Mysql::addColumn,Phalcon\Db\Dialect\Mysql::createTable, andPhalcon\Db\Dialect\Mysql::modifyColumnemitINVISIBLEafter 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::describeColumnsreverse-engineers the flag from theEXTRAcolumn ofinformation_schema.COLUMNS(already in the result set since item view->setParamToView crash the server with complex arrays #1's switch fromSHOW FULL COLUMNS) - substring-matched so the flag is still detected when MySQL concatenates it with other extras likeINVISIBLE STORED GENERATED. The new method is declared as a commented@todo v7stub onPhalcon\Contracts\Db\Columnto avoid breaking third-party implementors during the v5 lineCREATE INDEX CONCURRENTLYsupport via a newconcurrentlydefinition-array key onPhalcon\Db\Index(defaultfalse).Phalcon\Db\Index::isConcurrent(): boolexposes the flag at runtime.Phalcon\Db\Dialect\Postgresql::addIndexnow emitsCONCURRENTLYbetween theINDEXkeyword 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 v7stub onPhalcon\Contracts\Db\Indexto avoid breaking third-party implementors during the v5 linePhalcon\Db\Dialect:createMaterializedView(string $viewName, array $definition, string $schemaName = null): string(definition takes a requiredsqlkey, same shape ascreateView),dropMaterializedView(string $viewName, string $schemaName = null, bool $ifExists = true): string, andrefreshMaterializedView(string $viewName, string $schemaName = null, bool $concurrent = false): string(passing$concurrent = trueemitsREFRESH MATERIALIZED VIEW CONCURRENTLY ...for non-blocking refresh - requires a unique index on the view).Phalcon\Db\Dialect\Postgresqloverrides all three to emit the correct SQL; the base implementations throwPhalcon\Db\Exception, which is inherited unchanged byPhalcon\Db\Dialect\MysqlandPhalcon\Db\Dialect\Sqlite(neither engine has a materialized-view concept).Phalcon\Db\Adapter\AbstractAdaptergains three matchingcreateMaterializedView()/dropMaterializedView()/refreshMaterializedView()wrappers that execute the dialect-built SQL and return bool. The new methods are declared as commented@todo v7stubs onPhalcon\Contracts\Db\DialectandPhalcon\Contracts\Db\Adapter\Adapterto avoid breaking third-party implementors during the v5 linePhalcon\Db\ColumnandPhalcon\Db\Dialect\Postgresql. Ten newColumn::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::getColumnDefinitionrecognizes 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 existingdefaultbranches for these constants - users targeting those engines should pick a portable base type instead. Additionally, a new boolean definition-array keyarrayand aPhalcon\Db\Column::isArray(): boolaccessor expose array-column intent; whenisArray()istrue, the PostgreSQL dialect appends[]to the type (INTEGER[],TEXT[],INET[], etc.). MySQL and SQLite ignore the flag.Phalcon\Db\Adapter\Pdo\Postgresql::describeColumnsreverse-engineers the new types by matching thedata_typecolumn frominformation_schema.columnsand setsarraywhendata_typereportsARRAYor contains[]. The new method is declared as a commented@todo v7stub onPhalcon\Contracts\Db\Columnto avoid breaking third-party implementors during the v5 lineFOR SHAREshared-lock emission toPhalcon\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 optionalstring $modifier = ""second argument introduced by item Documentation typos #3, so callers can requestFOR SHARE NOWAIT/FOR SHARE SKIP LOCKEDvia thePhalcon\Contracts\Db\Dialect::LOCK_NOWAIT/LOCK_SKIP_LOCKEDconstants. The signature change is propagated toPhalcon\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 legacyLOCK IN SHARE MODEand silently ignores any modifier (the legacy syntax does not supportNOWAIT/SKIP LOCKED; users on MySQL 8.0+ who need those modifiers can useforUpdate()instead).Phalcon\Db\Adapter\AbstractAdapter::sharedLockpasses the modifier through to the dialectNOWAIT/SKIP LOCKEDrow-lock modifiers toforUpdate(). The dialect and adapterforUpdate()methods now accept an optional secondstring $modifier = ""argument; pass one of the new contract constantsPhalcon\Contracts\Db\Dialect::LOCK_NONE(default),Phalcon\Contracts\Db\Dialect::LOCK_NOWAIT, orPhalcon\Contracts\Db\Dialect::LOCK_SKIP_LOCKEDto emitSELECT … FOR UPDATE,SELECT … FOR UPDATE NOWAIT, orSELECT … FOR UPDATE SKIP LOCKEDrespectively. Recognized by MySQL 8.0+ and PostgreSQL 9.5+; SQLite has no row-level locking and silently ignores the modifier. Signature change is propagated toPhalcon\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), andPhalcon\Db\Adapter\AbstractAdapter::forUpdate(pass-through). Existing single-argument call sites are unaffected - the second parameter defaults to""ON CONFLICT (...) DO UPDATE SET ...upsert support to the Db dialect and adapter layers. New SQL-transformer methodPhalcon\Db\Dialect::onConflictUpdate(string $sqlQuery, array $conflictColumns, array $updateColumns): stringappends 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 inPhalcon\Db\Dialect::onConflictUpdateprovides the standard emission (inherited byPhalcon\Db\Dialect\PostgresqlandPhalcon\Db\Dialect\Sqlite);Phalcon\Db\Dialect\Mysql::onConflictUpdateoverrides to throw because MySQL'sINSERT ... ON DUPLICATE KEY UPDATEshape is incompatible (deferred to parser item Fix uninitialized variable #23). ThrowsPhalcon\Db\Exceptionwhen either theconflictColumnsorupdateColumnsarray is empty.Phalcon\Db\Adapter\AbstractAdapter::onConflictUpdateprovides the symmetric one-call passthrough. The new method is declared as a commented@todo v7stub onPhalcon\Contracts\Db\DialectandPhalcon\Contracts\Db\Adapter\Adapterto avoid breaking third-party implementors during the v5 linePhalcon\Contracts\Dbnamespace 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, andPhalcon\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 thePhalcon\Support\Collection\CollectionInterfacemigration pattern), so existing implementors and typehints continue to work in the v5 line.RETURNINGclause support to the Db dialect and adapter layers. New SQL-transformer methodPhalcon\Db\Dialect::returning(string $sqlQuery, array $columns): stringappends aRETURNINGclause to anINSERT/UPDATE/DELETEstatement; pass["*"]forRETURNING *or a list of column identifiers forRETURNING "col1", "col2".Phalcon\Db\Dialect\Postgresql::returningandPhalcon\Db\Dialect\Sqlite::returningprovide the emission (SQLite requires 3.35+). The base implementation inPhalcon\Db\Dialect::returningthrowsPhalcon\Db\Exception, which is inherited unchanged byPhalcon\Db\Dialect\Mysqlsince MySQL has no RETURNING construct. An emptycolumnsarray throws on PgSQL and SQLite.Phalcon\Db\Adapter\AbstractAdapter::returningprovides the symmetric one-call passthrough so users can do$connection->query($connection->returning($sql, ["id"])). The new method is declared as a commented@todo v7stub onPhalcon\Contracts\Db\DialectandPhalcon\Contracts\Db\Adapter\Adapterto avoid breaking third-party implementors during the v5 linePhalcon\Db\Index. Entries inside thecolumnsarray can now bePhalcon\Db\RawValueinstances; string entries continue to be treated as column identifiers and escaped. The basePhalcon\Db\Dialect::getIndexColumnList()helper detectsRawValueentries and per-dialect renders them - MySQL and PostgreSQL wrap each expression in its own parentheses (KEY idx ((LOWER(col)))andCREATE INDEX ON t ((lower(col)))respectively), while SQLite emits the expression verbatim (its grammar acceptsexprdirectly as anindexed-column). The helper gains an optionalbool $wrapExpressions = trueflag - 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 viapg_get_expr(pg_index.indexprs, ...), SQLite viasqlite_master.sqlparsing - 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 /RawValuetype)Phalcon\Db\Columnvia two new definition-array keys:generated(the SQL expression as a string;nullkeeps the column non-generated) andgenerationStored(bool,false→VIRTUAL,true→STORED; PostgreSQL ignores the flag and always emitsSTORED). 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 declaredefaultorautoIncrement. All three dialects (Mysql,Postgresql,Sqlite) emitGENERATED ALWAYS AS (<expr>) VIRTUAL\|STOREDfromaddColumn(),createTable(), and (where supported)modifyColumn(), and skip theDEFAULT/AUTO_INCREMENT/AUTOINCREMENTclauses for generated columns. Reverse-engineering throughdescribeColumns()is also wired up: MySQL switches fromSHOW FULL COLUMNSto an equivalentinformation_schema.COLUMNSquery that additionally returnsGENERATION_EXPRESSION; PostgreSQL extends itsinformation_schema.columnsquery withis_generatedandgeneration_expression; SQLite switches fromPRAGMA table_infotoPRAGMA table_xinfoso thehiddenflag (2→ VIRTUAL,3→ STORED) can populateisGenerated()/isGenerationStored(). SQLite cannot expose the generation expression through any pragma, sogetGenerationExpression()round-trips as an empty string for SQLite-introspected generated columns (documented limitation). The new methods are declared as commented@todo v7stubs onPhalcon\Db\ColumnInterfaceto avoid breaking third-party implementors during the v5 linePhalcon\Db\Indexvia a newwheredefinition-array key (string).Phalcon\Db\Index::getWhere(): stringexposes the configured predicate (empty string when none).Phalcon\Db\Dialect\Postgresql::addIndexandPhalcon\Db\Dialect\Sqlite::addIndexappendWHERE <expr>to the emittedCREATE INDEXstatement. MySQL has no partial-index feature and its dialect ignores the value. Reverse-engineering of the predicate is deferred for both PostgreSQL (requirespg_get_expr(pg_index.indpred, pg_index.indrelid)) and SQLite (requiressqlite_master.sqlparsing) - same conservative cutoff used for SQLite generation expressions in item view->setParamToView crash the server with complex arrays #1. ThrowsPhalcon\Db\Exceptionif the definition-arraywherekey is supplied with a non-string value. The new method is declared as a commented@todo v7stub onPhalcon\Contracts\Db\IndexASC/DESC) support onPhalcon\Db\Indexvia a newdirectionsdefinition-array key. The array is parallel tocolumns; trailing positions absent fromdirectionsdefault toASCat emission time.Phalcon\Db\Index::getDirections(): arrayexposes the configured list (empty array means "no per-column direction declared" - preserves the legacy plain(col1, col2)rendering exactly). A new protectedPhalcon\Db\Dialect::getIndexColumnList(IndexInterface)helper centralizes the direction-aware emission and is now used byPhalcon\Db\Dialect\Mysql::addIndex/createTable,Phalcon\Db\Dialect\Postgresql::addIndex/createTable, andPhalcon\Db\Dialect\Sqlite::addIndex/createTable.Phalcon\Db\Adapter\Pdo\Mysql::describeIndexesreverse-engineers directions from theCollationcolumn ofSHOW INDEXES(A= ASC,D= DESC, NULL = ASC); the resultingIndexonly carries a non-emptydirectionsarray 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.indoptionandsqlite_master.sqlparsing 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 v7stub onPhalcon\Contracts\Db\Indexto avoid breaking third-party implementors during the v5 linePhalcon\Db\Columnand the MySQL and PostgreSQL dialects. Eight newColumn::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::getColumnDefinitionandPhalcon\Db\Dialect\Postgresql::getColumnDefinitionemit 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 thedefaultbranch.Phalcon\Db\Adapter\Pdo\Mysql::describeColumnsreverse-engineers the new types bystarts_with-matching the column type - order in the switch was chosen so the longer multi-* / geometrycollection variants are matched before their shorter prefixes (linestringbeforepolygon, etc.). PostgreSQL reverse-engineering for spatial types is deferred becauseinformation_schema.data_typedoes not consistently expose PostGIS type names without joiningpg_type- users introspecting PostGIS schemas should query metadata directly until then. Caveat - read-side WKB hydration is not part of this change. APOINTselected directly withSELECT location FROM itemsstill returns raw WKB bytes (cphalcon issues #14769 and #13670); the workaround is to projectST_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 helperDEFAULTclauses by recognizingPhalcon\Db\RawValueinstances passed asdefinition["default"]. Previously each dialect quoted any non-numeric, non-CURRENT_TIMESTAMP, non-NULLdefault as a string literal - preventing legitimate expression defaults like MySQL 8.0.13+DEFAULT (UUID()), PostgreSQLDEFAULT 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), andPhalcon\Db\Dialect\Sqlite::addColumn/createTablenow detectRawValuedefaults and emitDEFAULT <raw>verbatim. Plain-scalar andCURRENT_TIMESTAMP/NULLkeyword defaults continue to take the existing whitelist path unchanged.Column::hasDefault()already treats aRawValueas a non-null default, soisAutoIncrement()semantics and the generated-column "no default allowed" guard from item view->setParamToView crash the server with complex arrays #1 remain correctdropColumn()on the SQLite dialect to emitALTER TABLE ... DROP COLUMN ...instead of throwing unconditionally - SQLite 3.35+ natively supportsALTER 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::dropColumnalready passes through the dialect output viaAbstractAdapter, so users now get a one-call$connection->dropColumn(...)on SQLite