isEmpty())->toBeTrue(); expect($params->count())->toBe(0); expect($params->toArray())->toBe([]); }); it('can be created from array', function () { $data = ['id' => 123, 'name' => 'John']; $params = QueryParameters::fromArray($data); expect($params->isEmpty())->toBeFalse(); expect($params->count())->toBe(2); expect($params->get('id'))->toBe(123); expect($params->get('name'))->toBe('John'); }); it('can add parameters immutably', function () { $params = QueryParameters::empty(); $newParams = $params->with('id', 123); expect($params->has('id'))->toBeFalse(); // Original unchanged expect($newParams->has('id'))->toBeTrue(); expect($newParams->get('id'))->toBe(123); }); it('can merge parameters', function () { $params1 = QueryParameters::fromArray(['id' => 123]); $params2 = $params1->merge(['name' => 'John', 'active' => true]); expect($params1->count())->toBe(1); // Original unchanged expect($params2->count())->toBe(3); expect($params2->get('id'))->toBe(123); expect($params2->get('name'))->toBe('John'); expect($params2->get('active'))->toBeTrue(); }); it('can remove parameters immutably', function () { $params = QueryParameters::fromArray(['id' => 123, 'name' => 'John']); $newParams = $params->without('name'); expect($params->has('name'))->toBeTrue(); // Original unchanged expect($newParams->has('name'))->toBeFalse(); expect($newParams->has('id'))->toBeTrue(); }); it('gets parameters with defaults', function () { $params = QueryParameters::fromArray(['id' => 123]); expect($params->get('id'))->toBe(123); expect($params->get('missing'))->toBeNull(); expect($params->get('missing', 'default'))->toBe('default'); }); it('normalizes parameter names', function () { $params = QueryParameters::fromArray([':id' => 123, 'name' => 'John']); expect($params->has('id'))->toBeTrue(); expect($params->has(':id'))->toBeTrue(); // Both work expect($params->get('id'))->toBe(123); expect($params->get(':id'))->toBe(123); }); it('converts to PDO array with colon prefixes', function () { $params = QueryParameters::fromArray(['id' => 123, ':name' => 'John']); $pdoArray = $params->toPdoArray(); expect($pdoArray)->toBe([':id' => 123, ':name' => 'John']); }); it('finds used parameters in SQL', function () { $params = QueryParameters::fromArray(['id' => 123, 'name' => 'John', 'unused' => 'test']); $sql = 'SELECT * FROM users WHERE id = :id AND name = :name'; $used = $params->getUsedParameters($sql); expect($used)->toBe(['id', 'name']); }); it('finds unused parameters', function () { $params = QueryParameters::fromArray(['id' => 123, 'name' => 'John', 'unused' => 'test']); $sql = 'SELECT * FROM users WHERE id = :id'; $unused = $params->getUnusedParameters($sql); expect($unused)->toBe(['name', 'unused']); }); it('validates SQL parameter requirements', function () { $params = QueryParameters::fromArray(['id' => 123]); $sql = 'SELECT * FROM users WHERE id = :id AND name = :name'; expect(fn () => $params->validateForSql($sql))->toThrow(InvalidArgumentException::class); }); it('validates SQL parameter requirements successfully', function () { $params = QueryParameters::fromArray(['id' => 123, 'name' => 'John']); $sql = 'SELECT * FROM users WHERE id = :id AND name = :name'; expect(fn () => $params->validateForSql($sql))->not->toThrow(InvalidArgumentException::class); }); it('determines PDO parameter types', function () { $params = QueryParameters::fromArray([ 'null_val' => null, 'bool_val' => true, 'int_val' => 123, 'string_val' => 'text', ]); expect($params->getPdoType('null_val'))->toBe(\PDO::PARAM_NULL); expect($params->getPdoType('bool_val'))->toBe(\PDO::PARAM_BOOL); expect($params->getPdoType('int_val'))->toBe(\PDO::PARAM_INT); expect($params->getPdoType('string_val'))->toBe(\PDO::PARAM_STR); }); it('throws exception for non-string parameter names', function () { expect(fn () => QueryParameters::fromArray([123 => 'value']))->toThrow(InvalidArgumentException::class); }); it('throws exception for empty parameter names', function () { expect(fn () => QueryParameters::fromArray(['' => 'value']))->toThrow(InvalidArgumentException::class); expect(fn () => QueryParameters::fromArray([':' => 'value']))->toThrow(InvalidArgumentException::class); }); it('throws exception for invalid parameter names', function () { expect(fn () => QueryParameters::fromArray(['1invalid' => 'value']))->toThrow(InvalidArgumentException::class); expect(fn () => QueryParameters::fromArray(['invalid-name' => 'value']))->toThrow(InvalidArgumentException::class); expect(fn () => QueryParameters::fromArray(['invalid name' => 'value']))->toThrow(InvalidArgumentException::class); }); it('allows valid parameter names', function () { expect(fn () => QueryParameters::fromArray(['valid' => 'value']))->not->toThrow(InvalidArgumentException::class); expect(fn () => QueryParameters::fromArray(['valid_name' => 'value']))->not->toThrow(InvalidArgumentException::class); expect(fn () => QueryParameters::fromArray(['valid123' => 'value']))->not->toThrow(InvalidArgumentException::class); expect(fn () => QueryParameters::fromArray(['_valid' => 'value']))->not->toThrow(InvalidArgumentException::class); expect(fn () => QueryParameters::fromArray([':valid' => 'value']))->not->toThrow(InvalidArgumentException::class); }); it('throws exception for non-scalar parameter values', function () { expect(fn () => QueryParameters::fromArray(['array' => [1, 2, 3]]))->toThrow(InvalidArgumentException::class); expect(fn () => QueryParameters::fromArray(['object' => new \stdClass()]))->toThrow(InvalidArgumentException::class); }); it('allows scalar and null parameter values', function () { expect(fn () => QueryParameters::fromArray([ 'string' => 'text', 'int' => 123, 'float' => 12.34, 'bool' => true, 'null' => null, ]))->not->toThrow(InvalidArgumentException::class); }); });