diff --git a/README.md b/README.md index 472b630..9f2e7fd 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ You get the ergonomics of an ORM without losing the transparency of SQL. Everyth * [Quick Start](#quick-start) * [1) Define your model](#1-define-your-model) - * [2) Create your-dbcontext](#2-create-your-dbcontext) + * [2) Create your DbContext](#2-create-your-dbcontext)(#2-create-your-dbcontext) * [3) Use a repository](#3-use-a-repository) * [Fluent SQL Builders](#fluent-sql-builders) @@ -32,6 +32,16 @@ You get the ergonomics of an ORM without losing the transparency of SQL. Everyth * [UpdateSql](#updatesql) * [DeleteSql](#deletesql) * [Single-Record Helpers](#single-record-helpers) +* [Custom Type Converters (NEW)](#custom-type-converters-new) + + * [When to use](#when-to-use) + * [Implement a converter](#implement-a-converter) + * [Annotate your field](#annotate-your-field) + * [How it works under the hood](#how-it-works-under-the-hood) + * [Examples](#examples) + * [Migrations & schema notes](#migrations--schema-notes) + * [FAQ for converters](#faq-for-converters) +* [Simple Event Bus (NEW)](#simple-event-bus-new) * [Async & Threading](#async--threading) * [Migrations](#migrations) * [Tuning & Pragmas](#tuning--pragmas) @@ -48,6 +58,8 @@ You get the ergonomics of an ORM without losing the transparency of SQL. Everyth ## Highlights * **Annotation Mapping**: Map entities with `@DbTableAnnotation` and `@DbColumnAnnotation` (PK, identity, nullability, ordinal). +* **Custom Type Converters (NEW)**: Store non‑primitive/complex types using `@DbConverterAnnotation` + `TypeConverter`. Full round‑trip on **INSERT/UPDATE/DELETE/SELECT**. +* **Simple Event Bus (NEW)**: Lightweight, zero‑dep, in‑process pub/sub for domain events (e.g., fire `TodoCreatedEvent` → handle in one place). * **Generic Repository**: Reusable `GenericRepository` handles async CRUD in a thread-safe way. * **Fluent Query Builders**: Type-aware `Select`, `UpdateSql`, `DeleteSql` compile to parameterized SQL with escaped identifiers. * **Safe by Default**: Parameter binding everywhere. Identifiers backticked. Null-safe APIs (e.g., `whereNull` / `whereNotNull`). @@ -104,7 +116,7 @@ import com.example.adbkit.entities.Todo; public class DbContext extends ADbContext { private static final String DB_NAME = "local.db"; - private static final int VERSION = 3; + private static final int VERSION = 5; public DbContext(Context context) { super(context, DB_NAME, VERSION); } @@ -250,6 +262,159 @@ todos.deleteById(rowsRes -> { }, 123); ``` +--- +## Custom Type Converters (NEW) + +Many apps need to persist **enums, dates, JSON objects, UUIDs** or any domain‑specific value object. With custom converters you can declare **how to serialize** your field to a SQLite‑friendly type and **how to deserialize** it back. + +### When to use + +Use converters whenever a field is not directly representable as a primitive SQLite column, or when you want a custom storage representation (e.g., `Date` as epoch millis instead of ISO `TEXT`). + +### Implement a converter + +Create a class that implements `TypeConverter` where: + +* `F` = field type in your model (e.g., `EventType`, `Date`) +* `D` = database column type (e.g., `String`, `Long`, `byte[]`) + +```java +import lib.persistence.converters.TypeConverter; + +public final class EventTypeConverter implements TypeConverter { + @Override public String toDatabaseValue(EventType v) { return v == null ? null : v.name(); } + @Override public EventType fromDatabaseValue(String s) { return s == null ? null : EventType.valueOf(s); } + @Override public String sqliteType() { return "TEXT"; } // column type in DDL +} + +public final class DateMillisConverter implements TypeConverter { + @Override public Long toDatabaseValue(java.util.Date v) { return v == null ? null : v.getTime(); } + @Override public java.util.Date fromDatabaseValue(Long t) { return t == null ? null : new java.util.Date(t); } + @Override public String sqliteType() { return "INTEGER"; } +} +``` + +> `sqliteType()` informs the table generator which SQLite type to emit in `CREATE TABLE`. + +### Annotate your field + +Attach a converter to a field with `@DbConverterAnnotation`: + +```java +import lib.persistence.annotations.*; + +@DbTableAnnotation(name = "events") +public class Event { + @DbColumnAnnotation(ordinal = 1, isPrimaryKey = true, isIdentity = true) + int id; + + @DbColumnAnnotation(ordinal = 2, name = "event_type") + @DbConverterAnnotation(converter = EventTypeConverter.class) + EventType type; + + @DbColumnAnnotation(ordinal = 3, name = "event_message") + String message; + + @DbColumnAnnotation(ordinal = 4, name = "created_at") + @DbConverterAnnotation(converter = DateMillisConverter.class) + java.util.Date createdAt; + + public Event() {} +} +``` + +### How it works under the hood + +* **DDL generation**: `CreateTableCommand` inspects fields; if a field has a converter, its `sqliteType()` determines the column type. +* **INSERT/UPDATE**: `Mapper.objectToContentValues` calls your converter’s `toDatabaseValue(...)` before writing into `ContentValues`. +* **SELECT**: `Mapper.cursorToObject` fetches the raw DB value and calls `fromDatabaseValue(...)` to set the field. +* **DELETE/UPDATE WHERE (PK)**: If a **primary key** field is annotated with a converter, the key is transformed with `toDatabaseValue(...)` before binding to the `WHERE` clause. +* **Caching**: Converters are cached internally for performance; you don’t have to register them manually. + +### Examples + +**1) Enum ↔ TEXT** + +```java +@DbConverterAnnotation(converter = EventTypeConverter.class) +EventType type; // stored as TEXT (e.g., "CREATED") +``` + +**2) Date ↔ INTEGER (epoch millis)** + +```java +@DbConverterAnnotation(converter = DateMillisConverter.class) +Date createdAt; // stored as INTEGER +``` + +**3) JSON ↔ TEXT** + +```java +public final class AddressJsonConverter implements TypeConverter { + @Override public String toDatabaseValue(Address v) { return v == null ? null : v.toJson(); } + @Override public Address fromDatabaseValue(String s) { return s == null ? null : Address.fromJson(s); } + @Override public String sqliteType() { return "TEXT"; } +} +``` +### Migrations & schema notes + +* Changing a field to use a converter **may change the column type** (e.g., `TEXT` → `INTEGER`). Provide a migration step if your table already exists. +* Identity integer PKs remain `INTEGER PRIMARY KEY AUTOINCREMENT` and are **not** updated via SET. +* Ensure `ordinal` values are unique per entity and add a **no‑arg constructor** for reflection. + +### FAQ for converters + +* **Do I need to register converters?** No. Attach with `@DbConverterAnnotation` on fields. Instances are cached internally. +* **Are converters used in custom `Select` builders?** Builders bind your literal arguments. Mapping back to objects always goes through `Mapper.cursorToObject`, so fields with converters are correctly materialized. +* **Composite PKs?** Supported. If a PK field has a converter, it’s applied to the `WHERE` binding. + +--- + +# Simple Event Bus (NEW) + +A tiny, zero‑dependency pub/sub helper to emit domain events from repositories/UI and handle them in one place. Great for logging/auditing (e.g., store `Event` rows when a `Todo` is created/updated/deleted). + +### API + +```java +public final class SimpleEventBus { + // Register a single consumer per event type + public static void subscribe(Class eventType, java.util.function.Consumer listener); + public static void post(T event); // Synchronous, in‑process +} +``` + +> Current implementation keeps **one subscriber per event class** (a simple `Map, Consumer>`). If you need fan‑out (N subscribers), replace the `Consumer` with a `List>` — the public API can stay the same. + +### Built‑in sample events + +```java +// app/src/main/java/com/example/adbkit/events +class TodoCreatedEvent { public final Todo todo; /* ctor */ } +class TodoUpdatedEvent { public final Todo todo; /* ctor */ } +class TodoDeletedEvent { public final Todo todo; /* ctor */ } +``` + +### Wiring example (MainActivity) + +```java +// Subscribe once (e.g., in onCreate) +SimpleEventBus.subscribe(TodoCreatedEvent.class, e -> { + Event ev = new Event(EventType.CREATED, "Yeni Todo: " + e.todo.id + " title: " + e.todo.title); + eventRepository.insert(ev, r -> { /* handle DbResult */ }); +}); + +// Post from your repository callbacks +SimpleEventBus.post(new TodoCreatedEvent(createdTodo)); +``` + +This example persists an `Event` row with `EventType` and message. `EventType` is stored as `TEXT` via `EventTypeConverter`; `created_at` as epoch millis via `DateMillisConverter`. + +### Notes + +* Calls are **synchronous** on the caller thread; keep handlers light and push heavy work to your existing executors. +* The bus is package‑private and simple by design. For cross‑module or sticky events, bring your favorite event lib. + --- ## Async & Threading @@ -286,6 +451,18 @@ new MigrationStep() { }; ``` +Use versioned steps or simple recreate for demos: + +```java +new MigrationStep() { + public int from() { return 4; } + public int to() { return 5; } + public void apply(SQLiteDatabase db) { + db.execSQL(CreateTableCommand.build(Event.class).getQuery()); + } +}; +``` + --- ## Tuning & Pragmas @@ -332,7 +509,7 @@ todos.insert(item, result -> { A: Only if you want to. Builders cover most cases; raw SQL is still available via `rawQuery`. **Q: How are booleans stored?** -A: As `INTEGER` (0/1). Dates as ISO strings when using `LocalDate`/`LocalDateTime`. +A: As `INTEGER` (0/1). Dates as ISO strings when using `LocalDate`/`LocalDateTime`. Dates can also be stored via converters (e.g., epoch millis with `DateMillisConverter`), or as text if you provide a different converter. **Q: Composite primary keys?** A: Fully supported in `DeleteCommand`, `UpdateCommand`, and builder APIs. diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 42201fc..a7f3a9e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - + { + Event newEvent = new Event(EventType.CREATED, "Todo: " + event.todo.id + " title: " + event.todo.title); + + eventRepository.insert(newEvent, result -> { + if (!result.isSuccess()) { + Exception e = ((DbResult.Error) result).getException(); + Log.e(TAG, "CREATE - Hata: " + e.getMessage()); + } + }); + +// eventRepository.insert(newEvent, new DbCallback() { +// @Override +// public void onResult(DbResult result) { +// if (!result.isSuccess()) { +// Exception e = ((DbResult.Error) result).getException(); +// Log.e(TAG, "CREATE - Hata: " + e.getMessage()); +// } +// } +// }); + }); + + SimpleEventBus.subscribe(TodoUpdatedEvent.class, event -> { + Event newEvent = new Event(EventType.UPDATED, "Todo: " + event.todo.id + " title: " + event.todo.title); + + eventRepository.insert(newEvent, result -> { + if (!result.isSuccess()) { + Exception e = ((DbResult.Error) result).getException(); + Log.e(TAG, "UPDATE - Hata: " + e.getMessage()); + } + }); + +// eventRepository.insert(newEvent, new DbCallback() { +// @Override +// public void onResult(DbResult result) { +// if (!result.isSuccess()) { +// Exception e = ((DbResult.Error) result).getException(); +// Log.e(TAG, "UPDATE - Hata: " + e.getMessage()); +// } +// } +// }); + }); + + SimpleEventBus.subscribe(TodoDeletedEvent.class, event -> { + Event newEvent = new Event(EventType.DELETED, "Todo: " + event.todo.id + " title: " + event.todo.title); + eventRepository.insert(newEvent, result -> { + if (!result.isSuccess()) { + Exception e = ((DbResult.Error) result).getException(); + Log.e(TAG, "DELETE - Hata: " + e.getMessage()); + } + }); + }); // Örnek bir işlem başlatmak için bir buton tanımlayalım Button startDbOpsButton = findViewById(R.id.start_db_ops_button); @@ -119,6 +181,7 @@ public void onResult(DbResult result) { Todo updatedTodo = ((DbResult.Success) result).getData(); Log.d(TAG, "UPDATE - Başarılı: " + updatedTodo); + SimpleEventBus.post(new TodoUpdatedEvent(updatedTodo)); // Bir sonraki adıma geçelim: okuma readUpdatedTodo(updatedTodo.id); } else { @@ -160,8 +223,23 @@ private void deleteTodo(Todo todoToDelete) { public void onResult(DbResult result) { if (result.isSuccess()) { Log.d(TAG, "DELETE - Başarılı: Todo silindi."); + + SimpleEventBus.post(new TodoDeletedEvent(todoToDelete)); + // Son kontrol için tüm todoları tekrar okuyalım readAllTodos(); + + eventRepository.selectAll(new DbCallback>() { + @Override + public void onResult(DbResult> result) { + ArrayList events = ((DbResult.Success>) result).getData(); + Log.d(TAG, "READ ALL - Başarılı, bulunan Todo sayısı: " + events.size()); + for (Event event : events) { + Log.d(TAG, "READ ALL - Todo: " + event.toString()); + } + } + }); + } else { Exception e = ((DbResult.Error) result).getException(); Log.e(TAG, "DELETE - Hata: " + e.getMessage()); diff --git a/app/src/main/java/com/example/adbkit/entities/DateMillisConverter.java b/app/src/main/java/com/example/adbkit/entities/DateMillisConverter.java new file mode 100644 index 0000000..33e409f --- /dev/null +++ b/app/src/main/java/com/example/adbkit/entities/DateMillisConverter.java @@ -0,0 +1,11 @@ +package com.example.adbkit.entities; + +import java.util.Date; + +import lib.persistence.converters.TypeConverter; + +public final class DateMillisConverter implements TypeConverter { + @Override public Long toDatabaseValue(Date v) { return v == null ? null : v.getTime(); } + @Override public Date fromDatabaseValue(Long t) { return t == null ? null : new Date(t); } + @Override public String sqliteType() { return "INTEGER"; } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/adbkit/entities/Event.java b/app/src/main/java/com/example/adbkit/entities/Event.java new file mode 100644 index 0000000..ddb33bd --- /dev/null +++ b/app/src/main/java/com/example/adbkit/entities/Event.java @@ -0,0 +1,41 @@ +package com.example.adbkit.entities; + +import lib.persistence.annotations.DbColumnAnnotation; +import lib.persistence.annotations.DbConverterAnnotation; +import lib.persistence.annotations.DbTableAnnotation; + +@DbTableAnnotation(name = "events") +public class Event { + @DbColumnAnnotation(ordinal = 1, isPrimaryKey = true, isIdentity = true) + private int id; + + @DbColumnAnnotation(ordinal = 2, name = "event_type") + @DbConverterAnnotation(converter = EventTypeConverter.class) + private EventType type; + + @DbColumnAnnotation(ordinal = 3, name = "event_message") + private String message; + + @DbColumnAnnotation(ordinal = 4, name = "created_at") + @DbConverterAnnotation(converter = DateMillisConverter.class) + private java.util.Date createdAt; + + public Event(){ + + } + public Event(EventType type, String message){ + this.type = type; + this.message = message; + this.createdAt = java.util.Date.from(new java.util.Date().toInstant()); + } + + @Override + public String toString() { + return "Event{" + + "id=" + id + + ", type=" + type + + ", message='" + message + '\'' + + ", createdAt=" + createdAt + + '}'; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/adbkit/entities/EventType.java b/app/src/main/java/com/example/adbkit/entities/EventType.java new file mode 100644 index 0000000..d3e2060 --- /dev/null +++ b/app/src/main/java/com/example/adbkit/entities/EventType.java @@ -0,0 +1,7 @@ +package com.example.adbkit.entities; + +public enum EventType { + CREATED, + UPDATED, + DELETED +} diff --git a/app/src/main/java/com/example/adbkit/entities/EventTypeConverter.java b/app/src/main/java/com/example/adbkit/entities/EventTypeConverter.java new file mode 100644 index 0000000..f795e28 --- /dev/null +++ b/app/src/main/java/com/example/adbkit/entities/EventTypeConverter.java @@ -0,0 +1,9 @@ +package com.example.adbkit.entities; + +import lib.persistence.converters.TypeConverter; + +public final class EventTypeConverter implements TypeConverter { + @Override public String toDatabaseValue(EventType v) { return v == null ? null : v.name(); } + @Override public EventType fromDatabaseValue(String s) { return s == null ? null : EventType.valueOf(s); } + @Override public String sqliteType() { return "TEXT"; } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/adbkit/events/SimpleEventBus.java b/app/src/main/java/com/example/adbkit/events/SimpleEventBus.java new file mode 100644 index 0000000..c6d0abe --- /dev/null +++ b/app/src/main/java/com/example/adbkit/events/SimpleEventBus.java @@ -0,0 +1,24 @@ +package com.example.adbkit.events; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; + +public class SimpleEventBus { + private static final Map, Consumer> subscribers = new ConcurrentHashMap<>(); + + // Olay aboneliği + @SuppressWarnings("unchecked") + public static void subscribe(Class eventType, Consumer listener) { + subscribers.put(eventType, listener); + } + + // Olay yayınlama + @SuppressWarnings("unchecked") + public static void post(T event) { + Consumer listener = (Consumer) subscribers.get(event.getClass()); + if (listener != null) { + listener.accept(event); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/adbkit/events/TodoCreatedEvent.java b/app/src/main/java/com/example/adbkit/events/TodoCreatedEvent.java new file mode 100644 index 0000000..1a2244a --- /dev/null +++ b/app/src/main/java/com/example/adbkit/events/TodoCreatedEvent.java @@ -0,0 +1,10 @@ +package com.example.adbkit.events; + +import com.example.adbkit.entities.Todo; + +public class TodoCreatedEvent { + public final Todo todo; + public TodoCreatedEvent(Todo todo) { + this.todo = todo; + } +} diff --git a/app/src/main/java/com/example/adbkit/events/TodoDeletedEvent.java b/app/src/main/java/com/example/adbkit/events/TodoDeletedEvent.java new file mode 100644 index 0000000..6d4fea7 --- /dev/null +++ b/app/src/main/java/com/example/adbkit/events/TodoDeletedEvent.java @@ -0,0 +1,10 @@ +package com.example.adbkit.events; + +import com.example.adbkit.entities.Todo; + +public class TodoDeletedEvent { + public final Todo todo; + public TodoDeletedEvent(Todo todo) { + this.todo = todo; + } +} diff --git a/app/src/main/java/com/example/adbkit/events/TodoUpdatedEvent.java b/app/src/main/java/com/example/adbkit/events/TodoUpdatedEvent.java new file mode 100644 index 0000000..45f4f4f --- /dev/null +++ b/app/src/main/java/com/example/adbkit/events/TodoUpdatedEvent.java @@ -0,0 +1,10 @@ +package com.example.adbkit.events; + +import com.example.adbkit.entities.Todo; + +public class TodoUpdatedEvent { + public final Todo todo; + public TodoUpdatedEvent(Todo todo) { + this.todo = todo; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/adbkit/repositories/EventRepository.java b/app/src/main/java/com/example/adbkit/repositories/EventRepository.java new file mode 100644 index 0000000..446f59f --- /dev/null +++ b/app/src/main/java/com/example/adbkit/repositories/EventRepository.java @@ -0,0 +1,12 @@ +package com.example.adbkit.repositories; + +import com.example.adbkit.entities.Event; + +import lib.persistence.GenericRepository; +import lib.persistence.IDbContext; + +public class EventRepository extends GenericRepository { + public EventRepository(IDbContext context) { + super(context, Event.class); + } +} diff --git a/app/src/main/java/com/example/adbkit/repositories/TodoRepository.java b/app/src/main/java/com/example/adbkit/repositories/TodoRepository.java index 984dcd2..ef5a349 100644 --- a/app/src/main/java/com/example/adbkit/repositories/TodoRepository.java +++ b/app/src/main/java/com/example/adbkit/repositories/TodoRepository.java @@ -1,11 +1,9 @@ package com.example.adbkit.repositories; +import com.example.adbkit.entities.Todo; + import lib.persistence.GenericRepository; import lib.persistence.IDbContext; -import lib.persistence.command.query.Select; -import lib.persistence.command.query.SelectQuery; -import com.example.adbkit.entities.Todo; -import com.example.adbkit.DbContext; public class TodoRepository extends GenericRepository { diff --git a/app/src/main/java/lib/persistence/GenericRepository.java b/app/src/main/java/lib/persistence/GenericRepository.java index 9dbbe33..23cc8e5 100644 --- a/app/src/main/java/lib/persistence/GenericRepository.java +++ b/app/src/main/java/lib/persistence/GenericRepository.java @@ -3,8 +3,6 @@ import android.database.Cursor; -import com.example.adbkit.DbContext; - import java.lang.reflect.Field; import java.util.ArrayList; import java.util.HashMap; diff --git a/app/src/main/java/lib/persistence/RepositoryFactory.java b/app/src/main/java/lib/persistence/RepositoryFactory.java index 568df24..6a8ef33 100644 --- a/app/src/main/java/lib/persistence/RepositoryFactory.java +++ b/app/src/main/java/lib/persistence/RepositoryFactory.java @@ -3,7 +3,7 @@ import android.content.Context; import com.example.adbkit.MyApplication; - +import com.example.adbkit.repositories.EventRepository; import com.example.adbkit.repositories.TodoRepository; public final class RepositoryFactory { @@ -14,6 +14,12 @@ public static TodoRepository getTodoRepository(Context appContext) { // TodoRepository constructor'ı artık IDbContext almalı return new TodoRepository(application.getDbContext()); } + + public static EventRepository getEventRepository(Context appContext) { + MyApplication application = (MyApplication) appContext.getApplicationContext(); + // TodoRepository constructor'ı artık IDbContext almalı + return new EventRepository(application.getDbContext()); + } } /* public class RepositoryFactory { diff --git a/app/src/main/java/lib/persistence/annotations/DbConverterAnnotation.java b/app/src/main/java/lib/persistence/annotations/DbConverterAnnotation.java new file mode 100644 index 0000000..06a34ed --- /dev/null +++ b/app/src/main/java/lib/persistence/annotations/DbConverterAnnotation.java @@ -0,0 +1,14 @@ +package lib.persistence.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import lib.persistence.converters.TypeConverter; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface DbConverterAnnotation { + Class> converter(); +} diff --git a/app/src/main/java/lib/persistence/command/definition/CreateTableCommand.java b/app/src/main/java/lib/persistence/command/definition/CreateTableCommand.java index 4f46714..cfbfe33 100644 --- a/app/src/main/java/lib/persistence/command/definition/CreateTableCommand.java +++ b/app/src/main/java/lib/persistence/command/definition/CreateTableCommand.java @@ -1,21 +1,25 @@ // lib/persistence/command/definition/CreateTableCommand.java package lib.persistence.command.definition; -import lib.persistence.annotations.DbTableAnnotation; -import lib.persistence.profile.DbColumn; -import lib.persistence.profile.DbDataType; -import lib.persistence.profile.Mapper; +import static lib.persistence.SqlNames.qId; import java.util.ArrayList; import java.util.StringJoiner; -import static lib.persistence.SqlNames.qId; +import lib.persistence.profile.DbColumn; +import lib.persistence.profile.DbDataType; +import lib.persistence.profile.Mapper; public class CreateTableCommand { private final String query; - private CreateTableCommand(String query) { this.query = query; } - public static CreateTableCommand build(Class type) { return build(type, (String[]) null); } + private CreateTableCommand(String query) { + this.query = query; + } + + public static CreateTableCommand build(Class type) { + return build(type, (String[]) null); + } public static CreateTableCommand build(Class type, String... tableConstraints) { if (type == null) throw new IllegalArgumentException("type boş olamaz"); @@ -35,7 +39,8 @@ public static CreateTableCommand build(Class type, String... tableConstraints for (DbColumn c : cols) { StringBuilder d = new StringBuilder(); - d.append(qId(c.getColumnName())).append(' ').append(toSqlType(c.getDataType())); + //d.append(qId(c.getColumnName())).append(' ').append(toSqlType(c.getDataType())); + d.append(qId(c.getColumnName())).append(' ').append(columnSqlType(c)); // Sadece tek PK varsa ve sütun düzeyinde ifade etmek istiyorsak: if (pks.size() == 1 && pks.get(0) == c) { @@ -67,14 +72,28 @@ public static CreateTableCommand build(Class type, String... tableConstraints return new CreateTableCommand(sql); } + // YENİ: Converter bildirimi varsa onu kullan; yoksa mevcut DbDataType -> SQL mapping + private static String columnSqlType(DbColumn c) { + String fromConverter = c.getSqliteType(); + if (fromConverter != null && !fromConverter.isEmpty()) return fromConverter; + return toSqlType(c.getDataType()); + } + private static String toSqlType(DbDataType t) { switch (t) { - case INTEGER: return "INTEGER"; - case REAL: return "REAL"; - case BLOB: return "BLOB"; + case INTEGER: + return "INTEGER"; + case REAL: + return "REAL"; + case BLOB: + return "BLOB"; case TEXT: - default: return "TEXT"; + default: + return "TEXT"; } } - public String getQuery() { return query; } + + public String getQuery() { + return query; + } } \ No newline at end of file diff --git a/app/src/main/java/lib/persistence/command/manipulation/DeleteCommand.java b/app/src/main/java/lib/persistence/command/manipulation/DeleteCommand.java index 41f3ca2..36a5dcb 100644 --- a/app/src/main/java/lib/persistence/command/manipulation/DeleteCommand.java +++ b/app/src/main/java/lib/persistence/command/manipulation/DeleteCommand.java @@ -4,14 +4,16 @@ import static lib.persistence.SqlNames.qCol; import static lib.persistence.SqlNames.qId; -import lib.persistence.profile.DbColumn; -import lib.persistence.profile.Mapper; - import java.lang.reflect.Field; -import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; +import lib.persistence.annotations.DbConverterAnnotation; +import lib.persistence.converters.ConverterRegistry; +import lib.persistence.converters.TypeConverter; +import lib.persistence.profile.DbColumn; +import lib.persistence.profile.Mapper; + /** * DELETE komutu derleyicisi: * - entity'den (build(entity)) PK değerlerini okuyarak @@ -45,18 +47,37 @@ public static DeleteCommand build(Object entity) { .collect(Collectors.joining(" AND ")); String[] args = new String[pks.size()]; +// try { +// for (int i = 0; i < pks.size(); i++) { +// Field f = Mapper.findField(type, pks.get(i).getFieldName()); +// f.setAccessible(true); +// Object v = f.get(entity); +// if (v == null) throw new IllegalStateException("PK değeri null olamaz: " + pks.get(i).getFieldName()); +// args[i] = String.valueOf(v); +// } +// } catch (ReflectiveOperationException e) { +// throw new RuntimeException("PK değer(ler)i okunamadı", e); +// } + try { for (int i = 0; i < pks.size(); i++) { - Field f = Mapper.findField(type, pks.get(i).getFieldName()); + DbColumn pk = pks.get(i); + Field f = Mapper.findField(type, pk.getFieldName()); f.setAccessible(true); Object v = f.get(entity); - if (v == null) throw new IllegalStateException("PK değeri null olamaz: " + pks.get(i).getFieldName()); - args[i] = String.valueOf(v); + if (v == null) throw new IllegalStateException("PK değeri null olamaz: " + pk.getFieldName()); + // PK alanında converter varsa DB değerine çevir + DbConverterAnnotation ann = f.getAnnotation(DbConverterAnnotation.class); + if (ann != null) { + TypeConverter conv = ConverterRegistry.getOrCreate(ann.converter()); + @SuppressWarnings({"rawtypes","unchecked"}) + Object dbVal = ((TypeConverter) conv).toDatabaseValue(v); + args[i] = (dbVal == null) ? null : String.valueOf(dbVal); + } else { + args[i] = String.valueOf(v); + } } - } catch (ReflectiveOperationException e) { - throw new RuntimeException("PK değer(ler)i okunamadı", e); - } - + } catch (Exception e) { throw new RuntimeException(e); } return new DeleteCommand(qId(rawTable), where, args); } @@ -81,9 +102,16 @@ public static DeleteCommand build(Class type, Object... primaryKeyValues) { .collect(Collectors.joining(" AND ")); String[] args = new String[pks.size()]; +// for (int i = 0; i < pks.size(); i++) { +// Object v = primaryKeyValues[i]; +// if (v == null) throw new IllegalArgumentException("PK değeri null olamaz: " + pks.get(i).getColumnName()); +// args[i] = String.valueOf(v); +// } + for (int i = 0; i < pks.size(); i++) { Object v = primaryKeyValues[i]; if (v == null) throw new IllegalArgumentException("PK değeri null olamaz: " + pks.get(i).getColumnName()); + // Burada çağıran taraf DB tipini verebilir; tip dönüştürmeye zorlamıyoruz args[i] = String.valueOf(v); } diff --git a/app/src/main/java/lib/persistence/command/manipulation/DeleteSql.java b/app/src/main/java/lib/persistence/command/manipulation/DeleteSql.java index 41847f2..4386527 100644 --- a/app/src/main/java/lib/persistence/command/manipulation/DeleteSql.java +++ b/app/src/main/java/lib/persistence/command/manipulation/DeleteSql.java @@ -2,11 +2,11 @@ import static lib.persistence.SqlNames.qCol; -import lib.persistence.annotations.DbTableAnnotation; - import java.util.ArrayList; import java.util.List; +import lib.persistence.annotations.DbTableAnnotation; + public class DeleteSql { private final Class type; private final String tableName; diff --git a/app/src/main/java/lib/persistence/command/manipulation/InsertCommand.java b/app/src/main/java/lib/persistence/command/manipulation/InsertCommand.java index ee4ccf5..d9e714b 100644 --- a/app/src/main/java/lib/persistence/command/manipulation/InsertCommand.java +++ b/app/src/main/java/lib/persistence/command/manipulation/InsertCommand.java @@ -2,6 +2,7 @@ import android.content.ContentValues; + import lib.persistence.profile.Mapper; diff --git a/app/src/main/java/lib/persistence/command/manipulation/UpdateCommand.java b/app/src/main/java/lib/persistence/command/manipulation/UpdateCommand.java index 2d10bef..c3c08ed 100644 --- a/app/src/main/java/lib/persistence/command/manipulation/UpdateCommand.java +++ b/app/src/main/java/lib/persistence/command/manipulation/UpdateCommand.java @@ -1,16 +1,18 @@ // lib/persistence/command/manipulation/UpdateCommand.java package lib.persistence.command.manipulation; -import static lib.persistence.SqlNames.qId; - import android.content.ContentValues; -import lib.persistence.profile.DbColumn; -import lib.persistence.profile.Mapper; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.stream.Collectors; +import lib.persistence.annotations.DbConverterAnnotation; +import lib.persistence.converters.ConverterRegistry; +import lib.persistence.converters.TypeConverter; +import lib.persistence.profile.DbColumn; +import lib.persistence.profile.Mapper; + public class UpdateCommand { private final String tableName; // RAW (backticksiz) private final ContentValues values; @@ -21,6 +23,33 @@ private UpdateCommand(String tableName, ContentValues values, String whereClause this.tableName = tableName; this.values = values; this.whereClause = whereClause; this.whereArgs = whereArgs; } +// public static UpdateCommand build(Object entity) { +// Class type = entity.getClass(); +// String table = Mapper.getTableName(type); // RAW +// ArrayList cols = Mapper.classToDbColumns(type); +// +// ArrayList pks = new ArrayList<>(); +// for (DbColumn c : cols) if (c.isPrimaryKey()) pks.add(c); +// if (pks.isEmpty()) throw new IllegalStateException("Update requires PK"); +// +// // identity hariç tüm alanları CV'ye alır; sonra PK'ları da CV'den çıkarırız +// ContentValues cv = Mapper.objectToContentValues(entity); +// for (DbColumn pkCol : pks) cv.remove(pkCol.getColumnName()); +// +// String where = pks.stream().map(c -> qId(c.getColumnName()) + " = ?").collect(Collectors.joining(" AND ")); +// String[] args = new String[pks.size()]; +// try { +// for (int i = 0; i < pks.size(); i++) { +// Field f = Mapper.findField(type, pks.get(i).getFieldName()); +// f.setAccessible(true); +// Object v = f.get(entity); +// args[i] = v == null ? null : String.valueOf(v); +// } +// } catch (Exception e) { throw new RuntimeException(e); } +// +// return new UpdateCommand(table, cv, where, args); // table RAW +// } + public static UpdateCommand build(Object entity) { Class type = entity.getClass(); String table = Mapper.getTableName(type); // RAW @@ -28,20 +57,37 @@ public static UpdateCommand build(Object entity) { ArrayList pks = new ArrayList<>(); for (DbColumn c : cols) if (c.isPrimaryKey()) pks.add(c); - if (pks.isEmpty()) throw new IllegalStateException("Update requires PK"); + if (pks.isEmpty()) throw new IllegalStateException("Primary key tanımı yok: " + type.getName()); - // identity hariç tüm alanları CV'ye alır; sonra PK'ları da CV'den çıkarırız + // SET kısmı: converter’lı doğru ContentValues (PK'lar otomatik dahil olabilir → aşağıda gerekirse kaldırırız) ContentValues cv = Mapper.objectToContentValues(entity); - for (DbColumn pkCol : pks) cv.remove(pkCol.getColumnName()); + // Güvenli tarafta kalmak için PK kolonlarını SET’ten çıkar (genelde PK update edilmez) + for (DbColumn pk : pks) { + if (cv.containsKey(pk.getColumnName())) cv.remove(pk.getColumnName()); + } - String where = pks.stream().map(c -> qId(c.getColumnName()) + " = ?").collect(Collectors.joining(" AND ")); + // WHERE kısmı (PK’lar) — converter desteği ile + String where = pks.stream() + .map(c -> "`" + c.getColumnName() + "` = ?") + .collect(Collectors.joining(" AND ")); String[] args = new String[pks.size()]; try { for (int i = 0; i < pks.size(); i++) { - Field f = Mapper.findField(type, pks.get(i).getFieldName()); + DbColumn pk = pks.get(i); + Field f = Mapper.findField(type, pk.getFieldName()); f.setAccessible(true); Object v = f.get(entity); - args[i] = v == null ? null : String.valueOf(v); + if (v == null) { args[i] = null; continue; } + // PK alanında converter varsa DB değerine çevir + DbConverterAnnotation ann = f.getAnnotation(DbConverterAnnotation.class); + if (ann != null) { + TypeConverter conv = ConverterRegistry.getOrCreate(ann.converter()); + @SuppressWarnings({"rawtypes","unchecked"}) + Object dbVal = ((TypeConverter) conv).toDatabaseValue(v); + args[i] = (dbVal == null) ? null : String.valueOf(dbVal); + } else { + args[i] = String.valueOf(v); + } } } catch (Exception e) { throw new RuntimeException(e); } diff --git a/app/src/main/java/lib/persistence/command/manipulation/UpdateSql.java b/app/src/main/java/lib/persistence/command/manipulation/UpdateSql.java index f745474..72b983f 100644 --- a/app/src/main/java/lib/persistence/command/manipulation/UpdateSql.java +++ b/app/src/main/java/lib/persistence/command/manipulation/UpdateSql.java @@ -2,13 +2,14 @@ package lib.persistence.command.manipulation; import android.content.ContentValues; -import lib.persistence.annotations.DbTableAnnotation; -import lib.persistence.profile.Mapper; -import lib.persistence.profile.DbColumn; import java.util.ArrayList; import java.util.List; +import lib.persistence.annotations.DbTableAnnotation; +import lib.persistence.profile.DbColumn; +import lib.persistence.profile.Mapper; + public class UpdateSql { private final Class type; private final String tableName; diff --git a/app/src/main/java/lib/persistence/command/query/GetQuery.java b/app/src/main/java/lib/persistence/command/query/GetQuery.java index 7ba8fdb..ebfa783 100644 --- a/app/src/main/java/lib/persistence/command/query/GetQuery.java +++ b/app/src/main/java/lib/persistence/command/query/GetQuery.java @@ -2,11 +2,11 @@ import static lib.persistence.SqlNames.qId; +import java.util.ArrayList; + import lib.persistence.profile.DbColumn; import lib.persistence.profile.Mapper; -import java.util.ArrayList; - /** * Tekil kayıt okumak için basit SELECT … WHERE PK = ? LIMIT 1 sorgusu. * Not: Çoklu PK durumunda ilk PK kolonu kullanılır (ihtiyaç olursa overload eklenir). diff --git a/app/src/main/java/lib/persistence/command/query/Select.java b/app/src/main/java/lib/persistence/command/query/Select.java index d8d4e86..7581cba 100644 --- a/app/src/main/java/lib/persistence/command/query/Select.java +++ b/app/src/main/java/lib/persistence/command/query/Select.java @@ -1,17 +1,15 @@ package lib.persistence.command.query; +import static lib.persistence.SqlNames.qCol; + import android.database.Cursor; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.function.Function; import lib.persistence.profile.Mapper; -import static lib.persistence.SqlNames.qCol; -import static lib.persistence.SqlNames.qId; - /** * Güvenli, parametreli ve tipli SELECT builder. * - from(Class) -> tablo adı Mapper'dan gelir diff --git a/app/src/main/java/lib/persistence/command/query/SelectQuery.java b/app/src/main/java/lib/persistence/command/query/SelectQuery.java index 797cba9..01c04df 100644 --- a/app/src/main/java/lib/persistence/command/query/SelectQuery.java +++ b/app/src/main/java/lib/persistence/command/query/SelectQuery.java @@ -1,12 +1,6 @@ package lib.persistence.command.query; -import android.database.Cursor; -import lib.persistence.profile.Mapper; -import lib.persistence.profile.RowMapper; - - -import java.util.*; import android.database.Cursor; import java.util.function.Function; diff --git a/app/src/main/java/lib/persistence/converters/ConverterRegistry.java b/app/src/main/java/lib/persistence/converters/ConverterRegistry.java new file mode 100644 index 0000000..91d5031 --- /dev/null +++ b/app/src/main/java/lib/persistence/converters/ConverterRegistry.java @@ -0,0 +1,18 @@ +package lib.persistence.converters; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public final class ConverterRegistry { + private static final Map, Object> INSTANCES = new ConcurrentHashMap<>(); + private ConverterRegistry() {} + + @SuppressWarnings("unchecked") + public static C getOrCreate(Class type) { + return (C) INSTANCES.computeIfAbsent(type, ConverterRegistry::newInstance); + } + private static Object newInstance(Class c) { + try { return c.getDeclaredConstructor().newInstance(); } + catch (Exception e) { throw new IllegalStateException("Cannot instantiate converter: " + c, e); } + } +} diff --git a/app/src/main/java/lib/persistence/converters/TypeConverter.java b/app/src/main/java/lib/persistence/converters/TypeConverter.java new file mode 100644 index 0000000..fb96269 --- /dev/null +++ b/app/src/main/java/lib/persistence/converters/TypeConverter.java @@ -0,0 +1,11 @@ +package lib.persistence.converters; + +// F: model alan tipi (EventType, Date, vs.) +// D: DB'ye yazılacak temel tip (String, Long, Double, byte[]) +public interface TypeConverter { + D toDatabaseValue(F fieldValue); + F fromDatabaseValue(D databaseValue); + + // Bu converter hangi SQLite tipini kullanır? "TEXT" | "INTEGER" | "REAL" | "BLOB" + default String sqliteType() { return "TEXT"; } +} diff --git a/app/src/main/java/lib/persistence/migration/Migrations.java b/app/src/main/java/lib/persistence/migration/Migrations.java index 55a3a5b..d6dd5e6 100644 --- a/app/src/main/java/lib/persistence/migration/Migrations.java +++ b/app/src/main/java/lib/persistence/migration/Migrations.java @@ -1,9 +1,14 @@ package lib.persistence.migration; import android.database.sqlite.SQLiteDatabase; + +import com.example.adbkit.entities.Event; + import java.util.Arrays; import java.util.List; +import lib.persistence.command.definition.CreateTableCommand; + public final class Migrations { private Migrations() {} @@ -11,14 +16,36 @@ private Migrations() {} private static final List STEPS = Arrays.asList( // ÖRNEK: v1 -> v2 : todos tablosuna notes kolonu ekle new MigrationStep() { - public int from() { return 1; } - public int to() { return 2; } + public int from() { + return 1; + } + + public int to() { + return 2; + } + public void apply(SQLiteDatabase db) { db.execSQL("ALTER TABLE `todos` ADD COLUMN `notes` TEXT"); // Gerekirse indeks/geri doldurma vb. ekle } } + , + new MigrationStep() { + @Override + public int from() { + return 4; + } + + @Override + public int to() { + return 5; + } + @Override + public void apply(SQLiteDatabase db) throws Exception { + db.execSQL(CreateTableCommand.build(Event.class).getQuery()); + } + } // yeni adımlar... ); diff --git a/app/src/main/java/lib/persistence/profile/DbColumn.java b/app/src/main/java/lib/persistence/profile/DbColumn.java index 68db495..dc7107f 100644 --- a/app/src/main/java/lib/persistence/profile/DbColumn.java +++ b/app/src/main/java/lib/persistence/profile/DbColumn.java @@ -9,7 +9,7 @@ public class DbColumn { private final boolean primaryKey; private final boolean identity; private final boolean nullable; - + private String sqliteType; // "TEXT","INTEGER","REAL","BLOB" veya null public DbColumn(int ordinal, String fieldName, @@ -35,4 +35,7 @@ public DbColumn(int ordinal, public boolean isPrimaryKey() { return primaryKey; } public boolean isIdentity() { return identity; } public boolean isNullable() { return nullable; } + + public String getSqliteType() { return sqliteType; } + public void setSqliteType(String sqliteType) { this.sqliteType = sqliteType; } } \ No newline at end of file diff --git a/app/src/main/java/lib/persistence/profile/Mapper.java b/app/src/main/java/lib/persistence/profile/Mapper.java index a40e045..baf270a 100644 --- a/app/src/main/java/lib/persistence/profile/Mapper.java +++ b/app/src/main/java/lib/persistence/profile/Mapper.java @@ -4,22 +4,27 @@ import android.content.ContentValues; import android.database.Cursor; - import java.lang.reflect.Field; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; -import java.util.*; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; - import lib.persistence.annotations.DbColumnAnnotation; +import lib.persistence.annotations.DbConverterAnnotation; import lib.persistence.annotations.DbTableAnnotation; +import lib.persistence.converters.ConverterRegistry; +import lib.persistence.converters.TypeConverter; public final class Mapper { private static final Map, List> COLUMNS_CACHE = new ConcurrentHashMap<>(); private static final Map, String> TABLE_NAME_CACHE = new ConcurrentHashMap<>(); - + private static final Map> FIELD_CONVERTER_CACHE = new ConcurrentHashMap<>(); private static final DateTimeFormatter ISO_DATE = DateTimeFormatter.ISO_DATE; private static final DateTimeFormatter ISO_DATE_TIME = DateTimeFormatter.ISO_DATE_TIME; @@ -56,7 +61,7 @@ private static List scanColumns(Class type) { f.setAccessible(true); String columnName = ann.name().isEmpty() ? f.getName() : ann.name(); DbDataType dataType = toDbDataType(f.getType()); - list.add(new DbColumn( + DbColumn column = new DbColumn( ann.ordinal(), f.getName(), columnName, @@ -64,7 +69,22 @@ private static List scanColumns(Class type) { ann.isPrimaryKey(), ann.isIdentity(), ann.isNullable() - )); + ); + + DbConverterAnnotation convAnn = f.getAnnotation(DbConverterAnnotation.class); + + if (convAnn != null) { + TypeConverter conv = ConverterRegistry.getOrCreate(convAnn.converter()); + // DbColumn'a (varsa) sqliteType işle + try { + if (column.getSqliteType() == null) { + column.setSqliteType(conv.sqliteType()); // "TEXT" | "INTEGER" | "REAL" | "BLOB" + } + } catch (NoSuchMethodError | Exception ignored) { + // DbColumn'a sqliteType daha eklenmediyse sessiz geç (DDL tarafında infer edilir) + } + } + list.add(column); } cur = cur.getSuperclass(); } @@ -88,26 +108,63 @@ private static DbDataType toDbDataType(Class javaType) { } // --- Object → ContentValues --- - public static ContentValues objectToContentValues(Object entity) { - if (entity == null) throw new IllegalArgumentException("entity null"); - Class type = entity.getClass(); - ContentValues cv = new ContentValues(); +// public static ContentValues objectToContentValues(Object entity) { +// if (entity == null) throw new IllegalArgumentException("entity null"); +// Class type = entity.getClass(); +// ContentValues cv = new ContentValues(); +// +// for (DbColumn col : classToDbColumns(type)) { +// // Identity alanlar DB tarafından set edilir → atla +// if (col.isIdentity()) continue; +// +// Object value; +// try { +// Field f = findField(type, col.getFieldName()); +// f.setAccessible(true); +// value = f.get(entity); +// } catch (ReflectiveOperationException e) { +// throw new RuntimeException("Alan okunamadı: " + col.getFieldName(), e); +// } +// +// // Tek noktadan tip yazımı +// putInContentValues(cv, col, value); +// } +// return cv; +// } - for (DbColumn col : classToDbColumns(type)) { - // Identity alanlar DB tarafından set edilir → atla - if (col.isIdentity()) continue; + public static ContentValues objectToContentValues(Object object) { + ContentValues cv = new ContentValues(); + List columns = classToDbColumns(object.getClass()); + try { + for (DbColumn column : columns) { + if (column.isIdentity()) continue; - Object value; - try { - Field f = findField(type, col.getFieldName()); + Field f = findField(object.getClass(), column.getFieldName()); f.setAccessible(true); - value = f.get(entity); - } catch (ReflectiveOperationException e) { - throw new RuntimeException("Alan okunamadı: " + col.getFieldName(), e); + Object value = f.get(object); + + DbConverterAnnotation convAnn = f.getAnnotation(DbConverterAnnotation.class); + if (convAnn != null) { + TypeConverter conv = FIELD_CONVERTER_CACHE.computeIfAbsent( + f, k -> ConverterRegistry.getOrCreate(convAnn.converter()) + ); + @SuppressWarnings({"rawtypes","unchecked"}) + Object dbVal = ((TypeConverter) conv).toDatabaseValue(value); + // Metadata tarafında sqliteType boşsa doldur + try { + if (column.getSqliteType() == null) { + column.setSqliteType(conv.sqliteType()); + } + } catch (NoSuchMethodError ignored) {} + // Anahtar/Değer yaz + putInContentValues(cv, column, dbVal); + } else { + // Mevcut yol + putInContentValues(cv, column, value); + } } - - // Tek noktadan tip yazımı - putInContentValues(cv, col, value); + } catch (Exception e) { + throw new RuntimeException("objectToContentValues hata", e); } return cv; } @@ -154,7 +211,33 @@ public static T cursorToObject(Cursor cursor, Class type) { for (DbColumn c : cols) { int idx = cursor.getColumnIndex(c.getColumnName()); if (idx < 0) continue; // seçilmemiş olabilir - setFieldValue(instance, c, cursor, idx); + + Field f = findField(type, c.getFieldName()); + f.setAccessible(true); + + // Converter var mı? + DbConverterAnnotation convAnn = f.getAnnotation(DbConverterAnnotation.class); + if (convAnn != null) { + TypeConverter conv = FIELD_CONVERTER_CACHE.computeIfAbsent( + f, k -> ConverterRegistry.getOrCreate(convAnn.converter()) + ); + // sqliteType: önce metadata (DbColumn), yoksa converter bildirimi + String sType; + try { + sType = (c.getSqliteType() != null) ? c.getSqliteType() : conv.sqliteType(); + } catch (NoSuchMethodError e) { + sType = "TEXT"; + } + Object dbVal = readBySqliteType(cursor, idx, sType); + @SuppressWarnings({"rawtypes","unchecked"}) + Object modelVal = ((TypeConverter) conv).fromDatabaseValue(dbVal); + f.set(instance, modelVal); + } else { + // Eski yol (primitive/string mapping) + setFieldValue(instance, c, cursor, idx); + } + + // setFieldValue(instance, c, cursor, idx); } return instance; } catch (Exception e) { @@ -162,6 +245,28 @@ public static T cursorToObject(Cursor cursor, Class type) { } } + // YENİ: sqliteType'a göre Cursor'dan ham değer oku (INTEGER→getLong, REAL→getDouble, TEXT→getString, BLOB→getBlob) + private static Object readBySqliteType(Cursor c, int idx, String sqliteType) { + if (c.isNull(idx)) return null; + if (sqliteType != null) { + String t = sqliteType.toUpperCase(Locale.ROOT); + switch (t) { + case "INTEGER": return c.getLong(idx); // ÖNEMLİ: long + case "REAL": return c.getDouble(idx); + case "TEXT": return c.getString(idx); + case "BLOB": return c.getBlob(idx); + } + } + // Tip bilinmiyorsa Cursor'dan sez + switch (c.getType(idx)) { + case Cursor.FIELD_TYPE_INTEGER: return c.getLong(idx); + case Cursor.FIELD_TYPE_FLOAT: return c.getDouble(idx); + case Cursor.FIELD_TYPE_STRING: return c.getString(idx); + case Cursor.FIELD_TYPE_BLOB: return c.getBlob(idx); + default: return null; + } + } + private static void setFieldValue(T instance, DbColumn c, Cursor cursor, int idx) throws Exception { Field f = findField(instance.getClass(), c.getFieldName()); f.setAccessible(true); diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index ad6b260..61a57f6 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -1,4 +1,4 @@ - +