Skip to content

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

Description

@simon-mundy

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: Untyped array boundaries (AbstractConnection::$connectionParameters, ColumnInterface::getOptions()) cause mixed-* fallout downstream #169 (typing the option arrays), phpdb-mysql#79 (the adapter side), the statements umbrella, [RFC]: DDL standalone index, view, rename and truncate statements, and typed table options #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 Integer length option renders the display width after NOT NULL #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 ([RFC]: DDL standalone index, view, rename and truncate statements, and typed table options #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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions