Skip to content

Check constraint: ExpressionInterface expressions throw TypeError, empty expressions render invalid SQL, name argument has no default #177

Description

@simon-mundy

Package Version

0.5.0 (also 0.6.x-dev @ e037464a and the 0.6.x head)

Php Version

8.4

Database Engine

MySQL

Basic Information

Sql\Ddl\Constraint\Check documents its expression as string|ExpressionInterface (property docblock Check.php#L16, constructor docblock #L21 — the parameter at #L23 is untyped) but always wraps it in new Literal($this->expression) (#L43), and Argument\Literal::__construct() takes string. So an Expression is accepted at construction and blows up at render.

Three problems:

  1. An ExpressionInterface expression throws TypeError at render.
  2. An empty expression renders a bare CONSTRAINT chk`` header — the !== '' guard at L41 quietly drops the `CHECK (%s)` part while the name guard at L36 still emits `CONSTRAINT %s`.
  3. __construct($expression, ?string $name) gives $name no default, so an unnamed check has to be written new Check('id > 0', null). PrimaryKey and UniqueKey default the name (ForeignKey requires it, as a non-nullable first parameter); Check is the odd one out in being required yet nullable.

test/unit/Sql/Ddl/Constraint/CheckTest.php only ever exercises new Check('id>0', 'foo').

Steps to Reproduce

  1. new Check(new Expression('a > ?', [1]), 'chk') added to a CreateTable, then getSqlString($mysqlPlatform).
  2. new Check('', 'chk') added to a CreateTable with one column, then getSqlString($mysqlPlatform).
  3. new Check('id > 0') with no second argument.

Expected Behavior

  1. When the expression is an ExpressionInterface, merge its getExpressionData() into the check's — spec CHECK (<inner spec>), values appended — so values and identifiers inside it are quoted by the platform instead of being flattened to a literal. I have prototyped this: it renders CHECK (a > '1') and CHECK (a>b), and MySQL accepts both. A plain string stays a Literal; it is an expression slot by design and should be documented as such.
  2. Throw PhpDb\Sql\Exception\InvalidArgumentException for an empty string at construction — the same exception Sql\Expression::setExpression() already throws for an empty string. Nothing under Sql\Ddl validates anything today, so that is the precedent.
  3. ?string $name = null.

There is no prepare path to worry about: DDL objects extend AbstractSql, not AbstractPreparableSql, and MySQL itself rejects a parameter marker inside a CHECK at execute time (ERROR 3815). Values in a check are always inlined.

Test plan:

  • new Check(new Expression('a > ?', [1]), 'chk') renders CONSTRAINT `chk` CHECK (a > '1') via getSqlString() on the MySQL platform.
  • new Check('') throws InvalidArgumentException.
  • new Check('id > 0') compiles and renders CHECK (id > 0).
  • Column-level use still renders inline: `age` INTEGER NOT NULL CONSTRAINT `chk_age` CHECK (age >= 0) (accepted by MySQL 8.4.10, reproduced on 8.0.46).
  • CheckTest covers all four. If this gets an integration test: CHECK names are schema-wide in MySQL (3822 on a duplicate), so use unique names per test.

Actual behavior?

TypeError: PhpDb\Sql\Argument\Literal::__construct(): Argument #1 ($literal) must be of type string, PhpDb\Sql\Expression given
  1. (MySQL AdapterPlatform)
CREATE TABLE `x` (
    `a` INTEGER NOT NULL ,
    CONSTRAINT `chk`
)
  1. ArgumentCountError — too few arguments.

Additional Info

Reproduced on 0.5.x, e037464a and the 0.6.x head. From an internal DDL audit (not published — the finding is reproduced above). The Check expression slot also appears in the literal-slots RFC (#181) as a documented by-design Literal.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions