diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..1bec35e --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/dbnavigator.xml b/.idea/dbnavigator.xml new file mode 100644 index 0000000..1b2f6e3 --- /dev/null +++ b/.idea/dbnavigator.xml @@ -0,0 +1,409 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 08237f1..f16dea7 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -4,7 +4,7 @@ - + \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index cecc3c0..d1453cd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,7 +12,9 @@ repositories { dependencies { testImplementation(kotlin("test")) implementation("org.xerial:sqlite-jdbc:3.40.1.0") + implementation ("mysql:mysql-connector-java:8.0.33") implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.22") + implementation("org.postgresql:postgresql:42.6.0") } tasks.test { diff --git a/orm.db b/orm.db index f7f36c4..aab6b0b 100644 Binary files a/orm.db and b/orm.db differ diff --git a/src/main/kotlin/ormapping/Main.kt b/src/main/kotlin/ormapping/Main.kt index a636865..bba8689 100644 --- a/src/main/kotlin/ormapping/Main.kt +++ b/src/main/kotlin/ormapping/Main.kt @@ -1,11 +1,13 @@ package ormapping -import ormapping.command.CommandExecutor +import ormapping.command.* import ormapping.connection.DatabaseConfig import ormapping.connection.SQLiteConnection import ormapping.entity.Entity import ormapping.table.Table +import ormapping.sql.* +// Model danych data class Employee( var id: Int, @@ -13,8 +15,20 @@ data class Employee( ) : Entity object Employees : Table("employees", Employee::class) { - var id = integer("id").primaryKey() - var name = varchar("name", 255) + val id = integer("id").primaryKey() + val name = varchar("name", 255) +} + +data class Department( + var id: Int, + var employee_id: Int, + var department_name: String, +) : Entity + +object Departments : Table("departments", Department::class) { + val id = integer("id").primaryKey() + val employeeId = integer("employee_id") + val departmentName = varchar("department_name", 255) } fun main() { @@ -23,58 +37,203 @@ fun main() { ) val connection = SQLiteConnection.create(config) val executor = CommandExecutor(connection) - - // 1. Tworzymy dwóch pracowników - val employee1 = Employee(-1, "Jan Kowalski") + + println("=== Test 1: Tworzenie tabel ===") + try { + val createEmployeesTable = executor.createTable() + .fromTable(Employees) + + + println("Wygenerowane polecenie SQL:") + println(createEmployeesTable.build()) + + val createDepartmentsTable = executor.createTable() + .fromTable(Departments) + + println("Wygenerowane polecenie SQL:") + println(createDepartmentsTable.build()) + + executor.executeSQL(createEmployeesTable) + executor.executeSQL(createDepartmentsTable) + + println("Tabele zostały utworzone.") + } catch (e: Exception) { + println("Błąd podczas tworzenia tabel: ${e.message}") + } + + println("\n=== Test 2: Wstawianie danych ===") + val employee1 = Employee(1, "Jan Kowalski") val employee2 = Employee(2, "Anna Nowak") - println("Utworzeni pracownicy:") - println("Employee 1: $employee1") - println("Employee 2: $employee2") - println() - - // 2. Zapisujemy ich do bazy - executor.persist(Employees, employee1, employee2) - println("Pracownicy zostali zapisani do bazy") - println() - - // 3. Odczytujemy ich z bazy i wyświetlamy - val found1 = executor.find(Employees, 1) - val found2 = executor.find(Employees, 2) - println("Odczytani z bazy pracownicy:") - println("Found 1: $found1") - println("Found 2: $found2") - println() - - // 4. Usuwamy pierwszego pracownika - executor.delete(Employees, 1) - println("Usunięto pracownika 1") - println() - - // 5. Modyfikujemy drugiego pracownika - found2?.let { - it.name = "Anna Kowalska" // zmiana nazwiska - executor.update(Employees, it) - println("Zmodyfikowano pracownika 2") + val department1 = Department(1, 1, "HR") + val department2 = Department(2, 2, "IT") + + try { + executor.persist(Employees, employee1, employee2) + executor.persist(Departments, department1, department2) + println("Dane zostały wstawione.") + } catch (e: Exception) { + println("Błąd podczas wstawiania danych: ${e.message}") + } + + println("\n=== Test 3: Sprawdzenie zapisanych danych przez SQL DSL (WHERE) ===") + try { + val selectBuilder = executor.createSelect() + .select("*") + .from(Employees) + .where("id IN (1, 2)") + + val sql = selectBuilder.build() + println("Generated SQL Command:\n$sql") + + val selectCommand = executor.executeSQL(selectBuilder) as SelectCommand + println("Wyniki zapytania SELECT:") + selectCommand.printResults() + } catch (e: Exception) { + println("Błąd podczas SELECT: ${e.message}") + } + + println("\n=== Test 4: Zapytania JOIN ===") + try { + val selectBuilderLeftJoin = executor.createSelect() + .select(Employees.id, Employees.name, Departments.departmentName) + .from(Employees) + .leftJoin(Departments, Employees.id , Departments.employeeId) + .where("departments.department_name IS NOT NULL") + + val sqlLeftJoin = selectBuilderLeftJoin.build() + println("Generated SQL (LEFT JOIN):\n$sqlLeftJoin") + + val selectBuilderInnerJoin = executor.createSelect() + .select(Employees.id, Employees.name, Departments.departmentName) + .from(Employees) + .innerJoin(Departments, Employees.id, Departments.employeeId) + .where("departments.department_name = 'IT'") + + val sqlInnerJoin = selectBuilderInnerJoin.build() + println("Generated SQL (INNER JOIN):\n$sqlInnerJoin") + + } catch (e: Exception) { + println("Błąd przy testach JOIN: ${e.message}") } - println() - - // 6. Próbujemy odczytać obu pracowników (jeden powinien być null) - val afterDelete1 = executor.find(Employees, 1) - val afterUpdate2 = executor.find(Employees, 2) - println("Stan po usunięciu/modyfikacji:") - println("Employee 1 (powinien być null): $afterDelete1") - println("Employee 2 (zmodyfikowany): $afterUpdate2") - println() - - // 7. Usuwamy drugiego pracownika - executor.delete(Employees, 2) - println("Usunięto pracownika 2") - println() - - // 8. Próbujemy odczytać obu pracowników (oba null) - val final1 = executor.find(Employees, 1) - val final2 = executor.find(Employees, 2) - println("Stan końcowy (oba powinny być null):") - println("Employee 1: $final1") - println("Employee 2: $final2") + + println("\n=== Test 5: GROUP BY, HAVING, ORDER BY ===") + try { + val selectGroupByHaving = executor.createSelect() + .select(Departments.departmentName, "COUNT(*) AS employee_count") + .from(Departments) + .groupBy(Departments.departmentName) + .having("employee_count > 1") + + val sqlGroupByHaving = selectGroupByHaving.build() + println("Generated SQL (GROUP BY, HAVING):\n$sqlGroupByHaving") + + val selectOrderBy = executor.createSelect() + .select(Employees.id, Employees.name) + .from(Employees) + .orderBy("name ASC", "id DESC") + + val sqlOrderBy = selectOrderBy.build() + println("Generated SQL (ORDER BY):\n$sqlOrderBy") + + } catch (e: Exception) { + println("Błąd przy testach GROUP BY / HAVING / ORDER BY: ${e.message}") + } + + println("\n=== Test 6: UNION i UNION ALL ===") + try { + // Drugie zapytanie SELECT + val selectUnion = executor.createSelect() + .select(Employees.id, Employees.name) + .from(Employees) + .where("id > 2") + .build() + + // Budowanie zapytania UNION + val unionBuilder = executor.createSelect() + .select(Employees.id, Employees.name) + .from(Employees) + .where("id <= 2") + .union(selectUnion) // Dodaj drugie zapytanie jako część UNION + + // Generowanie finalnego SQL + val sqlUnion = unionBuilder.build() + println("Generated SQL (UNION, UNION ALL):\n$sqlUnion") + + } catch (e: Exception) { + println("Błąd przy testach UNION: ${e.message}") + } + println("\n=== Test 7: Funkcje agregujące ===") + try { + val selectAggregate = executor.createSelect() + .select( + SelectBuilder().count("id") + " AS total_employees", + SelectBuilder().sum("id") + " AS total_id", + SelectBuilder().avg("id") + " AS avg_id" + ) + .from(Employees) + + val sqlAggregate = selectAggregate.build() + println("Generated SQL (Aggregate Functions):\n$sqlAggregate") + + } catch (e: Exception) { + println("Błąd przy testach funkcji agregujących: ${e.message}") + } + + println("\n=== Test 8: Podzapytania (Subqueries) ===") + try { + val subQuery = executor.createSelect() + .select("AVG(id)") + .from(Employees) + .build() + + val selectSubQuery = executor.createSelect() + .select(Employees.id, Employees.name) + .from(Employees) + .where("id > ($subQuery)") + + val sqlSubQuery = selectSubQuery.build() + println("Generated SQL (Subquery):\n$sqlSubQuery") + + } catch (e: Exception) { + println("Błąd przy testach podzapytań: ${e.message}") + } + + println("\n=== Test 9: Usuwanie danych ===") + try { + val deleteBuilder = executor.createDelete() + .from(Employees) + .where("id = 1") + + val sqlDelete = deleteBuilder.build() + println("Generated SQL Command:\n$sqlDelete") + + val deleteCommand = executor.executeSQL(deleteBuilder) as DeleteCommand + println("Usunięto rekordów: ${deleteCommand.getAffectedRows()}") + } catch (e: Exception) { + println("Błąd podczas usuwania danych: ${e.message}") + } + + println("\n=== Test 10: Aktualizacja danych ===") + try { + val updatedEmployee = Employee(2, "Anna Kowalska") + executor.update(Employees, updatedEmployee) + println("Dane zostały zaktualizowane.") + } catch (e: Exception) { + println("Błąd podczas aktualizacji danych: ${e.message}") + } + + println("\n=== Test 11: Usuwanie tabel ===") + try { + val dropEmployeesTable = executor.dropTable(Employees) + val dropDepartmentsTable = executor.dropTable(Departments) + + executor.executeSQL(dropEmployeesTable) + executor.executeSQL(dropDepartmentsTable) + + println("Tabele zostały usunięte.") + } catch (e: Exception) { + println("Błąd podczas usuwania tabel: ${e.message}") + } + + connection.close() } diff --git a/src/main/kotlin/ormapping/command/Command.kt b/src/main/kotlin/ormapping/command/Command.kt new file mode 100644 index 0000000..3274bf7 --- /dev/null +++ b/src/main/kotlin/ormapping/command/Command.kt @@ -0,0 +1,16 @@ +// Command.kt +package ormapping.command + +import ormapping.connection.DatabaseConnection + +/** + * Bazowa klasa abstrakcyjna dla wzorca Command + * Wszystkie konkretne komendy muszą dziedziczyć po tej klasie + */ +abstract class Command { + /** + * Metoda wykonująca komendę + * @param connection Połączenie z bazą danych + */ + abstract fun execute(connection: DatabaseConnection) +} \ No newline at end of file diff --git a/src/main/kotlin/ormapping/command/CommandExecutor.kt b/src/main/kotlin/ormapping/command/CommandExecutor.kt index 5e5ba99..6fbb81e 100644 --- a/src/main/kotlin/ormapping/command/CommandExecutor.kt +++ b/src/main/kotlin/ormapping/command/CommandExecutor.kt @@ -1,6 +1,8 @@ package ormapping.command + import ormapping.connection.DatabaseConnection +import ormapping.sql.* import ormapping.entity.Entity import ormapping.table.CascadeType import ormapping.table.Relation @@ -15,6 +17,27 @@ import kotlin.reflect.full.memberProperties class CommandExecutor( private val connection: DatabaseConnection, ) { + fun createSelect(): SelectBuilder = SelectBuilder() + + fun createDelete(): DeleteBuilder = DeleteBuilder() + + fun createTable(): CreateTableBuilder = CreateTableBuilder() + + fun dropTable(table: Table<*>): DropTableBuilder { + return DropTableBuilder(connection.getDialect(), table, this) + } + // Metoda wykonująca zbudowane zapytanie + fun executeSQL(builder: SQLBuilder): SQLCommand { + val sql = builder.build() + return when (builder) { + is SelectBuilder -> SelectCommand(sql) + is DeleteBuilder -> DeleteCommand(sql) + is CreateTableBuilder -> CreateTableCommand(sql) + is DropTableBuilder -> DropTableCommand(sql) + else -> throw IllegalArgumentException("Unknown builder type") + }.also { it.execute(connection) } + } + fun find(table: Table, value: Any): T? { val primaryKeyColumns = table.primaryKey if (primaryKeyColumns.isEmpty()) { diff --git a/src/main/kotlin/ormapping/command/SQLCommand.kt b/src/main/kotlin/ormapping/command/SQLCommand.kt new file mode 100644 index 0000000..17fec1b --- /dev/null +++ b/src/main/kotlin/ormapping/command/SQLCommand.kt @@ -0,0 +1,63 @@ +// SQLCommand.kt +package ormapping.command + +import ormapping.connection.DatabaseConnection +import java.sql.ResultSet + +abstract class SQLCommand(protected val sql: String) : Command() { + abstract override fun execute(connection: DatabaseConnection) +} + +class SelectCommand(sql: String) : SQLCommand(sql) { + private lateinit var resultSet: ResultSet + + override fun execute(connection: DatabaseConnection) { + resultSet = connection.getConnection().prepareStatement(sql).executeQuery() + } + + fun getResults(): ResultSet = resultSet + + fun printResults() { + while (resultSet.next()) { + val metaData = resultSet.metaData + val columnCount = metaData.columnCount + + for (i in 1..columnCount) { + val columnName = metaData.getColumnName(i) + val value = resultSet.getString(i) + print("$columnName: $value | ") + } + println() + } + } +} + +class DeleteCommand(sql: String) : SQLCommand(sql) { + private var affectedRows: Int = 0 + + override fun execute(connection: DatabaseConnection) { + affectedRows = connection.getConnection().prepareStatement(sql).executeUpdate() + } + + fun getAffectedRows(): Int = affectedRows +} + +class CreateTableCommand(sql: String) : SQLCommand(sql) { + private var success: Boolean = false + + override fun execute(connection: DatabaseConnection) { + success = connection.getConnection().prepareStatement(sql).execute() + } + + fun isSuccess(): Boolean = success +} + +class DropTableCommand(sql: String) : SQLCommand(sql) { + private var success: Boolean = false + + override fun execute(connection: DatabaseConnection) { + success = connection.getConnection().prepareStatement(sql).execute() + } + + fun isSuccess(): Boolean = success +} \ No newline at end of file diff --git a/src/main/kotlin/ormapping/sql/CreateTableBuilder.kt b/src/main/kotlin/ormapping/sql/CreateTableBuilder.kt new file mode 100644 index 0000000..0d24b3b --- /dev/null +++ b/src/main/kotlin/ormapping/sql/CreateTableBuilder.kt @@ -0,0 +1,131 @@ +package ormapping.sql + +import ormapping.table.Column +import ormapping.table.Table +import java.math.BigDecimal +import java.time.LocalDate +import kotlin.reflect.KClass + +/** + * Builder, który na podstawie struktury obiektu Table<*> + * generuje polecenie CREATE TABLE ... w SQL. + */ +class CreateTableBuilder : SQLBuilder { + private var tableName: String = "" + private val columns = mutableListOf() + private val constraints = mutableListOf() + + /** + * Odczytuje metadane z obiektu [table] i tworzy definicje kolumn, kluczy obcych etc. + */ + fun fromTable(table: Table<*>): CreateTableBuilder { + // Ustawiamy nazwę tabeli + tableName = table._name + + // Dodajemy kolumny + table.columns.forEach { col -> + val sqlType = mapKClassToSQLType(col.type, col) + val colConstraints = buildColumnConstraints(col) + // Składamy definicję kolumny np. "id INTEGER PRIMARY KEY NOT NULL" + column(col.name, sqlType, *colConstraints.toTypedArray()) + } + + // Jeżeli mamy klucze obce, można je też tu przetwarzać i dodawać do constraints + table.foreignKeys.forEach { fk -> + // Dla uproszczenia: zakładamy, że jest tylko jedna kolumna w kluczu (lub bierzemy pierwszą). + val targetColumn = fk.targetColumns.firstOrNull() ?: return@forEach + + // FOREIGN KEY (orders_customer_id) REFERENCES customers(id) + val referencingColumn = "${fk.targetTable}_$targetColumn" + val referenceDefinition = "${fk.targetTable}($targetColumn)" + foreignKey(referencingColumn, referenceDefinition) + } + + return this + } + + /** + * Ręczne ustawienie nazwy tabeli (jeśli chcemy). + */ + fun name(name: String): CreateTableBuilder { + tableName = name + return this + } + + /** + * Dodaje definicję kolumny w stylu: + * "id INTEGER PRIMARY KEY", + * "name VARCHAR(255) NOT NULL" + */ + fun column(name: String, type: String, vararg modifiers: String): CreateTableBuilder { + val definition = listOf(type, *modifiers).joinToString(" ") + columns.add("$name $definition") + return this + } + + /** + * Dodaje constraint PRIMARY KEY. + * Można go użyć do definicji wielokolumnowych kluczy głównych, + * np. primaryKey("col1", "col2"). + */ + fun primaryKey(vararg columns: String): CreateTableBuilder { + constraints.add("PRIMARY KEY (${columns.joinToString(", ")})") + return this + } + + /** + * Dodaje constraint FOREIGN KEY (column) REFERENCES reference. + */ + fun foreignKey(column: String, reference: String): CreateTableBuilder { + constraints.add("FOREIGN KEY ($column) REFERENCES $reference") + return this + } + + /** + * Na końcu składamy instrukcję CREATE TABLE. + */ + override fun build(): String = buildString { + append("CREATE TABLE $tableName (\n") + append(columns.joinToString(",\n")) + if (constraints.isNotEmpty()) { + append(",\n") + append(constraints.joinToString(",\n")) + } + append("\n)") + } + + /** + * Mapa typów z `KClass` na konkretny typ SQL (dla prostych przypadków). + */ + private fun mapKClassToSQLType(kClass: KClass<*>, column: Column<*>): String { + return when (kClass) { + Int::class -> "INTEGER" + String::class -> { + // jeżeli mamy ustawioną długość > 0, to generujemy VARCHAR, w przeciwnym razie TEXT + if (column.length > 0) { + "VARCHAR(${column.length})" + } else { + "TEXT" + } + } + Boolean::class -> "BOOLEAN" + BigDecimal::class -> { + // Używamy precision i scale w definicji np. DECIMAL(10,2) + "DECIMAL(${column.precision},${column.scale})" + } + LocalDate::class -> "DATE" + else -> "TEXT" // fallback, można rzucić wyjątek albo inaczej obsłużyć + } + } + + /** + * Zbiera listę constraintów dla pojedynczej kolumny (PRIMARY KEY, NOT NULL, UNIQUE). + */ + private fun buildColumnConstraints(column: Column<*>): List { + val constraints = mutableListOf() + if (column.primaryKey) constraints.add("PRIMARY KEY") + if (!column.nullable) constraints.add("NOT NULL") + if (column.unique) constraints.add("UNIQUE") + return constraints + } +} diff --git a/src/main/kotlin/ormapping/sql/DeleteBuilder.kt b/src/main/kotlin/ormapping/sql/DeleteBuilder.kt new file mode 100644 index 0000000..69544f0 --- /dev/null +++ b/src/main/kotlin/ormapping/sql/DeleteBuilder.kt @@ -0,0 +1,91 @@ +package ormapping.sql + +import ormapping.table.Column +import ormapping.table.Table + +/** + * Builder, który generuje instrukcję DELETE FROM ... + * z opcjonalnym WHERE oraz (w pewnych dialektach) CASCADE. + */ +class DeleteBuilder : SQLBuilder { + private var tableName: String = "" + private val conditions = mutableListOf() + private var cascade = false + + /** + * Ustawia tabelę (na podstawie obiektu `Table<*>`), + * z której chcemy usuwać rekordy. + */ + fun from(table: Table<*>): DeleteBuilder { + this.tableName = table._name + return this + } + + /** + * Ustawia tabelę (za pomocą nazwy w postaci String), + * z której chcemy usuwać rekordy. + */ + fun from(tableName: String): DeleteBuilder { + this.tableName = tableName + return this + } + + /** + * Dodaje warunek do klauzuli WHERE (jako String). + * Można dodać wiele warunków, będą łączone klauzulą AND. + */ + fun where(condition: String): DeleteBuilder { + conditions.add(condition) + return this + } + + /** + * Przeciążona metoda `where`, pozwala budować + * proste warunki typu `id = 5` na podstawie kolumny z Table. + * + * Przykład użycia: + * .where(Employees.id, "=", 1) + */ + fun where(column: Column<*>, operator: String, value: Any?): DeleteBuilder { + val condition = buildString { + append(column.name) + append(" ") + append(operator) + append(" ") + // Jeżeli wartość jest Stringiem, dodaj cudzysłowy + if (value is String) { + append("'$value'") + } else { + append(value) + } + } + conditions.add(condition) + return this + } + + /** + * Ustawia kasowanie kaskadowe (zależne od dialektu SQL). + * W standardzie SQL CASCADE pojawia się głównie przy DROP TABLE + * albo przy usuwaniu rekordów, do których istnieją klucze obce + * zdefiniowane z CASCADE DELETE. Traktuj opcjonalnie. + */ + fun cascade(): DeleteBuilder { + cascade = true + return this + } + + /** + * Składa finalną komendę SQL, np: + * DELETE FROM employees WHERE id = 1 [CASCADE] + */ + override fun build(): String = buildString { + append("DELETE FROM $tableName") + if (conditions.isNotEmpty()) { + append(" WHERE ") + append(conditions.joinToString(" AND ")) + } + if (cascade) { + append(" CASCADE") + } + } +} diff --git a/src/main/kotlin/ormapping/sql/DropTableBuilder.kt b/src/main/kotlin/ormapping/sql/DropTableBuilder.kt new file mode 100644 index 0000000..108adf4 --- /dev/null +++ b/src/main/kotlin/ormapping/sql/DropTableBuilder.kt @@ -0,0 +1,74 @@ +package ormapping.sql + +import ormapping.command.CommandExecutor +import ormapping.connection.DatabaseConnection +import ormapping.dialect.SQLDialect +import ormapping.table.Table + +/** + * Builder, który generuje instrukcję: + * DROP TABLE [IF EXISTS] tableName [CASCADE] + */ +class DropTableBuilder( + private val dialect: SQLDialect, + table: Table<*>, + private val executor: CommandExecutor +) : SQLBuilder { + + private var tableName: String = table._name + private var ifExists = false + private var cascade = false + + /** + * Ustawia tabelę na podstawie obiektu `Table<*>`. + */ + fun fromTable(table: Table<*>): DropTableBuilder { + this.tableName = table._name + return this + } + + /** + * Ustawia tabelę na podstawie nazwy w postaci String. + */ + fun from(tableName: String): DropTableBuilder { + this.tableName = tableName + return this + } + + /** + * Dodaje klauzulę IF EXISTS (jeśli dialekt ją wspiera). + * Przykład: DROP TABLE IF EXISTS ... + */ + fun ifExists(): DropTableBuilder { + this.ifExists = true + return this + } + + /** + * Dodaje klauzulę CASCADE (jeśli dialekt ją wspiera). + * Przykład: DROP TABLE ... CASCADE + */ + fun cascade(): DropTableBuilder { + this.cascade = true + return this + } + + /** + * Buduje finalne zapytanie w stylu: + * DROP TABLE [IF EXISTS] tableName [CASCADE] + */ + override fun build(): String = buildString { + append("DROP TABLE ") + if (ifExists) { + // Dialekt np. PostgreSQL wspiera IF EXISTS, + // inne bazy mogą to ignorować lub rzucić błąd. + append("IF EXISTS ") + } + append(tableName) + if (cascade) { + // W PostgreSQL: "CASCADE"; + // w innych dialektach może to nie działać lub działać inaczej. + append(" CASCADE") + } + } +} diff --git a/src/main/kotlin/ormapping/sql/SQLBuilder.kt b/src/main/kotlin/ormapping/sql/SQLBuilder.kt new file mode 100644 index 0000000..5f76ed5 --- /dev/null +++ b/src/main/kotlin/ormapping/sql/SQLBuilder.kt @@ -0,0 +1,6 @@ +// SQLBuilder.kt +package ormapping.sql + +interface SQLBuilder { + fun build(): String +} \ No newline at end of file diff --git a/src/main/kotlin/ormapping/sql/SelectBuilder.kt b/src/main/kotlin/ormapping/sql/SelectBuilder.kt new file mode 100644 index 0000000..8c900e0 --- /dev/null +++ b/src/main/kotlin/ormapping/sql/SelectBuilder.kt @@ -0,0 +1,154 @@ +package ormapping.sql + +import ormapping.table.Column +import ormapping.table.Table + +class SelectBuilder : SQLBuilder { + private val columns = mutableListOf() + private val tables = mutableListOf() + private val joins = mutableListOf() + private val conditions = mutableListOf() + private val groupBy = mutableListOf() + private val orderBy = mutableListOf() + private var distinct = false + private var limit: Int? = null + private val having = mutableListOf() + private val unions = mutableListOf() + private val tableAliases = mutableMapOf() // Mapowanie nazwy kolumny -> aliasu tabeli + + fun union(query: String): SelectBuilder { + unions.add("UNION $query") + return this + } + + fun unionAll(query: String): SelectBuilder { + unions.add("UNION ALL $query") + return this + } + + fun having(condition: String): SelectBuilder { + having.add(condition) + return this + } + + fun select(vararg cols: Any): SelectBuilder { + columns.addAll(cols.map { + when (it) { + is Column<*> -> { + "${it.table._name}.${it.name}" + } + is String -> it + else -> throw IllegalArgumentException("Unsupported column type: $it") + } + }) + return this + } + + fun from(table: Table<*>, alias: String? = null): SelectBuilder { + val tableAlias = alias ?: table._name + tables.add("${table._name} ${alias.orEmpty()}".trim()) + table.columns.forEach { column -> tableAliases[column.name] = tableAlias } // Rejestracja aliasów + return this + } + + fun innerJoin(table: Table<*>, columnLeft: Column<*>, columnRight: Column<*>): SelectBuilder { + val joinStatement = "INNER JOIN ${table._name} ON ${columnLeft.table._name}.${columnLeft.name} = ${columnRight.table._name}.${columnRight.name}".trim() + joins.add(joinStatement) +// table.columns.forEach { column -> tableAliases[column.name] = alias ?: table._name } // Rejestracja aliasów + table.columns.forEach { column -> + tableAliases[column.name] = table._name + } + + return this + } + + fun leftJoin(table: Table<*>, columnLeft: Column<*>, columnRight: Column<*>): SelectBuilder { + val joinStatement = "LEFT JOIN ${table._name} ON ${columnLeft.table._name}.${columnLeft.name} = ${columnRight.table._name}.${columnRight.name}".trim() + joins.add(joinStatement) +// table.columns.forEach { column -> tableAliases[column.name] = alias ?: table._name } // Rejestracja aliasów + table.columns.forEach { column -> + tableAliases[column.name] = table._name + } + + return this + } + + fun where(vararg conditions: Any): SelectBuilder { + this.conditions.addAll(conditions.map { + when (it) { + is Column<*> -> { + val tableName = tableAliases[it.name] ?: throw IllegalArgumentException("Alias for column ${it.name} not set") + "$tableName.${it.name}" + } + is String -> it + else -> throw IllegalArgumentException("Unsupported condition type: $it") + } + }) + return this + } + + fun groupBy(vararg cols: Any): SelectBuilder { + groupBy.addAll(cols.map { + when (it) { + is Column<*> -> { + val tableName = tableAliases[it.name] ?: throw IllegalArgumentException("Alias for column ${it.name} not set") + "$tableName.${it.name}" + } + is String -> it + else -> throw IllegalArgumentException("Unsupported column type in GROUP BY: $it") + } + }) + return this + } + + fun orderBy(vararg cols: Any): SelectBuilder { + orderBy.addAll(cols.map { + when (it) { + is Column<*> -> { + val tableName = tableAliases[it.name] ?: throw IllegalArgumentException("Alias for column ${it.name} not set") + "$tableName.${it.name}" + } + is String -> it + else -> throw IllegalArgumentException("Unsupported column type in ORDER BY: $it") + } + }) + return this + } + + fun distinct(): SelectBuilder { + distinct = true + return this + } + + fun limit(value: Int): SelectBuilder { + limit = value + return this + } + + fun count(column: String = "*"): String = "COUNT($column)" + fun max(column: String): String = "MAX($column)" + fun min(column: String): String = "MIN($column)" + fun avg(column: String): String = "AVG($column)" + fun sum(column: String): String = "SUM($column)" + + override fun build(): String { + if (tables.isEmpty()) throw IllegalStateException("FROM clause is required") + return buildString { + append("SELECT ") + if (distinct) append("DISTINCT ") + append(columns.joinToString(", ").ifEmpty { "*" }) + append(" FROM ") + append(tables.joinToString(", ")) + if (joins.isNotEmpty()) append(" ").append(joins.joinToString(" ")) + if (conditions.isNotEmpty()) append(" WHERE ").append(conditions.joinToString(" AND ")) + if (groupBy.isNotEmpty()) append(" GROUP BY ").append(groupBy.joinToString(", ")) + if (having.isNotEmpty()) append(" HAVING ").append(having.joinToString(" AND ")) + if (orderBy.isNotEmpty()) append(" ORDER BY ").append(orderBy.joinToString(", ")) + limit?.let { append(" LIMIT $it") } + if (unions.isNotEmpty()) { + append(" ") + append(unions.joinToString(" ")) + } + } + } +} diff --git a/src/main/kotlin/ormapping/table/Column.kt b/src/main/kotlin/ormapping/table/Column.kt index a194cc9..0c9ef9d 100644 --- a/src/main/kotlin/ormapping/table/Column.kt +++ b/src/main/kotlin/ormapping/table/Column.kt @@ -10,8 +10,11 @@ class Column( var primaryKey: Boolean = false, var nullable: Boolean = false, var unique: Boolean = false, - private val length: Int = 0, - private val scale: Int = 0, - private val precision: Int = 0, -) + val length: Int = 0, + val scale: Int = 0, + val precision: Int = 0, + +) { + lateinit var table: Table<*> +} diff --git a/src/main/kotlin/ormapping/table/Table.kt b/src/main/kotlin/ormapping/table/Table.kt index 25714ea..f297f85 100644 --- a/src/main/kotlin/ormapping/table/Table.kt +++ b/src/main/kotlin/ormapping/table/Table.kt @@ -28,30 +28,40 @@ abstract class Table( private val _primaryKey = mutableListOf>() val primaryKey: List> get() = _primaryKey.toList() + + fun addForeignKey(fk: ForeignKey) { + _foreignKeys.add(fk) + } fun integer(name: String): Column = Column(name, Int::class).also { _columns.add(it) + it.table = this } fun varchar(name: String, length: Int): Column = Column(name, String::class, length = length).also { _columns.add(it) + it.table = this } fun text(name: String): Column = Column(name, String::class).also { _columns.add(it) + it.table = this } fun boolean(name: String): Column = Column(name, Boolean::class).also { _columns.add(it) + it.table = this } fun date(name: String): Column = Column(name, LocalDate::class).also { _columns.add(it) + it.table = this } fun decimal(name: String, precision: Int, scale: Int): Column = Column(name, BigDecimal::class, precision = precision, scale = scale).also { _columns.add(it) + it.table = this }