* @license MIT */ declare(strict_types=1); namespace BitBadger\PDODocument; use Exception; /** * Criteria for a field WHERE clause */ class Field { /** * Constructor * * NOTE: Use static construction methods rather than using this directly; different properties are required * depending on the operation being performed. This is only public so it can be tested. * * @param string $fieldName The name of the field * @param Op $op The operation by which the field will be compared * @param mixed $value The value of the field * @param string $paramName The name of the parameter to which this should be bound * @param string $qualifier A table qualifier for the `data` column */ public function __construct(public string $fieldName = '', public Op $op = Op::Equal, public mixed $value = '', public string $paramName = '', public string $qualifier = '') { } /** * Append the parameter name and value to the given associative array * * @param array $existing The existing parameters * @return array The given parameter array with this field's name and value(s) appended */ public function appendParameter(array $existing): array { switch ($this->op) { case Op::Exists: case Op::NotExists: break; case Op::Between: $existing["{$this->paramName}min"] = $this->value[0]; $existing["{$this->paramName}max"] = $this->value[1]; break; case Op::In: for ($idx = 0; $idx < count($this->value); $idx++) { $existing["{$this->paramName}_$idx"] = $this->value[$idx]; } break; case Op::InArray: $mkString = Configuration::mode("Append parameters for InArray condition") === Mode::PgSQL; $values = $this->value['values']; for ($idx = 0; $idx < count($values); $idx++) { $existing["{$this->paramName}_$idx"] = $mkString ? "$values[$idx]" : $values[$idx]; } break; default: $existing[$this->paramName] = $this->value; } return $existing; } /** * Derive the path for this field * * @param bool $asJSON Whether the field should be treated as JSON in the query (optional, default false) * @return string The path for this field * @throws Exception If the database mode has not been set */ public function path(bool $asJSON = false): string { $extra = $asJSON ? '' : '>'; if (str_contains($this->fieldName, '.')) { $mode = Configuration::mode('determine field path'); if ($mode === Mode::PgSQL) { return "data#>$extra'{" . implode(',', explode('.', $this->fieldName)) . "}'"; } if ($mode === Mode::SQLite) { $parts = explode('.', $this->fieldName); $last = array_pop($parts); return "data->'" . implode("'->'", $parts) . "'->$extra'$last'"; } } return "data->$extra'$this->fieldName'"; } /** * Get the WHERE clause fragment for this parameter * * @return string The WHERE clause fragment for this parameter * @throws Exception|DocumentException If the database mode has not been set */ public function toWhere(): string { $mode = Configuration::mode('make field WHERE clause'); $fieldName = (empty($this->qualifier) ? '' : "$this->qualifier.") . $this->path($this->op === Op::InArray); $fieldPath = match ($mode) { Mode::PgSQL => match (true) { $this->op === Op::Between, $this->op === Op::In => is_numeric($this->value[0]) ? "($fieldName)::numeric" : $fieldName, is_numeric($this->value) => "($fieldName)::numeric", default => $fieldName, }, default => $fieldName, }; $criteria = match ($this->op) { Op::Exists, Op::NotExists => '', Op::Between => " {$this->paramName}min AND {$this->paramName}max", Op::In => ' (' . $this->inParameterNames() . ')', Op::InArray => $mode === Mode::PgSQL ? ' ARRAY[' . $this->inParameterNames() . ']' : '', default => " $this->paramName", }; return $mode === Mode::SQLite && $this->op === Op::InArray ? "EXISTS (SELECT 1 FROM json_each({$this->value['table']}.data, '\$.$this->fieldName') WHERE value IN (" . $this->inParameterNames() . '))' : $fieldPath . ' ' . $this->op->toSQL() . $criteria; } /** * Create parameter names for an IN clause * * @return string A comma-delimited string of parameter names */ private function inParameterNames(): string { $values = $this->op === Op::In ? $this->value : $this->value['values']; return implode(', ', array_map(fn($value, $key) => "{$this->paramName}_$key", $values, range(0, count($values) - 1))); } /** * Create an equals (=) field criterion * * @param string $fieldName The name of the field against which the value will be compared * @param mixed $value The value for which equality will be checked * @param string $paramName The name of the parameter to which this should be bound (optional; generated if blank) * @return self The field with the requested criterion */ public static function equal(string $fieldName, mixed $value, string $paramName = ''): self { return new self($fieldName, Op::Equal, $value, $paramName); } /** * Create an equals (=) field criterion _(alias for `Field.equal`)_ * * @param string $fieldName The name of the field against which the value will be compared * @param mixed $value The value for which equality will be checked * @param string $paramName The name of the parameter to which this should be bound (optional; generated if blank) * @return self The field with the requested criterion */ public static function EQ(string $fieldName, mixed $value, string $paramName = ''): self { return self::equal($fieldName, $value, $paramName); } /** * Create a greater than (>) field criterion * * @param string $fieldName The name of the field against which the value will be compared * @param mixed $value The value for the greater than comparison * @param string $paramName The name of the parameter to which this should be bound (optional; generated if blank) * @return self The field with the requested criterion */ public static function greater(string $fieldName, mixed $value, string $paramName = ''): self { return new self($fieldName, Op::Greater, $value, $paramName); } /** * Create a greater than (>) field criterion _(alias for `Field.greater`)_ * * @param string $fieldName The name of the field against which the value will be compared * @param mixed $value The value for the greater than comparison * @param string $paramName The name of the parameter to which this should be bound (optional; generated if blank) * @return self The field with the requested criterion */ public static function GT(string $fieldName, mixed $value, string $paramName = ''): self { return self::greater($fieldName, $value, $paramName); } /** * Create a greater than or equal to (>=) field criterion * * @param string $fieldName The name of the field against which the value will be compared * @param mixed $value The value for the greater than or equal to comparison * @param string $paramName The name of the parameter to which this should be bound (optional; generated if blank) * @return self The field with the requested criterion */ public static function greaterOrEqual(string $fieldName, mixed $value, string $paramName = ''): self { return new self($fieldName, Op::GreaterOrEqual, $value, $paramName); } /** * Create a greater than or equal to (>=) field criterion _(alias for `Field.greaterOrEqual`)_ * * @param string $fieldName The name of the field against which the value will be compared * @param mixed $value The value for the greater than or equal to comparison * @param string $paramName The name of the parameter to which this should be bound (optional; generated if blank) * @return self The field with the requested criterion */ public static function GE(string $fieldName, mixed $value, string $paramName = ''): self { return self::greaterOrEqual($fieldName, $value, $paramName); } /** * Create a less than (<) field criterion * * @param string $fieldName The name of the field against which the value will be compared * @param mixed $value The value for the less than comparison * @param string $paramName The name of the parameter to which this should be bound (optional; generated if blank) * @return self The field with the requested criterion */ public static function less(string $fieldName, mixed $value, string $paramName = ''): self { return new self($fieldName, Op::Less, $value, $paramName); } /** * Create a less than (<) field criterion _(alias for `Field.less`)_ * * @param string $fieldName The name of the field against which the value will be compared * @param mixed $value The value for the less than comparison * @param string $paramName The name of the parameter to which this should be bound (optional; generated if blank) * @return self The field with the requested criterion */ public static function LT(string $fieldName, mixed $value, string $paramName = ''): self { return self::less($fieldName, $value, $paramName); } /** * Create a less than or equal to (<=) field criterion * * @param string $fieldName The name of the field against which the value will be compared * @param mixed $value The value for the less than or equal to comparison * @param string $paramName The name of the parameter to which this should be bound (optional; generated if blank) * @return self The field with the requested criterion */ public static function lessOrEqual(string $fieldName, mixed $value, string $paramName = ''): self { return new self($fieldName, Op::LessOrEqual, $value, $paramName); } /** * Create a less than or equal to (<=) field criterion _(alias for `Field.lessOrEqual`)_ * * @param string $fieldName The name of the field against which the value will be compared * @param mixed $value The value for the less than or equal to comparison * @param string $paramName The name of the parameter to which this should be bound (optional; generated if blank) * @return self The field with the requested criterion */ public static function LE(string $fieldName, mixed $value, string $paramName = ''): self { return self::lessOrEqual($fieldName, $value, $paramName); } /** * Create a not equals (<>) field criterion * * @param string $fieldName The name of the field against which the value will be compared * @param mixed $value The value for the not equals comparison * @param string $paramName The name of the parameter to which this should be bound (optional; generated if blank) * @return self The field with the requested criterion */ public static function notEqual(string $fieldName, mixed $value, string $paramName = ''): self { return new self($fieldName, Op::NotEqual, $value, $paramName); } /** * Create a not equals (<>) field criterion _(alias for `Field.notEqual`)_ * * @param string $fieldName The name of the field against which the value will be compared * @param mixed $value The value for the not equals comparison * @param string $paramName The name of the parameter to which this should be bound (optional; generated if blank) * @return self The field with the requested criterion */ public static function NE(string $fieldName, mixed $value, string $paramName = ''): self { return self::notEqual($fieldName, $value, $paramName); } /** * Create a BETWEEN field criterion * * @param string $fieldName The name of the field against which the value will be compared * @param mixed $minValue The lower value for range * @param mixed $maxValue The upper value for the range * @param string $paramName The name of the parameter to which this should be bound (optional; generated if blank) * @return self The field with the requested criterion */ public static function between(string $fieldName, mixed $minValue, mixed $maxValue, string $paramName = ''): self { return new self($fieldName, Op::Between, [$minValue, $maxValue], $paramName); } /** * Create a BETWEEN field criterion _(alias for `Field.between`)_ * * @param string $fieldName The name of the field against which the value will be compared * @param mixed $minValue The lower value for range * @param mixed $maxValue The upper value for the range * @param string $paramName The name of the parameter to which this should be bound (optional; generated if blank) * @return self The field with the requested criterion */ public static function BT(string $fieldName, mixed $minValue, mixed $maxValue, string $paramName = ''): self { return self::between($fieldName, $minValue, $maxValue, $paramName); } /** * Create an IN field criterion * * @param string $fieldName The name of the field against which the values will be compared * @param mixed[] $values The potential matching values for the field * @param string $paramName The name of the parameter to which this should be bound (optional; generated if blank) * @return self The field with the requested criterion */ public static function in(string $fieldName, array $values, string $paramName = ''): self { return new self($fieldName, Op::In, $values, $paramName); } /** * Create an IN ARRAY field criterion * * @param string $fieldName The name of the field against which the values will be compared * @param string $tableName The table name where this field is located * @param mixed[] $values The potential matching values for the field * @param string $paramName The name of the parameter to which this should be bound (optional; generated if blank) * @return self The field with the requested criterion */ public static function inArray(string $fieldName, string $tableName, array $values, string $paramName = ''): self { return new self($fieldName, Op::InArray, ['table' => $tableName, 'values' => $values], $paramName); } /** * Create an exists (IS NOT NULL) field criterion * * @param string $fieldName The name of the field for which existence will be checked * @return self The field with the requested criterion */ public static function exists(string $fieldName): self { return new self($fieldName, Op::Exists, '', ''); } /** * Create an exists (IS NOT NULL) field criterion _(alias for `Field.exists`)_ * * @param string $fieldName The name of the field for which existence will be checked * @return self The field with the requested criterion */ public static function EX(string $fieldName): self { return self::exists($fieldName); } /** * Create a not exists (IS NULL) field criterion * * @param string $fieldName The name of the field for which non-existence will be checked * @return self The field with the requested criterion */ public static function notExists(string $fieldName): self { return new self($fieldName, Op::NotExists, '', ''); } /** * Create a not exists (IS NULL) field criterion _(alias for `Field.notExists`)_ * * @param string $fieldName The name of the field for which non-existence will be checked * @return self The field with the requested criterion */ public static function NEX(string $fieldName): self { return self::notExists($fieldName); } /** * Create a named fields (used for creating fields for ORDER BY clauses) * * Prepend the field name with 'n:' to treat the field as a number; prepend the field name with 'i:' to perform * a case-insensitive ordering. * * @param string $name The name of the field, plus any direction for the ordering * @return self */ public static function named(string $name): self { return new self($name, Op::Equal, '', ''); } }