withParameter('user_id', 'INT') ->withBody('SELECT * FROM users WHERE id = user_id') ->returnsType('TABLE'); expect($procedure->getName())->toBe('get_user_by_id'); expect($procedure->getParameters())->toHaveCount(1); expect($procedure->getParameters()[0]['name'])->toBe('user_id'); expect($procedure->getParameters()[0]['type'])->toBe('INT'); expect($procedure->getBody())->toBe('SELECT * FROM users WHERE id = user_id'); expect($procedure->getReturnType())->toBe('TABLE'); }); test('generates MySQL stored procedure SQL', function () { $procedure = StoredProcedureDefinition::create('calculate_order_total') ->withParameter('order_id', 'INT') ->withParameter('include_tax', 'BOOLEAN', true) ->withBody(' DECLARE total DECIMAL(10,2); SELECT SUM(price * quantity) INTO total FROM order_items WHERE order_id = order_id; IF include_tax THEN SET total = total * 1.19; END IF; RETURN total; ') ->returnsType('DECIMAL(10,2)'); $sql = $procedure->toSql('mysql'); expect($sql)->toContain('CREATE PROCEDURE `calculate_order_total`'); expect($sql)->toContain('IN `order_id` INT'); expect($sql)->toContain('IN `include_tax` BOOLEAN'); expect($sql)->toContain('DECLARE total DECIMAL(10,2)'); expect($sql)->toContain('DELIMITER'); }); test('generates PostgreSQL stored procedure SQL', function () { $procedure = StoredProcedureDefinition::create('get_active_users') ->withParameter('min_login_count', 'INT') ->withBody(' RETURN QUERY SELECT * FROM users WHERE active = true AND login_count >= min_login_count ORDER BY last_login DESC; ') ->returnsType('SETOF users'); $sql = $procedure->toSql('pgsql'); expect($sql)->toContain('CREATE OR REPLACE FUNCTION get_active_users'); expect($sql)->toContain('min_login_count INT'); expect($sql)->toContain('RETURNS SETOF users'); expect($sql)->toContain('LANGUAGE plpgsql'); }); test('generates SQLite stored procedure SQL as user-defined function', function () { $procedure = StoredProcedureDefinition::create('calculate_age') ->withParameter('birth_date', 'TEXT') ->withBody(' RETURN (julianday("now") - julianday(birth_date)) / 365.25; ') ->returnsType('REAL'); $sql = $procedure->toSql('sqlite'); expect($sql)->toContain('CREATE FUNCTION calculate_age'); expect($sql)->toContain('(birth_date TEXT)'); expect($sql)->toContain('RETURNS REAL'); }); test('throws exception for unsupported database driver', function () { $procedure = StoredProcedureDefinition::create('test_procedure') ->withBody('SELECT 1') ->returnsType('INT'); expect(fn () => $procedure->toSql('unsupported_driver')) ->toThrow(\InvalidArgumentException::class, 'Unsupported database driver: unsupported_driver'); }); test('validates procedure name', function () { expect(fn () => StoredProcedureDefinition::create('')) ->toThrow(\InvalidArgumentException::class, 'Procedure name cannot be empty'); expect(fn () => StoredProcedureDefinition::create('invalid-name')) ->toThrow(\InvalidArgumentException::class, 'Invalid procedure name: invalid-name'); }); test('validates parameter name', function () { $procedure = StoredProcedureDefinition::create('test_procedure'); expect(fn () => $procedure->withParameter('', 'INT')) ->toThrow(\InvalidArgumentException::class, 'Parameter name cannot be empty'); expect(fn () => $procedure->withParameter('invalid-name', 'INT')) ->toThrow(\InvalidArgumentException::class, 'Invalid parameter name: invalid-name'); }); test('validates body is not empty before generating SQL', function () { $procedure = StoredProcedureDefinition::create('test_procedure'); expect(fn () => $procedure->toSql('mysql')) ->toThrow(\InvalidArgumentException::class, 'Procedure body cannot be empty'); });