Hacktoberfest 2026: as issues que os mantenedores marcaram para outubro, abertas e boas para iniciantes. Ver issues do Hacktoberfest

[RFC]: Replace caller-supplied Literal slots in Sql\Ddl with backed enums and typed arguments

Aberta
#181 0 comentários 0 reações 1 responsável Ver no GitHub

Mantenedores costumam responder em até 1 dia

@simon-mundy já está trabalhando nisso.

Desde 4/9/2026.

Avaliação

Esta issue ainda não foi avaliada.

Descrição

enhancement
Proposed Version

0.6.0 for the enums, typed arguments and string-compat setters; the string-getter retype waits for the next major.

Basic Information

Every place under src/Sql/Ddl/ where a caller-supplied string becomes an Argument\Literal or is concatenated into a spec string gets a backed enum (closed value sets) or a typed int (numeric slots), with a one-minor string-compat path. This is the core counterpart of phpdb-mysql#79.

Background

The DDL layer renders three kinds of argument: Argument\Identifier (quoted as an identifier), Argument\Value (quoted or bound) and Argument\Literal (inserted verbatim). Going through every getExpressionData() under src/Sql/Ddl/ (and processTableOptions() for table options), these are the places where a Literal, or a bare string concatenated into the spec, comes from the caller rather than a class constant:

Class Slot Origin Rendered probe
Index column prefix length untyped array $lengths, concatenated into the spec INDEX `i`(`email`(20) INJECT))
Index USING type setType(string) -> Literal USING HASH; DROP
ForeignKey ON DELETE / ON UPDATE rule string setters -> Literal ON DELETE CASCADE; DROP
Integer display width options['length'] (untyped via the constructor array, bool|string via setOption()), concatenated INTEGER NOT NULL (11) (also a positional bug, #178)
Check expression untyped constructor with a string|ExpressionInterface docblock -> Literal an expression slot by design; an Expression is accepted at construction and fails at render (#177)
CreateTable / AlterTable processTableOptions() table option key setOption(string $name, ...), strtoupper() only ENGINE = INNODB; DROP TABLE Y; -- = 'x'

Example: setOption('row_format', 'DYNAMIC'), 'algorithm' and 'lock' with a plain string render a quoted value MySQL rejects with 1064. Only a Literal works, and nothing tells the caller that.

Safe by construction and not proposed to change: Column::$type (protected, set by the subclass), AbstractLengthColumn / AbstractPrecisionColumn lengths (built from ?int), AbstractTimestampColumn's fixed ON UPDATE CURRENT_TIMESTAMP, all identifier slots, Column defaults given as an Argument\Literal, and table-option values given as a Sql\Literal. Those last two are raw by definition.

Considerations
  • Docs bug. Only Argument\Literal itself says a Literal default is raw. Column::setDefault() and the DDL docs don't, and docs/book/sql-ddl/columns.md:340-346, alter-drop.md, examples.md and intro.md all show setDefault('CURRENT_TIMESTAMP') producing an unquoted default. It actually renders DEFAULT 'CURRENT_TIMESTAMP', which MySQL rejects (1067). Worth its own issue; listed here because it is the other side of the same invariant.
  • Getters. getOnDeleteRule(): string, getOnUpdateRule(): string and Index::getType(): ?string are asserted as strings in ForeignKeyTest.php:106-132 and IndexTest.php:67. Retyping them in a minor would be a BC break, so they stay until the next major.
  • SET DEFAULT is valid grammar that InnoDB doesn't honour (on 8.0.46 the DDL is accepted and RESTRICT is enforced at DML time). Keep it in the enum, document the caveat.
  • Index prefix length: MySQL rejects 0 with 1391, so positive-int is exactly the server's bound.
  • Integer display width is itself deprecated since 8.0.17; keeping it as a typed int is for the other platforms, and the MySQL decorator will drop it.
  • Overlaps: #169 (typing the option arrays), phpdb-mysql#79 (the adapter side), the statements umbrella, #179 (RowFormat/Algorithm/Lock and key validation live there).
Proposal(s)
  1. Sql\Ddl\Constraint\ReferentialAction: string { NoAction = 'NO ACTION'; Restrict = 'RESTRICT'; Cascade = 'CASCADE'; SetNull = 'SET NULL'; SetDefault = 'SET DEFAULT' } for ForeignKey::setOnDeleteRule() / setOnUpdateRule() and the constructor.
  2. Sql\Ddl\Index\IndexType: string { BTree = 'BTREE'; Hash = 'HASH' } for Index::setType().
  3. Index::$lengths typed list<positive-int> (validated in the constructor) and rendered as Literal((string) $int) in the values array, not in the spec.
  4. Integer length as int (see #178).
  5. Table option keys validated against a pattern or an enum of known options; string values keep going through quoteTrustedValue(), keyword values through RowFormat / Algorithm / Lock enums (#179).

Compatibility:

  • Setters keep accepting string for one minor, resolved with Enum::tryFrom(strtoupper(trim($value))) and rejected with InvalidArgumentException when unknown. Passing a string is deprecated in the docblock and removed at the next major.
  • Getters keep returning string for the same period. Add enum-returning siblings now (getOnDeleteAction(): ReferentialAction, getOnUpdateAction(): ReferentialAction, Index::getIndexType(): ?IndexType) and retype the string getters at the next major.

Test plan:

  • No getExpressionData() under src/Sql/Ddl/ constructs an Argument\Literal from a caller string except Check (documented) and Column defaults (documented in setDefault() and the docs, with the CURRENT_TIMESTAMP examples corrected).
  • No getExpressionData() or processTableOptions() concatenates a caller value into the spec string.
  • Each enum has a unit test for accepted values, the string-compat path and rejection.
  • The probe strings above render quoted/escaped or throw; none reaches SQL verbatim.
  • Existing string-getter tests pass unchanged.
Appendix/Additional Info
  • #91 (backed enums for metadata constants), #169 (untyped option arrays), phpdb-mysql#79 (the same fix on the adapter side).
  • From an internal DDL audit (not published). The full slot table there includes the adapter side; the core rows are reproduced above.
Linguagem predominante
PHP
Estrelas
18
Forks
8
Merge médio
6d 11h
PRs com merge (30d)
14

Preparar o ambiente

  • Inclui um Dockerfile ou arquivo Docker Compose
  • Sem modelo de pull request
  • Sem guia de contribuição

Primeiros passos

  1. Leia a issue inteira e depois o guia de contribuição do projeto.
  2. Comente na issue dizendo que vai assumir — evita que duas pessoas façam o mesmo trabalho.
  3. Faça um fork do repositório e trabalhe em uma branch.
  4. Abra um pull request que referencie o número da issue.

Mais de php-db/phpdb

Todas as issues de php-db/phpdb

Issues semelhantes

Mais issues de PHP

Receba novas issues na sua caixa de entrada

Um resumo curto de issues do GitHub para quem está começando.