import 'dart:io'; import 'package:postgres/postgres.dart'; final class PostgresConnectionConfig { const PostgresConnectionConfig({ required this.host, required this.port, required this.database, required this.username, required this.password, this.sslMode = SslMode.disable, }); factory PostgresConnectionConfig.fromEnvironment([ Map? environment, ]) { final env = environment ?? Platform.environment; return PostgresConnectionConfig( host: _requiredEnv(env, 'DATABASE_HOST'), port: int.tryParse(env['DATABASE_PORT'] ?? '') ?? 5432, database: _requiredEnv(env, 'DATABASE_NAME'), username: _requiredEnv(env, 'DATABASE_USER'), password: _requiredEnv(env, 'DATABASE_PASSWORD'), ); } final String host; final int port; final String database; final String username; final String password; final SslMode sslMode; } final class PostgresConnectionFactory { const PostgresConnectionFactory(this.config) : databaseUrl = null; const PostgresConnectionFactory.fromDatabaseUrl(this.databaseUrl) : config = null; final PostgresConnectionConfig? config; final String? databaseUrl; Future open() { final databaseUrl = this.databaseUrl; if (databaseUrl != null) { return Connection.openFromUrl(databaseUrl); } final config = this.config!; return Connection.open( Endpoint( host: config.host, port: config.port, database: config.database, username: config.username, password: config.password, ), settings: ConnectionSettings(sslMode: config.sslMode), ); } } final class PostgresMigrator { const PostgresMigrator(this.connection); final Connection connection; Future applyMigrations({String migrationsPath = 'migrations'}) async { final directory = Directory(migrationsPath); if (!await directory.exists()) { throw StateError('Migrations directory not found: $migrationsPath'); } final files = await directory .list() .where((entity) => entity is File && entity.path.endsWith('.sql')) .cast() .toList() ..sort((left, right) => left.path.compareTo(right.path)); for (final file in files) { final sql = await file.readAsString(); for (final statement in _splitSqlStatements(sql)) { await connection.execute(statement); } } } } String _requiredEnv(Map environment, String key) { final value = environment[key]; if (value == null || value.trim().isEmpty) { throw StateError('Missing required environment variable: $key'); } return value; } List _splitSqlStatements(String sql) { final statements = []; final buffer = StringBuffer(); var inSingleQuote = false; var inDoubleQuote = false; String? dollarQuoteTag; for (var index = 0; index < sql.length; index++) { final char = sql[index]; final next = index + 1 < sql.length ? sql[index + 1] : null; if (dollarQuoteTag != null) { if (sql.startsWith(dollarQuoteTag, index)) { buffer.write(dollarQuoteTag); index += dollarQuoteTag.length - 1; dollarQuoteTag = null; } else { buffer.write(char); } continue; } if (!inSingleQuote && !inDoubleQuote && char == r'$') { final tag = _readDollarQuoteTag(sql, index); if (tag != null) { dollarQuoteTag = tag; buffer.write(tag); index += tag.length - 1; continue; } } if (!inDoubleQuote && char == "'") { buffer.write(char); if (inSingleQuote && next == "'") { index++; buffer.write(next); } else { inSingleQuote = !inSingleQuote; } continue; } if (!inSingleQuote && char == '"') { inDoubleQuote = !inDoubleQuote; buffer.write(char); continue; } if (!inSingleQuote && !inDoubleQuote && char == ';') { final statement = buffer.toString().trim(); if (statement.isNotEmpty) { statements.add(statement); } buffer.clear(); continue; } buffer.write(char); } final statement = buffer.toString().trim(); if (statement.isNotEmpty) { statements.add(statement); } return statements; } String? _readDollarQuoteTag(String sql, int start) { final end = sql.indexOf(r'$', start + 1); if (end == -1) { return null; } final tagName = sql.substring(start + 1, end); final validTag = RegExp(r'^[A-Za-z_][A-Za-z0-9_]*$|^$').hasMatch(tagName); return validTag ? sql.substring(start, end + 1) : null; }