From 77970cf93d1b23db71efd8651721c5e6fd4c0246 Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Tue, 9 Jun 2026 14:46:19 +0200 Subject: [PATCH 01/57] Add skill for AI to run tests. Signed-off-by: Alexander Lowey-Weber --- .claude/skills/naksha-test/SKILL.md | 70 +++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 .claude/skills/naksha-test/SKILL.md diff --git a/.claude/skills/naksha-test/SKILL.md b/.claude/skills/naksha-test/SKILL.md new file mode 100644 index 000000000..67218d6f2 --- /dev/null +++ b/.claude/skills/naksha-test/SKILL.md @@ -0,0 +1,70 @@ +--- +name: naksha-test +description: Use ONLY when asked to run tests for the Naksha project. Do NOT use for other projects or general testing questions. +--- + +# General +Most tests require a database. If the tests are executed without environment variables, they will start docker containers. When unclear, ask the user if he wants to run the tests using automatically created docker containers, or if he prefers to run the tests against his own, possible local, PostgresQL test database. + +# Environment Variables +All environment variables contain some placeholders that need to be replaced: + +- `{host}`: The host of the PostgresQL cluster. If not given any other instructions, assume `localhost`. +- `{port}`: Needs to be replaced by you with the port at which the database is listening. If not given any other instructions, assume `5432`. +- `{user}`: Needs to be replaced by you with the user. If not given any other instructions, assume `postgres`. +- `{password}`: Needs to be replaced by you with the password. If not given any other instructions, assume `password`. + +You can test the connection to the database. If you detect that the connection to the database fails due to wrong credentials or hostname, ask the user for host, port, user, and password _(whatever needed)_. What he does not provide, use defaults. Tell the user the defaults. + +## Library tests (here-naksha-lib-psql) +Only needs one variable. If not set, Docker auto-starts: + +```bash +export NAKSHA_TEST_PSQL_DB_URL="jdbc:postgresql://{host}:{port}/postgres?user={user}&password={password}&ssl=false" +``` + +## Server tests (here-naksha-app-service) +Needs all variables. These tests require a running Naksha server and will fail without one. Skip unless explicitly asked: + +```bash +export NAKSHA_APP_SERVICE_TEST_CONTEXT=LOCAL_STANDALONE +export NAKSHA_TEST_STORAGE_ID=local_psql_test_storage +export HUB_ADMIN_STORAGE_ID=local_psql_test_storage +export NAKSHA_TEST_PSQL_DB_URL="jdbc:postgresql://{host}:{port}/postgres?user={user}&password={password}&ssl=false" +export NAKSHA_TEST_ADMIN_DB_URL="jdbc:postgresql://{host}:{port}/postgres?user={user}&password={password}&ssl=false" +export NAKSHA_TEST_DATA_DB_URL="jdbc:postgresql://{host}:{port}/postgres?user={user}&password={password}&ssl=false" +``` + +# Commands + +## All library tests (JVM): +Docker auto-starts if no env vars are set: + +```bash +./gradlew :here-naksha-lib-model:jvmTest :here-naksha-lib-psql:jvmTest :here-naksha-lib-jbon:jvmTest :here-naksha-lib-geo:jvmTest +``` + +## All JVM tests: +Docker auto-starts if no env vars are set: + +```bash +./gradlew jvmTest +``` + +## All library tests (JS): +```bash +./gradlew :here-naksha-lib-model:jsTest :here-naksha-lib-jbon:jsTest :here-naksha-lib-geo:jsTest +``` + +## Server tests +Only run if user explicitly asks. Requires a running Naksha server: + +```bash +./gradlew :here-naksha-app-service:jvmTest +``` + +# Common Issues +- Kotlintest discovery errors: If `here-naksha-lib-psql:jvmTest` fails with test discovery errors, try `./gradlew clean` first +- Docker not available: The psql tests require Docker. If Docker isn't running, set `NAKSHA_TEST_PSQL_DB_URL` to an external Postgres instance +- Port conflicts: The Docker container uses host port 15432. If this port is in use, the container will fail to start +- Server tests fail with ConnectException: This is expected when no Naksha server is running. Skip these tests unless the server is available. From 7a0451fe6f9f56b8197044804dd60ebdaffda729 Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Tue, 9 Jun 2026 15:06:36 +0200 Subject: [PATCH 02/57] Minor fix in delete and update feature tests. Signed-off-by: Alexander Lowey-Weber --- .claude/skills/naksha-test/SKILL.md | 10 +++++----- .../commonTest/kotlin/naksha/psql/DeleteFeatureBase.kt | 5 +++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.claude/skills/naksha-test/SKILL.md b/.claude/skills/naksha-test/SKILL.md index 67218d6f2..4ba73af23 100644 --- a/.claude/skills/naksha-test/SKILL.md +++ b/.claude/skills/naksha-test/SKILL.md @@ -4,7 +4,7 @@ description: Use ONLY when asked to run tests for the Naksha project. Do NOT use --- # General -Most tests require a database. If the tests are executed without environment variables, they will start docker containers. When unclear, ask the user if he wants to run the tests using automatically created docker containers, or if he prefers to run the tests against his own, possible local, PostgresQL test database. +Most tests require a database. If the tests are executed without environment variables, they will start docker containers. When unclear, ask the user if they want to run the tests using automatically created Docker containers, or if they prefer to run the tests against their own, possibly local, PostgresQL test database. # Environment Variables All environment variables contain some placeholders that need to be replaced: @@ -14,7 +14,7 @@ All environment variables contain some placeholders that need to be replaced: - `{user}`: Needs to be replaced by you with the user. If not given any other instructions, assume `postgres`. - `{password}`: Needs to be replaced by you with the password. If not given any other instructions, assume `password`. -You can test the connection to the database. If you detect that the connection to the database fails due to wrong credentials or hostname, ask the user for host, port, user, and password _(whatever needed)_. What he does not provide, use defaults. Tell the user the defaults. +You can test the connection to the database. If you detect that the connection to the database fails due to wrong credentials or hostname, ask the user for host, port, user, and password _(whatever is needed)_. Use defaults for any value not provided. Tell the user the defaults. ## Library tests (here-naksha-lib-psql) Only needs one variable. If not set, Docker auto-starts: @@ -44,8 +44,8 @@ Docker auto-starts if no env vars are set: ./gradlew :here-naksha-lib-model:jvmTest :here-naksha-lib-psql:jvmTest :here-naksha-lib-jbon:jvmTest :here-naksha-lib-geo:jvmTest ``` -## All JVM tests: -Docker auto-starts if no env vars are set: +## All JVM tests (includes server tests that will fail without a running server): +Docker auto-starts if no env vars are set. This includes `here-naksha-app-service:jvmTest` which requires a running Naksha server and will fail with `ConnectException` if no server is available: ```bash ./gradlew jvmTest @@ -67,4 +67,4 @@ Only run if user explicitly asks. Requires a running Naksha server: - Kotlintest discovery errors: If `here-naksha-lib-psql:jvmTest` fails with test discovery errors, try `./gradlew clean` first - Docker not available: The psql tests require Docker. If Docker isn't running, set `NAKSHA_TEST_PSQL_DB_URL` to an external Postgres instance - Port conflicts: The Docker container uses host port 15432. If this port is in use, the container will fail to start -- Server tests fail with ConnectException: This is expected when no Naksha server is running. Skip these tests unless the server is available. +- Server tests fail with ConnectException: This is expected when no Naksha server is running. Skip these tests unless the server is available diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/DeleteFeatureBase.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/DeleteFeatureBase.kt index d1f157a59..bc5f2bbc7 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/DeleteFeatureBase.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/DeleteFeatureBase.kt @@ -67,7 +67,8 @@ abstract class DeleteFeatureBase( versions = 10 }).apply { // this = SuccessResponse assertEquals(1, features.size) - assertSame(Action.CREATED, Action.fromValue((featureTupleList[0]?.tuple?.getLongMember(naksha.model.objects.StandardMembers.Version)!!.toInt() and 3) ?: -1)) + val firstTuple = featureTupleList[0]?.tuple + assertSame(Action.CREATED, Action.fromValue((firstTuple?.getLongMember(naksha.model.objects.StandardMembers.Version)?.toInt() ?: -1) and 3)) } // verify if delete table contains element @@ -186,4 +187,4 @@ abstract class DeleteFeatureBase( val deleteFeature = assertNotNull(deleteFeatureResp.features[0]) assertEquals(feature.id, deleteFeature.id) } -} \ No newline at end of file +} From d5ac1241b0b44748555a56a9d6d847cc3ccfb2a5 Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Tue, 9 Jun 2026 15:34:31 +0200 Subject: [PATCH 03/57] Add JSON path to standard members. Signed-off-by: Alexander Lowey-Weber --- .../naksha/model/objects/StandardMembers.kt | 57 ++++++++----------- 1 file changed, 25 insertions(+), 32 deletions(-) diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt index a525414e4..a3c43eef0 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt @@ -218,7 +218,7 @@ class StandardMembers private constructor() { * @since 3.0 */ @JvmField @JsStatic - val UpdatedAt = Member("updated_at", MemberType.INT64) + val UpdatedAt = Member("updated_at", MemberType.INT64, JsonPath("properties", "@ns:com:here:xyz", "updatedAt")) /** * `created_at` — millisecond epoch timestamp of the initial creation. `null` means the @@ -226,7 +226,7 @@ class StandardMembers private constructor() { * @since 3.0 */ @JvmField @JsStatic - val CreatedAt = Member("created_at", MemberType.INT64) + val CreatedAt = Member("created_at", MemberType.INT64, JsonPath("properties", "@ns:com:here:xyz", "createdAt")) /** * `author_ts` — millisecond epoch timestamp of the last author change. `null` means the @@ -234,35 +234,35 @@ class StandardMembers private constructor() { * @since 3.0 */ @JvmField @JsStatic - val AuthorTimestamp = Member("author_ts", MemberType.INT64) + val AuthorTimestamp = Member("author_ts", MemberType.INT64, JsonPath("properties", "@ns:com:here:xyz", "authorTs")) /** * `cv0` — custom numeric value 0 (`FLOAT64`). `null` if not used. Default member. * @since 3.0 */ @JvmField @JsStatic - val CustomValue0 = Member("cv0", MemberType.FLOAT64) + val CustomValue0 = Member("cv0", MemberType.FLOAT64, JsonPath("properties", "@ns:com:here:xyz", "cv0")) /** * `cv1` — custom numeric value 1 (`FLOAT64`). `null` if not used. Default member. * @since 3.0 */ @JvmField @JsStatic - val CustomValue1 = Member("cv1", MemberType.FLOAT64) + val CustomValue1 = Member("cv1", MemberType.FLOAT64, JsonPath("properties", "@ns:com:here:xyz", "cv1")) /** * `cv2` — custom numeric value 2 (`FLOAT64`). `null` if not used. Default member. * @since 3.0 */ @JvmField @JsStatic - val CustomValue2 = Member("cv2", MemberType.FLOAT64) + val CustomValue2 = Member("cv2", MemberType.FLOAT64, JsonPath("properties", "@ns:com:here:xyz", "cv2")) /** * `cv3` — custom numeric value 3 (`FLOAT64`). `null` if not used. Default member. * @since 3.0 */ @JvmField @JsStatic - val CustomValue3 = Member("cv3", MemberType.FLOAT64) + val CustomValue3 = Member("cv3", MemberType.FLOAT64, JsonPath("properties", "@ns:com:here:xyz", "cv3")) /** * `hash` — content hash of the tuple, computed by the storage. `null` if not recorded. @@ -270,7 +270,7 @@ class StandardMembers private constructor() { * @since 3.0 */ @JvmField @JsStatic - val Hash = Member("hash", MemberType.INT32) + val Hash = Member("hash", MemberType.INT32, JsonPath("properties", "@ns:com:here:xyz", "hash")) /** * `here_tile` — HERE tile key (binary) of the reference point. `null` if not known. @@ -278,14 +278,14 @@ class StandardMembers private constructor() { * @since 3.0 */ @JvmField @JsStatic - val HereTile = Member("here_tile", MemberType.INT32) + val HereTile = Member("here_tile", MemberType.INT32, JsonPath("properties", "@ns:com:here:xyz", "hereTile")) /** * `cc` — change-count: how many times this feature has been modified. Default member. * @since 3.0 */ @JvmField @JsStatic - val ChangeCount = Member("cc", MemberType.INT32) + val ChangeCount = Member("cc", MemberType.INT32, JsonPath("properties", "@ns:com:here:xyz", "changeCount")) /** * `base_tn` — base tuple-number (`BYTE_ARRAY`), set when a three-way merge was performed. @@ -293,21 +293,21 @@ class StandardMembers private constructor() { * @since 3.0 */ @JvmField @JsStatic - val BaseTupleNumber = Member("base_tn", MemberType.BYTE_ARRAY) + val BaseTupleNumber = Member("base_tn", MemberType.BYTE_ARRAY, JsonPath("properties", "@ns:com:here:xyz", "base")) /** * `app_id` — identifier of the application that wrote this tuple. Default member. * @since 3.0 */ @JvmField @JsStatic - val AppId = Member("app_id", MemberType.STRING) + val AppId = Member("app_id", MemberType.STRING, JsonPath("properties", "@ns:com:here:xyz", "appId")) /** * `author` — identifier of the human author that takes ownership for this tuple. Default member. * @since 3.0 */ @JvmField @JsStatic - val Author = Member("author", MemberType.STRING) + val Author = Member("author", MemberType.STRING, JsonPath("properties", "@ns:com:here:xyz", "author")) /** * `origin` — stringified reference to the originating feature when this feature was forked or @@ -315,7 +315,7 @@ class StandardMembers private constructor() { * @since 3.0 */ @JvmField @JsStatic - val Origin = Member("origin", MemberType.STRING) + val Origin = Member("origin", MemberType.STRING, JsonPath("properties", "@ns:com:here:xyz", "origin")) /** * `target` — stringified reference to the feature into which this feature was joined. @@ -323,7 +323,7 @@ class StandardMembers private constructor() { * @since 3.0 */ @JvmField @JsStatic - val Target = Member("target", MemberType.STRING) + val Target = Member("target", MemberType.STRING, JsonPath("properties", "@ns:com:here:xyz", "target")) /** * `ft` — feature-type string. `null` when it matches the collection's @@ -332,35 +332,35 @@ class StandardMembers private constructor() { * @since 3.0 */ @JvmField @JsStatic - val FeatureType = Member("ft", MemberType.STRING) + val FeatureType = Member("ft", MemberType.STRING, JsonPath("properties", "featureType")) /** * `cs0` — custom string value 0. `null` if not used. Default member. * @since 3.0 */ @JvmField @JsStatic - val CustomString0 = Member("cs0", MemberType.STRING) + val CustomString0 = Member("cs0", MemberType.STRING, JsonPath("properties", "@ns:com:here:xyz", "cs0")) /** * `cs1` — custom string value 1. `null` if not used. Default member. * @since 3.0 */ @JvmField @JsStatic - val CustomString1 = Member("cs1", MemberType.STRING) + val CustomString1 = Member("cs1", MemberType.STRING, JsonPath("properties", "@ns:com:here:xyz", "cs1")) /** * `cs2` — custom string value 2. `null` if not used. Default member. * @since 3.0 */ @JvmField @JsStatic - val CustomString2 = Member("cs2", MemberType.STRING) + val CustomString2 = Member("cs2", MemberType.STRING, JsonPath("properties", "@ns:com:here:xyz", "cs2")) /** * `cs3` — custom string value 3. `null` if not used. Default member. * @since 3.0 */ @JvmField @JsStatic - val CustomString3 = Member("cs3", MemberType.STRING) + val CustomString3 = Member("cs3", MemberType.STRING, JsonPath("properties", "@ns:com:here:xyz", "cs3")) /** * `tags` — feature tags, the classic XYZ tags array located at @@ -371,7 +371,7 @@ class StandardMembers private constructor() { * @since 3.0 */ @JvmField @JsStatic - val Tags = Member("tags", MemberType.SET) + val Tags = Member("tags", MemberType.SET, JsonPath("properties", "@ns:com:here:xyz", "tags")) /** * `ref_point` — geometry reference point (always a single point), stored as TWKB. Used to @@ -380,7 +380,7 @@ class StandardMembers private constructor() { * @since 3.0 */ @JvmField @JsStatic - val ReferencePoint = Member("ref_point", MemberType.SPATIAL) + val ReferencePoint = Member("ref_point", MemberType.SPATIAL, JsonPath("referencePoint")) /** * `geo` — feature geometry stored as TWKB. `null` if the feature has no geometry. @@ -388,21 +388,14 @@ class StandardMembers private constructor() { * @since 3.0 */ @JvmField @JsStatic - val Geometry = Member("geo", MemberType.SPATIAL) + val Geometry = Member("geo", MemberType.SPATIAL, JsonPath("geometry")) /** * `attachment` — arbitrary binary attachment. `null` if unused. Default member. * @since 3.0 */ @JvmField @JsStatic - val Attachment = Member("attachment", MemberType.BYTE_ARRAY) - - /** - * `data_encoding` — feature data encoding string. `null` if using default. Default member. - * @since 3.0 - */ - @JvmField @JsStatic - val DataEncoding = Member("data_encoding", MemberType.STRING) + val Attachment = Member("attachment", MemberType.BYTE_ARRAY, JsonPath("attachment")) /** * The names of all [MANDATORY] members, for fast lookup. @@ -425,7 +418,7 @@ class StandardMembers private constructor() { BaseTupleNumber, AppId, Author, Origin, Target, FeatureType, CustomString0, CustomString1, CustomString2, CustomString3, - Tags, ReferencePoint, Geometry, Attachment, DataEncoding + Tags, ReferencePoint, Geometry, Attachment ) /** From 74327817831ef3c14b34194089ea130fb1b5591e Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Wed, 10 Jun 2026 09:07:18 +0200 Subject: [PATCH 04/57] Replaced hardcoded strings with StandardMembers references. Signed-off-by: Alexander Lowey-Weber --- .../commonMain/kotlin/naksha/model/Naksha.kt | 99 ++++++++++--------- .../kotlin/naksha/model/StorageTx.kt | 54 +++++----- .../commonMain/kotlin/naksha/model/Tuple.kt | 2 +- .../naksha/model/objects/StandardMembers.kt | 10 +- .../kotlin/naksha/psql/PgWriterUpdate.kt | 23 ++--- .../kotlin/naksha/psql/PgWriterUpsert.kt | 17 ++-- 6 files changed, 108 insertions(+), 97 deletions(-) diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt index ba73897ce..10c72cec1 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt @@ -9,6 +9,7 @@ import naksha.base.Platform.PlatformCompanion.gzipInflate import naksha.base.Platform.PlatformCompanion.md5 import naksha.base.Platform.PlatformCompanion.toJSON import naksha.geo.GeoUtil.GeoUtil_C.fromTWKB +import naksha.model.objects.StandardMembers import naksha.geo.GeoUtil.GeoUtil_C.toTWKB import naksha.geo.SpGeometry import naksha.jbon.* @@ -509,35 +510,35 @@ class Naksha private constructor() { val tn = feature.tupleNumber val storage = getStorageByNumber(tn.storageNumber) val encoding = dataEncoding ?: storage?.getDataEncoding(feature) ?: DEFAULT_DATA_ENCODING - val members = HeapBook() - members.put("id", feature.id) - members.put("app_id", xyz.appId) - members.put("updated_at", xyz.updatedAt) - members.put("created_at", if (xyz.updatedAt == xyz.createdAt) null else xyz.createdAt) - members.put("author_ts", if (xyz.updatedAt == xyz.authorTs) null else xyz.authorTs) - members.put("author", xyz.author) - members.put("data_encoding", encoding.toString()) - members.put("cc", xyz.changeCount) - members.put("hash", xyz.hash ?: 0) - members.put("here_tile", xyz.hereTile ?: 0) - members.put("ft", xyz.featureType) - members.put("cv0", xyz.cv0) - members.put("cv1", xyz.cv1) - members.put("cv2", xyz.cv2) - members.put("cv3", xyz.cv3) - members.put("cs0", xyz.cs0) - members.put("cs1", xyz.cs1) - members.put("cs2", xyz.cs2) - members.put("cs3", xyz.cs3) val dict = dictionary ?: storage?.getDictionary(feature.id) val featureBytes = encodeFeature(feature, encoding, dict) val geoBytes = encodeGeometry(feature.geometry) val refPoint = encodeGeometry(feature.referencePoint) val tagsJson = encodeTagList(xyz.tags) - members.put("geo", geoBytes) - members.put("ref_point", refPoint) - members.put("tags", tagsJson) - members.put("attachment", attachment) + val members = HeapBook() + members.put(StandardMembers.Id.name, feature.id) + members.put(StandardMembers.AppId.name, xyz.appId) + members.put(StandardMembers.UpdatedAt.name, xyz.updatedAt) + members.put(StandardMembers.CreatedAt.name, if (xyz.updatedAt == xyz.createdAt) null else xyz.createdAt) + members.put(StandardMembers.AuthorTimestamp.name, if (xyz.updatedAt == xyz.authorTs) null else xyz.authorTs) + members.put(StandardMembers.Author.name, xyz.author) + members.put(StandardMembers.DataEncoding.name, encoding.toString()) + members.put(StandardMembers.ChangeCount.name, xyz.changeCount) + members.put(StandardMembers.Hash.name, xyz.hash ?: 0) + members.put(StandardMembers.HereTile.name, xyz.hereTile ?: 0) + members.put(StandardMembers.FeatureType.name, xyz.featureType) + members.put(StandardMembers.CustomValue0.name, xyz.cv0) + members.put(StandardMembers.CustomValue1.name, xyz.cv1) + members.put(StandardMembers.CustomValue2.name, xyz.cv2) + members.put(StandardMembers.CustomValue3.name, xyz.cv3) + members.put(StandardMembers.CustomString0.name, xyz.cs0) + members.put(StandardMembers.CustomString1.name, xyz.cs1) + members.put(StandardMembers.CustomString2.name, xyz.cs2) + members.put(StandardMembers.CustomString3.name, xyz.cs3) + members.put(StandardMembers.Geometry.name, geoBytes) + members.put(StandardMembers.ReferencePoint.name, refPoint) + members.put(StandardMembers.Tags.name, tagsJson) + members.put(StandardMembers.Attachment.name, attachment) return Tuple( storageNumber = tn.storageNumber, mapNumber = tn.mapNumber, @@ -571,34 +572,34 @@ class Naksha private constructor() { val tn = feature.tupleNumber val dict = storage.getEncodingDictionary(feature) val encoding = storage.getDataEncoding(feature) - val members = HeapBook() - members.put("id", feature.id) - members.put("app_id", xyz.appId) - members.put("updated_at", xyz.updatedAt) - members.put("created_at", if (xyz.updatedAt == xyz.createdAt) null else xyz.createdAt) - members.put("author_ts", if (xyz.updatedAt == xyz.authorTs) null else xyz.authorTs) - members.put("author", xyz.author) - members.put("data_encoding", encoding.toString()) - members.put("cc", xyz.changeCount) - members.put("hash", xyz.hash ?: 0) - members.put("here_tile", xyz.hereTile ?: 0) - members.put("ft", xyz.featureType) - members.put("cv0", xyz.cv0) - members.put("cv1", xyz.cv1) - members.put("cv2", xyz.cv2) - members.put("cv3", xyz.cv3) - members.put("cs0", xyz.cs0) - members.put("cs1", xyz.cs1) - members.put("cs2", xyz.cs2) - members.put("cs3", xyz.cs3) val featureBytes = encodeFeature(feature, encoding, dict) val geoBytes = encodeGeometry(feature.geometry) val refPoint = encodeGeometry(feature.referencePoint) val tagsJson = encodeTagList(xyz.tags) - members.put("geo", geoBytes) - members.put("ref_point", refPoint) - members.put("tags", tagsJson) - members.put("attachment", attachment) + val members = HeapBook() + members.put(StandardMembers.Id.name, feature.id) + members.put(StandardMembers.AppId.name, xyz.appId) + members.put(StandardMembers.UpdatedAt.name, xyz.updatedAt) + members.put(StandardMembers.CreatedAt.name, if (xyz.updatedAt == xyz.createdAt) null else xyz.createdAt) + members.put(StandardMembers.AuthorTimestamp.name, if (xyz.updatedAt == xyz.authorTs) null else xyz.authorTs) + members.put(StandardMembers.Author.name, xyz.author) + members.put(StandardMembers.DataEncoding.name, encoding.toString()) + members.put(StandardMembers.ChangeCount.name, xyz.changeCount) + members.put(StandardMembers.Hash.name, xyz.hash ?: 0) + members.put(StandardMembers.HereTile.name, xyz.hereTile ?: 0) + members.put(StandardMembers.FeatureType.name, xyz.featureType) + members.put(StandardMembers.CustomValue0.name, xyz.cv0) + members.put(StandardMembers.CustomValue1.name, xyz.cv1) + members.put(StandardMembers.CustomValue2.name, xyz.cv2) + members.put(StandardMembers.CustomValue3.name, xyz.cv3) + members.put(StandardMembers.CustomString0.name, xyz.cs0) + members.put(StandardMembers.CustomString1.name, xyz.cs1) + members.put(StandardMembers.CustomString2.name, xyz.cs2) + members.put(StandardMembers.CustomString3.name, xyz.cs3) + members.put(StandardMembers.Geometry.name, geoBytes) + members.put(StandardMembers.ReferencePoint.name, refPoint) + members.put(StandardMembers.Tags.name, tagsJson) + members.put(StandardMembers.Attachment.name, attachment) return Tuple( storageNumber = tn.storageNumber, mapNumber = tn.mapNumber, @@ -1049,4 +1050,4 @@ class Naksha private constructor() { _adminOptions.set(value) } } -} \ No newline at end of file +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/StorageTx.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/StorageTx.kt index ad11c9354..8609a7071 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/StorageTx.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/StorageTx.kt @@ -137,27 +137,27 @@ open class StorageTx private constructor( } val featureType = if (collection.defaultFeatureType == feature.featureType) null else feature.featureType val members = HeapBook() - members.put("updated_at", updatedAt) - members.put("created_at", createdAt) - members.put("author_ts", authorTs) - members.put("author", author) - members.put("app_id", appId) - members.put("data_encoding", dataEncoding.toString()) - members.put("cc", xyz.changeCount + 1) - members.put("hash", calculateHash(feature)) - members.put("here_tile", calculateHereTile(feature)) - members.put("id", feature.id) - members.put("origin", null) - members.put("target", null) - members.put("ft", featureType) - members.put("cv0", xyz.cv0) - members.put("cv1", xyz.cv1) - members.put("cv2", xyz.cv2) - members.put("cv3", xyz.cv3) - members.put("cs0", xyz.cs0) - members.put("cs1", xyz.cs1) - members.put("cs2", xyz.cs2) - members.put("cs3", xyz.cs3) + members.put(StandardMembers.UpdatedAt.name, updatedAt) + members.put(StandardMembers.CreatedAt.name, createdAt) + members.put(StandardMembers.AuthorTimestamp.name, authorTs) + members.put(StandardMembers.Author.name, author) + members.put(StandardMembers.AppId.name, appId) + members.put(StandardMembers.DataEncoding.name, dataEncoding.toString()) + members.put(StandardMembers.ChangeCount.name, xyz.changeCount + 1) + members.put(StandardMembers.Hash.name, calculateHash(feature)) + members.put(StandardMembers.HereTile.name, calculateHereTile(feature)) + members.put(StandardMembers.Id.name, feature.id) + members.put(StandardMembers.Origin.name, null) + members.put(StandardMembers.Target.name, null) + members.put(StandardMembers.FeatureType.name, featureType) + members.put(StandardMembers.CustomValue0.name, xyz.cv0) + members.put(StandardMembers.CustomValue1.name, xyz.cv1) + members.put(StandardMembers.CustomValue2.name, xyz.cv2) + members.put(StandardMembers.CustomValue3.name, xyz.cv3) + members.put(StandardMembers.CustomString0.name, xyz.cs0) + members.put(StandardMembers.CustomString1.name, xyz.cs1) + members.put(StandardMembers.CustomString2.name, xyz.cs2) + members.put(StandardMembers.CustomString3.name, xyz.cs3) return members } @@ -191,11 +191,11 @@ open class StorageTx private constructor( val geoBytes = Naksha.encodeGeometry(feature.geometry) val refPoint = Naksha.encodeGeometry(feature.referencePoint) val tagsJson = Naksha.encodeTagList(xyz.tags) - if (members is naksha.jbon.HeapBook) { - members.put("geo", geoBytes) - members.put("ref_point", refPoint) - members.put("tags", tagsJson) - members.put("attachment", attachment) + if (members is HeapBook) { + members.put(StandardMembers.Geometry.name, geoBytes) + members.put(StandardMembers.ReferencePoint.name, refPoint) + members.put(StandardMembers.Tags.name, tagsJson) + members.put(StandardMembers.Attachment.name, attachment) } return Tuple( storageNumber = storageNumber, @@ -273,4 +273,4 @@ open class StorageTx private constructor( private fun getDataEncoding(feature: NakshaFeature, collection: NakshaCollection): DataEncoding = storage?.getDataEncoding(feature, collection) ?: Naksha.DEFAULT_DATA_ENCODING -} \ No newline at end of file +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Tuple.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Tuple.kt index 8c024a0bf..2a635f09b 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Tuple.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Tuple.kt @@ -276,7 +276,7 @@ data class Tuple( */ val dataEncoding: DataEncoding get() { - val str = members?.getByName("data_encoding") as? String ?: return Naksha.DEFAULT_DATA_ENCODING + val str = getStringMember(StandardMembers.DataEncoding) ?: return Naksha.DEFAULT_DATA_ENCODING return try { DataEncoding.fromString(str) } catch (_: Exception) { Naksha.DEFAULT_DATA_ENCODING } } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt index a3c43eef0..0c71d2b94 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt @@ -397,6 +397,14 @@ class StandardMembers private constructor() { @JvmField @JsStatic val Attachment = Member("attachment", MemberType.BYTE_ARRAY, JsonPath("attachment")) + /** + * `data_encoding` — the encoding used for the serialised feature blob (e.g. `JBON2`, `JSON_GZIP`). + * `null` if not specified, in which case the storage default applies. Default member. + * @since 3.0 + */ + @JvmField @JsStatic + val DataEncoding = Member("data_encoding", MemberType.STRING) + /** * The names of all [MANDATORY] members, for fast lookup. * @since 3.0 @@ -418,7 +426,7 @@ class StandardMembers private constructor() { BaseTupleNumber, AppId, Author, Origin, Target, FeatureType, CustomString0, CustomString1, CustomString2, CustomString3, - Tags, ReferencePoint, Geometry, Attachment + Tags, ReferencePoint, Geometry, Attachment, DataEncoding ) /** diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt index c6e5b661e..04cec87de 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt @@ -4,6 +4,7 @@ import naksha.base.Platform import naksha.base.Platform.PlatformCompanion.logger import naksha.base.PlatformUtil import naksha.model.* +import naksha.model.objects.StandardMembers import naksha.model.objects.StoreMode /** @@ -181,13 +182,13 @@ LEFT JOIN inserted ON inserted.id = new_row.id } // Patch back all BYTE_ARRAY columns whose stored value may differ from what the client sent // (sentinel "undefined" causes the DB to retain the existing value). - val geo = if (PgColumn.geo in keepableByteCols) rows.getByteArray(rowNum, PgColumn.geo.name) else tuple.getByteArray(naksha.model.objects.StandardMembers.Geometry) - val referencePoint = if (PgColumn.ref_point in keepableByteCols) rows.getByteArray(rowNum, PgColumn.ref_point.name) else tuple.getByteArray(naksha.model.objects.StandardMembers.ReferencePoint) - val tags = tuple.getStringMember(naksha.model.objects.StandardMembers.Tags) - val attachment = if (PgColumn.attachment in keepableByteCols) rows.getByteArray(rowNum, PgColumn.attachment.name) else tuple.getByteArray(naksha.model.objects.StandardMembers.Attachment) - val oldGeo = tuple.getByteArray(naksha.model.objects.StandardMembers.Geometry) - val oldRefPoint = tuple.getByteArray(naksha.model.objects.StandardMembers.ReferencePoint) - val oldAttachment = tuple.getByteArray(naksha.model.objects.StandardMembers.Attachment) + val geo = if (PgColumn.geo in keepableByteCols) rows.getByteArray(rowNum, PgColumn.geo.name) else tuple.getByteArray(StandardMembers.Geometry) + val referencePoint = if (PgColumn.ref_point in keepableByteCols) rows.getByteArray(rowNum, PgColumn.ref_point.name) else tuple.getByteArray(StandardMembers.ReferencePoint) + val tags = tuple.getStringMember(StandardMembers.Tags) + val attachment = if (PgColumn.attachment in keepableByteCols) rows.getByteArray(rowNum, PgColumn.attachment.name) else tuple.getByteArray(StandardMembers.Attachment) + val oldGeo = tuple.getByteArray(StandardMembers.Geometry) + val oldRefPoint = tuple.getByteArray(StandardMembers.ReferencePoint) + val oldAttachment = tuple.getByteArray(StandardMembers.Attachment) val needsPatch = (oldGeo == null || !oldGeo.contentEquals(geo ?: ByteArray(0))) || (oldRefPoint == null || !oldRefPoint.contentEquals(referencePoint ?: ByteArray(0))) || (oldAttachment == null || !oldAttachment.contentEquals(attachment ?: ByteArray(0))) @@ -195,10 +196,10 @@ LEFT JOIN inserted ON inserted.id = new_row.id val m = tuple.members val newMembers = if (m is naksha.jbon.HeapBook) { val dict = m.copy() - dict.put("geo", geo) - dict.put("ref_point", referencePoint) - dict.put("tags", tags) - dict.put("attachment", attachment) + dict.put(StandardMembers.Geometry.name, geo) + dict.put(StandardMembers.ReferencePoint.name, referencePoint) + dict.put(StandardMembers.Tags.name, tags) + dict.put(StandardMembers.Attachment.name, attachment) dict } else m write.tuple = tuple.copy(members = newMembers) diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt index 24a107053..7683f8996 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt @@ -4,6 +4,7 @@ import naksha.base.Platform import naksha.base.Platform.PlatformCompanion.logger import naksha.base.PlatformUtil import naksha.model.* +import naksha.model.objects.StandardMembers import naksha.model.objects.StoreMode /** @@ -206,18 +207,18 @@ ${if (head_to_history.isNotEmpty()) "LEFT JOIN head_to_history ON head_to_histor val tuple = write.tuple ?: throw generalException("Missing tuple for feature '$id'") // Read back all keepable BYTE_ARRAY columns — the DB may have substituted the sentinel // with the existing value, so the in-memory tuple must reflect the final stored state. - val geo = if (PgColumn.geo in keepableByteCols) outRows.getByteArray(row, PgColumn.geo.name) else tuple.getByteArray(naksha.model.objects.StandardMembers.Geometry) - val referencePoint = if (PgColumn.ref_point in keepableByteCols) outRows.getByteArray(row, PgColumn.ref_point.name) else tuple.getByteArray(naksha.model.objects.StandardMembers.ReferencePoint) - val tags = tuple.getStringMember(naksha.model.objects.StandardMembers.Tags) - val attachment = if (PgColumn.attachment in keepableByteCols) outRows.getByteArray(row, PgColumn.attachment.name) else tuple.getByteArray(naksha.model.objects.StandardMembers.Attachment) + val geo = if (PgColumn.geo in keepableByteCols) outRows.getByteArray(row, PgColumn.geo.name) else tuple.getByteArray(StandardMembers.Geometry) + val referencePoint = if (PgColumn.ref_point in keepableByteCols) outRows.getByteArray(row, PgColumn.ref_point.name) else tuple.getByteArray(StandardMembers.ReferencePoint) + val tags = tuple.getStringMember(StandardMembers.Tags) + val attachment = if (PgColumn.attachment in keepableByteCols) outRows.getByteArray(row, PgColumn.attachment.name) else tuple.getByteArray(StandardMembers.Attachment) write.tupleNumber = updated_tn val m = tuple.members val newMembers = if (m is naksha.jbon.HeapBook) { val dict = m.copy() - dict.put("geo", geo) - dict.put("ref_point", referencePoint) - dict.put("tags", tags) - dict.put("attachment", attachment) + dict.put(StandardMembers.Geometry.name, geo) + dict.put(StandardMembers.ReferencePoint.name, referencePoint) + dict.put(StandardMembers.Tags.name, tags) + dict.put(StandardMembers.Attachment.name, attachment) dict } else m write.tuple = tuple.copy( From 6501f6b7803f8d41388c533e15a6ea8297de5079 Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Wed, 10 Jun 2026 09:49:56 +0200 Subject: [PATCH 05/57] Import StandardMembers Signed-off-by: Alexander Lowey-Weber --- .../src/commonMain/kotlin/naksha/model/Guid.kt | 3 ++- .../src/commonMain/kotlin/naksha/model/Naksha.kt | 8 +++++--- .../kotlin/naksha/model/request/FeatureTuple.kt | 3 ++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Guid.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Guid.kt index 448dfcb37..ee405a7e0 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Guid.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Guid.kt @@ -5,6 +5,7 @@ package naksha.model import naksha.base.Platform.PlatformCompanion.decodeURIComponent import naksha.base.Platform.PlatformCompanion.encodeURIComponent import naksha.model.objects.NakshaFeature +import naksha.model.objects.StandardMembers import kotlin.js.JsExport import kotlin.js.JsName import kotlin.js.JsStatic @@ -119,7 +120,7 @@ data class Guid( @JsStatic @JvmStatic fun fromTuple(tuple: Tuple): Guid { - val id = tuple.getStringMember(naksha.model.objects.StandardMembers.Id) ?: tuple.featureNumber.toString() + val id = tuple.getStringMember(StandardMembers.Id) ?: tuple.featureNumber.toString() return Guid(id, tuple.tupleNumber) } } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt index 10c72cec1..4ce2c9077 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt @@ -476,13 +476,15 @@ class Naksha private constructor() { @JvmOverloads fun decodeTuple(tuple: Tuple, dictionaryReader: IDictReader? = null): NakshaFeature { val sn = tuple.storageNumber + val dataEncodingStr = tuple.getStringMember(StandardMembers.DataEncoding) + val dataEncoding = if (dataEncodingStr.isNullOrEmpty()) DEFAULT_DATA_ENCODING else DataEncoding.fromString(dataEncodingStr) val dictReader = dictionaryReader ?: getStorageByNumber(sn) ?: cache.getDictReader(sn) - val feature = decodeFeature(tuple.feature, dictReader) ?: NakshaFeature() + val feature = decodeFeature(tuple.feature, dataEncoding, dictReader) ?: NakshaFeature() feature.properties.xyz = XyzNs.fromTuple(tuple) val xyz = feature.properties.xyz - val tags = tuple.getTagList(naksha.model.objects.StandardMembers.Tags) + val tags = tuple.getTagList(StandardMembers.Tags) if (tags != null) xyz.tags = tags - val geo = tuple.getByteArray(naksha.model.objects.StandardMembers.Geometry) + val geo = tuple.getByteArray(StandardMembers.Geometry) if (geo != null) feature.geometry = decodeGeometry(geo) return feature } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/FeatureTuple.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/FeatureTuple.kt index 20b0bae71..91aef4f54 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/FeatureTuple.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/FeatureTuple.kt @@ -5,6 +5,7 @@ package naksha.model.request import naksha.base.Platform import naksha.model.* import naksha.model.objects.NakshaFeature +import naksha.model.objects.StandardMembers import kotlin.js.JsExport import kotlin.js.JsName import kotlin.jvm.JvmField @@ -55,7 +56,7 @@ open class FeatureTuple( */ val id: String? get() { - val member = tuple?.getStringMember(naksha.model.objects.StandardMembers.Id) + val member = tuple?.getStringMember(StandardMembers.Id) if (member != null) return member return feature?.id } From efab25b30266ce2ef1c51410f03d1f012331e493 Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Wed, 10 Jun 2026 10:29:57 +0200 Subject: [PATCH 06/57] Renamed map into path in members, because thats what it actually is. Signed-off-by: Alexander Lowey-Weber --- .../kotlin/naksha/model/objects/Member.kt | 36 +++++++++---------- .../naksha/model/objects/StandardMembers.kt | 3 +- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt index 8d9fb9562..00e2ef412 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt @@ -13,7 +13,7 @@ import kotlin.js.JsName * A column materialized on a [NakshaCollection] — either a mandatory/default built-in column or a * user-defined one. * - * At write time, the storage walks the feature using [map], extracts the value, coerces it to the + * At write time, the storage walks the feature using [path], extracts the value, coerces it to the * [dataType], and stores it in a storage-specific column derived from [name]. The value also remains * in the encoded feature blob. * @@ -21,7 +21,7 @@ import kotlin.js.JsName * Mandatory columns (e.g. `fn`, `version`, `id`, `feature`) are injected by the storage and must * not be redeclared by the client with a different type. * - * If [map] is not set, the storage defaults to `["properties", ]` at write time. + * If [path] is not set, the storage defaults to `["properties", ]` at write time. * @since 3.0 */ @JsExport @@ -31,14 +31,14 @@ open class Member() : AnyObject() { * Construct a member with a name and the given data type. * @param name the member name. * @param dataType the data type; defaults to [MemberType.STRING]. - * @param map the JSON path to read the value from; defaults to `["properties", name]` when null. + * @param path the JSON path to read the value from; defaults to `["properties", name]` when null. * @since 3.0 */ @JsName("of") - constructor(name: String, dataType: MemberType = MemberType.STRING, map: JsonPath? = null) : this() { + constructor(name: String, dataType: MemberType = MemberType.STRING, path: JsonPath? = null) : this() { this.name = name this.dataType = dataType - if (map != null) this.map = map + this.path = path } /** @@ -90,38 +90,38 @@ open class Member() : AnyObject() { * Each segment must match `^[A-Za-z_][A-Za-z0-9_]*$`. There is no array indexing in v3.0. * @since 3.0 */ - var map: JsonPath? by MAP + var path: JsonPath? by PATH - /** True iff the underlying map has an entry for [map]. */ - fun hasMap(): Boolean = hasRaw("map") + /** True iff the underlying map has an entry for [path]. */ + fun hasPath(): Boolean = hasRaw("path") - /** Remove [map] from the underlying map; returns this for chaining. */ - fun removeMap(): Member { - removeRaw("map") + /** Remove [path] from the underlying map; returns this for chaining. */ + fun removePath(): Member { + removeRaw("path") return this } - /** Fluent setter for [map]; returns this for chaining. */ - fun withMap(value: JsonPath?): Member { - map = value + /** Fluent setter for [path]; returns this for chaining. */ + fun withPath(value: JsonPath?): Member { + path = value return this } /** * Returns the effective JSON path to read this member from a feature. * - * If [map] is explicitly set, returns its contents; otherwise returns `["properties", name]`. + * If [path] is explicitly set, returns its contents; otherwise returns `["properties", name]`. * @since 3.0 */ fun effectivePath(): List { - val m = map - return if (m != null && m.isNotEmpty()) m.filterNotNull().toList() + val path: JsonPath? = this.path + return if (!path.isNullOrEmpty()) path.filterNotNull().toList() else listOf("properties", name) } companion object Member_C { private val NAME = NotNullProperty(String::class) { _, _ -> "" } private val DATA_TYPE = NotNullEnum(MemberType::class) { _, _ -> MemberType.STRING } - private val MAP = NullableProperty(JsonPath::class) + private val PATH = NullableProperty(JsonPath::class) } } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt index 0c71d2b94..3d4cfc7f4 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt @@ -5,7 +5,6 @@ package naksha.model.objects import kotlin.js.JsExport import kotlin.js.JsStatic import kotlin.jvm.JvmField -import kotlin.jvm.JvmStatic /** * The canonical set of standard members that every Naksha storage understands. @@ -15,7 +14,7 @@ import kotlin.jvm.JvmStatic * ### Mandatory members * These are always managed and persisted by the storage, regardless of whether they appear in * [NakshaCollection.members]. Clients must not declare them with a different [MemberType]. - * Their [Member.map] is always `null` — the storage resolves their values internally. + * Their [Member.path] is always `null` — the storage resolves their values internally. * * - [StandardMembers_C.DatabaseNumber] — database-number (storage-level identifier) * - [StandardMembers_C.CatalogNumber] — catalog-number (map-level identifier) From a763efbb49ef17e3987ea55613c34e73c2e5549a Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Wed, 10 Jun 2026 13:46:17 +0200 Subject: [PATCH 07/57] Improve the member handling, partially removed the hardcoded workarounds. Signed-off-by: Alexander Lowey-Weber --- .../naksha/model/FeatureMemberValues.kt | 200 ++++++++++++++++++ .../kotlin/naksha/model/IMemberProcessor.kt | 20 ++ .../kotlin/naksha/model/ISession.kt | 22 ++ .../kotlin/naksha/model/StorageTx.kt | 56 +++-- .../naksha/psql/PgCustomMemberValues.kt | 2 +- .../kotlin/naksha/psql/PgSession.kt | 27 +++ 6 files changed, 306 insertions(+), 21 deletions(-) create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/FeatureMemberValues.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/IMemberProcessor.kt diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/FeatureMemberValues.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/FeatureMemberValues.kt new file mode 100644 index 000000000..83f9efc2e --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/FeatureMemberValues.kt @@ -0,0 +1,200 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model + +import naksha.base.AnyObject +import naksha.base.Int64 +import naksha.base.Platform.PlatformCompanion.logger +import naksha.base.Platform.PlatformCompanion.toJSON +import naksha.geo.GeoUtil.GeoUtil_C.toTWKB +import naksha.geo.SpGeometry +import naksha.model.objects.MemberType +import naksha.model.objects.NakshaFeature + +/** + * Helpers to walk a [NakshaFeature] by JSON path and coerce raw values to the expected [MemberType]. + * + * - [walkFeature]: descend a [NakshaFeature] using the member's path; returns _null_ if the path is missing. + * - [coerce]: coerce a raw value to the type of the member; returns _null_ and logs a warning on mismatch. + * + * @since 3.0 + */ +object FeatureMemberValues { + + /** + * Descend a [NakshaFeature] using the given [path] segments; returns _null_ if any segment is missing. + * + * @param feature the feature to walk. + * @param path the list of path segments (e.g. `["properties", "@ns:com:here:xyz", "updatedAt"]`). + * @return the value at the path, or _null_ if the path does not exist. + */ + fun walkFeature(feature: NakshaFeature, path: List): Any? { + var current: Any? = feature + for (segment in path) { + if (current == null) return null + current = when (current) { + is AnyObject -> current.getRaw(segment) + else -> return null + } + } + return current + } + + /** + * Coerce a raw value to the type expected by the member. + * + * For [MemberType.SPATIAL], the value is expected to be a [SpGeometry] (or [naksha.geo.SpPoint]) which will be + * encoded as TWKB bytes. For other types, the value is expected to already be the raw JSON value. + * + * @param value the raw value extracted from the feature. + * @param type the expected [MemberType]. + * @param featureId the feature identifier (for logging). + * @param memberName the member name (for logging). + * @return the coerced value, or _null_ if coercion failed. + */ + fun coerce(value: Any?, type: MemberType, featureId: String, memberName: String): Any? { + if (value == null) return null + return when (type) { + MemberType.BOOLEAN -> coerceBoolean(value, featureId, memberName) + MemberType.INT8 -> coerceInt8(value, featureId, memberName) + MemberType.INT16 -> coerceInt16(value, featureId, memberName) + MemberType.INT32 -> coerceInt32(value, featureId, memberName) + MemberType.INT64 -> coerceInt64(value, featureId, memberName) + MemberType.FLOAT32 -> coerceFloat32(value, featureId, memberName) + MemberType.FLOAT64 -> coerceFloat64(value, featureId, memberName) + MemberType.STRING -> coerceString(value, featureId, memberName) + MemberType.BYTE_ARRAY -> coerceByteArray(value, featureId, memberName) + MemberType.SPATIAL -> coerceSpatial(value, featureId, memberName) + MemberType.TAGS -> coerceTags(value, featureId, memberName) + MemberType.TAGS_FROM_ARRAY -> coerceTagsFromArray(value, featureId, memberName) + else -> { + warnMismatch(featureId, memberName, type.toString(), value) + null + } + } + } + + // ------------------------------------------------------------------------- + // Individual coercion implementations + // ------------------------------------------------------------------------- + + private fun coerceBoolean(value: Any, featureId: String, memberName: String): Boolean? = when (value) { + is Boolean -> value + else -> { warnMismatch(featureId, memberName, "boolean", value); null } + } + + private fun coerceInt8(value: Any, featureId: String, memberName: String): Short? { + val asLong = numberToLongOrNull(value) ?: return null.also { warnMismatch(featureId, memberName, "int8", value) } + if (asLong !in Byte.MIN_VALUE.toLong()..Byte.MAX_VALUE.toLong()) { + warnMismatch(featureId, memberName, "int8 (out of range)", value) + return null + } + return asLong.toShort() + } + + private fun coerceInt16(value: Any, featureId: String, memberName: String): Short? { + val asLong = numberToLongOrNull(value) ?: return null.also { warnMismatch(featureId, memberName, "int16", value) } + if (asLong !in Short.MIN_VALUE.toLong()..Short.MAX_VALUE.toLong()) { + warnMismatch(featureId, memberName, "int16 (out of range)", value) + return null + } + return asLong.toShort() + } + + private fun coerceInt32(value: Any, featureId: String, memberName: String): Int? { + val asLong = numberToLongOrNull(value) ?: return null.also { warnMismatch(featureId, memberName, "int32", value) } + if (asLong !in Int.MIN_VALUE.toLong()..Int.MAX_VALUE.toLong()) { + warnMismatch(featureId, memberName, "int32 (out of range)", value) + return null + } + return asLong.toInt() + } + + private fun coerceInt64(value: Any, featureId: String, memberName: String): Int64? = when (value) { + is Int64 -> value + is Int -> Int64(value.toLong()) + is Long -> Int64(value) + is Short -> Int64(value.toLong()) + is Byte -> Int64(value.toLong()) + is Double -> if (value.isFinite() && value == value.toLong().toDouble()) Int64(value.toLong()) else { warnMismatch(featureId, memberName, "int64", value); null } + is Float -> if (value.isFinite() && value == value.toLong().toFloat()) Int64(value.toLong()) else { warnMismatch(featureId, memberName, "int64", value); null } + else -> { warnMismatch(featureId, memberName, "int64", value); null } + } + + private fun coerceFloat32(value: Any, featureId: String, memberName: String): Float? = when (value) { + is Float -> value + is Double -> value.toFloat() + is Int -> value.toFloat() + is Long -> value.toFloat() + is Int64 -> value.toLong().toFloat() + is Short -> value.toFloat() + is Byte -> value.toFloat() + else -> { warnMismatch(featureId, memberName, "float32", value); null } + } + + private fun coerceFloat64(value: Any, featureId: String, memberName: String): Double? = when (value) { + is Double -> value + is Float -> value.toDouble() + is Int -> value.toDouble() + is Long -> value.toDouble() + is Int64 -> value.toLong().toDouble() + is Short -> value.toDouble() + is Byte -> value.toDouble() + else -> { warnMismatch(featureId, memberName, "float64", value); null } + } + + private fun coerceString(value: Any, featureId: String, memberName: String): String? = when (value) { + is String -> value + else -> { warnMismatch(featureId, memberName, "string", value); null } + } + + private fun coerceByteArray(value: Any, featureId: String, memberName: String): ByteArray? = when (value) { + is ByteArray -> value + else -> { warnMismatch(featureId, memberName, "byte_array", value); null } + } + + /** + * Coerce a spatial value to TWKB bytes. + * + * The expected input is a [SpGeometry] (which may be a [naksha.geo.SpPoint] or any other geometry subclass). + * The geometry is encoded as TWKB and returned as a [ByteArray]. + */ + private fun coerceSpatial(value: Any, featureId: String, memberName: String): ByteArray? = when (value) { + is SpGeometry -> toTWKB(value) + is ByteArray -> value + else -> { warnMismatch(featureId, memberName, "spatial", value); null } + } + + private fun coerceTags(value: Any, featureId: String, memberName: String): String? { + if (value !is AnyObject) { + warnMismatch(featureId, memberName, "tags", value) + return null + } + return try { toJSON(value) } catch (_: Exception) { warnMismatch(featureId, memberName, "tags", value); null } + } + + private fun coerceTagsFromArray(value: Any, featureId: String, memberName: String): String? { + val tagList = when (value) { + is TagList -> value + is AnyObject -> value.proxy(TagList::class) + else -> { warnMismatch(featureId, memberName, "tags_from_array", value); return null } + } + val tagMap = tagList.toTagMap() + return try { toJSON(tagMap) } catch (_: Exception) { warnMismatch(featureId, memberName, "tags_from_array", value); null } + } + + private fun numberToLongOrNull(value: Any): Long? = when (value) { + is Byte -> value.toLong() + is Short -> value.toLong() + is Int -> value.toLong() + is Long -> value + is Int64 -> value.toLong() + is Float -> if (value.isFinite() && value == value.toLong().toFloat()) value.toLong() else null + is Double -> if (value.isFinite() && value == value.toLong().toDouble()) value.toLong() else null + else -> null + } + + private fun warnMismatch(featureId: String, memberName: String, expected: String, value: Any) { + logger.warn("Member '$memberName' on feature '$featureId': expected $expected, got ${value::class.simpleName}") + } +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/IMemberProcessor.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/IMemberProcessor.kt new file mode 100644 index 000000000..d468cbf45 --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/IMemberProcessor.kt @@ -0,0 +1,20 @@ +package naksha.model + +import naksha.model.objects.Member +import naksha.model.objects.NakshaCollection +import naksha.model.objects.NakshaFeature + +/** + * Optional callback, invoked by the storage after the value of a member has been extracted. This allowing some business logic to mutate the value before it is actually persisted. + */ +fun interface IMemberProcessor { + /** + * @param session The current session as context. + * @param collection The collection in which the feature is located. + * @param feature The feature being processed. + * @param member The name of the member that is processed. + * @param value The value that is about to be extracted from the feature, and to be persisted in a dedicated storage slot. + * @return the value that should be stored, can be just the same as the given one or any other acceptable primitive. + */ + fun processMember(session: ISession, collection: NakshaCollection, feature: NakshaFeature, member: Member, value: Any?): Any? +} \ No newline at end of file diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/ISession.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/ISession.kt index 8d52156e4..a7555c382 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/ISession.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/ISession.kt @@ -49,6 +49,28 @@ interface ISession : AutoCloseable { */ val options: SessionOptions + /** + * Removes all registered member processors. + * @return this. + */ + fun clearMemberProcessors(): ISession + + /** + * Add the given member processor. If the same processor is already added, the call is a no-op. Processors are invoked in the order in which they were added. + * @param memberName The name of the member as specified in the collection. + * @param memberProcessor The processor. + * @return this. + */ + fun addMemberProcessor(memberName: String, memberProcessor: IMemberProcessor): ISession + + /** + * Remove the given member processor. + * @param memberName The name of the member as specified in the collection. + * @param memberProcessor The processor. + * @return this. + */ + fun removeMemberProcessor(memberName: String, memberProcessor: IMemberProcessor): ISession + // TODO: Define a streaming API (full table scan) to consume all features from a collection. // This API is designed to backup data, or to execute a read request with a huge cardinality, // therefore it should always operate on snapshots (specific versions). diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/StorageTx.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/StorageTx.kt index 8609a7071..c100c5959 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/StorageTx.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/StorageTx.kt @@ -101,6 +101,11 @@ open class StorageTx private constructor( /** * Method to create the new members dict for the given write action on a feature. * + * Members are extracted from the feature using the [effective path][naksha.model.objects.Member.effectivePath] + * of each member declared in [collection.members]. When [collection.members] is `null`, all + * [StandardMembers.DEFAULT] are extracted. Session-derived and computed values (e.g. [updatedAt], [hash]) + * are written after the feature walk to override any values from the feature. + * * - Throws [NakshaError.ILLEGAL_ARGUMENT], if the given arguments are not sufficient to generate the new metadata. * @param map the map into which to persist the feature. * @param collection the collection into which to persist the feature. @@ -124,6 +129,23 @@ open class StorageTx private constructor( if (isExistingFeature && xyz.guid == null) { throw illegalArg("$action with atomic=$atomic requires that the feature has a UUID!") } + + // Determine the list of members to extract from the feature. + val membersToExtract = collection.members?.toList() ?: StandardMembers.DEFAULT + + // Walk the feature using each member's effective path and coerce to the expected type. + val members = HeapBook() + for (member in membersToExtract) { + if (member == null) continue + // Skip mandatory members that are managed by the storage and have no JSON path. + if (StandardMembers.MANDATORY_NAMES.contains(member.name)) continue + val path = member.effectivePath() + val raw = FeatureMemberValues.walkFeature(feature, path) + val coerced = FeatureMemberValues.coerce(raw, member.dataType, feature.id, member.name) + members.put(member.name, coerced) + } + + // Override with session-derived and computed values. val updatedAt: Int64 = this.updatedAt val createdAt: Int64? = if (isExistingFeature) xyz.createdAt else null val author: String? @@ -136,7 +158,7 @@ open class StorageTx private constructor( authorTs = xyz.authorTs } val featureType = if (collection.defaultFeatureType == feature.featureType) null else feature.featureType - val members = HeapBook() + members.put(StandardMembers.UpdatedAt.name, updatedAt) members.put(StandardMembers.CreatedAt.name, createdAt) members.put(StandardMembers.AuthorTimestamp.name, authorTs) @@ -147,17 +169,8 @@ open class StorageTx private constructor( members.put(StandardMembers.Hash.name, calculateHash(feature)) members.put(StandardMembers.HereTile.name, calculateHereTile(feature)) members.put(StandardMembers.Id.name, feature.id) - members.put(StandardMembers.Origin.name, null) - members.put(StandardMembers.Target.name, null) members.put(StandardMembers.FeatureType.name, featureType) - members.put(StandardMembers.CustomValue0.name, xyz.cv0) - members.put(StandardMembers.CustomValue1.name, xyz.cv1) - members.put(StandardMembers.CustomValue2.name, xyz.cv2) - members.put(StandardMembers.CustomValue3.name, xyz.cv3) - members.put(StandardMembers.CustomString0.name, xyz.cs0) - members.put(StandardMembers.CustomString1.name, xyz.cs1) - members.put(StandardMembers.CustomString2.name, xyz.cs2) - members.put(StandardMembers.CustomString3.name, xyz.cs3) + return members } @@ -180,23 +193,26 @@ open class StorageTx private constructor( atomic: Boolean = false ): Tuple { val members = buildMembers(map, collection, feature, action, atomic) - val dataEncoding = getDataEncoding(feature, collection) + // Override geometry, referencePoint, tags, and attachment with encoded values from the feature. + // These are the canonical values at tuple time (the feature walk may have extracted them from + // stale JSON paths, but the direct feature properties are authoritative). val xyz = feature.properties.xyz - val actionBits = Int64(action.intValue.toLong()) - val featureNumber = feature.featureNumber - val versionVal = version.txn and Int64(-4L) or actionBits - val nextVersion: Int64 = if (map.id == Naksha.ADMIN_MAP && collection.id == Naksha.TRANSACTIONS_COL) versionVal else Int64(-1L) - val dict = dictReader?.getEncodingDictionary(feature) - val featureBytes = Naksha.encodeFeature(feature, dataEncoding, dict) val geoBytes = Naksha.encodeGeometry(feature.geometry) - val refPoint = Naksha.encodeGeometry(feature.referencePoint) + val refPointBytes = Naksha.encodeGeometry(feature.referencePoint) val tagsJson = Naksha.encodeTagList(xyz.tags) if (members is HeapBook) { members.put(StandardMembers.Geometry.name, geoBytes) - members.put(StandardMembers.ReferencePoint.name, refPoint) + members.put(StandardMembers.ReferencePoint.name, refPointBytes) members.put(StandardMembers.Tags.name, tagsJson) members.put(StandardMembers.Attachment.name, attachment) } + val dataEncoding = getDataEncoding(feature, collection) + val actionBits = Int64(action.intValue.toLong()) + val featureNumber = feature.featureNumber + val versionVal = version.txn and Int64(-4L) or actionBits + val nextVersion: Int64 = if (map.id == Naksha.ADMIN_MAP && collection.id == Naksha.TRANSACTIONS_COL) versionVal else Int64(-1L) + val dict = dictReader?.getEncodingDictionary(feature) + val featureBytes = Naksha.encodeFeature(feature, dataEncoding, dict) return Tuple( storageNumber = storageNumber, mapNumber = map.number, diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCustomMemberValues.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCustomMemberValues.kt index 2d9be1de9..4b6c05d3b 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCustomMemberValues.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCustomMemberValues.kt @@ -2,6 +2,7 @@ package naksha.psql +import naksha.model.FeatureMemberValues import naksha.base.AnyList import naksha.base.AnyObject import naksha.base.Int64 @@ -11,7 +12,6 @@ import naksha.base.Platform.PlatformCompanion.logger import naksha.base.Platform.PlatformCompanion.toJSON import naksha.model.NakshaError import naksha.model.NakshaException -import naksha.model.TagList import naksha.model.objects.Member import naksha.model.objects.MemberList import naksha.model.objects.MemberType diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgSession.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgSession.kt index f4888dd4b..ec18ea749 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgSession.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgSession.kt @@ -341,6 +341,33 @@ open class PgSession( private var _closed = false + /** + * Registered member processors, keyed by member name. + * Processors are invoked in the order in which they were added. + * @since 3.0 + */ + private val memberProcessors: MutableMap> = mutableMapOf() + + override fun clearMemberProcessors(): ISession { + memberProcessors.clear() + return this + } + + override fun addMemberProcessor(memberName: String, memberProcessor: IMemberProcessor): ISession { + var processors = memberProcessors[memberName] + if (processors == null) { + processors = mutableListOf() + memberProcessors[memberName] = processors + } + processors.add(memberProcessor) + return this + } + + override fun removeMemberProcessor(memberName: String, memberProcessor: IMemberProcessor): ISession { + memberProcessors[memberName]?.remove(memberProcessor) + return this + } + override fun isClosed(): Boolean = _closed override fun close() { From a0b1d0bb1a25065720a1a80d9c2a4bbf99744eb9 Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Wed, 10 Jun 2026 13:46:35 +0200 Subject: [PATCH 08/57] Little architectural overview generated by AI. Signed-off-by: Alexander Lowey-Weber --- docs/ai/architecture-overview.md | 206 +++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 docs/ai/architecture-overview.md diff --git a/docs/ai/architecture-overview.md b/docs/ai/architecture-overview.md new file mode 100644 index 000000000..da5e2bca6 --- /dev/null +++ b/docs/ai/architecture-overview.md @@ -0,0 +1,206 @@ +# Architecture Overview + +## Architecture Overview + +The codebase follows a **two-layer abstraction** pattern: + +### Layer 1: `lib-model` — Storage-agnostic interfaces +- **`ISession`** → core session API with `execute(request: Request): Response` +- **`IStorage`** → storage lifecycle, session creation +- **`IWriteSession`** → extends read session with `commit()`, `rollback()`, `useTransaction()` +- **`StorageTx`** → tuple encoding/decoding, member building +- **`AbstractStorage`** → base class all storages must extend (caching, lifecycle) + +### Layer 2: `lib-psql` — PostgreSQL implementation +- **`PgSession`** → implements `ISession`, manages PG connections +- **`PgStorage`** → extends `AbstractStorage`, manages PG connection pools +- **`PgWriter`** → stateful writer, dispatches to operation-specific classes +- **`PgWriterInsert/Upsert/Update/Delete`** → SQL generation per operation + +## Code Flow: Writing a New Feature + +``` +Client Code + │ + ├── 1. Build Write instruction + │ Write().createFeature(collection, feature) + │ Write().upsertFeature(mapId, colId, feature) + │ └── Sets: mapId, collectionId, op=CREATE/UPSERT, feature + │ + ├── 2. Wrap in WriteRequest + │ WriteRequest().add(write) + │ + ├── 3. Execute on session + │ session.execute(writeRequest) + │ │ + │ ├── PgSession.execute() routes to writer + │ │ writer = PgWriter(session, useSavepoint) + │ │ writer.execute(request.writes) + │ │ │ + │ │ ├── prepareWrite() + │ │ │ ├── Resolves mapId → PgMap (from adminMap cache/DB) + │ │ │ ├── Resolves colId → PgCollection + │ │ │ ├── For map/collection creates: calls createPgMap/createPgCollection + │ │ │ └── For features: builds PgWrite wrapper + │ │ │ + │ │ ├── groupOperations() + │ │ │ ├── Groups writes by map → collection → partition → op + │ │ │ └── For CREATE/UPSERT/UPDATE: calls StorageTx.created()/updated() + │ │ │ └── Builds Tuple: + │ │ │ ├── buildMembers() → IBook with metadata (updated_at, author, hash, etc.) + │ │ │ ├── Encodes feature (Naksha.encodeFeature → JBON/JSON bytes) + │ │ │ ├── Encodes geometry (Naksha.encodeGeometry → TWKB bytes) + │ │ │ └── Returns Tuple(storage#, map#, col#, fn, version, members, feature) + │ │ │ + │ │ └── executeWrite(map, col, partition, byOp) + │ │ ├── PgWriterInsert.execute(conn) → INSERT SQL + │ │ ├── PgWriterUpsert.execute(conn) → CTE-based UPSERT SQL + │ │ ├── PgWriterUpdate.execute(conn) → UPDATE with version check + │ │ └── PgWriterDelete.execute(conn) → tombstone/PURGE SQL + │ │ + │ └── Returns SuccessResponse with tuple numbers + │ + └── 4. Commit + session.commit() + ├── Persists transaction record to admin map + └── conn.commit() +``` + +## Key Abstractions for New Features + +| Concept | File | Purpose | +|---------|------|---------| +| **`Write`** | `lib-model/..request/Write.kt` | DSL for CRUD ops: `createFeature()`, `upsertFeature()`, etc. | +| **`WriteOp`** | `lib-model/..request/WriteOp.kt` | Enum: CREATE, UPSERT, UPDATE, DELETE, PURGE | +| **`Tuple`** | `lib-model/../Tuple.kt` | Immutable feature state: address (storage/map/col/fn/version) + members + feature bytes | +| **`StorageTx`** | `lib-model/../StorageTx.kt` | Builds Tuples from features: `created()`, `updated()`, `deleted()` | +| **`IMemberProcessor`** | `lib-model/../IMemberProcessor.kt` | Extension point for pre-persistence member mutation | +| **`PgWriter`** | `lib-psql/../PgWriter.kt` | Groups writes, dispatches to op-specific writers | +| **`PgWriterInsert`** | `lib-psql/../PgWriterInsert.kt` | SQL INSERT generation | +| **`PgWriterUpsert`** | `lib-psql/../PgWriterUpsert.kt` | CTE-based conditional insert/update | +| **`PgSession`** | `lib-psql/../PgSession.kt` | Connection management, `execute()` routing | + +## Extension Points + +1. **New storage backend**: Extend `AbstractStorage`, implement `ISession`, `PgWriter`-equivalent classes +2. **Custom member processing**: `session.addMemberProcessor(memberName, processor)` — hooks into pre-persistence pipeline +3. **New write operations**: Add to `WriteOp` enum, create new `PgWriter*` class, add dispatch in `PgWriter.executeWrite()` + +## Members Extraction: From NakshaFeature to PostgreSQL Columns + +### Three-Stage Write Path + +**Stage 1: `StorageTx.buildMembers()` → `IBook` (in-memory members dict)** + +File: `lib-model/../StorageTx.kt:114-162` + +During `PgWriter.groupOperations()`, each write calls `tx.created()`/`tx.updated()`/`tx.deleted()` which invokes `buildTuple()` → `buildMembers()`. A `HeapBook` is created with all standard members extracted from the `NakshaFeature`: + +``` +NakshaFeature + ├── feature.id → StandardMembers.Id + ├── feature.geometry → StandardMembers.Geometry (TWKB bytes) + ├── feature.referencePoint → StandardMembers.ReferencePoint (TWKB bytes) + ├── feature.properties.xyz.updatedAt → StandardMembers.UpdatedAt + ├── feature.properties.xyz.createdAt → StandardMembers.CreatedAt + ├── feature.properties.xyz.author → StandardMembers.Author + ├── feature.properties.xyz.authorTs → StandardMembers.AuthorTimestamp + ├── feature.properties.xyz.appId → StandardMembers.AppId + ├── feature.properties.xyz.changeCount → StandardMembers.ChangeCount (+1) + ├── feature.properties.xyz.tags → StandardMembers.Tags (JSON string) + ├── feature.properties.xyz.hash → StandardMembers.Hash (computed) + ├── feature.properties.xyz.hereTile → StandardMembers.HereTile (computed) + ├── feature.properties.xyz.featureType → StandardMembers.FeatureType + ├── feature.properties.xyz.cv0-3 → StandardMembers.CustomValue0-3 + ├── feature.properties.xyz.cs0-3 → StandardMembers.CustomString0-3 + └── attachment → StandardMembers.Attachment +``` + +The resulting `IBook` is stored on `Tuple.members`. + +**Stage 2: `PgColumnRows[row] = tuple` → Members into column arrays** + +File: `lib-psql/../PgColumnRows.kt:384-417` + +In the `PgWriterInsert`/`PgWriterUpsert`/`PgWriterUpdate` constructors, the `inRows` (`PgColumnRows`) is populated: + +```kotlin +// PgWriterInsert.kt:30-37 +for (write in writes) { + val tuple = write.tuple + if (tuple != null) { + inRows[i] = tuple // extracts IBook → column arrays + inRows.setCustomMembers(i, write.feature, members) // custom members + i++ + } +} +``` + +`PgColumnRows.set(row, tuple)` walks `tuple.members` by name and assigns each value into the corresponding typed column array (e.g., `set(row, PgColumn.updated_at, members.getByName("updated_at") as? Int64)`). + +**Stage 3: `inRows.values()` → PostgreSQL UNNEST** + +File: `lib-psql/../PgWriterInsert.kt:43-98` + +The column arrays are passed as prepared statement parameters to a multi-row `UNNEST` INSERT: + +```sql +WITH new_row AS ( + SELECT * FROM UNNEST($1, $2, $3, ...) AS t(fn, version, id, feature, ...) +) +INSERT INTO head_table (fn, version, id, feature, ...) +SELECT * FROM new_row +``` + +### Custom Members Flow + +File: `lib-psql/../PgCustomMemberValues.kt` + +For user-declared custom members on the collection: + +1. **`PgWriterInsert.init`** → `inRows.addCustomMembers(collection.head.members)` — adds column entries for each custom member +2. **`PgColumnRows.setCustomMembers(row, feature, members)`** — walks the feature using each member's `effectivePath()` via `PgCustomMemberValues.walkFeature()`, coerces the type via `PgCustomMemberValues.coerce()`, sets the column value +3. **`PgColumnRows[row] = tuple`** does NOT handle custom members — only the built-in `StandardMembers` + +### Members Book Creation — All Locations + +| Location | File:Line | Purpose | +|---------|-----------|---------| +| `StorageTx.buildMembers()` | `StorageTx.kt:139` | **Write path** — creates `HeapBook` from `NakshaFeature`, called during tuple construction in `PgWriter.groupOperations()` | +| `Naksha.decodeTuple()` | `Naksha.kt:520` | **Read path** — creates `HeapBook` when decoding a `Tuple` back into a `NakshaFeature` | +| `Naksha.decodeTuple()` | `Naksha.kt:581` | **Read path (alt)** — second decode path for `Tuple` → `NakshaFeature` | +| `PgColumnRows.getTuple()` | `PgColumnRows.kt:304` | **Read path** — creates `PgRowDict` (implements `IBook`) wrapping DB row columns, assigned to `Tuple.members` | + +### End-to-End Data Flow + +``` + WRITE PATH READ PATH + ┌─────────────────────┐ ┌──────────────────────┐ + NakshaFeature│ StorageTx │ │ PgRowDict │ + properties│ .buildMembers() │ │ (PgColumnRows[row]) │ + │ ───────────────── │ │ ─────────────────── │ + │ xyz.updatedAt ─────┼─→ IBook │ column "updated_at" │──→ Tuple.members.getByName() + │ xyz.author ───────┼─→ .put() │ column "author" │──→ NakshaFeature.xyz.author + │ geometry ───────┼─→ .put() │ column "geo" │──→ NakshaFeature.geometry + │ feature blob │ │ column "feature" │──→ NakshaFeature (decode) + └────────┌────────────┘ └──────────┬───────────┘ + │ │ + ▼ ▲ + ┌─────────────────────┐ ┌──────────────────────┐ + │ PgColumnRows │ │ PgColumnRows │ + │ .set(row, tuple) │──→ SQL │ .add(cursor) │ + │ .setCustomMembers()│ UNNEST │ .getTuple(row) │ + └─────────────────────┘ └──────────────────────┘ + │ ▲ + ▼ │ + ┌─────────────────────┐ ┌──────────────────────┐ + │ PostgreSQL │ │ PostgreSQL │ + │ HEAD table │ ────┐ │ HEAD/HISTORY │ + │ (fn, version, id, │ │ │ SELECT ... │ + │ feature, geo, │ │ │ FROM head_table │ + │ author, ...) │ │ │ WHERE ... │ + └─────────────────────┘ │ └──────────────────────┘ + │ + PostgreSQL DB │ + │ +``` From 69df3e33e0d9fe72c38019a5d1a323751aba9b04 Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Wed, 10 Jun 2026 13:56:36 +0200 Subject: [PATCH 09/57] Fix errors left over from conflicts after rebasing. Signed-off-by: Alexander Lowey-Weber --- .../src/commonMain/kotlin/naksha/model/Naksha.kt | 2 +- .../src/commonMain/kotlin/naksha/psql/PgCustomMemberValues.kt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt index 4ce2c9077..f0c1dee05 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt @@ -479,7 +479,7 @@ class Naksha private constructor() { val dataEncodingStr = tuple.getStringMember(StandardMembers.DataEncoding) val dataEncoding = if (dataEncodingStr.isNullOrEmpty()) DEFAULT_DATA_ENCODING else DataEncoding.fromString(dataEncodingStr) val dictReader = dictionaryReader ?: getStorageByNumber(sn) ?: cache.getDictReader(sn) - val feature = decodeFeature(tuple.feature, dataEncoding, dictReader) ?: NakshaFeature() + val feature = decodeFeature(tuple.feature, dictReader) ?: NakshaFeature() feature.properties.xyz = XyzNs.fromTuple(tuple) val xyz = feature.properties.xyz val tags = tuple.getTagList(StandardMembers.Tags) diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCustomMemberValues.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCustomMemberValues.kt index 4b6c05d3b..f6aeb03ec 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCustomMemberValues.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCustomMemberValues.kt @@ -12,6 +12,7 @@ import naksha.base.Platform.PlatformCompanion.logger import naksha.base.Platform.PlatformCompanion.toJSON import naksha.model.NakshaError import naksha.model.NakshaException +import naksha.model.TagList import naksha.model.objects.Member import naksha.model.objects.MemberList import naksha.model.objects.MemberType From 06e3fa3827829c8be2f092e9a0ff19ed7c006553 Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Thu, 11 Jun 2026 14:26:05 +0200 Subject: [PATCH 10/57] Improve member handling, so that the path is always used and features can be put together as wanted. Signed-off-by: Alexander Lowey-Weber --- docs/latest/JBON2.md | 18 +- .../app/service/http/auth/JWTPayload.java | 2 +- .../service/http/auth/XyzHubActionMatrix.java | 2 +- .../service/http/auth/XyzHubAttributeMap.java | 4 +- .../http/tasks/WriteFeatureApiTask.java | 2 +- .../naksha/app/service/UpdateFeatureTest.java | 10 +- .../here/naksha/cli/utils/JsonParserTest.java | 2 +- .../kotlin/naksha/base/AnyObject.kt | 1 - .../commonMain/kotlin/naksha/base/Proxy.kt | 351 +++++++++++++++- .../models/ContextXyzFeatureResponse.java | 4 +- .../core/models/indexing/ConstraintAll.java | 2 +- .../core/models/indexing/ConstraintCheck.java | 2 +- .../core/models/indexing/ConstraintNot.java | 2 +- .../core/models/indexing/ConstraintOne.java | 2 +- .../lib/core/models/indexing/Index.java | 2 +- .../core/models/indexing/IndexProperty.java | 2 +- .../naksha/lib/core/models/naksha/Space.java | 10 +- .../core/models/naksha/SpaceProperties.java | 2 +- .../models/storage/ContextWriteFeatures.java | 4 +- .../storage/ReadFeaturesProxyWrapper.java | 3 +- .../here/naksha/lib/core/JsonMappingTest.java | 8 +- .../lib/extmanager/ExtensionCacheTest.java | 2 +- .../DefaultStorageHandlerProperties.java | 2 +- .../DefaultViewHandlerProperties.java | 2 +- .../handlers/TagFilterHandlerProperties.java | 6 +- .../IntHandlerForEventHandlerConfigs.java | 2 +- .../handlers/val/MockValidationHandler.java | 2 +- .../naksha/lib/hub/NakshaHubWiringTest.java | 2 +- .../commonMain/kotlin/naksha/jbon/HeapBook.kt | 14 +- .../naksha/mom10/Mom10Transformation.java | 2 +- .../kotlin/naksha/model/ISession.kt | 26 +- .../naksha/model/MemberProcessorList.kt | 15 + .../kotlin/naksha/model/MemberProcessorMap.kt | 112 +++++ .../kotlin/naksha/model/Metadata.kt | 32 +- .../commonMain/kotlin/naksha/model/Naksha.kt | 244 +++++------ .../kotlin/naksha/model/StorageTx.kt | 138 +----- .../commonMain/kotlin/naksha/model/Tuple.kt | 2 +- .../kotlin/naksha/model/TupleNumberVariant.kt | 9 +- .../commonMain/kotlin/naksha/model/Version.kt | 31 +- .../commonMain/kotlin/naksha/model/XyzNs.kt | 24 +- .../kotlin/naksha/model/objects/Index.kt | 16 +- .../kotlin/naksha/model/objects/JsonPath.kt | 32 +- .../kotlin/naksha/model/objects/Member.kt | 67 ++- .../kotlin/naksha/model/objects/MemberList.kt | 41 +- .../kotlin/naksha/model/objects/MemberType.kt | 7 + .../naksha/model/objects/NakshaCollection.kt | 35 ++ .../naksha/model/objects/NakshaFeature.kt | 2 +- .../kotlin/naksha/model/objects/NakshaTx.kt | 2 +- .../naksha/model/objects/StandardIndices.kt | 64 +-- .../naksha/model/objects/StandardMembers.kt | 395 ++---------------- .../kotlin/naksha/model/objects/XyzIndices.kt | 18 + .../kotlin/naksha/model/objects/XyzMembers.kt | 250 +++++++++++ .../kotlin/naksha/model/MemberTest.kt | 2 +- .../kotlin/naksha/psql/PgSession.kt | 24 +- .../commonMain/kotlin/naksha/psql/PgUtil.kt | 17 - .../kotlin/naksha/psql/PgWriterUpdate.kt | 10 +- .../kotlin/naksha/psql/PgWriterUpsert.kt | 8 +- .../kotlin/naksha/psql/AttachmentTest.kt | 34 +- .../kotlin/naksha/psql/ChainCollectionTest.kt | 14 + .../kotlin/naksha/psql/PgUtilTest.kt | 126 +++--- .../naksha/psql/TupleNumberPersistenceTest.kt | 2 - .../kotlin/naksha/psql/UpdateFeatureTest.kt | 18 +- .../lib/view/ViewWriteSessionTests.java | 4 +- .../storage/http/HttpStorageProperties.java | 2 +- .../http/HttpStoragePropertiesTest.java | 2 +- 65 files changed, 1302 insertions(+), 992 deletions(-) create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/MemberProcessorList.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/MemberProcessorMap.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzIndices.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzMembers.kt diff --git a/docs/latest/JBON2.md b/docs/latest/JBON2.md index 6184dd94e..56fa693d5 100644 --- a/docs/latest/JBON2.md +++ b/docs/latest/JBON2.md @@ -868,15 +868,15 @@ The `database_number` of the `global` [book] **MUST** match the `database_number The `members` [book] is per-tuple and travels with the tuple. This means, the storage need to keep the content of the `members` [book] next to the `tuple` and always read them together. It can embed the members into the tuple, or do something else. For example, in `lib-psql` _(the PostgreSQL implementation of `lib-data` storage API)_ the members are stored as own dedicated columns. So, `lib-psql` will store `feature` _(the actual tuple)_, `fn`, `version`, `global_book_fn`, `next_version`, and `id` as dedicated database columns. If more members are defined for a collection, then `lib-psql` will generate more columns in the storage. This is as well the reason why the encoder always needs the collection specification, because the members are not arbitrary, they **MUST** match exactly the specified ones in the collection definition. However, for the decoder this is not important, the decoder has references in the _feature_ that refer to the member slots, therefore, it does not need any knowledge about the collection. It is able to decode the tuple with only the members [book] provided by the storage. -Some `elements` of the `members` [book] have a pre-defined meaning _(therefore custom members **MUST** have an `index` starting at 4)_: - -| i | Alias | Path | Type | Description | -|------|------------------|---------|---------------|-------------------------------------------------------------------------------------------------------------------------------------------------------| -| 0 | `tn` | `tn` | [TupleNumber] | The [Tuple-Number] of this tuple. | -| 1 | `global_book_fn` | (none) | [int]? | The _optional_ feature-number of the `global` [book] needed to decode; `null` if no global book is needed. | -| 2 | `next_version` | `nv` | [uint56] | The next version of the tuple; if the tuple is in _HEAD_ state the value will be `9_007_199_254_740_991L` _(JavaScript's `Number.MAX_SAFE_INTEGER`)_. | -| 3 | `id` | `id` | [String]? | The _optional_ identifier of this tuple; a string when the feature-number is negative; `null` when the feature-number is positive (≥ 0). | -| 4... | ... | ... | [indexable]? | All custom members appended starting here, types **MUST** be [indexable]. | +Some `elements` of the `members` [book] have a pre-defined meaning: + +| Name | Path | Type | Description | +|------------------|------------------------------------|---------------|------------------------------------------------------------------------------------------------------------------------------------------| +| `tn` | `properties->@ns:com:here:xyz->tn` | [TupleNumber] | The [Tuple-Number] of this tuple. | +| `global_book_fn` | `properties->@ns:com:here:xyz->gb` | [int]? | The _optional_ feature-number of the `global` [book] needed to decode; `null` if no global book is needed. | +| `next_version` | `properties->@ns:com:here:xyz->nv` | [uint56] | The next version of the tuple; if the tuple is in _HEAD_ state the value will be `9_007_199_254_740_991L`. | +| `id` | `id` | [String]? | The _optional_ identifier of this tuple; a string when the feature-number is negative; `null` when the feature-number is positive (≥ 0). | +| ... | ... | [indexable]? | All custom members appended starting here, types **MUST** be [indexable]. | The `next_version` MUST be encoded as [uint56] _(**lead-in** `0000_1101`)_, so it can be patched in place without changing the byte size of the tuple. diff --git a/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/auth/JWTPayload.java b/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/auth/JWTPayload.java index b2697f856..2f615e9a3 100644 --- a/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/auth/JWTPayload.java +++ b/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/auth/JWTPayload.java @@ -60,7 +60,7 @@ public class JWTPayload { if (urm == null) { return null; } - final ServiceUserRights hereActionMatrix = urm.get(URMServiceId.NAKSHA); + final ServiceUserRights hereActionMatrix = urm.getPath(URMServiceId.NAKSHA); if (hereActionMatrix == null) { return null; } diff --git a/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/auth/XyzHubActionMatrix.java b/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/auth/XyzHubActionMatrix.java index b7e852411..c3966860e 100644 --- a/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/auth/XyzHubActionMatrix.java +++ b/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/auth/XyzHubActionMatrix.java @@ -186,7 +186,7 @@ public void createConnector(@NotNull EventHandlerConfig eventHandler) { addAction(MANAGE_CONNECTORS, XyzHubAttributeMap.ofConnector(eventHandler)); // MANAGE_PACKAGES right is needed to add the connector to a packages. - List packages = JvmBoxingUtil.box(eventHandler.get(XyzHubAttributeMap.PACKAGES), StringList.class); + List packages = JvmBoxingUtil.box(eventHandler.getPath(XyzHubAttributeMap.PACKAGES), StringList.class); for (final @NotNull String packageId : packages) { addAction(MANAGE_PACKAGES, XyzHubAttributeMap.ofPackage(packageId)); } diff --git a/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/auth/XyzHubAttributeMap.java b/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/auth/XyzHubAttributeMap.java index eb0e5a0e0..f79312aa6 100644 --- a/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/auth/XyzHubAttributeMap.java +++ b/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/auth/XyzHubAttributeMap.java @@ -116,8 +116,8 @@ public class XyzHubAttributeMap extends AttributeMap { attributeMap.withValue( XyzHubAttributeMap.AUTHOR, eventHandler.getProperties().getXyz().getAuthor()); - if (eventHandler.get(XyzHubAttributeMap.PACKAGES) != null) { - attributeMap.withValue(XyzHubAttributeMap.PACKAGES, eventHandler.get(XyzHubAttributeMap.PACKAGES)); // oneOf + if (eventHandler.getPath(XyzHubAttributeMap.PACKAGES) != null) { + attributeMap.withValue(XyzHubAttributeMap.PACKAGES, eventHandler.getPath(XyzHubAttributeMap.PACKAGES)); // oneOf } return attributeMap; } diff --git a/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/WriteFeatureApiTask.java b/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/WriteFeatureApiTask.java index ce3c7af83..1759bce52 100644 --- a/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/WriteFeatureApiTask.java +++ b/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/WriteFeatureApiTask.java @@ -327,7 +327,7 @@ private XyzResponse patchAndPreProcess( // Prepare WriteRequest - separating insert from updates and keeping the order of the features from the request WriteRequest insertsAndUpdates = new WriteRequest(); for (NakshaFeature featureFromRequest : featuresFromRequest) { - NakshaFeature correspondingExistingFeature = existingFeaturesById.get(featureFromRequest.getId()); + NakshaFeature correspondingExistingFeature = existingFeaturesById.getPath(featureFromRequest.getId()); if (correspondingExistingFeature == null) { // Feature not yet persisted - just insert preProcessor.preProcess(featureFromRequest); diff --git a/here-naksha-app-service/src/jvmTest/java/com/here/naksha/app/service/UpdateFeatureTest.java b/here-naksha-app-service/src/jvmTest/java/com/here/naksha/app/service/UpdateFeatureTest.java index dcef88c91..746448a21 100644 --- a/here-naksha-app-service/src/jvmTest/java/com/here/naksha/app/service/UpdateFeatureTest.java +++ b/here-naksha-app-service/src/jvmTest/java/com/here/naksha/app/service/UpdateFeatureTest.java @@ -209,7 +209,7 @@ void tc0505_testUpdateFeaturesWithUuid() throws Exception { Assertions.assertNotNull(responseFeatureCollection); final NakshaFeature updatedFeature = responseFeatureCollection.getFeatures().get(0); - Assertions.assertEquals("30", updatedFeature.getProperties().get("speedLimit")); + Assertions.assertEquals("30", updatedFeature.getProperties().getPath("speedLimit")); // Execute request, outdated UUID, should fail feature.setProperties(newPropsOutdatedUuid); @@ -243,7 +243,7 @@ void tc0505_testUpdateFeaturesWithUuid() throws Exception { final XyzFeatureCollection featureCollection = parseJson(responseOverriding.body(), XyzFeatureCollection.class); Assertions.assertNotNull(featureCollection); final NakshaFeature overridenFeature = featureCollection.getFeatures().get(0); - Assertions.assertEquals("yesyesyes", overridenFeature.getProperties().get("overriden")); + Assertions.assertEquals("yesyesyes", overridenFeature.getProperties().getPath("overriden")); // Old properties like speedLimit should no longer be available // The feature has been completely overwritten by the PUT request with null UUID Assertions.assertFalse(overridenFeature.getProperties().containsKey("speedLimit")); @@ -278,8 +278,8 @@ void tc0506_testUpdateFeatureWithUuid() throws Exception { // And: update properties match final NakshaFeature updatedFeature = parseJson(responseUpdateSuccess.body(), NakshaFeature.class); - Assertions.assertEquals("30", updatedFeature.getProperties().get("speedLimit")); - Assertions.assertEquals("tc_506", updatedFeature.getProperties().get("this_test_id")); + Assertions.assertEquals("30", updatedFeature.getProperties().getPath("speedLimit")); + Assertions.assertEquals("tc_506", updatedFeature.getProperties().getPath("this_test_id")); // When: trying to update with outdated UUID final NakshaProperties newPropsOutdatedUuid = newPropsOldUuid.copy(true); @@ -302,7 +302,7 @@ void tc0506_testUpdateFeatureWithUuid() throws Exception { // Then: update suceeds assertThat(responseOverriding).hasStatus(200); final NakshaFeature overridenFeature = parseJson(responseOverriding.body(), NakshaFeature.class); - Assertions.assertEquals("yesyesyes", overridenFeature.getProperties().get("overriden")); + Assertions.assertEquals("yesyesyes", overridenFeature.getProperties().getPath("overriden")); // Old properties like isImportant should no longer be available // The feature has been completely overwritten by the PUT request with null UUID Assertions.assertFalse(overridenFeature.getProperties().containsKey("this_test_id")); diff --git a/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/utils/JsonParserTest.java b/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/utils/JsonParserTest.java index e974852a0..283c74ba3 100644 --- a/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/utils/JsonParserTest.java +++ b/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/utils/JsonParserTest.java @@ -105,6 +105,6 @@ void shouldProvideObject(@TempDir Path dir) throws IOException { // Then: key and value are present assertNotNull(object); - assertEquals("value", object.get("key")); + assertEquals("value", object.getPath("key")); } } \ No newline at end of file diff --git a/here-naksha-lib-base/src/commonMain/kotlin/naksha/base/AnyObject.kt b/here-naksha-lib-base/src/commonMain/kotlin/naksha/base/AnyObject.kt index 5b7276f88..59f3a0e89 100644 --- a/here-naksha-lib-base/src/commonMain/kotlin/naksha/base/AnyObject.kt +++ b/here-naksha-lib-base/src/commonMain/kotlin/naksha/base/AnyObject.kt @@ -12,4 +12,3 @@ import kotlin.js.JsExport @JsExport open class AnyObject : MapProxy(String::class, Any::class) - diff --git a/here-naksha-lib-base/src/commonMain/kotlin/naksha/base/Proxy.kt b/here-naksha-lib-base/src/commonMain/kotlin/naksha/base/Proxy.kt index e6640e41c..056d8d47e 100644 --- a/here-naksha-lib-base/src/commonMain/kotlin/naksha/base/Proxy.kt +++ b/here-naksha-lib-base/src/commonMain/kotlin/naksha/base/Proxy.kt @@ -3,12 +3,16 @@ package naksha.base import naksha.base.Platform.PlatformCompanion.isNil +import naksha.base.PlatformListApi.PlatformListApiCompanion.array_get +import naksha.base.PlatformListApi.PlatformListApiCompanion.array_set +import naksha.base.PlatformMapApi.PlatformMapApiCompanion.map_get +import naksha.base.PlatformMapApi.PlatformMapApiCompanion.map_set import naksha.base.fn.Fn0 import naksha.base.fn.Fn1 import kotlin.js.JsExport +import kotlin.js.JsName import kotlin.js.JsStatic import kotlin.jvm.JvmStatic -import kotlin.jvm.JvmSuppressWildcards import kotlin.reflect.KClass /** @@ -228,4 +232,347 @@ abstract class Proxy : PlatformObject { */ fun contentDeepEquals(other: Proxy): Boolean = PlatformUtil.deepEquals(this, other) -} + /** + * Get the property from the given path. + * + * @param path the JSON path to query. + * @return the value at the path or `null`, if the path does not exist or the value is actually `null`. + * @since 3.0 + */ + @JsName("getPathByList") + fun getPath(path: List): Any? { + var current: Any? = this.platformObject() + for (key in path) { + if (key is String) { + if (current !is PlatformMap) return null + current = map_get(current, key) + continue + } + if (key is Number) { + if (current !is PlatformList) return null + val index = key.toInt() + current = array_get(current, index) + continue + } + return null + } + if (current is PlatformMap) return Platform.proxy(current, AnyObject::class) + if (current is PlatformList) return Platform.proxy(current, AnyList::class) + if (current is PlatformDataView) return Platform.proxy(current, DataViewProxy::class) + return current + } + + /** + * Set the property at the given path. Creates the path, if it does not exist yet. Throws an [RuntimeException] if the path exists, but is of wrong type, for example an array is expected, but an object found or vice versa. + * + * @param value the value to set. + * @param path the JSON path to mutate. + * @return the previous value. + * @since 3.0 + */ + @JsName("setPathByList") + fun setPath(value: Any?, path: List): Any? { + val pathEnd = path.size + val pathLast = pathEnd - 1 + var current: Any = this.platformObject() + for (i in 0 until pathLast) { + val key = path[i] + if (key is String) { + if (current !is PlatformMap) throw RuntimeException("Invalid value at key '$key', expected object") + // --- current is PlatformMap --- + val value = map_get(current, key) + if (value == null) { + // The key does not exist, check if we should create a map or list. + val next_key = path[i + 1] + if (next_key is String) { + val new_map = Platform.newMap() + map_set(current, key, new_map) + current = new_map + continue + } + if (next_key is Number) { + val new_list = Platform.newList() + map_set(current, key, new_list) + current = new_list + continue + } + // The next key is invalid + throw RuntimeException("Invalid key in path: '$next_key' at position ${i+1}") + } + current = value + continue + } + if (key is Number) { + if (current !is PlatformList) throw RuntimeException("Invalid value at key '$key', expected array") + // --- current is PlatformList --- + val index: Int = key.toInt() + val value = array_get(current, index) + if (value == null) { + // The index does not exist, check if we should create a map or list. + val next_key = path[i + 1] + if (next_key is String) { + val new_map = Platform.newMap() + array_set(current, index, new_map) + current = new_map + continue + } + if (next_key is Number) { + val new_list = Platform.newList() + array_set(current, index, new_list) + current = new_list + continue + } + // The next key is invalid + throw RuntimeException("Invalid key in path: '$next_key' at position ${i+1}") + } + current = value + continue + } + throw RuntimeException("Invalid key in path: '$key' at position $i") + } + val key = path[pathLast] + if (key is String) { + if (current !is PlatformMap) throw RuntimeException("Invalid value at key '$key', expected object") + val oldValue = map_get(current, key) + map_set(current, key, value) + return oldValue + } + if (key is Number) { + if (current !is PlatformList) throw RuntimeException("Invalid value at key '$key', expected array") + val index = key.toInt() + val oldValue = array_get(current, index) + array_set(current, index, value) + return oldValue + } + throw RuntimeException("Invalid key in path: '$key' at position $pathLast") + } + + /** + * Get the property from the given path. + * + * @param path the JSON path to query. + * @return the value at the path or `null`, if the path does not exist or the value is actually `null`. + * @since 3.0 + */ + @JsName("getPathByArray") + fun getPath(path: Array, length: Int = path.size): Any? { + var current: Any? = this.platformObject() + for (i in 0 until length) { + if (i >= path.size) return null + val key = path[i] + if (key is String) { + if (current !is PlatformMap) return null + current = map_get(current, key) + continue + } + if (key is Number) { + if (current !is PlatformList) return null + val index = key.toInt() + current = array_get(current, index) + continue + } + return null + } + if (current is PlatformMap) return Platform.proxy(current, AnyObject::class) + if (current is PlatformList) return Platform.proxy(current, AnyList::class) + if (current is PlatformDataView) return Platform.proxy(current, DataViewProxy::class) + return current + } + + /** + * Set the property at the given path. Creates the path, if it does not exist yet. Throws an [RuntimeException] if the path exists, but is of wrong type, for example an array is expected, but an object found or vice versa. + * + * @param value the value to set. + * @param path the JSON path to mutate. + * @return the previous value. + * @since 3.0 + */ + @JsName("setPathByArray") + fun setPath(value: Any?, path: Array, pathEnd: Int = path.size): Any? { + val pathLast = pathEnd - 1 + var current: Any = this.platformObject() + for (i in 0 until pathLast) { + val key = path[i] + if (key is String) { + if (current !is PlatformMap) throw RuntimeException("Invalid value at key '$key', expected object") + // --- current is PlatformMap --- + val value = map_get(current, key) + if (value == null) { + // The key does not exist, check if we should create a map or list. + val next_key = path[i + 1] + if (next_key is String) { + val new_map = Platform.newMap() + map_set(current, key, new_map) + current = new_map + continue + } + if (next_key is Number) { + val new_list = Platform.newList() + map_set(current, key, new_list) + current = new_list + continue + } + // The next key is invalid + throw RuntimeException("Invalid key in path: '$next_key' at position ${i+1}") + } + current = value + continue + } + if (key is Number) { + if (current !is PlatformList) throw RuntimeException("Invalid value at key '$key', expected array") + // --- current is PlatformList --- + val index: Int = key.toInt() + val value = array_get(current, index) + if (value == null) { + // The index does not exist, check if we should create a map or list. + val next_key = path[i + 1] + if (next_key is String) { + val new_map = Platform.newMap() + array_set(current, index, new_map) + current = new_map + continue + } + if (next_key is Number) { + val new_list = Platform.newList() + array_set(current, index, new_list) + current = new_list + continue + } + // The next key is invalid + throw RuntimeException("Invalid key in path: '$next_key' at position ${i+1}") + } + current = value + continue + } + throw RuntimeException("Invalid key in path: '$key' at position $i") + } + val key = path[pathLast] + if (key is String) { + if (current !is PlatformMap) throw RuntimeException("Invalid value at key '$key', expected object") + val oldValue = map_get(current, key) + map_set(current, key, value) + return oldValue + } + if (key is Number) { + if (current !is PlatformList) throw RuntimeException("Invalid value at key '$key', expected array") + val index = key.toInt() + val oldValue = array_get(current, index) + array_set(current, index, value) + return oldValue + } + throw RuntimeException("Invalid key in path: '$key' at position $pathLast") + } + + /** + * Get the property from the given path. + * + * @param path the JSON path to query. + * @return the value at the path or `null`, if the path does not exist or the value is actually `null`. + * @since 3.0 + */ + fun getPath(vararg path: Any): Any? { + var current: Any? = this.platformObject() + for (key in path) { + if (key is String) { + if (current !is PlatformMap) return null + current = map_get(current, key) + continue + } + if (key is Number) { + if (current !is PlatformList) return null + val index = key.toInt() + current = array_get(current, index) + continue + } + return null + } + if (current is PlatformMap) return Platform.proxy(current, AnyObject::class) + if (current is PlatformList) return Platform.proxy(current, AnyList::class) + if (current is PlatformDataView) return Platform.proxy(current, DataViewProxy::class) + return current + } + + /** + * Set the property at the given path. Creates the path, if it does not exist yet. Throws an [RuntimeException] if the path exists, but is of wrong type, for example an array is expected, but an object found or vice versa. + * + * @param value the value to set. + * @param path the JSON path to mutate. + * @return the previous value. + * @since 3.0 + */ + fun setPath(value: Any?, vararg path: Any): Any? { + val pathEnd = path.size + val pathLast = pathEnd - 1 + var current: Any = this.platformObject() + for (i in 0 until pathLast) { + val key = path[i] + if (key is String) { + if (current !is PlatformMap) throw RuntimeException("Invalid value at key '$key', expected object") + // --- current is PlatformMap --- + val value = map_get(current, key) + if (value == null) { + // The key does not exist, check if we should create a map or list. + val next_key = path[i + 1] + if (next_key is String) { + val new_map = Platform.newMap() + map_set(current, key, new_map) + current = new_map + continue + } + if (next_key is Number) { + val new_list = Platform.newList() + map_set(current, key, new_list) + current = new_list + continue + } + // The next key is invalid + throw RuntimeException("Invalid key in path: '$next_key' at position ${i+1}") + } + current = value + continue + } + if (key is Number) { + if (current !is PlatformList) throw RuntimeException("Invalid value at key '$key', expected array") + // --- current is PlatformList --- + val index: Int = key.toInt() + val value = array_get(current, index) + if (value == null) { + // The index does not exist, check if we should create a map or list. + val next_key = path[i + 1] + if (next_key is String) { + val new_map = Platform.newMap() + array_set(current, index, new_map) + current = new_map + continue + } + if (next_key is Number) { + val new_list = Platform.newList() + array_set(current, index, new_list) + current = new_list + continue + } + // The next key is invalid + throw RuntimeException("Invalid key in path: '$next_key' at position ${i+1}") + } + current = value + continue + } + throw RuntimeException("Invalid key in path: '$key' at position $i") + } + val key = path[pathLast] + if (key is String) { + if (current !is PlatformMap) throw RuntimeException("Invalid value at key '$key', expected object") + val oldValue = map_get(current, key) + map_set(current, key, value) + return oldValue + } + if (key is Number) { + if (current !is PlatformList) throw RuntimeException("Invalid value at key '$key', expected array") + val index = key.toInt() + val oldValue = array_get(current, index) + array_set(current, index, value) + return oldValue + } + throw RuntimeException("Invalid key in path: '$key' at position $pathLast") + } +} \ No newline at end of file diff --git a/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/ContextXyzFeatureResponse.java b/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/ContextXyzFeatureResponse.java index dd4f9382b..2c3029288 100644 --- a/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/ContextXyzFeatureResponse.java +++ b/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/ContextXyzFeatureResponse.java @@ -44,7 +44,7 @@ public void setFeatures(@NotNull List nakshaFeatures) { */ @ApiStatus.AvailableSince(NakshaVersion.v2_0_11) public @Nullable List getContext() { - return JvmBoxingUtil.box(get(CONTEXT_KEY), NakshaFeatureList.class); + return JvmBoxingUtil.box(getPath(CONTEXT_KEY), NakshaFeatureList.class); } @ApiStatus.AvailableSince(NakshaVersion.v2_0_11) @@ -66,7 +66,7 @@ public void setContext(@Nullable NakshaFeatureList contextFeatures) { */ @ApiStatus.AvailableSince(NakshaVersion.v2_0_11) public @Nullable List getViolations() { - return JvmBoxingUtil.box(get(VIOLATIONS_KEY), NakshaFeatureList.class); + return JvmBoxingUtil.box(getPath(VIOLATIONS_KEY), NakshaFeatureList.class); } @ApiStatus.AvailableSince(NakshaVersion.v2_0_11) diff --git a/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/indexing/ConstraintAll.java b/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/indexing/ConstraintAll.java index dba7c74b8..12478fb92 100644 --- a/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/indexing/ConstraintAll.java +++ b/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/indexing/ConstraintAll.java @@ -29,7 +29,7 @@ public class ConstraintAll extends Constraint { * The constraints that all need to hold true (AND). */ public List getOf() { - return JvmBoxingUtil.box(get(OF), ConstraintList.class); + return JvmBoxingUtil.box(getPath(OF), ConstraintList.class); } public void setOf(List of) { diff --git a/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/indexing/ConstraintCheck.java b/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/indexing/ConstraintCheck.java index fdddeca95..1a22edc7f 100644 --- a/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/indexing/ConstraintCheck.java +++ b/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/indexing/ConstraintCheck.java @@ -66,7 +66,7 @@ public enum Test { /** The check to perform. */ public Test getTest() { - return JvmBoxingUtil.box(get(TEST), Test.class); + return JvmBoxingUtil.box(getPath(TEST), Test.class); } public void setTest(Test test) { diff --git a/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/indexing/ConstraintNot.java b/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/indexing/ConstraintNot.java index 9fb42aa82..f3cf76203 100644 --- a/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/indexing/ConstraintNot.java +++ b/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/indexing/ConstraintNot.java @@ -29,7 +29,7 @@ public class ConstraintNot extends Constraint { * The constraints that should be negated. */ public List getOf() { - return JvmBoxingUtil.box(get(OF), ConstraintList.class); + return JvmBoxingUtil.box(getPath(OF), ConstraintList.class); } public void setOf(List of) { diff --git a/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/indexing/ConstraintOne.java b/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/indexing/ConstraintOne.java index 96f87a3d9..e3052e169 100644 --- a/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/indexing/ConstraintOne.java +++ b/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/indexing/ConstraintOne.java @@ -29,7 +29,7 @@ public class ConstraintOne extends Constraint { * The constraints of which at least one need to hold true (OR). */ public List getOf() { - return JvmBoxingUtil.box(get(OF), ConstraintList.class); + return JvmBoxingUtil.box(getPath(OF), ConstraintList.class); } public void setOf(List of) { diff --git a/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/indexing/Index.java b/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/indexing/Index.java index 0ba5c3911..2463a506f 100644 --- a/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/indexing/Index.java +++ b/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/indexing/Index.java @@ -63,7 +63,7 @@ public void setIndexHistory(boolean indexHistory) { /** All properties that should be included in this index. */ public List getIndexProperties() { - return JvmBoxingUtil.box(getProperties().get(NESTED_INDEX_PROPS), IndexProperties.class); + return JvmBoxingUtil.box(getProperties().getPath(NESTED_INDEX_PROPS), IndexProperties.class); } public void setProperties(List properties) { diff --git a/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/indexing/IndexProperty.java b/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/indexing/IndexProperty.java index 06e50adc3..33989f870 100644 --- a/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/indexing/IndexProperty.java +++ b/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/indexing/IndexProperty.java @@ -54,7 +54,7 @@ public void setAsc(boolean asc) { * Optionally decide if {@code null} values should be ordered first or last. If not explicitly defined, automatically decided. */ public Nulls getNulls() { - return JvmBoxingUtil.box(get(NULLS), Nulls.class); + return JvmBoxingUtil.box(getPath(NULLS), Nulls.class); } public void setNulls(Nulls nulls) { diff --git a/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/naksha/Space.java b/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/naksha/Space.java index ec9e27f92..eb7c7b9a7 100644 --- a/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/naksha/Space.java +++ b/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/naksha/Space.java @@ -116,7 +116,7 @@ public void setShared(final boolean shared) { * Copyright information for the data in the space. */ public List getCopyright() { - return JvmBoxingUtil.box(get(COPYRIGHT), Copyright.List.class); + return JvmBoxingUtil.box(getPath(COPYRIGHT), Copyright.List.class); } public void setCopyright(final List copyright) { @@ -134,7 +134,7 @@ public void setCopyright(final List copyright) { * Information about the license bound to the data within the space. For valid keywords see {@link License}. */ public License getLicense() { - return JvmBoxingUtil.box(get(LICENSE), License.class); + return JvmBoxingUtil.box(getPath(LICENSE), License.class); } public void setLicense(final License license) { @@ -150,7 +150,7 @@ public void setLicense(final License license) { * List of packages that this space belongs to. */ public List<@NotNull String> getPackages() { - return JvmBoxingUtil.box(get(PACKAGES), StringList.class); + return JvmBoxingUtil.box(getPath(PACKAGES), StringList.class); } public void setPackages(final List<@NotNull String> packages) { @@ -178,7 +178,7 @@ public void setReadOnly(final boolean readOnly) { * but the result can be bad. */ public @Nullable Map<@NotNull String, @NotNull Index> getIndices() { - return JvmBoxingUtil.box(get(INDICES), Index.Map.class); + return JvmBoxingUtil.box(getPath(INDICES), Index.Map.class); } public void setIndices(@Nullable Map<@NotNull String, @NotNull Index> indices) { @@ -192,7 +192,7 @@ public void setIndices(@Nullable Map<@NotNull String, @NotNull Index> indices) { * will fail, if the space does not fulfill the constraint. */ public @Nullable Map<@NotNull String, @NotNull Constraint> getConstraints() { - return JvmBoxingUtil.box(get(CONSTRAINTS), ConstraintMap.class); + return JvmBoxingUtil.box(getPath(CONSTRAINTS), ConstraintMap.class); } public void setConstraints(@Nullable Map<@NotNull String, @NotNull Constraint> constraints) { diff --git a/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/naksha/SpaceProperties.java b/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/naksha/SpaceProperties.java index e85b31818..04ca29c15 100644 --- a/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/naksha/SpaceProperties.java +++ b/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/naksha/SpaceProperties.java @@ -38,7 +38,7 @@ public class SpaceProperties extends NakshaProperties { * The backend storage collection details specified at space level */ public @Nullable NakshaCollection getCollection() { - return JvmBoxingUtil.box(get(NAKSHA_COLLECTION), NakshaCollection.class); + return JvmBoxingUtil.box(getPath(NAKSHA_COLLECTION), NakshaCollection.class); } public void setCollection(final @Nullable NakshaCollection collection) { diff --git a/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/storage/ContextWriteFeatures.java b/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/storage/ContextWriteFeatures.java index cff15668f..06079d47f 100644 --- a/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/storage/ContextWriteFeatures.java +++ b/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/storage/ContextWriteFeatures.java @@ -43,7 +43,7 @@ public abstract class ContextWriteFeatures extends WriteRequest { */ @ApiStatus.AvailableSince(NakshaVersion.v2_0_11) public @Nullable List getContext() { - return JvmBoxingUtil.box(get(CONTEXT_KEY), NakshaFeatureList.class); + return JvmBoxingUtil.box(getPath(CONTEXT_KEY), NakshaFeatureList.class); } @ApiStatus.AvailableSince(NakshaVersion.v2_0_11) @@ -65,7 +65,7 @@ public void setContext(@Nullable NakshaFeatureList contextFeatures) { */ @ApiStatus.AvailableSince(NakshaVersion.v2_0_11) public @Nullable List getViolations() { - return JvmBoxingUtil.box(get(VIOLATIONS_KEY), NakshaFeatureList.class); + return JvmBoxingUtil.box(getPath(VIOLATIONS_KEY), NakshaFeatureList.class); } @ApiStatus.AvailableSince(NakshaVersion.v2_0_11) diff --git a/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/storage/ReadFeaturesProxyWrapper.java b/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/storage/ReadFeaturesProxyWrapper.java index f42944f78..ca8955242 100644 --- a/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/storage/ReadFeaturesProxyWrapper.java +++ b/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/storage/ReadFeaturesProxyWrapper.java @@ -27,7 +27,6 @@ import naksha.model.request.query.ITagQuery; import org.jetbrains.annotations.NotNull; -import java.util.HashMap; import java.util.Map; @@ -69,7 +68,7 @@ public ReadFeaturesProxyWrapper withFeatureIds(StringList featureIds){ } public Map getQueryParameters() { - return JvmBoxingUtil.box(get(QUERY_PARAMETERS), QueryParameterMap.class); + return JvmBoxingUtil.box(getPath(QUERY_PARAMETERS), QueryParameterMap.class); } public T getQueryParameter(String key) throws ClassCastException { diff --git a/here-naksha-lib-core/src/jvmTest/java/com/here/naksha/lib/core/JsonMappingTest.java b/here-naksha-lib-core/src/jvmTest/java/com/here/naksha/lib/core/JsonMappingTest.java index 2f19bc6f4..68ecb184e 100644 --- a/here-naksha-lib-core/src/jvmTest/java/com/here/naksha/lib/core/JsonMappingTest.java +++ b/here-naksha-lib-core/src/jvmTest/java/com/here/naksha/lib/core/JsonMappingTest.java @@ -25,12 +25,10 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.here.naksha.lib.core.util.json.Json; -import com.here.naksha.lib.core.util.json.JsonSerializable; + import java.io.IOException; import naksha.base.*; -import naksha.model.NakshaError; import naksha.model.objects.NakshaFeature; import org.junit.jupiter.api.Test; @@ -45,8 +43,8 @@ public void testDeserializeFeature() { final NakshaFeature obj = jvmMap.proxy(Platform.klassFor(NakshaFeature.class)); assertNotNull(obj); - assertEquals(5, (int) obj.getProperties().get("x")); - assertEquals("123", obj.get("otherProperty")); + assertEquals(5, (int) obj.getProperties().getPath("x")); + assertEquals("123", obj.getPath("otherProperty")); } @Test diff --git a/here-naksha-lib-ext-manager/src/jvmTest/java/com/here/naksha/lib/extmanager/ExtensionCacheTest.java b/here-naksha-lib-ext-manager/src/jvmTest/java/com/here/naksha/lib/extmanager/ExtensionCacheTest.java index 6824ca312..a140c6a0a 100644 --- a/here-naksha-lib-ext-manager/src/jvmTest/java/com/here/naksha/lib/extmanager/ExtensionCacheTest.java +++ b/here-naksha-lib-ext-manager/src/jvmTest/java/com/here/naksha/lib/extmanager/ExtensionCacheTest.java @@ -154,7 +154,7 @@ public void testGetCachedExtensionsVersionUpdate() throws IOException { Assertions.assertEquals(1, newCached.size()); Extension updatedExt = newCached.get(0); - Assertions.assertEquals("child_extension_1", updatedExt.get("extensionId")); + Assertions.assertEquals("child_extension_1", updatedExt.getPath("extensionId")); Assertions.assertEquals("2.0", updatedExt.getVersion()); String ext1KeyNew = updatedExt.getEnv() + ":" + updatedExt.getId(); diff --git a/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/DefaultStorageHandlerProperties.java b/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/DefaultStorageHandlerProperties.java index 2a2e34d15..5c062444e 100644 --- a/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/DefaultStorageHandlerProperties.java +++ b/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/DefaultStorageHandlerProperties.java @@ -64,7 +64,7 @@ public void setStorageId(final @Nullable String storageId) { * Details of the backend xyz collection to use. If undefined, the collection defined at the {@link SpaceProperties} level will be used. */ public @Nullable NakshaCollection getCollection() { - return JvmBoxingUtil.box(get(COLLECTION), NakshaCollection.class); + return JvmBoxingUtil.box(getPath(COLLECTION), NakshaCollection.class); } public void setCollection(final @Nullable NakshaCollection collection) { diff --git a/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/DefaultViewHandlerProperties.java b/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/DefaultViewHandlerProperties.java index 8f773d9c9..0df211555 100644 --- a/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/DefaultViewHandlerProperties.java +++ b/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/DefaultViewHandlerProperties.java @@ -51,7 +51,7 @@ public void setStorageId(@Nullable String storageId) { } public @Nullable List getSpaceIds() { - return JvmBoxingUtil.box(get(SPACE_IDS), StringList.class); + return JvmBoxingUtil.box(getPath(SPACE_IDS), StringList.class); } public void setSpaceIds(@Nullable List spaceIds) { diff --git a/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/TagFilterHandlerProperties.java b/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/TagFilterHandlerProperties.java index b3a1213b1..ca0f962ec 100644 --- a/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/TagFilterHandlerProperties.java +++ b/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/TagFilterHandlerProperties.java @@ -46,7 +46,7 @@ public class TagFilterHandlerProperties extends NakshaProperties { * {@link naksha.model.request.WriteRequest} operations. */ public @Nullable List getAdd() { - return JvmBoxingUtil.box(get(ADD_VALUES), StringList.class); + return JvmBoxingUtil.box(getPath(ADD_VALUES), StringList.class); } public void setAdd(@Nullable final List add) { @@ -58,7 +58,7 @@ public void setAdd(@Nullable final List add) { * {@link naksha.model.request.WriteRequest} operations. This is applied before {@link #getAdd()} operation. */ public @Nullable List getRemoveWithPrefixes() { - return JvmBoxingUtil.box(get(REMOVE_W_PREFIXES), StringList.class); + return JvmBoxingUtil.box(getPath(REMOVE_W_PREFIXES), StringList.class); } public void setRemoveWithPrefixes(final @Nullable List removeWithPrefixes) { @@ -70,7 +70,7 @@ public void setRemoveWithPrefixes(final @Nullable List removeWithPrefixe * handler. */ public @Nullable List getContains() { - return JvmBoxingUtil.box(get(CONTAINS_VALUES), StringList.class); + return JvmBoxingUtil.box(getPath(CONTAINS_VALUES), StringList.class); } public void setContains(@Nullable List contains) { diff --git a/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/internal/IntHandlerForEventHandlerConfigs.java b/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/internal/IntHandlerForEventHandlerConfigs.java index 96546245d..287e3a63b 100644 --- a/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/internal/IntHandlerForEventHandlerConfigs.java +++ b/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/internal/IntHandlerForEventHandlerConfigs.java @@ -215,7 +215,7 @@ private Response spaceExistenceValidation(List spaceIds) { private @NotNull Response storageValidation( @NotNull EventHandlerConfig eventHandler, @NotNull String storagePropertyName) { - Object storageIdProp = eventHandler.getProperties().get(storagePropertyName); + Object storageIdProp = eventHandler.getProperties().getPath(storagePropertyName); if (storageIdProp == null) { return new ErrorResponse( NakshaError.ILLEGAL_ARGUMENT, diff --git a/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/val/MockValidationHandler.java b/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/val/MockValidationHandler.java index 6530c73f0..821ea21df 100644 --- a/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/val/MockValidationHandler.java +++ b/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/val/MockValidationHandler.java @@ -127,7 +127,7 @@ protected EventProcessingStrategy processingStrategyFor(IEvent event) { // feature #3, will have 3 violations i.e. min(3,3) // feature #4, will have 3 violations i.e. min(4,3) int violationsCount = Math.min(featureCnt, totalViolations); - final Object momType = feature.get("momType"); + final Object momType = feature.getPath("momType"); violations.addAll(getNViolationsWithFeatureReference( violationsCount, feature, diff --git a/here-naksha-lib-hub/src/jvmTest/java/com/here/naksha/lib/hub/NakshaHubWiringTest.java b/here-naksha-lib-hub/src/jvmTest/java/com/here/naksha/lib/hub/NakshaHubWiringTest.java index fad91e86f..78b178a2d 100644 --- a/here-naksha-lib-hub/src/jvmTest/java/com/here/naksha/lib/hub/NakshaHubWiringTest.java +++ b/here-naksha-lib-hub/src/jvmTest/java/com/here/naksha/lib/hub/NakshaHubWiringTest.java @@ -235,7 +235,7 @@ void testCreateFeatureRequestWiring() throws Exception { verify(spyWriter, times(3)).execute(reqCaptor.capture()); assertTrue(reqCaptor.getValue() instanceof WriteRequest); final List requests = reqCaptor.getAllValues(); - final String collectionId = ((Map) space.getProperties().get("collection")) + final String collectionId = ((Map) space.getProperties().getPath("collection")) .get("id") .toString(); // TODO: this is ambiguous (see Space::getCollectionId), discuss // Verify: WriteFeature into collection got called diff --git a/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/HeapBook.kt b/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/HeapBook.kt index 07967819c..a3d44249c 100644 --- a/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/HeapBook.kt +++ b/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/HeapBook.kt @@ -62,17 +62,19 @@ class HeapBook : IBook { * If the name already exists, the value is updated in-place. * @param name the member name. * @param value the value. + * @return The index at which the value was placed. * @since 3.0.0 */ - fun put(name: String, value: Any?) { + fun put(name: String, value: Any?): Int { val index = _nameIndex[name] if (index != null) { _values[index] = value - } else { - val idx = _names.size - _names.add(name) - _nameIndex[name] = idx - _values.add(value) + return index } + val idx = _names.size + _names.add(name) + _nameIndex[name] = idx + _values.add(value) + return idx } } diff --git a/here-naksha-lib-mm-util/src/jvmMain/java/com/here/naksha/mom10/Mom10Transformation.java b/here-naksha-lib-mm-util/src/jvmMain/java/com/here/naksha/mom10/Mom10Transformation.java index 8590db2c7..4fb470423 100644 --- a/here-naksha-lib-mm-util/src/jvmMain/java/com/here/naksha/mom10/Mom10Transformation.java +++ b/here-naksha-lib-mm-util/src/jvmMain/java/com/here/naksha/mom10/Mom10Transformation.java @@ -41,7 +41,7 @@ public static void populatePreMom10Namespaces(@Nullable NakshaFeature feature) { } NakshaProperties properties = feature.getProperties(); - Map meta = (Map) properties.get(META); + Map meta = (Map) properties.getPath(META); if (meta != null && !meta.isEmpty()) { MomDeltaNs deltaNs = deltaNs(meta); properties.setDelta(deltaNs); diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/ISession.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/ISession.kt index a7555c382..50ec35e5f 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/ISession.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/ISession.kt @@ -50,26 +50,14 @@ interface ISession : AutoCloseable { val options: SessionOptions /** - * Removes all registered member processors. - * @return this. - */ - fun clearMemberProcessors(): ISession - - /** - * Add the given member processor. If the same processor is already added, the call is a no-op. Processors are invoked in the order in which they were added. - * @param memberName The name of the member as specified in the collection. - * @param memberProcessor The processor. - * @return this. - */ - fun addMemberProcessor(memberName: String, memberProcessor: IMemberProcessor): ISession - - /** - * Remove the given member processor. - * @param memberName The name of the member as specified in the collection. - * @param memberProcessor The processor. - * @return this. + * Returns the [MemberProcessorMap] for this session. + * + * Use the map to register, remove, or inspect [IMemberProcessor] instances for individual members. + * Processors are invoked in the order in which they were added. + * @return the member processor map. + * @since 3.0 */ - fun removeMemberProcessor(memberName: String, memberProcessor: IMemberProcessor): ISession + fun processors(): MemberProcessorMap // TODO: Define a streaming API (full table scan) to consume all features from a collection. // This API is designed to backup data, or to execute a read request with a huge cardinality, diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/MemberProcessorList.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/MemberProcessorList.kt new file mode 100644 index 000000000..cace1b8c1 --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/MemberProcessorList.kt @@ -0,0 +1,15 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model + +import kotlin.js.JsExport + +/** + * An ordered list of [IMemberProcessor] instances registered for a single member name. + * + * Processors are invoked in the order in which they were added. This class extends [ArrayList] + * to allow direct iteration and indexed access. + * @since 3.0 + */ +@JsExport +open class MemberProcessorList(private val delegate: MutableList = ArrayList()) : MutableList by delegate diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/MemberProcessorMap.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/MemberProcessorMap.kt new file mode 100644 index 000000000..24163a0a7 --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/MemberProcessorMap.kt @@ -0,0 +1,112 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model + +import kotlin.js.JsExport +import kotlin.js.JsName + +/** + * A mutable map from member name to the list of [IMemberProcessor] instances registered for that member. + * + * The map is backed by a [MutableMap] and provides additional convenience methods: + * - [addProcessor] — adds a processor for a member (skips if already present) + * - [removeProcessor] — removes a processor for a member + * + * Processors are stored in a [MemberProcessorList] and invoked in insertion order. + * @since 3.0 + */ +@JsExport +open class MemberProcessorMap : MutableMap { + + private val delegate: MutableMap = mutableMapOf() + + // ------------------------------------------------------------------------- + // Convenience methods + // ------------------------------------------------------------------------- + + /** + * Add a processor for the given member name. + * + * If the processor is already registered for this member, the call is a no-op. + * Processors are invoked in the order in which they were added. + * @param name The name of the member as specified in the collection. + * @param processor The processor to add. + */ + @JsName("addProcessor") + open fun addProcessor(name: String, processor: IMemberProcessor) { + var list = delegate[name] + if (list == null) { + list = MemberProcessorList() + delegate[name] = list + } + if (!list.contains(processor)) { + list.add(processor) + } + } + + /** + * Remove a processor for the given member name. + * + * If the list becomes empty after removal, the entry is removed from the map. + * @param name The name of the member as specified in the collection. + * @param processor The processor to remove. + * @return `true` if the processor was found and removed, `false` otherwise. + */ + @JsName("removeProcessor") + open fun removeProcessor(name: String, processor: IMemberProcessor): Boolean { + val list = delegate[name] ?: return false + val removed = list.remove(processor) + if (list.isEmpty()) { + delegate.remove(name) + } + return removed + } + + /** + * Returns the list of processors for the given member name, or `null` if none are registered. + * @param name The member name. + */ + @JsName("getProcessors") + open fun getProcessors(name: String): MemberProcessorList? = delegate[name] + + // ------------------------------------------------------------------------- + // MutableMap delegation + // ------------------------------------------------------------------------- + + override val size: Int + get() = delegate.size + + override fun isEmpty(): Boolean = delegate.isEmpty() + + override fun containsKey(key: String): Boolean = delegate.containsKey(key) + + override fun containsValue(value: MemberProcessorList): Boolean = delegate.containsValue(value) + + override fun get(key: String): MemberProcessorList? = delegate[key] + + override fun put(key: String, value: MemberProcessorList): MemberProcessorList? = delegate.put(key, value) + + override fun remove(key: String): MemberProcessorList? = delegate.remove(key) + + override fun putAll(from: Map) { + delegate.putAll(from) + } + + override fun clear() { + delegate.clear() + } + + override val keys: MutableSet + get() = delegate.keys + + override val values: MutableCollection + get() = delegate.values + + override val entries: MutableSet> + get() = delegate.entries + + /** + * Returns `true` if there are no processors registered for any member. + */ + fun isEmptyProcessors(): Boolean = delegate.isEmpty() +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Metadata.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Metadata.kt index 876a768f6..8b9ed66dc 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Metadata.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Metadata.kt @@ -151,28 +151,28 @@ data class Metadata( return Metadata( tupleNumber = other.tupleNumber, dataEncoding = other.dataEncoding, - updatedAt = other.getLongMember(StandardMembers.UpdatedAt), - createdAt = other.getLongMember(StandardMembers.CreatedAt), - authorTs = other.getLongMember(StandardMembers.AuthorTimestamp), + updatedAt = other.getLongMember(StandardMembers.XyzUpdatedAt), + createdAt = other.getLongMember(StandardMembers.CreatedAtXyz), + authorTs = other.getLongMember(StandardMembers.XyzAuthorTimestamp), nextVersion = if (other.nextVersion == Int64(-1L)) null else other.nextVersion, baseTupleNumber = null, - changeCount = other.getIntMember(StandardMembers.ChangeCount), + changeCount = other.getIntMember(StandardMembers.ChangeCountXyz), hash = other.getIntMember(StandardMembers.Hash), - hereTile = other.getIntMember(StandardMembers.HereTile), + hereTile = other.getIntMember(StandardMembers.HereTileXyz), id = other.getStringMember(StandardMembers.Id) ?: "undefined", - appId = other.getStringMember(StandardMembers.AppId) ?: NakshaContext.appId(), - author = other.getStringMember(StandardMembers.Author), - origin = other.getStringMember(StandardMembers.Origin), - target = other.getStringMember(StandardMembers.Target), + appId = other.getStringMember(StandardMembers.AppIdXyz) ?: NakshaContext.appId(), + author = other.getStringMember(StandardMembers.AuthorXyz), + origin = other.getStringMember(StandardMembers.OriginXyz), + target = other.getStringMember(StandardMembers.TargetXyz), ft = other.getStringMember(StandardMembers.FeatureType), - cv0 = other.getDoubleMember(StandardMembers.CustomValue0), - cv1 = other.getDoubleMember(StandardMembers.CustomValue1), - cv2 = other.getDoubleMember(StandardMembers.CustomValue2), - cv3 = other.getDoubleMember(StandardMembers.CustomValue3), - cs0 = other.getStringMember(StandardMembers.CustomString0), - cs1 = other.getStringMember(StandardMembers.CustomString1), + cv0 = other.getDoubleMember(StandardMembers.CustomValue0Xyz), + cv1 = other.getDoubleMember(StandardMembers.CustomValue1Xyz), + cv2 = other.getDoubleMember(StandardMembers.XyzCustomValue2), + cv3 = other.getDoubleMember(StandardMembers.XyzCustomValue3), + cs0 = other.getStringMember(StandardMembers.XyzCustomString0), + cs1 = other.getStringMember(StandardMembers.XyzCustomString1), cs2 = other.getStringMember(StandardMembers.CustomString2), - cs3 = other.getStringMember(StandardMembers.CustomString3), + cs3 = other.getStringMember(StandardMembers.XyzCustomString3), ) } return null diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt index f0c1dee05..ecddfbccd 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt @@ -16,8 +16,8 @@ import naksha.jbon.* import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_ARGUMENT import naksha.model.NakshaError.NakshaErrorCompanion.STORAGE_NOT_FOUND import naksha.model.NakshaVersion.Companion.CURRENT +import naksha.model.objects.NakshaCollection import naksha.model.objects.NakshaFeature -import naksha.model.objects.NakshaProperties import naksha.model.objects.NakshaStorage import kotlin.js.JsExport import kotlin.js.JsName @@ -482,7 +482,7 @@ class Naksha private constructor() { val feature = decodeFeature(tuple.feature, dictReader) ?: NakshaFeature() feature.properties.xyz = XyzNs.fromTuple(tuple) val xyz = feature.properties.xyz - val tags = tuple.getTagList(StandardMembers.Tags) + val tags = tuple.getTagList(StandardMembers.XyzTags) if (tags != null) xyz.tags = tags val geo = tuple.getByteArray(StandardMembers.Geometry) if (geo != null) feature.geometry = decodeGeometry(geo) @@ -490,164 +490,110 @@ class Naksha private constructor() { } /** - * Encode the given [NakshaFeature] into a [Tuple]. - * @param feature the feature to encode. - * @param attachment the attachment to encode; if any. - * @param dictionary the [book][IBook] to use to encode the feature; _null_ if encoding should be done storage agnostic. - * @param dataEncoding the feature encoding to use, or _null_ to fall back to the storage default and finally [DEFAULT_DATA_ENCODING]. - * @return the encoded [Tuple]. - * @since 3.0 - * @see [IStorage.getEncodingDictionary] - */ - @JsStatic - @JvmStatic - @JvmOverloads - fun encodeTuple( - feature: NakshaFeature, - attachment: ByteArray? = null, - dictionary: IBook? = null, - dataEncoding: DataEncoding? = null - ): Tuple { - val xyz = feature.properties.xyz - val tn = feature.tupleNumber - val storage = getStorageByNumber(tn.storageNumber) - val encoding = dataEncoding ?: storage?.getDataEncoding(feature) ?: DEFAULT_DATA_ENCODING - val dict = dictionary ?: storage?.getDictionary(feature.id) - val featureBytes = encodeFeature(feature, encoding, dict) - val geoBytes = encodeGeometry(feature.geometry) - val refPoint = encodeGeometry(feature.referencePoint) - val tagsJson = encodeTagList(xyz.tags) - val members = HeapBook() - members.put(StandardMembers.Id.name, feature.id) - members.put(StandardMembers.AppId.name, xyz.appId) - members.put(StandardMembers.UpdatedAt.name, xyz.updatedAt) - members.put(StandardMembers.CreatedAt.name, if (xyz.updatedAt == xyz.createdAt) null else xyz.createdAt) - members.put(StandardMembers.AuthorTimestamp.name, if (xyz.updatedAt == xyz.authorTs) null else xyz.authorTs) - members.put(StandardMembers.Author.name, xyz.author) - members.put(StandardMembers.DataEncoding.name, encoding.toString()) - members.put(StandardMembers.ChangeCount.name, xyz.changeCount) - members.put(StandardMembers.Hash.name, xyz.hash ?: 0) - members.put(StandardMembers.HereTile.name, xyz.hereTile ?: 0) - members.put(StandardMembers.FeatureType.name, xyz.featureType) - members.put(StandardMembers.CustomValue0.name, xyz.cv0) - members.put(StandardMembers.CustomValue1.name, xyz.cv1) - members.put(StandardMembers.CustomValue2.name, xyz.cv2) - members.put(StandardMembers.CustomValue3.name, xyz.cv3) - members.put(StandardMembers.CustomString0.name, xyz.cs0) - members.put(StandardMembers.CustomString1.name, xyz.cs1) - members.put(StandardMembers.CustomString2.name, xyz.cs2) - members.put(StandardMembers.CustomString3.name, xyz.cs3) - members.put(StandardMembers.Geometry.name, geoBytes) - members.put(StandardMembers.ReferencePoint.name, refPoint) - members.put(StandardMembers.Tags.name, tagsJson) - members.put(StandardMembers.Attachment.name, attachment) - return Tuple( - storageNumber = tn.storageNumber, - mapNumber = tn.mapNumber, - collectionNumber = tn.collectionNumber, - featureNumber = tn.featureNumber, - version = tn.version, - nextVersion = Int64(-1L), - members = members, - feature = featureBytes - ) - } - - /** - * Encode the given [NakshaFeature] into a [Tuple] for the given [storage][IStorage]. + * Encodes the given [NakshaFeature] into JBON2 bytes, extracting member values into the + * provided [membersBook] via a custom [IMemberEncoder]. + * + * The encoder walks the feature using [JbEncoder2]. When a value is encountered at a path + * that matches a member's [effectivePath][naksha.model.objects.Member.effectivePath], the + * member encoder: + * 1. Walks the feature to obtain the raw value (already available from the encoder) + * 2. Coerces the value to the expected [MemberType] + * 3. Invokes all registered [IMemberProcessor] instances for that member + * 4. Stores the final value in [membersBook] + * 5. Returns the member's index so the encoder writes a members-book reference + * + * If [collection.dataEncoding] is [DataEncoding.JBON2_GZIP] and the raw JBON2 bytes are + * at least 1000, the result is GZIP-compressed. However, if the compressed bytes are + * larger or equal to the source, the raw bytes are returned instead. * * @param feature the feature to encode. - * @param attachment the attachment to encode; if any. - * @param storage the [storage][IStorage] for which to encode the feature. - * @return the encoded [Tuple]. + * @param collection the collection that declares the members to extract. + * @param session the session for which to encode. + * @param membersBook the [HeapBook] to populate with extracted member values. + * @param globalBook the global book/dictionary to use for encoding; if any. + * @return the encoded feature bytes (JBON2, optionally GZIP-compressed). * @since 3.0 */ + @JsName("encodeFeatureWithMembers") @JsStatic @JvmStatic - @JsName("encodeTupleForStorage") - fun encodeTuple( + fun encodeFeature( feature: NakshaFeature, - attachment: ByteArray?, - storage: IStorage - ): Tuple { - val xyz = feature.properties.xyz - val tn = feature.tupleNumber - val dict = storage.getEncodingDictionary(feature) - val encoding = storage.getDataEncoding(feature) - val featureBytes = encodeFeature(feature, encoding, dict) - val geoBytes = encodeGeometry(feature.geometry) - val refPoint = encodeGeometry(feature.referencePoint) - val tagsJson = encodeTagList(xyz.tags) - val members = HeapBook() - members.put(StandardMembers.Id.name, feature.id) - members.put(StandardMembers.AppId.name, xyz.appId) - members.put(StandardMembers.UpdatedAt.name, xyz.updatedAt) - members.put(StandardMembers.CreatedAt.name, if (xyz.updatedAt == xyz.createdAt) null else xyz.createdAt) - members.put(StandardMembers.AuthorTimestamp.name, if (xyz.updatedAt == xyz.authorTs) null else xyz.authorTs) - members.put(StandardMembers.Author.name, xyz.author) - members.put(StandardMembers.DataEncoding.name, encoding.toString()) - members.put(StandardMembers.ChangeCount.name, xyz.changeCount) - members.put(StandardMembers.Hash.name, xyz.hash ?: 0) - members.put(StandardMembers.HereTile.name, xyz.hereTile ?: 0) - members.put(StandardMembers.FeatureType.name, xyz.featureType) - members.put(StandardMembers.CustomValue0.name, xyz.cv0) - members.put(StandardMembers.CustomValue1.name, xyz.cv1) - members.put(StandardMembers.CustomValue2.name, xyz.cv2) - members.put(StandardMembers.CustomValue3.name, xyz.cv3) - members.put(StandardMembers.CustomString0.name, xyz.cs0) - members.put(StandardMembers.CustomString1.name, xyz.cs1) - members.put(StandardMembers.CustomString2.name, xyz.cs2) - members.put(StandardMembers.CustomString3.name, xyz.cs3) - members.put(StandardMembers.Geometry.name, geoBytes) - members.put(StandardMembers.ReferencePoint.name, refPoint) - members.put(StandardMembers.Tags.name, tagsJson) - members.put(StandardMembers.Attachment.name, attachment) - return Tuple( - storageNumber = tn.storageNumber, - mapNumber = tn.mapNumber, - collectionNumber = tn.collectionNumber, - featureNumber = tn.featureNumber, - version = tn.version, - nextVersion = Int64(-1L), - members = members, - feature = featureBytes + collection: NakshaCollection, + session: IWriteSession, + membersBook: HeapBook, + globalBook: IBook? + ): ByteArray { + val members = collection.members?.toList() ?: StandardMembers.XYZ_MEMBERS + val processors = session.processors() + val rawTn = StandardMembers.Tn.read(collection) + val colTn: TupleNumber + if (rawTn is TupleNumber) { + colTn = rawTn + } else if (rawTn is String) { + colTn = TupleNumber.fromString(rawTn) + } else { + throw NakshaException(ILLEGAL_ARGUMENT, "The given collection does not have a valid tuple-number (uuid)") + } + if (colTn.featureNumber > Int.MAX_VALUE || colTn.featureNumber < Int.MIN_VALUE) { + throw NakshaException(ILLEGAL_ARGUMENT, "The given collection does have an invalid feature-number (uuid)") + } + // The feature is stored in the same database and catalog + val tn = TupleNumber( + // The feature is stored in the same database as the collection it is inserted into. + colTn.storageNumber, + // The feature is stored in the same catalog as the collection it is inserted into. + colTn.mapNumber, + // The feature-number of the collection is the collection-number of the feature stored in it. + colTn.featureNumber.toInt(), + // The feature-number of the actual feature. + feature.featureNumber, + // The version is the one of the transaction. + session.useTransaction().version ) - } - /** - * Encodes the given [NakshaFeature] into bytes, skipping over the [geometry][NakshaFeature.geometry], and the [XYZ-namespace][XyzNs]. - * @param feature the feature to encode. - * @param encoding the feature encoding to use. - * @param dict the dictionary to use for encoding; if any. - * @return the encoded feature. - * @since 3.0 - */ - @JsStatic - @JvmStatic - fun encodeFeature(feature: NakshaFeature?, encoding: DataEncoding, dict: IBook?): ByteArray? { - if (feature.isNullOrEmpty()) return null - var byteArray: ByteArray? = when (encoding) { - DataEncoding.JSON, DataEncoding.JSON_GZIP -> { - // We do not want to encode geometry. - val f = feature.copy(false) - f.removeRaw(NakshaFeature.GEOMETRY) - // We do not want to encode properties.@ns:com:here:xyz. - val p = feature.properties.copy(false) - p.removeRaw(NakshaProperties.XYZ_KEY) - toJSON(f).encodeToByteArray() - } - DataEncoding.JBON, DataEncoding.JBON_GZIP -> { - val encoder = JbEncoder(dict) - encoder.buildFeatureFromMap(feature) - } - DataEncoding.JBON2, DataEncoding.JBON2_GZIP -> { - val encoder = JbEncoder2(dict) - encoder.buildTupleFromMap(feature) + // Create the encoder with a custom member encoder. + val encoder = JbEncoder2(globalBook) + encoder.withMemberEncoder { path: Array, pathEnd: Int, value: Any? -> + // Build the current path key from the encoder's path. + members@ for (i in 0 until members.size) { + val member = members[i] ?: continue + val memberName = member.name + val memberPath = member.path + if (memberPath.size != pathEnd) continue + for (pi in 0 until pathEnd) { + if (path[pi] != memberPath[pi]) continue@members + } + // Path matches + var v = value + val procs = processors[memberName] + if (procs != null) { + for (proc in procs) { + v = proc.processMember(session, collection, feature, member, v) + } + } + // Coerce the value to the expected type. + v = FeatureMemberValues.coerce(value, member.dataType, feature.id, memberName) + + // Store in membersBook. + return@withMemberEncoder membersBook.put(memberName, v) } - else -> null + -1 + } + + // Add mandatory members. + membersBook.put("tn", ) + + // Encode the feature. + val raw = encoder.buildTupleFromMap(feature) + + // Optionally GZIP. + val encoding = collection.dataEncoding ?: Naksha.DEFAULT_DATA_ENCODING + if (encoding == DataEncoding.JBON2_GZIP && raw.size >= 1000) { + val compressed = gzipDeflate(raw) + if (compressed.size < raw.size) return compressed } - if (encoding.gzip && byteArray != null) byteArray = gzipDeflate(byteArray) - return byteArray + return raw } /** diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/StorageTx.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/StorageTx.kt index c100c5959..0b6e2d34a 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/StorageTx.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/StorageTx.kt @@ -3,11 +3,8 @@ package naksha.model import naksha.base.Int64 -import naksha.jbon.IBook import naksha.jbon.IDictReader import naksha.jbon.HeapBook -import naksha.model.Metadata.Metadata_C.calculateHash -import naksha.model.Metadata.Metadata_C.calculateHereTile import naksha.model.objects.* import kotlin.js.JsExport import kotlin.js.JsName @@ -41,7 +38,8 @@ open class StorageTx private constructor( /** * The unique version of the transaction. This value **should be** unique to this transaction. * @since 3.0 - * @see [Version.of] + * @see [Version.auto] + * @see [Version.manual] * @see [Version.now] */ val version: Version, @@ -65,6 +63,12 @@ open class StorageTx private constructor( * @since 3.0 */ val dictReader: IDictReader?, + + /** + * The session to which this transaction is attached. + * @since 3.0 + */ + val session: ISession, ) { @JsName("storageTxWithStorageNumber") @@ -74,7 +78,8 @@ open class StorageTx private constructor( appId: String, author: String?, dictReader: IDictReader?, - ): this(null, storageNumber, version, appId, author, dictReader) + session: ISession, + ): this(null, storageNumber, version, appId, author, dictReader, session) @JsName("storageTxWithStorage") constructor( @@ -83,7 +88,8 @@ open class StorageTx private constructor( appId: String, author: String?, dictReader: IDictReader?, - ): this(storage, storage.number, version, appId, author, dictReader) + session: ISession, + ): this(storage, storage.number, version, appId, author, dictReader, session) /** * The statistical transaction information, updated while this class is being used, should eventually be writted into the transaction-log of the storage. @@ -98,90 +104,12 @@ open class StorageTx private constructor( open val updatedAt: Int64 get() = transaction.time - /** - * Method to create the new members dict for the given write action on a feature. - * - * Members are extracted from the feature using the [effective path][naksha.model.objects.Member.effectivePath] - * of each member declared in [collection.members]. When [collection.members] is `null`, all - * [StandardMembers.DEFAULT] are extracted. Session-derived and computed values (e.g. [updatedAt], [hash]) - * are written after the feature walk to override any values from the feature. - * - * - Throws [NakshaError.ILLEGAL_ARGUMENT], if the given arguments are not sufficient to generate the new metadata. - * @param map the map into which to persist the feature. - * @param collection the collection into which to persist the feature. - * @param feature the new _(modified)_ state of the feature, for which the metadata should be created. - * @param action the [action][Action] being performed. - * @param atomic whether the write should be performed atomically. - * @return the new [IBook] with metadata members that is correct for the new state, based upon the given data. - * @since 3.0.0 - * @see [StorageTx] - */ - protected open fun buildMembers( - map: NakshaMap, - collection: NakshaCollection, - feature: NakshaFeature, - action: Action, - atomic: Boolean = false - ): IBook { - val dataEncoding = getDataEncoding(feature, collection) - val xyz = feature.properties.xyz - val isExistingFeature = !(action == Action.CREATED || (action == Action.UPDATED && !atomic)) - if (isExistingFeature && xyz.guid == null) { - throw illegalArg("$action with atomic=$atomic requires that the feature has a UUID!") - } - - // Determine the list of members to extract from the feature. - val membersToExtract = collection.members?.toList() ?: StandardMembers.DEFAULT - - // Walk the feature using each member's effective path and coerce to the expected type. - val members = HeapBook() - for (member in membersToExtract) { - if (member == null) continue - // Skip mandatory members that are managed by the storage and have no JSON path. - if (StandardMembers.MANDATORY_NAMES.contains(member.name)) continue - val path = member.effectivePath() - val raw = FeatureMemberValues.walkFeature(feature, path) - val coerced = FeatureMemberValues.coerce(raw, member.dataType, feature.id, member.name) - members.put(member.name, coerced) - } - - // Override with session-derived and computed values. - val updatedAt: Int64 = this.updatedAt - val createdAt: Int64? = if (isExistingFeature) xyz.createdAt else null - val author: String? - val authorTs: Int64? - if (xyz.author == null || xyz.author != this.author) { - author = this.author - authorTs = null - } else { - author = xyz.author - authorTs = xyz.authorTs - } - val featureType = if (collection.defaultFeatureType == feature.featureType) null else feature.featureType - - members.put(StandardMembers.UpdatedAt.name, updatedAt) - members.put(StandardMembers.CreatedAt.name, createdAt) - members.put(StandardMembers.AuthorTimestamp.name, authorTs) - members.put(StandardMembers.Author.name, author) - members.put(StandardMembers.AppId.name, appId) - members.put(StandardMembers.DataEncoding.name, dataEncoding.toString()) - members.put(StandardMembers.ChangeCount.name, xyz.changeCount + 1) - members.put(StandardMembers.Hash.name, calculateHash(feature)) - members.put(StandardMembers.HereTile.name, calculateHereTile(feature)) - members.put(StandardMembers.Id.name, feature.id) - members.put(StandardMembers.FeatureType.name, featureType) - - return members - } - /** * Builds the [Tuple] for the given write action. * @param map the map in which the feature is being persisted. * @param collection the collection in which the feature is being persisted. * @param feature the feature. * @param action the [action][Action] being performed. - * @param attachment the attachment. - * @param atomic whether the write should be performed atomically. * @return the encoded [Tuple]. */ private fun buildTuple( @@ -189,30 +117,15 @@ open class StorageTx private constructor( collection: NakshaCollection, feature: NakshaFeature, action: Action, - attachment: ByteArray?, - atomic: Boolean = false ): Tuple { - val members = buildMembers(map, collection, feature, action, atomic) - // Override geometry, referencePoint, tags, and attachment with encoded values from the feature. - // These are the canonical values at tuple time (the feature walk may have extracted them from - // stale JSON paths, but the direct feature properties are authoritative). - val xyz = feature.properties.xyz - val geoBytes = Naksha.encodeGeometry(feature.geometry) - val refPointBytes = Naksha.encodeGeometry(feature.referencePoint) - val tagsJson = Naksha.encodeTagList(xyz.tags) - if (members is HeapBook) { - members.put(StandardMembers.Geometry.name, geoBytes) - members.put(StandardMembers.ReferencePoint.name, refPointBytes) - members.put(StandardMembers.Tags.name, tagsJson) - members.put(StandardMembers.Attachment.name, attachment) - } - val dataEncoding = getDataEncoding(feature, collection) + val membersBook = HeapBook() + val globalBook = dictReader?.getEncodingDictionary(feature) + val featureBytes = Naksha.encodeFeature(feature, collection, session, membersBook, globalBook) + val actionBits = Int64(action.intValue.toLong()) val featureNumber = feature.featureNumber val versionVal = version.txn and Int64(-4L) or actionBits val nextVersion: Int64 = if (map.id == Naksha.ADMIN_MAP && collection.id == Naksha.TRANSACTIONS_COL) versionVal else Int64(-1L) - val dict = dictReader?.getEncodingDictionary(feature) - val featureBytes = Naksha.encodeFeature(feature, dataEncoding, dict) return Tuple( storageNumber = storageNumber, mapNumber = map.number, @@ -220,7 +133,7 @@ open class StorageTx private constructor( featureNumber = featureNumber, version = Version(versionVal), nextVersion = nextVersion, - members = members, + members = membersBook, feature = featureBytes ) } @@ -242,32 +155,26 @@ open class StorageTx private constructor( * @param map the map in which the feature is going to be created. * @param collection the collection in which the feature is going to be created. * @param feature the feature that was created. - * @param attachment the attachment. * @return the binary encoding of the [NakshaFeature] as [Tuple]. */ open fun created( map: NakshaMap, collection: NakshaCollection, feature: NakshaFeature, - attachment: ByteArray? - ): Tuple = buildTuple(map, collection, feature, Action.CREATED, attachment) + ): Tuple = buildTuple(map, collection, feature, Action.CREATED) /** * Convert the given [feature][NakshaFeature] into a [Tuple], when the feature was updated. * @param map the map in which the feature is going to be created. * @param collection the collection in which the feature is going to be created. * @param feature the feature that was created. - * @param attachment the attachment. - * @param atomic whether the write should be performed atomically. * @return the binary encoding of the [NakshaFeature] as [Tuple]. */ open fun updated( map: NakshaMap, collection: NakshaCollection, feature: NakshaFeature, - attachment: ByteArray?, - atomic: Boolean = false, - ): Tuple = buildTuple(map, collection, feature, Action.UPDATED, attachment, atomic) + ): Tuple = buildTuple(map, collection, feature, Action.UPDATED) /** * Convert the given [feature][NakshaFeature] into a [Tuple], when the feature was deleted. @@ -277,16 +184,11 @@ open class StorageTx private constructor( * @param map the map in which the feature is going to be created. * @param collection the collection in which the feature is going to be created. * @param feature the feature that was created. - * @param attachment the attachment. * @return the binary encoding of the [NakshaFeature] as [Tuple]. */ open fun deleted( map: NakshaMap, collection: NakshaCollection, feature: NakshaFeature, - attachment: ByteArray? - ): Tuple = buildTuple(map, collection, feature, Action.DELETED, attachment) - - private fun getDataEncoding(feature: NakshaFeature, collection: NakshaCollection): DataEncoding = - storage?.getDataEncoding(feature, collection) ?: Naksha.DEFAULT_DATA_ENCODING + ): Tuple = buildTuple(map, collection, feature, Action.DELETED) } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Tuple.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Tuple.kt index 2a635f09b..2869daeac 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Tuple.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Tuple.kt @@ -288,7 +288,7 @@ data class Tuple( fun toNakshaFeature(): NakshaFeature? { val feature = Naksha.decodeFeature(this.feature, null) ?: return null feature.properties.xyz = XyzNs.fromTuple(this) - val tags = getTagList(StandardMembers.Tags) + val tags = getTagList(StandardMembers.XyzTags) if (tags != null) { feature.properties.xyz.tags = tags } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumberVariant.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumberVariant.kt index c5e1a5ee3..fac5db471 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumberVariant.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumberVariant.kt @@ -188,14 +188,7 @@ class TupleNumberVariant internal constructor() : JsEnum() { private set /** - * The number of bytes that are shared for tuple-number's of this variant _(optional header size)_. - * - * The total header size is actually `8` byte, plus this value, meaning: - * - variant `0` encodes all values for each tuple-number, therefore no header place needed. - * - variant `1` encodes `storage-number` in the header, so `8` byte needed. - * - variant `2` encodes `storage-number`, and `map-number` in the header, so `12` byte needed. - * - variant `3` encodes `storage-number`, `map-number`, and `collection-number` in the header, so `16` byte needed. - * - variant `4` encodes `storage-number`, `map-number`, `collection-number`, and `feature-number` in the header, so `24` byte needed. + * The number of bytes that are shared in the header of a tuple-number-array for tuple-number's of this variant. * @since 3.0 * @see [BinaryUtil.writeSimpleHeader] * @see [sharedStorageNumber] diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Version.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Version.kt index fcf789d69..7de53e788 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Version.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Version.kt @@ -65,7 +65,7 @@ open class Version(@JvmField val txn: Int64) : Comparable { /** Maximum year value (15-bit, JS-safe upper bound). */ private const val YEAR_MAX = 32767 /** Minimum year for a dated version. */ - private const val YEAR_DATED_MIN = 16 + private const val YEAR_MIN = 16 /** Mask for the 30-bit sequence field. */ private val SEQ_30_MASK = Int64(0x3FFF_FFFF) @@ -110,7 +110,7 @@ open class Version(@JvmField val txn: Int64) : Comparable { } else { return Version(Int64(s.toLong())) } - } catch (e: Exception) { + } catch (_: Exception) { throw NakshaException(NakshaError.ILLEGAL_ARGUMENT, "Invalid version string: $s") } } @@ -132,8 +132,8 @@ open class Version(@JvmField val txn: Int64) : Comparable { @JsStatic @JvmOverloads fun auto(year: Int, month: Int, day: Int, seq: Int64, action: Action = Action.CREATED): Version { - require(year in YEAR_DATED_MIN..YEAR_MAX) { - "year must be in $YEAR_DATED_MIN..$YEAR_MAX, got $year" + require(year in YEAR_MIN..YEAR_MAX) { + "year must be in $YEAR_MIN..$YEAR_MAX, got $year" } require(month in 1..12) { "month must be in 1..12, got $month" } require(day in 1..31) { "day must be in 1..31, got $day" } @@ -187,7 +187,7 @@ open class Version(@JvmField val txn: Int64) : Comparable { } /** - * The _HEAD_ sentinel version (`txn == 0`). + * The _HEAD_ sentinel version _(9_007_199_254_740_991L aka `2^53-1`)_. * * When a [Tuple] is the current HEAD state its `nextVersion` is synthesised as this value * (the column is not physically stored in HEAD tables). @@ -195,7 +195,10 @@ open class Version(@JvmField val txn: Int64) : Comparable { */ @JvmField @JsStatic - val HEAD = Version(0L) + val HEAD = Version(9_007_199_254_740_991L) + // = 2^53-1, aka Number.MAX_SAFE_INTEGER + // 3n + (1073741823n << 2n) + (31n << 32n) + (15n << (32n+5n)) + (4095n << (32n+5n+4n)) = 9007199254740991n + // bitwise: 0x001f_ffff_ffff_ffff /** * The minimum valid dated version (year=16, month=1, day=1, seq=0, action=CREATED). @@ -203,7 +206,19 @@ open class Version(@JvmField val txn: Int64) : Comparable { */ @JvmField @JsStatic - val MIN = auto(16, 1, 1, Int64(0)) + val MIN = auto(16, 1, 1, Int64(0), Action.CREATED) + // 0n + (0n << 2n) + (1n << 32n) + (1n << (32n+5n)) + (16n << (32n+5n+4n)) = 35326106009600n + // bitwise: 0x0000_2021_0000_0000 + + /** + * The maximum valid dated version (year=4095, month=12, day=31, seq=1,073,741,823, action=VERSION). + * @since 3.0 + */ + @JvmField + @JsStatic + val MAX = auto(4095, 12, 31, Int64(1_073_741_823), Action.VERSION) + // 3n + (1073741823n << 2n) + (31n << 32n) + (12n << (32n+5n)) + (4095n << (32n+5n+4n)) = 9006786937880575n + // bitwise: 0x001f_ff9f_ffff_ffff /** * The minimum value of the 30-bit sequence field (zero). @@ -229,7 +244,7 @@ open class Version(@JvmField val txn: Int64) : Comparable { */ @JvmField @JsStatic - val SEQ_END: Int64 = Int64(1) shl 2 + val SEQ_INC: Int64 = Int64(1) shl 2 } private var _year = -1 diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/XyzNs.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/XyzNs.kt index d9c14a3ad..e47734cc2 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/XyzNs.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/XyzNs.kt @@ -199,11 +199,11 @@ class XyzNs : AnyObject() { val members = tuple.members val id = members?.getByName("id") as? String ?: tuple.featureNumber.toString() val guid = Guid(id, tn) - val updatedAt = tuple.getLongMember(StandardMembers.UpdatedAt) - val createdAt = tuple.getLongMember(StandardMembers.CreatedAt).let { + val updatedAt = tuple.getLongMember(StandardMembers.XyzUpdatedAt) + val createdAt = tuple.getLongMember(StandardMembers.CreatedAtXyz).let { if (it == Int64(0L)) updatedAt else it } - val authorTs = tuple.getLongMember(StandardMembers.AuthorTimestamp)?.let { + val authorTs = tuple.getLongMember(StandardMembers.XyzAuthorTimestamp)?.let { if (it == Int64(0)) updatedAt else it } ?: updatedAt val nextVersion = tuple.nextVersion @@ -222,17 +222,17 @@ class XyzNs : AnyObject() { if (createdAt != updatedAt) setRaw(CREATED_AT, createdAt) if (authorTs != updatedAt) setRaw(AUTHOR_TS, authorTs) setRaw(UPDATED_AT, updatedAt) - setRaw(CHANGE_COUNT, tuple.getIntMember(StandardMembers.ChangeCount)) - setRaw(APP_ID, tuple.getStringMember(StandardMembers.AppId)) - val author = tuple.getStringMember(StandardMembers.Author) + setRaw(CHANGE_COUNT, tuple.getIntMember(StandardMembers.ChangeCountXyz)) + setRaw(APP_ID, tuple.getStringMember(StandardMembers.AppIdXyz)) + val author = tuple.getStringMember(StandardMembers.AuthorXyz) if (author != null) setRaw(AUTHOR, author) setRaw(DATA_ENCODING, tuple.getStringMember(StandardMembers.DataEncoding)) setRaw(ACTION, tn.action.toString()) setRaw(HASH, tuple.getIntMember(StandardMembers.Hash)) - setRaw(HERE_TILE, tuple.getIntMember(StandardMembers.HereTile)) - val origin = tuple.getStringMember(StandardMembers.Origin) + setRaw(HERE_TILE, tuple.getIntMember(StandardMembers.HereTileXyz)) + val origin = tuple.getStringMember(StandardMembers.OriginXyz) if (origin != null) setRaw(ORIGIN, origin) - val target = tuple.getStringMember(StandardMembers.Target) + val target = tuple.getStringMember(StandardMembers.TargetXyz) if (target != null) setRaw(TARGET, target) val cv0 = members?.getByName("cv0") if (cv0 != null) setRaw(CV0, cv0 as? Double) @@ -242,13 +242,13 @@ class XyzNs : AnyObject() { if (cv2 != null) setRaw(CV2, cv2 as? Double) val cv3 = members?.getByName("cv3") if (cv3 != null) setRaw(CV3, cv3 as? Double) - val cs0 = tuple.getStringMember(StandardMembers.CustomString0) + val cs0 = tuple.getStringMember(StandardMembers.XyzCustomString0) if (cs0 != null) setRaw(CS0, cs0) - val cs1 = tuple.getStringMember(StandardMembers.CustomString1) + val cs1 = tuple.getStringMember(StandardMembers.XyzCustomString1) if (cs1 != null) setRaw(CS1, cs1) val cs2 = tuple.getStringMember(StandardMembers.CustomString2) if (cs2 != null) setRaw(CS2, cs2) - val cs3 = tuple.getStringMember(StandardMembers.CustomString3) + val cs3 = tuple.getStringMember(StandardMembers.XyzCustomString3) if (cs3 != null) setRaw(CS3, cs3) }.proxy(XyzNs::class) } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Index.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Index.kt index d026c970a..dc3fac781 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Index.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Index.kt @@ -135,19 +135,19 @@ open class Index() : AnyObject() { * Whether the index enforces uniqueness across the [on] columns. Defaults to `false`. * @since 3.0 */ - var unique: Boolean by UNIQUE + private var unique: Boolean by UNIQUE /** True iff the underlying map has an entry for [unique]. */ - fun hasUnique(): Boolean = hasRaw("unique") + fun isUnique(): Boolean = unique /** Remove [unique] from the underlying map; returns this for chaining. */ - fun removeUnique(): Index { + internal fun removeUnique(): Index { removeRaw("unique") return this } /** Fluent setter for [unique]; returns this for chaining. */ - fun withUnique(value: Boolean): Index { + internal fun withUnique(value: Boolean): Index { unique = value return this } @@ -158,19 +158,19 @@ open class Index() : AnyObject() { * attempt to recreate or drop it. Defaults to `false`. * @since 3.0 */ - var internal: Boolean by INTERNAL + private var internal: Boolean by INTERNAL /** True iff the underlying map has an entry for [internal]. */ - fun hasInternal(): Boolean = hasRaw("internal") + fun isInternal(): Boolean = internal /** Remove [internal] from the underlying map; returns this for chaining. */ - fun removeInternal(): Index { + internal fun removeInternal(): Index { removeRaw("internal") return this } /** Fluent setter for [internal]; returns this for chaining. */ - fun withInternal(value: Boolean): Index { + internal fun withInternal(value: Boolean): Index { internal = value return this } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/JsonPath.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/JsonPath.kt index 3461d3d76..2b8411cc4 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/JsonPath.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/JsonPath.kt @@ -2,20 +2,21 @@ package naksha.model.objects -import naksha.base.ListProxy +import naksha.base.AnyList +import naksha.model.NakshaError +import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_STATE +import naksha.model.NakshaException import kotlin.js.JsExport import kotlin.js.JsName /** - * An ordered list of object keys used to walk a JSON document. + * An ordered list of keys used to walk a JSON document. All entries must be either integer or strings, all other values are invalid. * - * For example, `JsonPath("properties", "age")` resolves the value at `feature.properties.age`. - * - * Each segment must match `^[A-Za-z_][A-Za-z0-9_]*$`. There is no array indexing in v3.0 — paths can only descend into object keys. + * For example, `JsonPath("properties", "age")` resolves the value at `properties.age` or `JsonPath("properties", "data", 0)` resolves the value at `properties.data[0]` with `data` expected to be an array. * @since 3.0 */ @JsExport -open class JsonPath() : ListProxy(String::class) { +open class JsonPath() : AnyList() { /** * Construct a path from a vararg of segments. @@ -23,7 +24,20 @@ open class JsonPath() : ListProxy(String::class) { * @since 3.0 */ @JsName("fromSegments") - constructor(vararg segments: String) : this() { - addAll(segments.toList()) + constructor(vararg segments: Any) : this() { + for (segment in segments) add(segment) + } + + /** + * Tests whether the path is valid, if not, throws an [NakshaException] with [ILLEGAL_STATE] error. Actually, the path must only contain strings and integers. + * @since 3.0 + */ + fun validate() { + for (i in 0 until this.size) { + val segment = this[i] ?: throw NakshaException(ILLEGAL_STATE, "Illegal NULL at path position $i") + if (segment !is String && segment !is Int) { + throw NakshaException(ILLEGAL_STATE, "Illegal value at path position $i, must be string or integer, found: '$segment'") + } + } } -} +} \ No newline at end of file diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt index 00e2ef412..cc717fcdb 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt @@ -5,7 +5,7 @@ package naksha.model.objects import naksha.base.AnyObject import naksha.base.NotNullEnum import naksha.base.NotNullProperty -import naksha.base.NullableProperty +import naksha.base.Proxy import kotlin.js.JsExport import kotlin.js.JsName @@ -25,7 +25,7 @@ import kotlin.js.JsName * @since 3.0 */ @JsExport -open class Member() : AnyObject() { +class Member() : AnyObject() { /** * Construct a member with a name and the given data type. @@ -38,7 +38,24 @@ open class Member() : AnyObject() { constructor(name: String, dataType: MemberType = MemberType.STRING, path: JsonPath? = null) : this() { this.name = name this.dataType = dataType + this.path = path ?: JsonPath(listOf("properties", name)) + this.path.validate() + } + + /** + * Construct a member copy with a different path, used to relocate standard members to different places. + * + * Specifically, this is used to relocate the Tuple-Number and other mandatory members into the deprecated XYZ namespace. + * @param origin The member to create a copy of. + * @param path The new path to relocate the member to. + * @since 3.0 + */ + @JsName("relocate") + constructor(origin: Member, path: JsonPath) : this() { + this.name = origin.name + this.dataType = origin.dataType this.path = path + this.path.validate() } /** @@ -90,38 +107,46 @@ open class Member() : AnyObject() { * Each segment must match `^[A-Za-z_][A-Za-z0-9_]*$`. There is no array indexing in v3.0. * @since 3.0 */ - var path: JsonPath? by PATH - - /** True iff the underlying map has an entry for [path]. */ - fun hasPath(): Boolean = hasRaw("path") - - /** Remove [path] from the underlying map; returns this for chaining. */ - fun removePath(): Member { - removeRaw("path") - return this - } + var path: JsonPath by PATH /** Fluent setter for [path]; returns this for chaining. */ fun withPath(value: JsonPath?): Member { - path = value + path = value ?: JsonPath(listOf("properties", name)) return this } /** - * Returns the effective JSON path to read this member from a feature. - * - * If [path] is explicitly set, returns its contents; otherwise returns `["properties", name]`. + * Read this member from the given proxy using the [path] of this member. + * @param proxy The proxy to read. + * @return the value of member in that proxy. + */ + fun read(proxy: Proxy): Any? = proxy.getPath(path) + + /** + * Whether this member is storage-managed (internal). When `true`, the storage controls the DDL for this member. Defaults to `false`. * @since 3.0 */ - fun effectivePath(): List { - val path: JsonPath? = this.path - return if (!path.isNullOrEmpty()) path.filterNotNull().toList() - else listOf("properties", name) + private var internal: Boolean by INTERNAL + + /** True iff the underlying map has an entry for [internal]. */ + fun isInternal(): Boolean = internal + + /** Remove [internal] from the underlying map; returns this for chaining. */ + internal fun removeInternal(): Member { + removeRaw("internal") + return this + } + + /** Fluent setter for [internal]; returns this for chaining. */ + internal fun withInternal(value: Boolean): Member { + internal = value + return this } companion object Member_C { private val NAME = NotNullProperty(String::class) { _, _ -> "" } private val DATA_TYPE = NotNullEnum(MemberType::class) { _, _ -> MemberType.STRING } - private val PATH = NullableProperty(JsonPath::class) + private val PATH = NotNullProperty(JsonPath::class) { self, _ -> JsonPath(listOf("properties", self.name)) } + private val INTERNAL = NotNullProperty(Boolean::class) { _, _ -> false } } } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberList.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberList.kt index 6097a0d51..cad247b5d 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberList.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberList.kt @@ -3,6 +3,8 @@ package naksha.model.objects import naksha.base.ListProxy +import naksha.model.NakshaError +import naksha.model.NakshaException import kotlin.js.JsExport import kotlin.js.JsName import kotlin.jvm.JvmStatic @@ -20,7 +22,16 @@ open class MemberList() : ListProxy(Member::class) { */ @JsName("fromMembers") constructor(vararg members: Member) : this() { - addAll(members.toList()) + for (member in members) add(member) + } + + /** + * Construct a list from a list of members. + * @since 3.0 + */ + @JsName("fromList") + constructor(members: List) : this() { + addAll(members) } companion object MemberList_C { @@ -34,4 +45,32 @@ open class MemberList() : ListProxy(Member::class) { fun of(vararg members: Member): MemberList = MemberList().apply { addAll(members.toList()) } } + + /** + * Get the member with the given name from this list. + * @param name The name of the member. + * @return The first member with that name or `null`, if no such member was found. + */ + fun get(name: String): Member? { + for (member in this) { + if (member != null && member.name == name) return member + } + return null + } + + /** + * Test whether this member list is valid, so does not have `null` entries and all members have unique names. Throws a [NakshaException], if any error is found. + */ + fun validate() { + for (i in 0 until this.size) { + val member = this[i] ?: throw NakshaException(NakshaError.ILLEGAL_STATE, "Member at index $i is null") + val memberName = member.name + for (j in (i + 1) until this.size) { + val later = this[j] ?: throw NakshaException(NakshaError.ILLEGAL_STATE, "Member at index $j is null") + if (memberName == later.name) { + throw NakshaException(NakshaError.ILLEGAL_STATE, "Member at index $i has same name as member at $j: $memberName") + } + } + } + } } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberType.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberType.kt index f818adb07..860cc6ad9 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberType.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberType.kt @@ -101,6 +101,13 @@ class MemberType : JsEnum() { @JvmField val BYTE_ARRAY = defIgnoreCase(MemberType::class, "byte_array") + /** + * A tuple-number, can be encoded as string or byte-array _(storage decides)_. To be used with [IndexType.BTREE]. + * @since 3.0 + */ + @JvmField + val TUPLE_NUMBER = defIgnoreCase(MemberType::class, "tuple_number") + /** * A geometry stored as raw [TWKB](https://github.com/nicowillis/twkb) bytes. * diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt index f68ba1db1..6953d7682 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt @@ -9,6 +9,8 @@ import naksha.geo.SpPoint import naksha.model.DataEncoding import naksha.model.Naksha import naksha.model.NakshaError +import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_ARGUMENT +import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_STATE import naksha.model.NakshaException import kotlin.js.JsExport import kotlin.js.JsName @@ -337,6 +339,39 @@ open class NakshaCollection() : NakshaFeature() { return this } + /** + * Returns the members list. If the member list is currently `null`, it creates it from [XyzMembers.ALL]. If the list does not contain the mandatory members, they will be added. + * @return the members list of this collection. + * @since 3.0 + */ + open fun useMembers(): MemberList { + var write: Boolean = false + var list = this.members + if (list == null) { + list = MemberList(XyzMembers.ALL) + write = true + } + // Ensure that the list does not contain null or duplicates. + list.validate() + // Ensure that the mandatory members are in. + for (mandatory in StandardMembers.MANDATORY) { + val found: Member? = list.get(mandatory.name) + if (found != null) { + // We require same name and data-type, but not same JSON path. + if (mandatory.dataType != found.dataType) { + throw NakshaException( + ILLEGAL_STATE, + "Member '${mandatory.name}' has different wrong data type: '${found.dataType}', expected '${mandatory.name}'" + ) + } + } else { + list.add(mandatory) + } + } + if (write) this.members = list + return list + } + /** * The indices to maintain on this collection. * diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaFeature.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaFeature.kt index 0927981c6..525313a12 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaFeature.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaFeature.kt @@ -118,7 +118,7 @@ open class NakshaFeature() : AnyObject() { /** * The feature-number of the feature. * - * If the feature is in [HEAD][TupleNumber.HEAD] state, so not yet persisted, and no custom feature number was set, then the method will calculate the feature-number from the [id]. A custom feature-number + * If the feature is in [HEAD][TupleNumber.HEAD] state, so not yet persisted, and no custom feature number was set, then the method will calculate the feature-number from the [id]. * @since 3.0 */ var featureNumber: Int64 diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaTx.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaTx.kt index 1b144ea3b..bbbcc6afd 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaTx.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaTx.kt @@ -96,7 +96,7 @@ open class NakshaTx : NakshaFeature() { override var id: String get() = getAs("id", String::class) ?: throw illegalState("The property 'id' must be a valid string") set(value) { - val txn = Int64(value.toLong()) + val txn = Int64(value.toLong(10)) setVersion(Version(txn)) } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardIndices.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardIndices.kt index cabe9dce1..99758c13f 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardIndices.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardIndices.kt @@ -5,47 +5,9 @@ package naksha.model.objects import kotlin.js.JsExport import kotlin.js.JsStatic import kotlin.jvm.JvmField -import kotlin.jvm.JvmStatic /** * The canonical set of standard indices that every Naksha storage understands. - * - * Indices are divided into two groups: - * - * ### Mandatory indices - * These are always created by the storage regardless of the [NakshaCollection.indices] list. - * Clients must not declare them manually. They are marked `internal = true` in the storage layer. - * - * - [StandardIndices_C.FeatureNumberUnique] — PRIMARY KEY on `fn` (all tables) - * - [StandardIndices_C.IdUnique] — UNIQUE on `id` WHERE `id IS NOT NULL` (HEAD / DELETED / META tables) - * - [StandardIndices_C.Id] — non-unique index on `id`, `fn`, `version` WHERE `id IS NOT NULL` (HISTORY tables) - * - [StandardIndices_C.Version] — non-unique index on `version` (all tables) - * - [StandardIndices_C.GlobalBookNumber] — conditional non-unique index on `gbn` WHERE `gbn IS NOT NULL` (all tables) - * - * ### Special indices - * These are **not** created by default. They must be explicitly declared in - * [NakshaCollection.indices] and are defined here so that all storage implementations share - * a consistent name and type contract. - * - * - [StandardIndices_C.PublishNumber] — BTREE on `pn` (WHERE `pn IS NOT NULL`) - * - [StandardIndices_C.PublishTime] — BTREE on `pt` (WHERE `pt IS NOT NULL`) - * - [StandardIndices_C.GlobalVersion] — BTREE on `gv` (WHERE `gv IS NOT NULL`) - * - * ### Default indices - * These are created automatically when [NakshaCollection.indices] is `null` (backward-compatible - * full schema). When [NakshaCollection.indices] is explicitly set (even to an empty list), only - * the mandatory indices plus the explicitly declared indices are created. - * - * - [StandardIndices_C.HereTile] — `here_tile`, `fn`, `version` - * - [StandardIndices_C.AppId] — `app_id`, `updated_at`, `fn`, `version` - * - [StandardIndices_C.Author] — `author`, `author_ts`, `fn`, `version` - * - [StandardIndices_C.Tags] — GIN set index over the tags - * - [StandardIndices_C.FeatureType] — `ft`, `fn`, `version` - * - [StandardIndices_C.CustomValue0] .. [StandardIndices_C.CustomValue3] — custom numeric values - * - [StandardIndices_C.CustomString0] .. [StandardIndices_C.CustomString3] — custom string values - * - [StandardIndices_C.ReferencePoint] — SP-GIST reference-point geometry - * - [StandardIndices_C.GistGeometry] — GIST geometry - * * @since 3.0 */ @JsExport @@ -143,6 +105,14 @@ class StandardIndices private constructor() { // Default indices — created when NakshaCollection.indices is null // ------------------------------------------------------------------------- + /** + * `gist_geo` — spatial ([IndexType.SPATIAL]) GIST index over the geometry member + * (WHERE `geo IS NOT NULL`). Default index. See [StandardMembers.Geometry] + * @since 3.0 + */ + @JvmField @JsStatic + val GistGeometry = Index("gist_geo", IndexType.SPATIAL, "geo") + /** * `here_tile` — index on `here_tile`, `fn`, `version` (WHERE `here_tile IS NOT NULL`). * Default index. @@ -246,14 +216,6 @@ class StandardIndices private constructor() { @JvmField @JsStatic val ReferencePoint = Index("ref_point", IndexType.SPATIAL, "ref_point") - /** - * `gist_geo` — spatial ([IndexType.SPATIAL]) GIST index over the geometry member - * (WHERE `geo IS NOT NULL`). Default index. - * @since 3.0 - */ - @JvmField @JsStatic - val GistGeometry = Index("gist_geo", IndexType.SPATIAL, "geo") - /** * All default indices (created when [NakshaCollection.indices] is `null`), in declaration order. * @@ -261,7 +223,7 @@ class StandardIndices private constructor() { * @since 3.0 */ @JvmField @JsStatic - val DEFAULT: List = listOf( + val XYZ_INDICES: List = listOf( HereTile, AppId, Author, @@ -274,11 +236,11 @@ class StandardIndices private constructor() { ) /** - * The names of all [DEFAULT] indices, for fast lookup. + * The names of all [XYZ_INDICES] indices, for fast lookup. * @since 3.0 */ @JvmField @JsStatic - val DEFAULT_NAMES: Set = DEFAULT.map { it.name }.toHashSet() + val DEFAULT_NAMES: Set = XYZ_INDICES.map { it.name }.toHashSet() /** * All special indices — not added automatically but recognised by all storage implementations. @@ -295,11 +257,11 @@ class StandardIndices private constructor() { val SPECIAL_NAMES: Set = SPECIAL.map { it.name }.toHashSet() /** - * All standard indices: [MANDATORY] followed by [DEFAULT] followed by [SPECIAL]. + * All standard indices: [MANDATORY] followed by [XYZ_INDICES] followed by [SPECIAL]. * @since 3.0 */ @JvmField @JsStatic - val ALL: List = MANDATORY + DEFAULT + SPECIAL + val ALL: List = MANDATORY + XYZ_INDICES + SPECIAL /** * The names of all standard indices, for fast lookup. diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt index 3d4cfc7f4..87ed9e765 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt @@ -8,56 +8,7 @@ import kotlin.jvm.JvmField /** * The canonical set of standard members that every Naksha storage understands. - * - * Members are divided into three groups: - * - * ### Mandatory members - * These are always managed and persisted by the storage, regardless of whether they appear in - * [NakshaCollection.members]. Clients must not declare them with a different [MemberType]. - * Their [Member.path] is always `null` — the storage resolves their values internally. - * - * - [StandardMembers_C.DatabaseNumber] — database-number (storage-level identifier) - * - [StandardMembers_C.CatalogNumber] — catalog-number (map-level identifier) - * - [StandardMembers_C.CollectionNumber] — collection-number (collection-level identifier) - * - [StandardMembers_C.FeatureNumber] — feature-number (per-collection feature identifier, routing key) - * - [StandardMembers_C.Version] — version of the tuple (with action bits in the lower 2 bits) - * - [StandardMembers_C.NextVersion] — version at which this state was superseded (history only) - * - [StandardMembers_C.Feature] — the serialised feature blob - * - [StandardMembers_C.GlobalBookNumber] — global-book-number for JBON2 global-book decoding (sequencer-assigned) - * - * ### Special members - * These members are **not** part of the default schema. They must be explicitly declared in - * [NakshaCollection.members] to be materialised. They are defined here so that all storage - * implementations share a consistent name and type contract. - * - * - [StandardMembers_C.PublishNumber] — publisher-assigned sequential visibility number (`pn`, INT64) - * - [StandardMembers_C.PublishTime] — epoch-millis timestamp when the publisher sequenced the transaction (`pt`, INT64) - * - [StandardMembers_C.GlobalVersion] — HERE global version number (`gv`, INT64) - * - * ### Default members - * These are included automatically when [NakshaCollection.members] is `null` (backward-compatible - * full schema). When [NakshaCollection.members] is explicitly set (even to an empty list), only - * the mandatory members plus the explicitly declared members are materialised. - * - * - [StandardMembers_C.UpdatedAt], [StandardMembers_C.CreatedAt], [StandardMembers_C.AuthorTimestamp] — timestamps - * - [StandardMembers_C.CustomValue0], [StandardMembers_C.CustomValue1], [StandardMembers_C.CustomValue2], [StandardMembers_C.CustomValue3] — custom numeric values - * - [StandardMembers_C.Hash] — content hash - * - [StandardMembers_C.HereTile] — HERE tile key of the reference point - * - [StandardMembers_C.ChangeCount] — change-count - * - [StandardMembers_C.BaseTupleNumber] — base tuple-number (three-way merge support) - * - [StandardMembers_C.Id] — feature string identifier (only for named features where `fn < 0`) - * - [StandardMembers_C.AppId] — application identifier - * - [StandardMembers_C.Author] — author identifier - * - [StandardMembers_C.Origin] — origin reference (forking / rebase support) - * - [StandardMembers_C.Target] — join target reference - * - [StandardMembers_C.FeatureType] — feature-type (only stored when it differs from the collection default) - * - [StandardMembers_C.CustomString0], [StandardMembers_C.CustomString1], [StandardMembers_C.CustomString2], [StandardMembers_C.CustomString3] — custom string values - * - [StandardMembers_C.Tags] — feature tags (set of unique strings, order preserved) - * - [StandardMembers_C.ReferencePoint] — geometry reference point (TWKB, [MemberType.SPATIAL]) - * - [StandardMembers_C.Geometry] — feature geometry (TWKB, [MemberType.SPATIAL]) - * - [StandardMembers_C.Attachment] — arbitrary binary attachment - * - * @since 3.0 + * @since 3.0 */ @JsExport class StandardMembers private constructor() { @@ -65,91 +16,59 @@ class StandardMembers private constructor() { companion object StandardMembers_C { // ------------------------------------------------------------------------- - // Mandatory members — storage-managed, always present, no JSON path + // Mandatory members — storage-managed, always present. // ------------------------------------------------------------------------- /** - * `dbn` — **Database-number** (`INT64`). The storage-level numeric identifier of the storage - * instance. Mandatory, storage-managed. Not materialised as a physical column in all - * implementations (e.g. `lib-psql` treats it as implicit context). - * @since 3.0 - */ - @JvmField @JsStatic - val DatabaseNumber = Member("dbn", MemberType.INT64) - - /** - * `catn` — **Catalog-number** (`INT32`). The map-level numeric identifier. Mandatory, - * storage-managed. Not materialised as a physical column in all implementations - * (e.g. `lib-psql` treats it as implicit schema context). - * @since 3.0 - */ - @JvmField @JsStatic - val CatalogNumber = Member("catn", MemberType.INT32) - - /** - * `coln` — **Collection-number** (`INT32`). The collection-level numeric identifier. Mandatory, - * storage-managed. Not materialised as a physical column in all implementations - * (e.g. `lib-psql` treats it as implicit context). - * @since 3.0 - */ - @JvmField @JsStatic - val CollectionNumber = Member("coln", MemberType.INT32) - - /** - * `fn` — **Feature-number** (`INT64`). The per-collection identifier of the feature. Together - * with [Version] this uniquely identifies a tuple. The lower 16 bits are used as the partition - * routing key. Mandatory, storage-managed. - * @since 3.0 - */ - @JvmField @JsStatic - val FeatureNumber = Member("fn", MemberType.INT64) - - /** - * `version` — **Version** (`INT64`). The transaction number of the tuple, with the action - * encoded in the lower 2 bits. Together with [FeatureNumber] this uniquely identifies a tuple. - * Mandatory, storage-managed. + * `tn` — **Tuple-Number**. The [naksha.model.TupleNumber] of this feature. This persists out of: + * - `database-number: long` - The database in which the feature is stored. + * - `catalog-number: int` - The catalog in which the feature is stored. + * - `collection-number: int` - The collection in which the feature is stored. + * - `feature-number: long` - The unique identifier of the feature within the collection. + * - `version: long` - The version of this feature state. + * + * Note all storages will persist the tuple-number like this, i.e. `lib-psql` will only store the `feature-number` as `fn` and the `version`. The other parts can be deducted from the storage location. * @since 3.0 */ @JvmField @JsStatic - val Version = Member("version", MemberType.INT64) + val Tn = Member("~tn", MemberType.TUPLE_NUMBER, JsonPath("tn")) - /** - * `next_version` — **Next-version** (`INT64`). The version at which this tuple was superseded + /** + * `nv` — **next-version** (`INT64`). The version at which this tuple was superseded * by the next state. Present only in history; in head the value is intrinsically the current * HEAD sentinel and is not stored as a physical member. Mandatory, storage-managed. * @since 3.0 */ @JvmField @JsStatic - val NextVersion = Member("next_version", MemberType.INT64) + val NextVersion = Member("~nv", MemberType.INT64, JsonPath("nv")) - /** - * `feature` — **Serialised feature** (`BYTE_ARRAY`). The encoded feature blob. The encoding - * is controlled by [NakshaCollection.dataEncoding]. Mandatory, storage-managed. + /** + * `gbn` — **Global-book-number** (`INT64`). References the `global_book_fn` in + * `naksha~books`, which carries the JBON2 global dictionary needed to decode this tuple's + * [Feature] blob when global-book encoding is used. Assigned by the sequencer; `null` when + * no global book is in use. Mandatory, storage-managed (but physically `null` until the + * sequencer populates it). + * + * A conditional non-unique index on this member is always created so that the sequencer can + * efficiently locate all tuples that reference a particular book. * @since 3.0 */ @JvmField @JsStatic - val Feature = Member("feature", MemberType.BYTE_ARRAY) + val GlobalBookNumber = Member("~gbn", MemberType.INT64, JsonPath("gbn")) /** - * `id` — feature string identifier. Either stringified [FeatureNumber] _(as positive decimal number, i.e. `12345678`)_ or the unique identifier of the feature, resulting in a negative [FeatureNumber], generated by hashing the `id`. + * `feature` — **Serialised feature** (`BYTE_ARRAY`). The encoded feature blob. The encoding is controlled by [NakshaCollection.dataEncoding]. Mandatory, storage-managed. The feature member is special in that it represents the feature itself, therefore the path is an empty list! * @since 3.0 */ @JvmField @JsStatic - val Id = Member("id", MemberType.STRING) + val Feature = Member("~feature", MemberType.BYTE_ARRAY, JsonPath()) /** - * `gbn` — **Global-book-number** (`INT64`). References the `global_book_fn` in - * `naksha~books`, which carries the JBON2 global dictionary needed to decode this tuple's - * [Feature] blob when global-book encoding is used. Assigned by the sequencer; `null` when - * no global book is in use. Mandatory, storage-managed (but physically `null` until the - * sequencer populates it). - * - * A conditional non-unique index on this member is always created so that the sequencer can - * efficiently locate all tuples that reference a particular book. + * `id` — feature string identifier. Either stringified `feature-number` _(as positive decimal number, i.e. `12345678`)_ or the unique identifier of the feature, resulting in a negative `feature-number`, generated by hashing the `id`. Technically, the storage can store `null`, when the `feature-number` is positive. * @since 3.0 */ @JvmField @JsStatic - val GlobalBookNumber = Member("gbn", MemberType.INT64) + val Id = Member("~id", MemberType.STRING, JsonPath("id")) /** * All mandatory members, in declaration order. @@ -160,18 +79,15 @@ class StandardMembers private constructor() { * @since 3.0 */ @JvmField @JsStatic - val MANDATORY: List = listOf( - DatabaseNumber, CatalogNumber, CollectionNumber, FeatureNumber, - Version, NextVersion, Feature, Id, GlobalBookNumber - ) + val MANDATORY: List = listOf(Tn, NextVersion, Feature, Id, GlobalBookNumber) // ------------------------------------------------------------------------- - // Special members — not in MANDATORY or DEFAULT; declared explicitly per collection + // Optional members. // ------------------------------------------------------------------------- /** * `pn` — **Publish-number** (`INT64`). A sequencer-assigned, gap-free sequential number - * ordered by **visibility** rather than execution order. Unlike [Version], which can have + * ordered by **visibility** rather than execution order. Unlike `version`, which can have * holes (rolled-back transactions) and allows concurrent transactions to commit out of * order (e.g. version 3 commits before version 2), the publish-number is a strict * monotonically increasing sequence with no gaps: after `5` comes `6`, guaranteed. @@ -208,179 +124,6 @@ class StandardMembers private constructor() { @JvmField @JsStatic val GlobalVersion = Member("gv", MemberType.INT64) - // ------------------------------------------------------------------------- - // Default members — included when NakshaCollection.members is null - // ------------------------------------------------------------------------- - - /** - * `updated_at` — millisecond epoch timestamp of the last modification. Default member. - * @since 3.0 - */ - @JvmField @JsStatic - val UpdatedAt = Member("updated_at", MemberType.INT64, JsonPath("properties", "@ns:com:here:xyz", "updatedAt")) - - /** - * `created_at` — millisecond epoch timestamp of the initial creation. `null` means the - * timestamp equals [UpdatedAt] (first-write optimisation). Default member. - * @since 3.0 - */ - @JvmField @JsStatic - val CreatedAt = Member("created_at", MemberType.INT64, JsonPath("properties", "@ns:com:here:xyz", "createdAt")) - - /** - * `author_ts` — millisecond epoch timestamp of the last author change. `null` means the - * timestamp equals [UpdatedAt]. Default member. - * @since 3.0 - */ - @JvmField @JsStatic - val AuthorTimestamp = Member("author_ts", MemberType.INT64, JsonPath("properties", "@ns:com:here:xyz", "authorTs")) - - /** - * `cv0` — custom numeric value 0 (`FLOAT64`). `null` if not used. Default member. - * @since 3.0 - */ - @JvmField @JsStatic - val CustomValue0 = Member("cv0", MemberType.FLOAT64, JsonPath("properties", "@ns:com:here:xyz", "cv0")) - - /** - * `cv1` — custom numeric value 1 (`FLOAT64`). `null` if not used. Default member. - * @since 3.0 - */ - @JvmField @JsStatic - val CustomValue1 = Member("cv1", MemberType.FLOAT64, JsonPath("properties", "@ns:com:here:xyz", "cv1")) - - /** - * `cv2` — custom numeric value 2 (`FLOAT64`). `null` if not used. Default member. - * @since 3.0 - */ - @JvmField @JsStatic - val CustomValue2 = Member("cv2", MemberType.FLOAT64, JsonPath("properties", "@ns:com:here:xyz", "cv2")) - - /** - * `cv3` — custom numeric value 3 (`FLOAT64`). `null` if not used. Default member. - * @since 3.0 - */ - @JvmField @JsStatic - val CustomValue3 = Member("cv3", MemberType.FLOAT64, JsonPath("properties", "@ns:com:here:xyz", "cv3")) - - /** - * `hash` — content hash of the tuple, computed by the storage. `null` if not recorded. - * Default member. - * @since 3.0 - */ - @JvmField @JsStatic - val Hash = Member("hash", MemberType.INT32, JsonPath("properties", "@ns:com:here:xyz", "hash")) - - /** - * `here_tile` — HERE tile key (binary) of the reference point. `null` if not known. - * Default member. - * @since 3.0 - */ - @JvmField @JsStatic - val HereTile = Member("here_tile", MemberType.INT32, JsonPath("properties", "@ns:com:here:xyz", "hereTile")) - - /** - * `cc` — change-count: how many times this feature has been modified. Default member. - * @since 3.0 - */ - @JvmField @JsStatic - val ChangeCount = Member("cc", MemberType.INT32, JsonPath("properties", "@ns:com:here:xyz", "changeCount")) - - /** - * `base_tn` — base tuple-number (`BYTE_ARRAY`), set when a three-way merge was performed. - * `null` otherwise. Default member. - * @since 3.0 - */ - @JvmField @JsStatic - val BaseTupleNumber = Member("base_tn", MemberType.BYTE_ARRAY, JsonPath("properties", "@ns:com:here:xyz", "base")) - - /** - * `app_id` — identifier of the application that wrote this tuple. Default member. - * @since 3.0 - */ - @JvmField @JsStatic - val AppId = Member("app_id", MemberType.STRING, JsonPath("properties", "@ns:com:here:xyz", "appId")) - - /** - * `author` — identifier of the human author that takes ownership for this tuple. Default member. - * @since 3.0 - */ - @JvmField @JsStatic - val Author = Member("author", MemberType.STRING, JsonPath("properties", "@ns:com:here:xyz", "author")) - - /** - * `origin` — stringified reference to the originating feature when this feature was forked or - * copied from another storage, map, or collection. Used for rebase support. Default member. - * @since 3.0 - */ - @JvmField @JsStatic - val Origin = Member("origin", MemberType.STRING, JsonPath("properties", "@ns:com:here:xyz", "origin")) - - /** - * `target` — stringified reference to the feature into which this feature was joined. - * Set when multiple features are merged into one. Default member. - * @since 3.0 - */ - @JvmField @JsStatic - val Target = Member("target", MemberType.STRING, JsonPath("properties", "@ns:com:here:xyz", "target")) - - /** - * `ft` — feature-type string. `null` when it matches the collection's - * [default feature type][NakshaCollection.defaultFeatureType], avoiding redundant storage. - * Default member. - * @since 3.0 - */ - @JvmField @JsStatic - val FeatureType = Member("ft", MemberType.STRING, JsonPath("properties", "featureType")) - - /** - * `cs0` — custom string value 0. `null` if not used. Default member. - * @since 3.0 - */ - @JvmField @JsStatic - val CustomString0 = Member("cs0", MemberType.STRING, JsonPath("properties", "@ns:com:here:xyz", "cs0")) - - /** - * `cs1` — custom string value 1. `null` if not used. Default member. - * @since 3.0 - */ - @JvmField @JsStatic - val CustomString1 = Member("cs1", MemberType.STRING, JsonPath("properties", "@ns:com:here:xyz", "cs1")) - - /** - * `cs2` — custom string value 2. `null` if not used. Default member. - * @since 3.0 - */ - @JvmField @JsStatic - val CustomString2 = Member("cs2", MemberType.STRING, JsonPath("properties", "@ns:com:here:xyz", "cs2")) - - /** - * `cs3` — custom string value 3. `null` if not used. Default member. - * @since 3.0 - */ - @JvmField @JsStatic - val CustomString3 = Member("cs3", MemberType.STRING, JsonPath("properties", "@ns:com:here:xyz", "cs3")) - - /** - * `tags` — feature tags, the classic XYZ tags array located at - * `properties -> @ns:com:here:xyz -> tags` (e.g. `["foo", "bar"]`), stored as a - * [set][MemberType.SET] of unique strings. The array is persisted unmodified, so the - * element order is preserved when reading the feature back. `null` if the feature has no - * tags. Supports element containment queries via [IndexType.SET]. Default member. - * @since 3.0 - */ - @JvmField @JsStatic - val Tags = Member("tags", MemberType.SET, JsonPath("properties", "@ns:com:here:xyz", "tags")) - - /** - * `ref_point` — geometry reference point (always a single point), stored as TWKB. Used to - * compute the [HereTile] value. `null` if the feature has no explicit reference point. - * Default member. - * @since 3.0 - */ - @JvmField @JsStatic - val ReferencePoint = Member("ref_point", MemberType.SPATIAL, JsonPath("referencePoint")) - /** * `geo` — feature geometry stored as TWKB. `null` if the feature has no geometry. * Default member. @@ -388,79 +131,5 @@ class StandardMembers private constructor() { */ @JvmField @JsStatic val Geometry = Member("geo", MemberType.SPATIAL, JsonPath("geometry")) - - /** - * `attachment` — arbitrary binary attachment. `null` if unused. Default member. - * @since 3.0 - */ - @JvmField @JsStatic - val Attachment = Member("attachment", MemberType.BYTE_ARRAY, JsonPath("attachment")) - - /** - * `data_encoding` — the encoding used for the serialised feature blob (e.g. `JBON2`, `JSON_GZIP`). - * `null` if not specified, in which case the storage default applies. Default member. - * @since 3.0 - */ - @JvmField @JsStatic - val DataEncoding = Member("data_encoding", MemberType.STRING) - - /** - * The names of all [MANDATORY] members, for fast lookup. - * @since 3.0 - */ - @JvmField @JsStatic - val MANDATORY_NAMES: Set = MANDATORY.map { it.name }.toHashSet() - - /** - * All default members (included when [NakshaCollection.members] is `null`), in declaration order. - * - * Does **not** include the [MANDATORY] members — those are always present regardless. - * @since 3.0 - */ - @JvmField @JsStatic - val DEFAULT: List = listOf( - UpdatedAt, CreatedAt, AuthorTimestamp, - CustomValue0, CustomValue1, CustomValue2, CustomValue3, - Hash, HereTile, ChangeCount, - BaseTupleNumber, - AppId, Author, Origin, Target, FeatureType, - CustomString0, CustomString1, CustomString2, CustomString3, - Tags, ReferencePoint, Geometry, Attachment, DataEncoding - ) - - /** - * The names of all [DEFAULT] members, for fast lookup. - * @since 3.0 - */ - @JvmField @JsStatic - val DEFAULT_NAMES: Set = DEFAULT.map { it.name }.toHashSet() - - /** - * All special members — not added automatically but recognised by all storage implementations. - * @since 3.0 - */ - @JvmField @JsStatic - val SPECIAL: List = listOf(PublishNumber, PublishTime, GlobalVersion) - - /** - * The names of all [SPECIAL] members, for fast lookup. - * @since 3.0 - */ - @JvmField @JsStatic - val SPECIAL_NAMES: Set = SPECIAL.map { it.name }.toHashSet() - - /** - * All standard members: [MANDATORY] followed by [DEFAULT] followed by [SPECIAL]. - * @since 3.0 - */ - @JvmField @JsStatic - val ALL: List = MANDATORY + DEFAULT + SPECIAL - - /** - * The names of all standard members, for fast lookup. - * @since 3.0 - */ - @JvmField @JsStatic - val ALL_NAMES: Set = ALL.map { it.name }.toHashSet() - } + } } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzIndices.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzIndices.kt new file mode 100644 index 000000000..ee1c8fe9f --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzIndices.kt @@ -0,0 +1,18 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model.objects + +import kotlin.js.JsExport +import kotlin.js.JsStatic +import kotlin.jvm.JvmField + +/** + * The canonical set of standard indices that every Naksha storage understands. + * @since 3.0 + */ +@JsExport +class XyzIndices private constructor() { + + companion object XyzIndices_C { + } +} \ No newline at end of file diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzMembers.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzMembers.kt new file mode 100644 index 000000000..320c5ff06 --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzMembers.kt @@ -0,0 +1,250 @@ +@file:OptIn(ExperimentalJsExport::class, ExperimentalJsStatic::class) + +package naksha.model.objects + +import kotlin.js.ExperimentalJsExport +import kotlin.js.ExperimentalJsStatic +import kotlin.js.JsExport +import kotlin.js.JsStatic +import kotlin.jvm.JvmField + +@JsExport +class XyzMembers private constructor() { + companion object XyzMembers_C { + // ------------------------------------------------------------------------- + // Mandatory members — storage-managed, always present. + // ------------------------------------------------------------------------- + + /** + * The same as [StandardMembers.Tn], but with a Data-Hub compatible path. + * @since 3.0 + */ + @JvmField @JsStatic + val XyzTn = Member("~tn", MemberType.TUPLE_NUMBER, JsonPath("properties", "@ns:com:here:xyz", "uuid")) + + /** + * The same as [StandardMembers.NextVersion], but with a Data-Hub compatible path. + * @since 3.0 + */ + @JvmField @JsStatic + val XyzNextVersion = Member("~nv", MemberType.INT64, JsonPath("properties", "@ns:com:here:xyz", "nextVersion")) + + /** + * The same as [StandardMembers.GlobalBookNumber], but with a Data-Hub compatible path. + * @since 3.0 + */ + @JvmField @JsStatic + val XyzGlobalBookNumber = Member("~gbn", MemberType.INT64, JsonPath("properties", "@ns:com:here:xyz", "gbn")) + + /** + * The same as [StandardMembers.Feature]. + * @since 3.0 + */ + @JvmField @JsStatic + val XyzFeature = Member("~feature", MemberType.BYTE_ARRAY, JsonPath()) + + /** + * The same as [StandardMembers.Feature]. + * @since 3.0 + */ + @JvmField @JsStatic + val XyzId = Member("~id", MemberType.STRING, JsonPath("id")) + + // ------------------------------------------------------------------------- + // Optional members. + // ------------------------------------------------------------------------- + + /** + * `geo` — feature geometry stored as TWKB. `null` if the feature has no geometry. + * Default member. + * @since 3.0 + */ + @JvmField @JsStatic + val XyzGeometry = Member("geo", MemberType.SPATIAL, JsonPath("geometry")) + + /** + * `updated_at` — millisecond epoch timestamp of the last modification. Default member. + * @since 3.0 + */ + @JvmField @JsStatic + val XyzUpdatedAt = Member("updated_at", MemberType.INT64, JsonPath("properties", "@ns:com:here:xyz", "updatedAt")) + + /** + * `created_at` — millisecond epoch timestamp of the initial creation. `null` means the + * timestamp equals [XyzUpdatedAt] (first-write optimisation). Default member. + * @since 3.0 + */ + @JvmField @JsStatic + val XyzCreatedAt = Member("created_at", MemberType.INT64, JsonPath("properties", "@ns:com:here:xyz", "createdAt")) + + /** + * `author_ts` — millisecond epoch timestamp of the last author change. `null` means the + * timestamp equals [XyzUpdatedAt]. Default member. + * @since 3.0 + */ + @JvmField @JsStatic + val XyzAuthorTimestamp = Member("author_ts", MemberType.INT64, JsonPath("properties", "@ns:com:here:xyz", "authorTs")) + + /** + * `hash` — content hash of the tuple, computed by the storage. `null` if not recorded. + * Default member. + * @since 3.0 + */ + @JvmField @JsStatic + val XyzHash = Member("hash", MemberType.INT32, JsonPath("properties", "@ns:com:here:xyz", "hash")) + + /** + * `here_tile` — HERE tile key (binary) of the reference point. `null` if not known. + * Default member. + * @since 3.0 + */ + @JvmField @JsStatic + val XyzHereTile = Member("here_tile", MemberType.INT32, JsonPath("properties", "@ns:com:here:xyz", "hereTile")) + + /** + * `cc` — change-count: how many times this feature has been modified. Default member. + * @since 3.0 + */ + @JvmField @JsStatic + val XyzChangeCount = Member("cc", MemberType.INT32, JsonPath("properties", "@ns:com:here:xyz", "changeCount")) + + /** + * `base_tn` — base tuple-number (`BYTE_ARRAY`), set when a three-way merge was performed. + * `null` otherwise. Default member. + * @since 3.0 + */ + @JvmField @JsStatic + val XyzBaseTn = Member("base_tn", MemberType.BYTE_ARRAY, JsonPath("properties", "@ns:com:here:xyz", "base")) + + /** + * `app_id` — identifier of the application that wrote this tuple. Default member. + * @since 3.0 + */ + @JvmField @JsStatic + val XyzAppId = Member("app_id", MemberType.STRING, JsonPath("properties", "@ns:com:here:xyz", "appId")) + + /** + * `author` — identifier of the human author that takes ownership for this tuple. Default member. + * @since 3.0 + */ + @JvmField @JsStatic + val XyzAuthor = Member("author", MemberType.STRING, JsonPath("properties", "@ns:com:here:xyz", "author")) + + /** + * `origin` — stringified reference to the originating feature when this feature was forked or + * copied from another storage, map, or collection. Used for rebase support. Default member. + * @since 3.0 + */ + @JvmField @JsStatic + val XyzOrigin = Member("origin", MemberType.STRING, JsonPath("properties", "@ns:com:here:xyz", "origin")) + + /** + * `target` — stringified reference to the feature into which this feature was joined. + * Set when multiple features are merged into one. Default member. + * @since 3.0 + */ + @JvmField @JsStatic + val XyzTarget = Member("target", MemberType.STRING, JsonPath("properties", "@ns:com:here:xyz", "target")) + + /** + * `ft` — feature-type string. `null` when it matches the collection's + * [default feature type][NakshaCollection.defaultFeatureType], avoiding redundant storage. + * Default member. + * @since 3.0 + */ + @JvmField @JsStatic + val XyzFeatureType = Member("ft", MemberType.STRING, JsonPath("properties", "featureType")) + + /** + * `cv0` — custom numeric value 0 (`FLOAT64`). `null` if not used. Default member. + * @since 3.0 + */ + @JvmField @JsStatic + val XyzCustomValue0 = Member("cv0", MemberType.FLOAT64, JsonPath("properties", "@ns:com:here:xyz", "cv0")) + + /** + * `cv1` — custom numeric value 1 (`FLOAT64`). `null` if not used. Default member. + * @since 3.0 + */ + @JvmField @JsStatic + val XyzCustomValue1 = Member("cv1", MemberType.FLOAT64, JsonPath("properties", "@ns:com:here:xyz", "cv1")) + + /** + * `cv2` — custom numeric value 2 (`FLOAT64`). `null` if not used. Default member. + * @since 3.0 + */ + @JvmField @JsStatic + val XyzCustomValue2 = Member("cv2", MemberType.FLOAT64, JsonPath("properties", "@ns:com:here:xyz", "cv2")) + + /** + * `cv3` — custom numeric value 3 (`FLOAT64`). `null` if not used. Default member. + * @since 3.0 + */ + @JvmField @JsStatic + val XyzCustomValue3 = Member("cv3", MemberType.FLOAT64, JsonPath("properties", "@ns:com:here:xyz", "cv3")) + + /** + * `cs0` — custom string value 0. `null` if not used. Default member. + * @since 3.0 + */ + @JvmField @JsStatic + val XyzCustomString0 = Member("cs0", MemberType.STRING, JsonPath("properties", "@ns:com:here:xyz", "cs0")) + + /** + * `cs1` — custom string value 1. `null` if not used. Default member. + * @since 3.0 + */ + @JvmField @JsStatic + val XyzCustomString1 = Member("cs1", MemberType.STRING, JsonPath("properties", "@ns:com:here:xyz", "cs1")) + + /** + * `cs2` — custom string value 2. `null` if not used. Default member. + * @since 3.0 + */ + @JvmField @JsStatic + val XyzCustomString2 = Member("cs2", MemberType.STRING, JsonPath("properties", "@ns:com:here:xyz", "cs2")) + + /** + * `cs3` — custom string value 3. `null` if not used. Default member. + * @since 3.0 + */ + @JvmField @JsStatic + val XyzCustomString3 = Member("cs3", MemberType.STRING, JsonPath("properties", "@ns:com:here:xyz", "cs3")) + + /** + * `tags` — feature tags, the classic XYZ tags array located at + * `properties -> @ns:com:here:xyz -> tags` (e.g. `["foo", "bar"]`), stored as a + * [set][MemberType.SET] of unique strings. The array is persisted unmodified, so the + * element order is preserved when reading the feature back. `null` if the feature has no + * tags. Supports element containment queries via [IndexType.SET]. Default member. + * @since 3.0 + */ + @JvmField @JsStatic + val XyzTags = Member("tags", MemberType.SET, JsonPath("properties", "@ns:com:here:xyz", "tags")) + + /** + * `ref_point` — geometry reference point (always a single point), stored as TWKB. Used to + * compute the [XyzHereTile] value. `null` if the feature has no explicit reference point. + * Default member. + * @since 3.0 + */ + @JvmField @JsStatic + val XyzReferencePoint = Member("ref_point", MemberType.SPATIAL, JsonPath("referencePoint")) + + /** + * All members of XYZ compatible features. + * @since 3.0 + */ + @JvmField @JsStatic + val ALL: List = listOf( + XyzTn, XyzNextVersion, XyzGlobalBookNumber, XyzFeature, XyzId, XyzGeometry, + // Optional members + XyzUpdatedAt, XyzCreatedAt, XyzAuthorTimestamp, + XyzHash, XyzHereTile, XyzChangeCount, XyzBaseTn, + XyzAppId, XyzAuthor, XyzOrigin, XyzTarget, XyzFeatureType, + XyzCustomValue0, XyzCustomValue1, XyzCustomValue2, XyzCustomValue3, + XyzCustomString0, XyzCustomString1, XyzCustomString2, XyzCustomString3, + XyzTags, XyzReferencePoint + ) + } +} \ No newline at end of file diff --git a/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/MemberTest.kt b/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/MemberTest.kt index 34d9b113c..9d9b1d9e4 100644 --- a/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/MemberTest.kt +++ b/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/MemberTest.kt @@ -106,7 +106,7 @@ class MemberTest { @Test fun standardTagsMemberDefaultsToSet() { - assertEquals(MemberType.SET, naksha.model.objects.StandardMembers.Tags.dataType) + assertEquals(MemberType.SET, naksha.model.objects.StandardMembers.XyzTags.dataType) assertEquals(IndexType.SET, naksha.model.objects.StandardIndices.Tags.type) } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgSession.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgSession.kt index ec18ea749..32e585827 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgSession.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgSession.kt @@ -234,7 +234,7 @@ open class PgSession( var tx: StorageTx? = this.tx if (tx == null) { val txn = storage.newConnection(options, false, null).use { conn -> storage.adminMap.newTxn(conn) } - tx = StorageTx(storage, txn.version, options.appId, options.author, storage.adminMap) + tx = StorageTx(storage, txn.version, options.appId, options.author, storage.adminMap, this) this.tx = tx } return tx @@ -346,27 +346,9 @@ open class PgSession( * Processors are invoked in the order in which they were added. * @since 3.0 */ - private val memberProcessors: MutableMap> = mutableMapOf() + private val _processors = MemberProcessorMap() - override fun clearMemberProcessors(): ISession { - memberProcessors.clear() - return this - } - - override fun addMemberProcessor(memberName: String, memberProcessor: IMemberProcessor): ISession { - var processors = memberProcessors[memberName] - if (processors == null) { - processors = mutableListOf() - memberProcessors[memberName] = processors - } - processors.add(memberProcessor) - return this - } - - override fun removeMemberProcessor(memberName: String, memberProcessor: IMemberProcessor): ISession { - memberProcessors[memberName]?.remove(memberProcessor) - return this - } + override fun processors(): MemberProcessorMap = _processors override fun isClosed(): Boolean = _closed diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgUtil.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgUtil.kt index b860125af..2a3f48a26 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgUtil.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgUtil.kt @@ -222,23 +222,6 @@ class PgUtil private constructor() { ) fun decodeFeature(bytes: ByteArray?, dictManager: IDictManager? = null): NakshaFeature? = Naksha.decodeFeature(bytes, dictManager) - /** - * Encodes the given [NakshaFeature] into bytes. - * @param feature the feature to encode. - * @param encoding the feature encoding to use. - * @param dict the dictionary to use for encoding; if any. - * @return the encoded feature. - * @since 3.0.0 - */ - @JsStatic - @JvmStatic - @Deprecated( - message = "Please use Naksha class instead", - replaceWith = ReplaceWith("Naksha.encodeFeature(feature, encoding, dict)"), - level = DeprecationLevel.WARNING - ) - fun encodeFeature(feature: NakshaFeature?, encoding: DataEncoding, dict: JbDictionary? = null): ByteArray? = Naksha.encodeFeature(feature, encoding, dict) - /** * Decode the Naksha tags from their raw `jsonb` text form. * @param json the JSON text to decode (value of the `tags` `jsonb` column). diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt index 04cec87de..abfe58809 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt @@ -184,11 +184,11 @@ LEFT JOIN inserted ON inserted.id = new_row.id // (sentinel "undefined" causes the DB to retain the existing value). val geo = if (PgColumn.geo in keepableByteCols) rows.getByteArray(rowNum, PgColumn.geo.name) else tuple.getByteArray(StandardMembers.Geometry) val referencePoint = if (PgColumn.ref_point in keepableByteCols) rows.getByteArray(rowNum, PgColumn.ref_point.name) else tuple.getByteArray(StandardMembers.ReferencePoint) - val tags = tuple.getStringMember(StandardMembers.Tags) - val attachment = if (PgColumn.attachment in keepableByteCols) rows.getByteArray(rowNum, PgColumn.attachment.name) else tuple.getByteArray(StandardMembers.Attachment) + val tags = tuple.getStringMember(StandardMembers.XyzTags) + val attachment = if (PgColumn.attachment in keepableByteCols) rows.getByteArray(rowNum, PgColumn.attachment.name) else tuple.getByteArray(StandardMembers.XyzAttachment) val oldGeo = tuple.getByteArray(StandardMembers.Geometry) val oldRefPoint = tuple.getByteArray(StandardMembers.ReferencePoint) - val oldAttachment = tuple.getByteArray(StandardMembers.Attachment) + val oldAttachment = tuple.getByteArray(StandardMembers.XyzAttachment) val needsPatch = (oldGeo == null || !oldGeo.contentEquals(geo ?: ByteArray(0))) || (oldRefPoint == null || !oldRefPoint.contentEquals(referencePoint ?: ByteArray(0))) || (oldAttachment == null || !oldAttachment.contentEquals(attachment ?: ByteArray(0))) @@ -198,8 +198,8 @@ LEFT JOIN inserted ON inserted.id = new_row.id val dict = m.copy() dict.put(StandardMembers.Geometry.name, geo) dict.put(StandardMembers.ReferencePoint.name, referencePoint) - dict.put(StandardMembers.Tags.name, tags) - dict.put(StandardMembers.Attachment.name, attachment) + dict.put(StandardMembers.XyzTags.name, tags) + dict.put(StandardMembers.XyzAttachment.name, attachment) dict } else m write.tuple = tuple.copy(members = newMembers) diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt index 7683f8996..4d6fb4269 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt @@ -209,16 +209,16 @@ ${if (head_to_history.isNotEmpty()) "LEFT JOIN head_to_history ON head_to_histor // with the existing value, so the in-memory tuple must reflect the final stored state. val geo = if (PgColumn.geo in keepableByteCols) outRows.getByteArray(row, PgColumn.geo.name) else tuple.getByteArray(StandardMembers.Geometry) val referencePoint = if (PgColumn.ref_point in keepableByteCols) outRows.getByteArray(row, PgColumn.ref_point.name) else tuple.getByteArray(StandardMembers.ReferencePoint) - val tags = tuple.getStringMember(StandardMembers.Tags) - val attachment = if (PgColumn.attachment in keepableByteCols) outRows.getByteArray(row, PgColumn.attachment.name) else tuple.getByteArray(StandardMembers.Attachment) + val tags = tuple.getStringMember(StandardMembers.XyzTags) + val attachment = if (PgColumn.attachment in keepableByteCols) outRows.getByteArray(row, PgColumn.attachment.name) else tuple.getByteArray(StandardMembers.XyzAttachment) write.tupleNumber = updated_tn val m = tuple.members val newMembers = if (m is naksha.jbon.HeapBook) { val dict = m.copy() dict.put(StandardMembers.Geometry.name, geo) dict.put(StandardMembers.ReferencePoint.name, referencePoint) - dict.put(StandardMembers.Tags.name, tags) - dict.put(StandardMembers.Attachment.name, attachment) + dict.put(StandardMembers.XyzTags.name, tags) + dict.put(StandardMembers.XyzAttachment.name, attachment) dict } else m write.tuple = tuple.copy( diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/AttachmentTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/AttachmentTest.kt index 9359f6471..ed1eeaf50 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/AttachmentTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/AttachmentTest.kt @@ -2,10 +2,8 @@ package naksha.psql import naksha.base.PlatformUtil import naksha.model.* -import naksha.model.objects.NakshaCollection import naksha.model.objects.NakshaFeature import naksha.model.request.* -import naksha.psql.PgTest.PgTest_C.TEST_MAP_ID import naksha.model.RandomFeatures.RandomFeatures_C.randomFeature import kotlin.test.* @@ -36,8 +34,8 @@ class AttachmentTest : PgTestBase() { assertEquals(1, featureTupleList.size) val featureTuple = assertNotNull(featureTupleList[0]) val tuple = assertNotNull(featureTuple.tuple) - assertNotNull(tuple.getByteArray(naksha.model.objects.StandardMembers.Attachment)) - assertContentEquals(attachmentBytes, tuple.getByteArray(naksha.model.objects.StandardMembers.Attachment)) + assertNotNull(tuple.getByteArray(naksha.model.objects.StandardMembers.XyzAttachment)) + assertContentEquals(attachmentBytes, tuple.getByteArray(naksha.model.objects.StandardMembers.XyzAttachment)) } // Read the feature @@ -57,8 +55,8 @@ class AttachmentTest : PgTestBase() { assertEquals(1, featureTupleList.size) val featureTuple = assertNotNull(featureTupleList[0]) val tuple = assertNotNull(featureTuple.tuple) - assertNotNull(tuple.getByteArray(naksha.model.objects.StandardMembers.Attachment)) - assertContentEquals(attachmentBytes, tuple.getByteArray(naksha.model.objects.StandardMembers.Attachment)) + assertNotNull(tuple.getByteArray(naksha.model.objects.StandardMembers.XyzAttachment)) + assertContentEquals(attachmentBytes, tuple.getByteArray(naksha.model.objects.StandardMembers.XyzAttachment)) } } @@ -90,8 +88,8 @@ class AttachmentTest : PgTestBase() { assertEquals(1, featureTupleList.size) val featureTuple = assertNotNull(featureTupleList[0]) val tuple = assertNotNull(featureTuple.tuple) - assertNotNull(tuple.getByteArray(naksha.model.objects.StandardMembers.Attachment)) - assertContentEquals(attachmentBytes, tuple.getByteArray(naksha.model.objects.StandardMembers.Attachment)) + assertNotNull(tuple.getByteArray(naksha.model.objects.StandardMembers.XyzAttachment)) + assertContentEquals(attachmentBytes, tuple.getByteArray(naksha.model.objects.StandardMembers.XyzAttachment)) } // Read the feature @@ -113,8 +111,8 @@ class AttachmentTest : PgTestBase() { assertEquals(1, featureTupleList.size) val featureTuple = assertNotNull(featureTupleList[0]) val tuple = assertNotNull(featureTuple.tuple) - assertNotNull(tuple.getByteArray(naksha.model.objects.StandardMembers.Attachment)) - assertContentEquals(attachmentBytes, tuple.getByteArray(naksha.model.objects.StandardMembers.Attachment)) + assertNotNull(tuple.getByteArray(naksha.model.objects.StandardMembers.XyzAttachment)) + assertContentEquals(attachmentBytes, tuple.getByteArray(naksha.model.objects.StandardMembers.XyzAttachment)) readFeature = feature } @@ -146,8 +144,8 @@ class AttachmentTest : PgTestBase() { assertEquals(1, featureTupleList.size) val featureTuple = assertNotNull(featureTupleList[0]) val tuple = assertNotNull(featureTuple.tuple) - assertNotNull(tuple.getByteArray(naksha.model.objects.StandardMembers.Attachment)) - assertContentEquals(attachmentBytes, tuple.getByteArray(naksha.model.objects.StandardMembers.Attachment)) + assertNotNull(tuple.getByteArray(naksha.model.objects.StandardMembers.XyzAttachment)) + assertContentEquals(attachmentBytes, tuple.getByteArray(naksha.model.objects.StandardMembers.XyzAttachment)) } } @@ -179,8 +177,8 @@ class AttachmentTest : PgTestBase() { assertEquals(1, featureTupleList.size) val featureTuple = assertNotNull(featureTupleList[0]) val tuple = assertNotNull(featureTuple.tuple) - assertNotNull(tuple.getByteArray(naksha.model.objects.StandardMembers.Attachment)) - assertContentEquals(attachmentBytes, tuple.getByteArray(naksha.model.objects.StandardMembers.Attachment)) + assertNotNull(tuple.getByteArray(naksha.model.objects.StandardMembers.XyzAttachment)) + assertContentEquals(attachmentBytes, tuple.getByteArray(naksha.model.objects.StandardMembers.XyzAttachment)) } // Read the feature @@ -202,8 +200,8 @@ class AttachmentTest : PgTestBase() { assertEquals(1, featureTupleList.size) val featureTuple = assertNotNull(featureTupleList[0]) val tuple = assertNotNull(featureTuple.tuple) - assertNotNull(tuple.getByteArray(naksha.model.objects.StandardMembers.Attachment)) - assertContentEquals(attachmentBytes, tuple.getByteArray(naksha.model.objects.StandardMembers.Attachment)) + assertNotNull(tuple.getByteArray(naksha.model.objects.StandardMembers.XyzAttachment)) + assertContentEquals(attachmentBytes, tuple.getByteArray(naksha.model.objects.StandardMembers.XyzAttachment)) readFeature = feature } @@ -235,8 +233,8 @@ class AttachmentTest : PgTestBase() { assertEquals(1, featureTupleList.size) val featureTuple = assertNotNull(featureTupleList[0]) val tuple = assertNotNull(featureTuple.tuple) - assertNotNull(tuple.getByteArray(naksha.model.objects.StandardMembers.Attachment)) - assertContentEquals(attachmentBytes, tuple.getByteArray(naksha.model.objects.StandardMembers.Attachment)) + assertNotNull(tuple.getByteArray(naksha.model.objects.StandardMembers.XyzAttachment)) + assertContentEquals(attachmentBytes, tuple.getByteArray(naksha.model.objects.StandardMembers.XyzAttachment)) } } } diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ChainCollectionTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ChainCollectionTest.kt index 6f1f80940..bcd7bd253 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ChainCollectionTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ChainCollectionTest.kt @@ -12,6 +12,7 @@ import naksha.model.request.ReadFeatures import naksha.model.request.Write import naksha.model.request.WriteRequest import kotlin.test.Test +import kotlin.test.assertContentEquals import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertNull @@ -64,6 +65,19 @@ class ChainCollectionTest : PgTestBase( else -> null } + @Test + fun allMembersShouldHaveAnEffectivePath() { + val members = assertNotNull(collection.members) + assertEquals(2, members.size) + assertEquals("left_fn", assertNotNull(members[0]).name) + assertContentEquals(listOf("properties", "left_fn"), assertNotNull(members[0]).effectivePath()) + assertNull(assertNotNull(members[0]).path) + + assertEquals("right_fn", assertNotNull(members[1]).name) + assertContentEquals(listOf("properties", "right_fn"), assertNotNull(members[1]).effectivePath()) + assertNull(assertNotNull(members[1]).path) + } + @Test fun shouldInsertAndReadChainFeatures() { // Given: three features forming a doubly-linked chain diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/PgUtilTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/PgUtilTest.kt index e727e9305..702930457 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/PgUtilTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/PgUtilTest.kt @@ -11,68 +11,68 @@ import kotlin.test.assertNotNull class PgUtilTest { - @Test - fun shouldDecodeEncodedFeature() { - // Given - val beforeEncoding = NakshaFeature().apply { - id = "feature_1" - properties = NakshaProperties().apply { - setRaw("featureType", "some_feature_type") - xyz = make( - "appId" to "someAppId", - "author" to "someAuthor" - ) - } - } +// @Test +// fun shouldDecodeEncodedFeature() { +// // Given +// val beforeEncoding = NakshaFeature().apply { +// id = "feature_1" +// properties = NakshaProperties().apply { +// setRaw("featureType", "some_feature_type") +// xyz = make( +// "appId" to "someAppId", +// "author" to "someAuthor" +// ) +// } +// } +// +// // When: +// val encoding = DataEncoding.JBON +// val encoded = PgUtil.encodeFeature(beforeEncoding, encoding) +// +// // And: +// val decoded = PgUtil.decodeFeature(encoded) +// +// // Then: features are equal but decoded one is missing Xyz +// // note: Xyz should be populated after decoding (it's not stored in `feature` column, it's scattered in other columns) +// assertNotNull(decoded) +// assertThatFeature(beforeEncoding) +// .isIdenticalTo(decoded, ignoreProps = true) +// .hasPropertiesThat { decodedProperties -> +// decodedProperties +// .hasFeatureType(beforeEncoding.properties.featureType) +// .hasXyzThat { it.isEmpty() } +// } +// } - // When: - val encoding = DataEncoding.JBON - val encoded = PgUtil.encodeFeature(beforeEncoding, encoding) - - // And: - val decoded = PgUtil.decodeFeature(encoded) - - // Then: features are equal but decoded one is missing Xyz - // note: Xyz should be populated after decoding (it's not stored in `feature` column, it's scattered in other columns) - assertNotNull(decoded) - assertThatFeature(beforeEncoding) - .isIdenticalTo(decoded, ignoreProps = true) - .hasPropertiesThat { decodedProperties -> - decodedProperties - .hasFeatureType(beforeEncoding.properties.featureType) - .hasXyzThat { it.isEmpty() } - } - } - - @Test - fun shouldDecodeJbon2EncodedFeature() { - for (encoding in listOf(DataEncoding.JBON2, DataEncoding.JBON2_GZIP)) { - // Given - val beforeEncoding = NakshaFeature().apply { - id = "feature_1" - properties = NakshaProperties().apply { - setRaw("featureType", "some_feature_type") - setRaw("name", "hello world") - xyz = make( - "appId" to "someAppId", - "author" to "someAuthor" - ) - } - } - - // When: - val encoded = PgUtil.encodeFeature(beforeEncoding, encoding) - val decoded = PgUtil.decodeFeature(encoded) - - // Then: same parity contract as JBON1 (Xyz lives in separate columns, not the feature blob). - assertNotNull(decoded, "decoded should not be null for $encoding") - assertThatFeature(beforeEncoding) - .isIdenticalTo(decoded, ignoreProps = true) - .hasPropertiesThat { decodedProperties -> - decodedProperties - .hasFeatureType(beforeEncoding.properties.featureType) - .hasXyzThat { it.isEmpty() } - } - } - } +// @Test +// fun shouldDecodeJbon2EncodedFeature() { +// for (encoding in listOf(DataEncoding.JBON2, DataEncoding.JBON2_GZIP)) { +// // Given +// val beforeEncoding = NakshaFeature().apply { +// id = "feature_1" +// properties = NakshaProperties().apply { +// setRaw("featureType", "some_feature_type") +// setRaw("name", "hello world") +// xyz = make( +// "appId" to "someAppId", +// "author" to "someAuthor" +// ) +// } +// } +// +// // When: +// val encoded = PgUtil.encodeFeature(beforeEncoding, encoding) +// val decoded = PgUtil.decodeFeature(encoded) +// +// // Then: same parity contract as JBON1 (Xyz lives in separate columns, not the feature blob). +// assertNotNull(decoded, "decoded should not be null for $encoding") +// assertThatFeature(beforeEncoding) +// .isIdenticalTo(decoded, ignoreProps = true) +// .hasPropertiesThat { decodedProperties -> +// decodedProperties +// .hasFeatureType(beforeEncoding.properties.featureType) +// .hasXyzThat { it.isEmpty() } +// } +// } +// } } \ No newline at end of file diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/TupleNumberPersistenceTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/TupleNumberPersistenceTest.kt index f35ffca90..4714792d7 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/TupleNumberPersistenceTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/TupleNumberPersistenceTest.kt @@ -7,12 +7,10 @@ import naksha.model.Action import naksha.model.Naksha import naksha.model.Naksha.NakshaCompanion.featureNumber import naksha.model.Naksha.NakshaCompanion.partitionNumber -import naksha.model.objects.NakshaCollection import naksha.model.objects.NakshaFeature import naksha.model.request.Write import naksha.model.request.WriteRequest import naksha.model.RandomFeatures -import naksha.psql.PgTest.PgTest_C.TEST_MAP_ID import kotlin.test.* class TupleNumberPersistenceTest : PgTestBase(collection = null, mapId = "") { diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/UpdateFeatureTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/UpdateFeatureTest.kt index 83b15b785..0f7b02fae 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/UpdateFeatureTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/UpdateFeatureTest.kt @@ -112,22 +112,22 @@ class UpdateFeatureTest : PgTestBase(collection = null, mapId = "") { assertNull(updatedTuple.getLongMember(naksha.model.objects.StandardMembers.NextVersion, Int64(-1L)).let { if (it == Int64(-1L)) null else it }) // Both tuples share the collection's feature encoding (action lives in version bits). assertEquals(createdTuple.dataEncoding, updatedTuple.dataEncoding) - assertEquals(1, createdTuple.getIntMember(naksha.model.objects.StandardMembers.ChangeCount)) - assertEquals(2, updatedTuple.getIntMember(naksha.model.objects.StandardMembers.ChangeCount)) + assertEquals(1, createdTuple.getIntMember(naksha.model.objects.StandardMembers.ChangeCountXyz)) + assertEquals(2, updatedTuple.getIntMember(naksha.model.objects.StandardMembers.ChangeCountXyz)) assertEquals(createdTuple.getByteArray(naksha.model.objects.StandardMembers.Geometry), updatedTuple.getByteArray(naksha.model.objects.StandardMembers.Geometry)) - assertEquals(createdTuple.getStringMember(naksha.model.objects.StandardMembers.Tags), updatedTuple.getStringMember(naksha.model.objects.StandardMembers.Tags)) + assertEquals(createdTuple.getStringMember(naksha.model.objects.StandardMembers.XyzTags), updatedTuple.getStringMember(naksha.model.objects.StandardMembers.XyzTags)) assertNotEquals(createdTuple.feature, updatedTuple.feature) assertEquals(createdTuple.getByteArray(naksha.model.objects.StandardMembers.ReferencePoint), updatedTuple.getByteArray(naksha.model.objects.StandardMembers.ReferencePoint)) assertNull(createdTuple.toNakshaFeature()?.properties["new_attr"]) assertEquals("some_value", updatedTuple.toNakshaFeature()?.properties["new_attr"]) - assertEquals(createdTuple.getLongMember(naksha.model.objects.StandardMembers.CreatedAt)?.let { if (it == Int64(0L)) null else it } ?: createdTuple.getLongMember(naksha.model.objects.StandardMembers.UpdatedAt), updatedTuple.getLongMember(naksha.model.objects.StandardMembers.CreatedAt)?.let { if (it == Int64(0L)) null else it }) - assertNotEquals(updatedTuple.getLongMember(naksha.model.objects.StandardMembers.CreatedAt), updatedTuple.getLongMember(naksha.model.objects.StandardMembers.UpdatedAt)) - assertNull(createdTuple.getLongMember(naksha.model.objects.StandardMembers.CreatedAt)?.let { if (it == Int64(0L)) null else it }) - assertNotNull(createdTuple.getLongMember(naksha.model.objects.StandardMembers.UpdatedAt)) - assertEquals(createdTuple.getIntMember(naksha.model.objects.StandardMembers.HereTile), updatedTuple.getIntMember(naksha.model.objects.StandardMembers.HereTile)) + assertEquals(createdTuple.getLongMember(naksha.model.objects.StandardMembers.CreatedAtXyz)?.let { if (it == Int64(0L)) null else it } ?: createdTuple.getLongMember(naksha.model.objects.StandardMembers.XyzUpdatedAt), updatedTuple.getLongMember(naksha.model.objects.StandardMembers.CreatedAtXyz)?.let { if (it == Int64(0L)) null else it }) + assertNotEquals(updatedTuple.getLongMember(naksha.model.objects.StandardMembers.CreatedAtXyz), updatedTuple.getLongMember(naksha.model.objects.StandardMembers.XyzUpdatedAt)) + assertNull(createdTuple.getLongMember(naksha.model.objects.StandardMembers.CreatedAtXyz)?.let { if (it == Int64(0L)) null else it }) + assertNotNull(createdTuple.getLongMember(naksha.model.objects.StandardMembers.XyzUpdatedAt)) + assertEquals(createdTuple.getIntMember(naksha.model.objects.StandardMembers.HereTileXyz), updatedTuple.getIntMember(naksha.model.objects.StandardMembers.HereTileXyz)) assertEquals(Action.UPDATED, updatedTuple.tupleNumber.action) assertEquals(Action.CREATED, createdTuple.tupleNumber.action) - assertNotEquals(createdTuple.getLongMember(naksha.model.objects.StandardMembers.AuthorTimestamp), updatedTuple.getLongMember(naksha.model.objects.StandardMembers.AuthorTimestamp)) + assertNotEquals(createdTuple.getLongMember(naksha.model.objects.StandardMembers.XyzAuthorTimestamp), updatedTuple.getLongMember(naksha.model.objects.StandardMembers.XyzAuthorTimestamp)) } @Test diff --git a/here-naksha-lib-view/src/jvmTest/java/com/here/naksha/lib/view/ViewWriteSessionTests.java b/here-naksha-lib-view/src/jvmTest/java/com/here/naksha/lib/view/ViewWriteSessionTests.java index 6f203d995..61b01b785 100644 --- a/here-naksha-lib-view/src/jvmTest/java/com/here/naksha/lib/view/ViewWriteSessionTests.java +++ b/here-naksha-lib-view/src/jvmTest/java/com/here/naksha/lib/view/ViewWriteSessionTests.java @@ -110,7 +110,7 @@ void readAndWrite_UsingViewWriteSession() { NakshaFeature feature = response1.getFeatures().get(0); assertEquals(1d, ((PointCoord) feature.getGeometry().getCoordinates()).getLongitude()); assertTrue(feature.getProperties().containsKey("testProperty")); - assertEquals("test", feature.getProperties().get("testProperty").toString()); + assertEquals("test", feature.getProperties().getPath("testProperty").toString()); assertSame(Action.UPDATED, response1.getFeatureTupleList().get(0).tuple.version.action()); writeSession.commit(); @@ -124,7 +124,7 @@ void readAndWrite_UsingViewWriteSession() { NakshaFeature updatedFeature = list.get(0); assertEquals(1d, ((PointCoord) updatedFeature.getGeometry().getCoordinates()).getLongitude()); assertTrue(updatedFeature.getProperties().containsKey("testProperty")); - assertEquals("test", updatedFeature.getProperties().get("testProperty").toString()); + assertEquals("test", updatedFeature.getProperties().getPath("testProperty").toString()); } @Test diff --git a/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/HttpStorageProperties.java b/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/HttpStorageProperties.java index 7a993d218..a712d19f5 100644 --- a/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/HttpStorageProperties.java +++ b/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/HttpStorageProperties.java @@ -116,7 +116,7 @@ public void setMaxRetries(final @Nullable Integer maxRetries) { * By default: 'Content-Type: application/json' and 'Accept-Encoding: gzip' */ public @NotNull Map getHeaders() { - final Object raw = get(HEADERS); + final Object raw = getPath(HEADERS); if (raw instanceof HeaderMap) { return (HeaderMap) raw; } diff --git a/here-naksha-storage-http/src/jvmTest/java/com/here/naksha/storage/http/HttpStoragePropertiesTest.java b/here-naksha-storage-http/src/jvmTest/java/com/here/naksha/storage/http/HttpStoragePropertiesTest.java index 138b17a26..ca220620a 100644 --- a/here-naksha-storage-http/src/jvmTest/java/com/here/naksha/storage/http/HttpStoragePropertiesTest.java +++ b/here-naksha-storage-http/src/jvmTest/java/com/here/naksha/storage/http/HttpStoragePropertiesTest.java @@ -127,7 +127,7 @@ void shouldPreserveHeadersWhenBoxingStoragePropertiesFromJson() { final HttpStorageProperties properties = JvmBoxingUtil.box(storage.getProperties(), HttpStorageProperties.class); assertNotNull(properties); - final Object rawHeaders = properties.get("headers"); + final Object rawHeaders = properties.getPath("headers"); assertInstanceOf(Map.class, rawHeaders); assertFalse(rawHeaders instanceof HttpStorageProperties.HeaderMap); From 3ebe8e31295af8374f188ad8a712f1b5ad3b72a0 Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Fri, 12 Jun 2026 09:51:10 +0200 Subject: [PATCH 11/57] Add support for BookType, delete metadata and repalce with members. Fix tuple encoding and decoding. Signed-off-by: Alexander Lowey-Weber --- .../service/http/tasks/AbstractApiTask.java | 18 +- .../activitylog/ActivityLogEnhancer.java | 4 +- .../activitylog/ActivityLogHandler.java | 7 +- .../activitylog/ActivityLogHandlerTest.java | 32 +- .../namespaces/XyzActivityLog.java | 4 +- .../naksha/lib/handlers/util/HandlerUtil.java | 2 +- .../com/here/naksha/lib/hub/NakshaHub.java | 4 +- .../commonMain/kotlin/naksha/jbon/BookType.kt | 38 ++ .../commonMain/kotlin/naksha/jbon/HeapBook.kt | 27 +- .../commonMain/kotlin/naksha/jbon/IBook.kt | 33 +- .../kotlin/naksha/jbon/JbDictionary.kt | 16 +- .../kotlin/naksha/jbon/JbEncoder.kt | 6 +- .../kotlin/naksha/jbon/JbEncoder2.kt | 6 +- .../kotlin/naksha/jbon/JbCoreTest.kt | 8 +- .../kotlin/naksha/jbon/Jbon2MembersTest.kt | 6 +- .../commonMain/kotlin/naksha/model/Action.kt | 62 +-- .../kotlin/naksha/model/BinaryUtil.kt | 2 +- .../kotlin/naksha/model/FetchMode.kt | 12 +- .../commonMain/kotlin/naksha/model/Guid.kt | 7 +- .../kotlin/naksha/model/IMetadataArray.kt | 21 - .../kotlin/naksha/model/LibModel.kt | 4 +- .../kotlin/naksha/model/Metadata.kt | 277 ----------- .../commonMain/kotlin/naksha/model/Naksha.kt | 193 +------- .../kotlin/naksha/model/StorageTx.kt | 97 +--- .../commonMain/kotlin/naksha/model/Tuple.kt | 432 ++++++++++++------ .../kotlin/naksha/model/TupleNumber.kt | 31 +- .../naksha/model/TupleNumberBinaryArray.kt | 6 +- .../kotlin/naksha/model/TupleNumberList.kt | 2 +- .../commonMain/kotlin/naksha/model/Version.kt | 53 +-- .../commonMain/kotlin/naksha/model/XyzNs.kt | 142 +++--- .../kotlin/naksha/model/objects/Member.kt | 134 ++++++ .../kotlin/naksha/model/objects/MemberType.kt | 30 ++ .../naksha/model/objects/NakshaCollection.kt | 35 +- .../naksha/model/objects/NakshaFeature.kt | 4 +- .../kotlin/naksha/model/objects/NakshaTx.kt | 2 +- .../model/objects/NakshaTxCollection.kt | 12 +- .../naksha/model/objects/StandardMembers.kt | 4 +- .../kotlin/naksha/model/objects/XyzMembers.kt | 6 +- .../naksha/model/request/FeatureTuple.kt | 8 +- .../naksha/model/request/PropertyFilter.kt | 2 +- .../kotlin/naksha/model/request/Write.kt | 2 +- .../naksha/model/request/query/MetaColumn.kt | 2 +- .../kotlin/naksha/model/TupleNumberTest.kt | 24 +- .../kotlin/naksha/model/PropertyFilterTest.kt | 4 +- .../kotlin/naksha/psql/PgAdminMap.kt | 2 +- .../kotlin/naksha/psql/PgColumnRows.kt | 21 +- .../kotlin/naksha/psql/PgQueryWhereBuilder.kt | 6 +- .../commonMain/kotlin/naksha/psql/PgWrite.kt | 6 +- .../kotlin/naksha/psql/PgWriterDelete.kt | 4 +- .../kotlin/naksha/psql/PgWriterUpdate.kt | 8 +- .../kotlin/naksha/psql/PgWriterUpsert.kt | 8 +- .../kotlin/naksha/psql/AttachmentTest.kt | 16 +- .../kotlin/naksha/psql/DeleteFeatureBase.kt | 14 +- .../kotlin/naksha/psql/HistoryUuidTest.kt | 13 +- .../kotlin/naksha/psql/InsertFeatureTest.kt | 6 +- .../kotlin/naksha/psql/ReadFeaturesAll.kt | 4 +- .../naksha/psql/ReadFeaturesByMetadataTest.kt | 4 +- .../naksha/psql/ReadFeaturesByOtherTns.kt | 2 +- .../kotlin/naksha/psql/ReadHistoryTest.kt | 34 +- .../kotlin/naksha/psql/ReadOrderedTest.kt | 3 +- .../naksha/psql/RecreateAfterDeleteTest.kt | 27 +- .../naksha/psql/TupleNumberPersistenceTest.kt | 4 +- .../kotlin/naksha/psql/UpdateFeatureTest.kt | 38 +- .../kotlin/naksha/psql/UpsertFeatureTest.kt | 10 +- .../com/here/naksha/lib/view/ViewTest.java | 10 +- .../lib/view/ViewWriteSessionTests.java | 8 +- 66 files changed, 923 insertions(+), 1146 deletions(-) create mode 100644 here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/BookType.kt delete mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/IMetadataArray.kt delete mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Metadata.kt diff --git a/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/AbstractApiTask.java b/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/AbstractApiTask.java index 99981982f..03a4a6f9b 100644 --- a/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/AbstractApiTask.java +++ b/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/AbstractApiTask.java @@ -211,9 +211,9 @@ private static String getIterateHandleAsString( return validatedErrorResponse; } else if (response instanceof SuccessResponse successResponse) { final Map> featureMap = postProcessedFeaturesByAction(successResponse, postProcessor); - final List insertedFeatures = featureMap.get(Action.CREATED); - final List updatedFeatures = featureMap.get(Action.UPDATED); - final List deletedFeatures = featureMap.get(Action.DELETED); + final List insertedFeatures = featureMap.get(Action.CREATE); + final List updatedFeatures = featureMap.get(Action.UPDATE); + final List deletedFeatures = featureMap.get(Action.DELETE); // extract violations if available List violations = null; if (successResponse instanceof ContextXyzFeatureResponse cr) { @@ -310,18 +310,18 @@ private static Map> postProcessedFeaturesByAction( for (NakshaFeature feature : features) { postProcessor.postProcess(feature); final Action action = feature.getProperties().getXyz().getAction(); - if (action == Action.CREATED) { + if (action == Action.CREATE) { insertedFeatures.add(feature); - } else if (action == Action.UPDATED) { + } else if (action == Action.UPDATE) { updatedFeatures.add(feature); - } else if (action == Action.DELETED) { + } else if (action == Action.DELETE) { deletedFeatures.add(feature); } } final Map> featuresByAction = new HashMap<>(); - featuresByAction.put(Action.CREATED, insertedFeatures); - featuresByAction.put(Action.UPDATED, updatedFeatures); - featuresByAction.put(Action.DELETED, deletedFeatures); + featuresByAction.put(Action.CREATE, insertedFeatures); + featuresByAction.put(Action.UPDATE, updatedFeatures); + featuresByAction.put(Action.DELETE, deletedFeatures); return featuresByAction; } } diff --git a/here-naksha-handler-activitylog/src/jvmMain/java/com/here/naksha/handler/activitylog/ActivityLogEnhancer.java b/here-naksha-handler-activitylog/src/jvmMain/java/com/here/naksha/handler/activitylog/ActivityLogEnhancer.java index eb2ddff77..5841ea39e 100644 --- a/here-naksha-handler-activitylog/src/jvmMain/java/com/here/naksha/handler/activitylog/ActivityLogEnhancer.java +++ b/here-naksha-handler-activitylog/src/jvmMain/java/com/here/naksha/handler/activitylog/ActivityLogEnhancer.java @@ -85,9 +85,9 @@ private static void propagateVirtualPuuid(@Nullable XyzNs xyzNamespace, @NotNull private static @Nullable JsonNode calculateDiff( @Nullable Action action, @NotNull NakshaFeature newFeature, @Nullable NakshaFeature oldFeature) { - if (action == null || Action.CREATED.equals(action) || Action.DELETED.equals(action)) { + if (action == null || Action.CREATE.equals(action) || Action.DELETE.equals(action)) { return null; - } else if (Action.UPDATED.equals(action)) { + } else if (Action.UPDATE.equals(action)) { if (oldFeature == null) { logger.warn( "Unable to calculate reversePatch for, missing predecessor for feature with uuid: {}, returning null", diff --git a/here-naksha-handler-activitylog/src/jvmMain/java/com/here/naksha/handler/activitylog/ActivityLogHandler.java b/here-naksha-handler-activitylog/src/jvmMain/java/com/here/naksha/handler/activitylog/ActivityLogHandler.java index c3fee0184..ffe16f250 100644 --- a/here-naksha-handler-activitylog/src/jvmMain/java/com/here/naksha/handler/activitylog/ActivityLogHandler.java +++ b/here-naksha-handler-activitylog/src/jvmMain/java/com/here/naksha/handler/activitylog/ActivityLogHandler.java @@ -58,7 +58,6 @@ import naksha.model.request.query.AnyOp; import naksha.model.request.query.MetaColumn; import naksha.model.request.query.MetaQuery; -import naksha.psql.PgLogLevel; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -152,7 +151,7 @@ private List featuresWithPredecessors(CollectedFeatures * Such tombstones represent features that never participated in activity logging and should be excluded. */ private static boolean isOrphanTombstone(FeatureWithPredecessor fwp) { - return Action.DELETED.equals(fwp.feature().getProperties().getXyz().getAction()) + return Action.DELETE.equals(fwp.feature().getProperties().getXyz().getAction()) && fwp.oldFeature() == null; } @@ -172,7 +171,7 @@ private ReadFeatures featuresWhereNextVersionIsOneOf(List tupleNumb // next_version is a plain int8 column, so we pass an Int64[] of the version values. Int64[] versions = new Int64[tupleNumbers.size()]; for (int i = 0; i < tupleNumbers.size(); i++) { - versions[i] = tupleNumbers.get(i).version.txn; + versions[i] = tupleNumbers.get(i).version.value; } MetaQuery nextVersionQuery = new MetaQuery(MetaColumn.nextVersion(), AnyOp.IS_ANY_OF, versions); ReadFeatures requestPredecessors = new ReadFeatures(); @@ -250,7 +249,7 @@ private static XyzNs xyzNs(NakshaFeature feature) { * nuuid (ie UPDATE & DELETE) */ private static String nuuidOrNullIfDeleted(XyzNs xyzNs) { - if (Action.DELETED.equals(xyzNs.getAction())) { + if (Action.DELETE.equals(xyzNs.getAction())) { return null; } else { return xyzNs.getNuuid(); diff --git a/here-naksha-handler-activitylog/src/jvmTest/java/com/here/naksha/handler/activitylog/ActivityLogHandlerTest.java b/here-naksha-handler-activitylog/src/jvmTest/java/com/here/naksha/handler/activitylog/ActivityLogHandlerTest.java index 41c98497e..ca3dae438 100644 --- a/here-naksha-handler-activitylog/src/jvmTest/java/com/here/naksha/handler/activitylog/ActivityLogHandlerTest.java +++ b/here-naksha-handler-activitylog/src/jvmTest/java/com/here/naksha/handler/activitylog/ActivityLogHandlerTest.java @@ -162,15 +162,15 @@ void shouldImmediatelySucceedOnWriteCollection() { void shouldComposeActivityFeatures() throws Exception { // Given: features uuid String featureId = "featureId"; - Guid initialUuid = guid(featureId, Version.now(new JvmInt64(1),Action.CREATED)); - Guid newUuid = guid(featureId, Version.now(new JvmInt64(2),Action.UPDATED)); + Guid initialUuid = guid(featureId, Version.now(new JvmInt64(1),Action.CREATE)); + Guid newUuid = guid(featureId, Version.now(new JvmInt64(2),Action.UPDATE)); // And: old version of feature NakshaFeature oldFeature = nakshaFeature(featureId) .withUuid(initialUuid.toString()) .withPuuid(null) .withNuuid(newUuid.toString()) - .withAction(Action.CREATED) + .withAction(Action.CREATE) .withCustomProperties( Map.of( "op", "old feature", @@ -183,7 +183,7 @@ void shouldComposeActivityFeatures() throws Exception { .withUuid(newUuid.toString()) .withPuuid(initialUuid.toString()) .withNuuid(null) - .withAction(Action.UPDATED) + .withAction(Action.UPDATE) .withCustomProperties(Map.of( "op", "new feature", "magicBoolean", true @@ -205,7 +205,7 @@ void shouldComposeActivityFeatures() throws Exception { singleFeature -> singleFeature .hasId(uuid(newFeature)) .hasActivityLogId(featureId) - .hasAction(Action.UPDATED.toString()) + .hasAction(Action.UPDATE.toString()) .hasReversePatch(""" { "add": 1, @@ -236,7 +236,7 @@ void shouldComposeActivityFeatures() throws Exception { void shouldNotCalculateReversePatchAfterCreation() throws Exception { // Given String featureId = "featureId"; - Guid createdGuid = guid(featureId, Version.now(new JvmInt64(0), Action.CREATED)); + Guid createdGuid = guid(featureId, Version.now(new JvmInt64(0), Action.CREATE)); // And: space storage that returns only some feature with 'CREATE' action configureSpaceStorage( @@ -244,7 +244,7 @@ void shouldNotCalculateReversePatchAfterCreation() throws Exception { nakshaFeature(featureId) .withUuid(createdGuid.toString()) .withPuuid(null) - .withAction(Action.CREATED) + .withAction(Action.CREATE) .build()) ), requestForMissingPredecessorsReturns(emptyList()) @@ -256,7 +256,7 @@ void shouldNotCalculateReversePatchAfterCreation() throws Exception { // Then: result does not bear any reverse patch assertThatResult(result) .hasActivityFeatures(feature -> feature - .hasAction(Action.CREATED.toString()) + .hasAction(Action.CREATE.toString()) .hasId(createdGuid.toString()) .hasActivityLogId(featureId) .hasReversePatch(null) @@ -269,8 +269,8 @@ void shouldNotCalculateDiffAfterDeletion() throws Exception { String featureId = "featureId"; Timestamp ts0 = Timestamp.fromMillis(T0); Timestamp ts1 = Timestamp.fromMillis(T1); - Version createdVersion = Version.auto(ts0.getYear(), ts0.getMonth(), ts0.getDay(), new JvmInt64(0), Action.CREATED); - Version deletedVersion = Version.auto(ts1.getYear(), ts1.getMonth(), ts1.getDay(), new JvmInt64(1), Action.DELETED); + Version createdVersion = Version.auto(ts0.getYear(), ts0.getMonth(), ts0.getDay(), new JvmInt64(0), Action.CREATE); + Version deletedVersion = Version.auto(ts1.getYear(), ts1.getMonth(), ts1.getDay(), new JvmInt64(1), Action.DELETE); Guid createdGuid = guid(featureId, createdVersion); Guid deletedGuid = guid(featureId, deletedVersion); @@ -279,8 +279,8 @@ void shouldNotCalculateDiffAfterDeletion() throws Exception { initialHistoryAwareRequestReturns(List.of( nakshaFeature(featureId) .withUuid(deletedGuid.toString()) - .withNuuid(null) - .withAction(Action.DELETED) + .withPuuid(createdGuid.toString()) + .withAction(Action.DELETE) .withCreatedAt(T0) .withUpdatedAt(T1) .build() @@ -288,8 +288,8 @@ void shouldNotCalculateDiffAfterDeletion() throws Exception { requestForMissingPredecessorsReturns(List.of( nakshaFeature("featureId") .withUuid(createdGuid.toString()) - .withNuuid(deletedGuid.toString()) - .withAction(Action.CREATED) + .withPuuid(null) + .withAction(Action.CREATE) .withCreatedAt(T0) .withUpdatedAt(T0) .build() @@ -303,7 +303,7 @@ void shouldNotCalculateDiffAfterDeletion() throws Exception { assertThatResult(result) .hasActivityFeatures( feature -> feature - .hasAction(Action.DELETED.toString()) + .hasAction(Action.DELETE.toString()) .hasId(deletedGuid.toString()) .hasActivityLogId(featureId) .hasReversePatch(null) @@ -493,7 +493,7 @@ private boolean containsNextVersionMetaQuery(ReadFeatures readFeatures, TupleNum } if (versions.size() != expectedTns.length) return false; return Arrays.stream(expectedTns) - .map(tn -> tn.version.txn) + .map(tn -> tn.version.value) .allMatch(expected -> versions.stream().anyMatch(expected::equals)); } diff --git a/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/geojson/implementation/namespaces/XyzActivityLog.java b/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/geojson/implementation/namespaces/XyzActivityLog.java index dcd976d90..ae423d402 100644 --- a/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/geojson/implementation/namespaces/XyzActivityLog.java +++ b/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/geojson/implementation/namespaces/XyzActivityLog.java @@ -97,12 +97,12 @@ public void setAction(@NotNull Action action) { } public boolean isDeleted() { - return Action.DELETED.toString().equals(getAction()); + return Action.DELETE.toString().equals(getAction()); } public void setDeleted(boolean deleted) { if (deleted) { - setAction(Action.DELETED); + setAction(Action.DELETE); } } diff --git a/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/util/HandlerUtil.java b/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/util/HandlerUtil.java index 43a98b96e..4f7f0f3cf 100644 --- a/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/util/HandlerUtil.java +++ b/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/util/HandlerUtil.java @@ -51,7 +51,7 @@ private HandlerUtil() {} final @Nullable List violations) { for (final NakshaFeature feature : features) { - feature.getProperties().getXyz().setRaw(XyzNs.ACTION, Action.UPDATED); + feature.getProperties().getXyz().setRaw(XyzNs.ACTION, Action.UPDATE); } // Create ContextResult with cursor, context and violations final ContextXyzFeatureResponse ctxResult = new ContextXyzFeatureResponse(); diff --git a/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/NakshaHub.java b/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/NakshaHub.java index 2a9c2a708..a91f54222 100644 --- a/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/NakshaHub.java +++ b/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/NakshaHub.java @@ -23,7 +23,7 @@ import static com.here.naksha.lib.core.HubInternalIdentifiers.EVENT_HANDLERS; import static com.here.naksha.lib.core.HubInternalIdentifiers.STORAGES; import static com.here.naksha.lib.core.exceptions.UncheckedException.unchecked; -import static naksha.model.Action.CREATED; +import static naksha.model.Action.CREATE; import static naksha.model.NakshaContext.currentContext; import static naksha.model.util.RequestHelper.createFeatureRequest; import static naksha.model.util.RequestHelper.readFeaturesByIdRequest; @@ -235,7 +235,7 @@ private void createAdminCollections(NakshaContext nakshaContext, String adminMap NakshaFeatureList createdCollections = successResponse.getFeatures(); for (NakshaFeature createdCollection : createdCollections) { if (Objects.equals( - CREATED.getValue(), + CREATE.getValue(), createdCollection.getProperties().getXyz().getAction())) { logger.info("Collection {} successfully created.", createdCollection.getId()); } diff --git a/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/BookType.kt b/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/BookType.kt new file mode 100644 index 000000000..d1bd69639 --- /dev/null +++ b/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/BookType.kt @@ -0,0 +1,38 @@ +@file:OptIn(ExperimentalJsExport::class) + +package naksha.jbon + +import kotlin.js.ExperimentalJsExport +import kotlin.js.JsExport +import kotlin.jvm.JvmField + +/** + * The type of the book as defined in JBON2 specification. + * @since 3.0 + */ +@JsExport +enum class BookType( + /** + * The type-number as being used in JBON2 encoding. + * @since 3.0 + */ + @JvmField val typeNumber: Int +) { + /** + * Local book, embedded into the `Tuple` binary. + * @since 3.0 + */ + LOCAL_BOOK(0), + + /** + * Members book, provided by the storage, members extrapolated into dedicated storage places. Can be embedded into the `Tuple`. + * @since 3.0 + */ + MEMBER_BOOK(1), + + /** + * Global book, persisted in the storage, shared between many `Tuple` to reduce size, compression utility. + * @since 3.0 + */ + GLOBAL_BOOK(2), +} \ No newline at end of file diff --git a/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/HeapBook.kt b/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/HeapBook.kt index a3d44249c..5563ccb98 100644 --- a/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/HeapBook.kt +++ b/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/HeapBook.kt @@ -2,14 +2,21 @@ package naksha.jbon +import naksha.base.Int64 import kotlin.js.JsExport +import kotlin.jvm.JvmOverloads /** * A mutable [IBook] implementation on the Java _HEAP_. * @since 3.0.0 */ @JsExport -class HeapBook : IBook { +class HeapBook( + override var bookType: BookType +) : IBook { + override var databaseNumber: Int64? = null + override var featureNumber: Int64? = null + private val _names = mutableListOf() private val _values = mutableListOf() private val _nameIndex = mutableMapOf() @@ -21,9 +28,9 @@ class HeapBook : IBook { override fun get(index: Int): Any? = _values.getOrNull(index) - override fun indexOf(string: String): Int = _nameIndex[string] ?: -1 + override fun indexOfString(string: String): Int = _nameIndex[string] ?: -1 - override fun stringAt(index: Int): String? { + override fun getStringAt(index: Int): String? { val name = _names.getOrNull(index) if (name != null) return name return _values.getOrNull(index) as? String @@ -31,7 +38,7 @@ class HeapBook : IBook { override fun hasNames(): Boolean = true - override fun getIndexOf(name: String): Int = _nameIndex[name] ?: -1 + override fun indexOfName(name: String): Int = _nameIndex[name] ?: -1 override fun getNameAt(index: Int): String? = _names.getOrNull(index) @@ -42,15 +49,21 @@ class HeapBook : IBook { return _values.getOrNull(i) } - override fun find(hash: Int): List = emptyList() + override fun getAllWithHash(hash: Int): List = emptyList() /** * Creates a shallow copy of this dictionary. + * @param bookType The [BookType] of the copy, if different. + * @param databaseNumber The database-number of the copy, if different. + * @param featureNumber The feature-number of the copy, if different. * @return a new [HeapBook] with the same entries. * @since 3.0.0 */ - fun copy(): HeapBook { - val c = HeapBook() + @JvmOverloads + fun copy(bookType: BookType = this.bookType, databaseNumber: Int64? = this.databaseNumber, featureNumber: Int64? = this.featureNumber): HeapBook { + val c = HeapBook(bookType) + c.databaseNumber = this.databaseNumber + c.featureNumber = this.featureNumber for (i in _names.indices) { c.put(_names[i], _values[i]) } diff --git a/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/IBook.kt b/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/IBook.kt index 8f3a78657..6d737bd48 100644 --- a/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/IBook.kt +++ b/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/IBook.kt @@ -2,6 +2,7 @@ package naksha.jbon +import naksha.base.Int64 import kotlin.js.JsExport /** @@ -20,7 +21,7 @@ import kotlin.js.JsExport @JsExport interface IBook { /** - * The identifier of the dictionary; if any. + * The optional custom identifier of the book; if any. * @since 3.0.0 */ val id: String? @@ -31,6 +32,24 @@ interface IBook { */ val length: Int + /** + * The book-type. + * @since 3.0 + */ + val bookType: BookType + + /** + * The database-number of the book, if this is a global book stored in a database. + * @since 3.0 + */ + val databaseNumber: Int64? + + /** + * The feature-number of the book, if this is a global book stored in a database. + * @since 3.0 + */ + val featureNumber: Int64? + /** * Returns the element at the given index. If no such index exists, returns _null_. * @param index the index to query. @@ -46,7 +65,7 @@ interface IBook { * @return the index of the given string or -1, if the string is not part of the dictionary. * @since 3.0.0 */ - fun indexOf(string: String): Int + fun indexOfString(string: String): Int /** * Returns the string at the given index. If no such index exists, returns _null_. @@ -54,7 +73,7 @@ interface IBook { * @return the string or _null_. * @since 3.0.0 */ - fun stringAt(index: Int): String? + fun getStringAt(index: Int): String? /** * Returns `true` if this dictionary contains a `memberNames` section — a parallel array @@ -70,7 +89,7 @@ interface IBook { * @return the index, or `-1`. * @since 3.0.0 */ - fun getIndexOf(name: String): Int = -1 + fun indexOfName(name: String): Int = -1 /** * Returns the name at the given index from the `memberNames` section, or `null` if no @@ -89,7 +108,7 @@ interface IBook { fun namesLength(): Int = 0 /** - * Returns the value associated with the given name by looking up the index via [getIndexOf] + * Returns the value associated with the given name by looking up the index via [indexOfName] * and then reading the value via [get]. Returns _null_ when the name is not found or the * index maps to a _null_ slot. * @param name the member name to look up. @@ -97,7 +116,7 @@ interface IBook { * @since 3.0.0 */ fun getByName(name: String): Any? { - val i = getIndexOf(name) + val i = indexOfName(name) return if (i < 0) null else get(i) } @@ -107,5 +126,5 @@ interface IBook { * @return a list of all entries that match the given hash. * @since 3.0.0 */ - fun find(hash: Int): List + fun getAllWithHash(hash: Int): List } \ No newline at end of file diff --git a/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/JbDictionary.kt b/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/JbDictionary.kt index 9e1c251d1..07fa354c2 100644 --- a/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/JbDictionary.kt +++ b/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/JbDictionary.kt @@ -1,10 +1,16 @@ +@file:OptIn(ExperimentalJsExport::class) + package naksha.jbon +import naksha.base.Int64 +import kotlin.js.ExperimentalJsExport import kotlin.js.JsExport -// TODO: Implement IBook +@JsExport +class JbDictionary(override val bookType: BookType = BookType.LOCAL_BOOK,) : JbStructDecoder(), IBook { + override var databaseNumber: Int64? = null + override var featureNumber: Int64? = null -class JbDictionary : JbStructDecoder(), IBook { /** * Cached ID of the dictionary, if any. */ @@ -110,11 +116,11 @@ class JbDictionary : JbStructDecoder(), IBook { return content[index] } - override fun stringAt(index: Int): String? { + override fun getStringAt(index: Int): String? { TODO("Not yet implemented") } - override fun find(hash: Int): List { + override fun getAllWithHash(hash: Int): List { TODO("Not yet implemented") } @@ -123,7 +129,7 @@ class JbDictionary : JbStructDecoder(), IBook { * as a side effect invoke [loadAll]. * @return The index of the given string or -1. */ - override fun indexOf(string: String): Int { + override fun indexOfString(string: String): Int { loadAll() val content = this.content val length = content.size diff --git a/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/JbEncoder.kt b/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/JbEncoder.kt index d68b5a372..6a8276d5d 100644 --- a/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/JbEncoder.kt +++ b/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/JbEncoder.kt @@ -533,7 +533,7 @@ open class JbEncoder(var global: IBook? = null) : Binary() { val isGlobal: Boolean var index = -1 if (global != null) { - index = global.indexOf(subString) + index = global.indexOfString(subString) if (index < 0) { // Let's try for URNs, which are for example "urn:here:mom:Topology:123456". // In that case, "urn:here:mom:Topology:" is always the same. @@ -547,7 +547,7 @@ open class JbEncoder(var global: IBook? = null) : Binary() { sb.clear() JbDecoder.readSubstring(this, wordStart, reversePos, sb) val prefix = sb.toString() - index = global.indexOf(prefix) + index = global.indexOfString(prefix) if (index >= 0) { // Found the prefix in the global dict, now we encode the prefix as reference. pos = encodeStringRef(wordStart, index, true, ADD_COLON) @@ -775,7 +775,7 @@ open class JbEncoder(var global: IBook? = null) : Binary() { val global = this.global var index: Int if (global != null) { - index = global.indexOf(key) + index = global.indexOfString(key) if (index >= 0) { encodeRef(index, true) return start diff --git a/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/JbEncoder2.kt b/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/JbEncoder2.kt index 05ee66bb6..4a5ed9b03 100644 --- a/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/JbEncoder2.kt +++ b/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/JbEncoder2.kt @@ -512,7 +512,7 @@ open class JbEncoder2(var global: IBook? = null) : Binary() { var index = -1 var book = JB2_REF_BOOK_LOCAL if (global != null) { - index = global.indexOf(subString) + index = global.indexOfString(subString) if (index < 0) { // Try URN-style prefixes ending in a colon. var reversePos = pos - 1 @@ -523,7 +523,7 @@ open class JbEncoder2(var global: IBook? = null) : Binary() { sb.clear() JbDecoder2.readSubstring(this, wordStart, reversePos, sb) val prefix = sb.toString() - val pidx = global.indexOf(prefix) + val pidx = global.indexOfString(prefix) if (pidx >= 0) { pos = encodeStringRef(wordStart, pidx, JB2_REF_BOOK_GLOBAL, JB2_ADD_COLON) i = reversePos + 1 @@ -672,7 +672,7 @@ open class JbEncoder2(var global: IBook? = null) : Binary() { val start = end val global = this.global if (global != null) { - val index = global.indexOf(key) + val index = global.indexOfString(key) if (index >= 0) { encodeRef(index, JB2_REF_BOOK_GLOBAL) return start diff --git a/here-naksha-lib-jbon/src/commonTest/kotlin/naksha/jbon/JbCoreTest.kt b/here-naksha-lib-jbon/src/commonTest/kotlin/naksha/jbon/JbCoreTest.kt index fcfd79338..cccd3d893 100644 --- a/here-naksha-lib-jbon/src/commonTest/kotlin/naksha/jbon/JbCoreTest.kt +++ b/here-naksha-lib-jbon/src/commonTest/kotlin/naksha/jbon/JbCoreTest.kt @@ -493,10 +493,10 @@ class JbCoreTest { assertEquals(dictId, dict.id) assertEquals("foo", dict.get(0)) assertEquals("bar", dict.get(1)) - assertEquals(0, dict.indexOf("foo")) - assertEquals(1, dict.indexOf("bar")) - assertEquals(-1, dict.indexOf(dictId)) - assertEquals(-1, dict.indexOf("notFound")) + assertEquals(0, dict.indexOfString("foo")) + assertEquals(1, dict.indexOfString("bar")) + assertEquals(-1, dict.indexOfString(dictId)) + assertEquals(-1, dict.indexOfString("notFound")) } @Test diff --git a/here-naksha-lib-jbon/src/jvmTest/kotlin/naksha/jbon/Jbon2MembersTest.kt b/here-naksha-lib-jbon/src/jvmTest/kotlin/naksha/jbon/Jbon2MembersTest.kt index ca08aaf79..3a16ba4de 100644 --- a/here-naksha-lib-jbon/src/jvmTest/kotlin/naksha/jbon/Jbon2MembersTest.kt +++ b/here-naksha-lib-jbon/src/jvmTest/kotlin/naksha/jbon/Jbon2MembersTest.kt @@ -32,9 +32,9 @@ class Jbon2MembersTest { override val id: String? = null override val length: Int get() = entries.size override fun get(index: Int): Any? = entries.getOrNull(index) - override fun indexOf(string: String): Int = entries.indexOfFirst { it == string } - override fun stringAt(index: Int): String? = entries.getOrNull(index)?.toString() - override fun find(hash: Int): List = emptyList() + override fun indexOfString(string: String): Int = entries.indexOfFirst { it == string } + override fun getStringAt(index: Int): String? = entries.getOrNull(index)?.toString() + override fun getAllWithHash(hash: Int): List = emptyList() } // ----------------------------------------------------------------------- diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Action.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Action.kt index 03cb79ed3..2a3b44659 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Action.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Action.kt @@ -8,13 +8,13 @@ import kotlin.jvm.JvmStatic import kotlin.reflect.KClass /** - * An enumeration about the action that actually was performed for a feature in a storage, being [CREATED], [UPDATED], or [DELETED]. + * An enumeration about the action that actually was performed for a feature in a storage, being [CREATE], [UPDATE], or [DELETE]. * - * The numeric [intValue] corresponds to the lower two bits of a [Version.txn]: - * - `0` ([CREATED]) — the feature was created in this version. - * - `1` ([UPDATED]) — the feature was updated in this version. - * - `2` ([DELETED]) — the feature was deleted in this version. - * - `3` ([VERSION]) — both action bits are set; used as a sentinel to indicate that the [Version.txn] value itself + * The numeric [intValue] corresponds to the lower two bits of a [Version.value]: + * - `0` ([CREATE]) — the feature was created in this version. + * - `1` ([UPDATE]) — the feature was updated in this version. + * - `2` ([DELETE]) — the feature was deleted in this version. + * - `3` ([VERSION]) — both action bits are set; used as a sentinel to indicate that the [Version.value] value itself * is being used as a version reference rather than encoding a state-change action. * * @since 1.0.0 @@ -29,17 +29,17 @@ class Action : JsEnum() { @Suppress("MemberVisibilityCanBePrivate") companion object Action_C { - internal const val CREATED_VALUE = 0 - internal const val CREATED_STRING = "CREATE" - internal const val CREATED_SHORT = "c" + internal const val CREATE_VALUE = 0 + internal const val CREATE_STRING = "CREATE" + internal const val CREATE_SHORT = "c" - internal const val UPDATED_VALUE = 1 - internal const val UPDATED_STRING = "UPDATE" - internal const val UPDATED_SHORT = "u" + internal const val UPDATE_VALUE = 1 + internal const val UPDATE_STRING = "UPDATE" + internal const val UPDATE_SHORT = "u" - internal const val DELETED_VALUE = 2 - internal const val DELETED_STRING = "DELETE" - internal const val DELETED_SHORT = "d" + internal const val DELETE_VALUE = 2 + internal const val DELETE_STRING = "DELETE" + internal const val DELETE_SHORT = "d" internal const val VERSION_VALUE = 3 internal const val VERSION_STRING = "VERSION" @@ -51,9 +51,9 @@ class Action : JsEnum() { */ @JsStatic @JvmField - val CREATED = defIgnoreCase(Action::class, CREATED_STRING) { self -> - self.intValue = CREATED_VALUE - self.shortId = CREATED_SHORT + val CREATE = defIgnoreCase(Action::class, CREATE_STRING) { self -> + self.intValue = CREATE_VALUE + self.shortId = CREATE_SHORT } /** @@ -62,9 +62,9 @@ class Action : JsEnum() { */ @JsStatic @JvmField - val UPDATED = defIgnoreCase(Action::class, UPDATED_STRING) { self -> - self.intValue = UPDATED_VALUE - self.shortId = UPDATED_SHORT + val UPDATE = defIgnoreCase(Action::class, UPDATE_STRING) { self -> + self.intValue = UPDATE_VALUE + self.shortId = UPDATE_SHORT } /** @@ -73,13 +73,13 @@ class Action : JsEnum() { */ @JsStatic @JvmField - val DELETED = defIgnoreCase(Action::class, DELETED_STRING) { self -> - self.intValue = DELETED_VALUE - self.shortId = DELETED_SHORT + val DELETE = defIgnoreCase(Action::class, DELETE_STRING) { self -> + self.intValue = DELETE_VALUE + self.shortId = DELETE_SHORT } /** - * Both action bits are set (`3`). Used as a sentinel to signal that the [Version.txn] value + * Both action bits are set (`3`). Used as a sentinel to signal that the [Version.value] value * is a version reference rather than a state-change action. Also returned by [fromValue] for * any unrecognised integer value. * @since 1.0.0 @@ -93,16 +93,16 @@ class Action : JsEnum() { // Full-name and short-name lookup map. private val FROM_STRING = mapOf( - Pair(CREATED_STRING, CREATED), Pair(CREATED_SHORT, CREATED), - Pair(UPDATED_STRING, UPDATED), Pair(UPDATED_SHORT, UPDATED), - Pair(DELETED_STRING, DELETED), Pair(DELETED_SHORT, DELETED), + Pair(CREATE_STRING, CREATE), Pair(CREATE_SHORT, CREATE), + Pair(UPDATE_STRING, UPDATE), Pair(UPDATE_SHORT, UPDATE), + Pair(DELETE_STRING, DELETE), Pair(DELETE_SHORT, DELETE), Pair(VERSION_STRING, VERSION), Pair(VERSION_SHORT, VERSION), ) private val FROM_VALUE = mapOf( - Pair(CREATED_VALUE, CREATED), - Pair(UPDATED_VALUE, UPDATED), - Pair(DELETED_VALUE, DELETED), + Pair(CREATE_VALUE, CREATE), + Pair(UPDATE_VALUE, UPDATE), + Pair(DELETE_VALUE, DELETE), Pair(VERSION_VALUE, VERSION), ) diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/BinaryUtil.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/BinaryUtil.kt index 9692937c1..ca15353e6 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/BinaryUtil.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/BinaryUtil.kt @@ -211,7 +211,7 @@ class BinaryUtil private constructor() { pos += 8 } else fn = featureNumber val txn = dataview_get_int64(view, pos) - return TupleNumber(sn, mn, cn, fn, Version(txn)) + return TupleNumber(sn, mn, cn, fn, txn) } } } \ No newline at end of file diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/FetchMode.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/FetchMode.kt index 7c6ef9a5c..f0e82e8c5 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/FetchMode.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/FetchMode.kt @@ -20,37 +20,37 @@ inline fun FetchMode(vararg modes: Int): FetchMode { } /** - * Set the [members][Tuple.members] bit. + * Set the [members][Tuple.membersBook] bit. * @since 3.0.0 */ inline fun FetchMode.withMeta(): Int = this or META_BIT /** - * Clear the [members][Tuple.members] bit. + * Clear the [members][Tuple.membersBook] bit. * @since 3.0.0 */ inline fun FetchMode.noMeta(): Int = this and META_CLEAR /** - * Test if the [members][Tuple.members] bit is set. + * Test if the [members][Tuple.membersBook] bit is set. * @since 3.0.0 */ inline fun FetchMode.fetchMeta(): Boolean = (this and META_BIT) == META_BIT /** - * Set the _feature_ bit (covers [feature][Tuple.feature] and tags). + * Set the _feature_ bit (covers [feature][Tuple.jbonBytes] and tags). * @since 3.0.0 */ inline fun FetchMode.withFeature(): Int = this or FEATURE_BIT /** - * Clear the _feature_ bit (covers [feature][Tuple.feature] and tags). + * Clear the _feature_ bit (covers [feature][Tuple.jbonBytes] and tags). * @since 3.0.0 */ inline fun FetchMode.noFeature(): Int = this and FEATURE_CLEAR /** - * Test if the _feature_ bit is set (which covers [feature][Tuple.feature] and tags). + * Test if the _feature_ bit is set (which covers [feature][Tuple.jbonBytes] and tags). * @since 3.0.0 */ inline fun FetchMode.fetchFeature(): Boolean = (this and FEATURE_BIT) == FEATURE_BIT diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Guid.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Guid.kt index ee405a7e0..6fce9329e 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Guid.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Guid.kt @@ -17,7 +17,7 @@ import kotlin.jvm.JvmStatic * * When [toString] is invoked, it is serialized into a [URN](https://datatracker.ietf.org/doc/html/rfc8141). It can be restored from a [URN](https://datatracker.ietf.org/doc/html/rfc8141) using the static helper [fromString]. The format of the URN is: * - * `urn:naksha:guid:{feature-id}:{storage-number}:{map-number}:{collection-number}:{feature-number}:{version}` + * `urn:naksha:guid:{feature-id}:{database-number}:{catalog-number}:{collection-number}:{feature-number}:{version}` * * The [Guid] is exposed through the [XYZ namespace][XyzNs] in the [uuid][XyzNs.uuid] property. * @since 3.0.0 @@ -119,9 +119,6 @@ data class Guid( */ @JsStatic @JvmStatic - fun fromTuple(tuple: Tuple): Guid { - val id = tuple.getStringMember(StandardMembers.Id) ?: tuple.featureNumber.toString() - return Guid(id, tuple.tupleNumber) - } + fun fromTuple(tuple: Tuple): Guid = Guid(tuple.id, tuple.tupleNumber) } } \ No newline at end of file diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/IMetadataArray.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/IMetadataArray.kt deleted file mode 100644 index 4f292193d..000000000 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/IMetadataArray.kt +++ /dev/null @@ -1,21 +0,0 @@ -@file:Suppress("OPT_IN_USAGE") - -package naksha.model - -import kotlin.js.JsExport - -/** - * An interface to an array of [Metadata]. - */ -@JsExport -interface IMetadataArray { - /** - * The amount of entries. - */ - val size: Int - - /** - * Returns the [Metadata] at the given index. - */ - operator fun get(index: Int): Metadata -} \ No newline at end of file diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/LibModel.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/LibModel.kt index 38fb53413..970c901ed 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/LibModel.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/LibModel.kt @@ -77,7 +77,7 @@ val LATENCY_MEMORY = Int64(0) val DEFAULT_DATA_ENCODING = Naksha.DEFAULT_DATA_ENCODING /** - * The [members][Tuple.members] bit. + * The [members][Tuple.membersBook] bit. * @since 3.0.0 */ const val META_BIT: FetchMode = 1 @@ -101,7 +101,7 @@ const val GEOMETRY_BIT: FetchMode = 2 const val GEOMETRY_CLEAR: FetchMode = GEOMETRY_BIT.inv() /** - * The _feature_ bit, covers [feature][Tuple.feature] and tags. + * The _feature_ bit, covers [feature][Tuple.jbonBytes] and tags. * @since 3.0.0 */ const val FEATURE_BIT: FetchMode = 4 diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Metadata.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Metadata.kt deleted file mode 100644 index 8b9ed66dc..000000000 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Metadata.kt +++ /dev/null @@ -1,277 +0,0 @@ -@file:Suppress("OPT_IN_USAGE") - -package naksha.model - -import naksha.base.Fnv1a32 -import naksha.base.Int64 -import naksha.base.Platform -import naksha.base.fn.Fn3 -import naksha.geo.HereTile -import naksha.model.objects.NakshaFeature -import naksha.model.objects.StandardMembers -import kotlin.concurrent.Volatile -import kotlin.js.JsExport -import kotlin.js.JsStatic -import kotlin.jvm.JvmField -import kotlin.jvm.JvmStatic - -/** - * The immutable on-heap representation of the metadata of a [Tuple]. - * - * This is mainly used by applications, therefore the default value of [tupleNumber] is [TupleNumber.HEAD]. - * @since 3.0.0 - */ -@JsExport -data class Metadata( - val tupleNumber: TupleNumber = TupleNumber.HEAD, - val dataEncoding: DataEncoding = DataEncoding.DEFAULT, - val updatedAt: Int64 = Platform.currentMillis(), - val createdAt: Int64? = null, - val authorTs: Int64? = null, - val nextVersion: Int64? = null, - val baseTupleNumber: TupleNumber? = null, - val changeCount: Int = 1, - val hash: Int = 0, - val hereTile: Int = 0, - val id: String, - val appId: String = NakshaContext.appId(), - val author: String? = NakshaContext.author(), - val origin: String? = null, - val target: String? = null, - val ft: String? = null, - val cv0: Double? = null, - val cv1: Double? = null, - val cv2: Double? = null, - val cv3: Double? = null, - val cs0: String? = null, - val cs1: String? = null, - val cs2: String? = null, - val cs3: String? = null, -) { - val storageNumber: Int64 - get() = tupleNumber.storageNumber - val mapNumber: Int - get() = tupleNumber.mapNumber - val collectionNumber: Int - get() = tupleNumber.collectionNumber - val featureNumber: Int64 - get() = tupleNumber.featureNumber - val partitionNumber: Int - get() = tupleNumber.partitionNumber - val version: Version - get() = tupleNumber.version - val txn: Int64 - get() = version.txn - - /** - * Tests if this describes a new state. - * @return _true_ if this describes a new state, not yet persisted; _false_, if it describes an existing state. - */ - fun isNew(): Boolean = tupleNumber == TupleNumber.HEAD - - @Volatile - private var _guid: Guid? = null - - /** - * Returns the [Guid]. - * @return the [Guid]. - */ - val guid: Guid - get() { - var guid = _guid - if (guid == null) { - guid = Guid(id, tupleNumber) - _guid = guid - } - return guid - } - - @Volatile - private var _originGuid: Guid? = null - - val originGuid: Guid? - get() { - var guid = _originGuid - if (guid == null) { - val origin = this.origin ?: return null - try { - guid = Guid.fromString(origin) - } catch (e: Exception) { - return null - } - _originGuid = guid - } - return guid - } - - @Volatile - private var _targetGuid: Guid? = null - - val targetGuid: Guid? - get() { - var guid = _targetGuid - if (guid == null) { - val target = this.target ?: return null - try { - guid = Guid.fromString(target) - } catch (e: Exception) { - return null - } - _targetGuid = guid - } - return guid - } - - /** - * Returns the action encoded in the lower two bits of [Version.txn]. - */ - fun action() : Action = Action.fromValue((version.txn.toInt()) and 3) - - override fun hashCode(): Int = tupleNumber.hashCode() - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is Metadata) return false - return guid == other.guid - } - override fun toString(): String = "$id:$tupleNumber" - - companion object Metadata_C { - - /** - - * Import other metadata into the heap representation. - * @param other the other metadata. - * @since 3.0.0 - */ - @JvmStatic - @JsStatic - fun fromOther(other: Any?): Metadata? { - if (other is Metadata) return other - if (other is Tuple) { - return Metadata( - tupleNumber = other.tupleNumber, - dataEncoding = other.dataEncoding, - updatedAt = other.getLongMember(StandardMembers.XyzUpdatedAt), - createdAt = other.getLongMember(StandardMembers.CreatedAtXyz), - authorTs = other.getLongMember(StandardMembers.XyzAuthorTimestamp), - nextVersion = if (other.nextVersion == Int64(-1L)) null else other.nextVersion, - baseTupleNumber = null, - changeCount = other.getIntMember(StandardMembers.ChangeCountXyz), - hash = other.getIntMember(StandardMembers.Hash), - hereTile = other.getIntMember(StandardMembers.HereTileXyz), - id = other.getStringMember(StandardMembers.Id) ?: "undefined", - appId = other.getStringMember(StandardMembers.AppIdXyz) ?: NakshaContext.appId(), - author = other.getStringMember(StandardMembers.AuthorXyz), - origin = other.getStringMember(StandardMembers.OriginXyz), - target = other.getStringMember(StandardMembers.TargetXyz), - ft = other.getStringMember(StandardMembers.FeatureType), - cv0 = other.getDoubleMember(StandardMembers.CustomValue0Xyz), - cv1 = other.getDoubleMember(StandardMembers.CustomValue1Xyz), - cv2 = other.getDoubleMember(StandardMembers.XyzCustomValue2), - cv3 = other.getDoubleMember(StandardMembers.XyzCustomValue3), - cs0 = other.getStringMember(StandardMembers.XyzCustomString0), - cs1 = other.getStringMember(StandardMembers.XyzCustomString1), - cs2 = other.getStringMember(StandardMembers.CustomString2), - cs3 = other.getStringMember(StandardMembers.XyzCustomString3), - ) - } - return null - } - - /** - * Creates the [Metadata] from the given [XYZ namespace][XyzNs]. - * - * If the given [XYZ namespace][XyzNs] is not from an existing, really stored feature, then the method returns _null_, what means, that the feature to which this [XYZ namespace][XyzNs] is attached is a client modified version. - * @param featureId the **feature-id**. - * @param featureType the **feature-type**. - * @param xyz the [XYZ namespace][XyzNs]. - * @return the [Metadata] created from it. - * @since 3.0.0 - * @see [XyzNs.fromMetadata] - */ - @JvmStatic - @JsStatic - fun fromXyzNs(featureId: String, featureType: String, xyz: XyzNs): Metadata? { - val guid = xyz.guid ?: return null - return Metadata( - tupleNumber = guid.tupleNumber, - nextVersion = xyz.nguid?.tupleNumber?.version?.txn, - baseTupleNumber = xyz.mguid?.tupleNumber, - dataEncoding = xyz.dataEncoding ?: DataEncoding.DEFAULT, - updatedAt = xyz.updatedAt, - createdAt = if (xyz.updatedAt == xyz.createdAt) null else xyz.createdAt, - authorTs = if (xyz.updatedAt == xyz.authorTs) null else xyz.authorTs, - hash = xyz.hash ?: 0, - changeCount = xyz.changeCount, - hereTile = xyz.hereTile ?: 0, // TODO: Fix me, update! - appId = xyz.appId, - author = xyz.author, - id = featureId, - origin = xyz.origin, - target = xyz.target, - ft = featureType, - cv0 = xyz.cv0, cv1 = xyz.cv1, cv2 = xyz.cv2, cv3 = xyz.cv3, - cs0 = xyz.cs0, cs1 = xyz.cs1, cs2 = xyz.cs2, cs3 = xyz.cs3, - ) - } - - /** - * The undefined metadata singleton. - * @since 3.0. - */ - @JvmField - @JsStatic - val UNDEFINED = Metadata( - tupleNumber = TupleNumber.HEAD, - dataEncoding = DataEncoding.DEFAULT, - updatedAt = Int64(0), - hash = 0, - changeCount = 0, - hereTile = 0, - id = "undefined", - appId = "undefined", - author = null, - ) - - /** - * Calculates the feature hash to be stored in [Metadata]. - * @param feature the feature. - * @param excludePaths an optional list of paths to exclude. - * @param excludeFn an optional function to call for the [feature], current path, current value to decide if the value should be excluded from hashing. - * @return the hash. - */ - @Suppress("UNUSED_PARAMETER") - @JvmStatic - @JsStatic - fun calculateHash( - feature: NakshaFeature, - excludePaths: List>? = null, - excludeFn: Fn3, Any?>? = null - ): Int { - // TODO: We need to calculate the hash above the feature itself. - // - Order keys first. - // - Exclude the given paths - // - Always exclude ["properties", "@ns:com:here:xyz"] - // - The purpose of the hash is to find similar entries - // - We only care about real data changes (not times, author, other metadata) - return Fnv1a32.string(0, feature.id) - } - - /** - * Calculate the HERE tile-id to be stored in [Metadata]. - * @param feature the feature for which to calculate the HERE tile-id. - * @return the HERE tile-id _(aka the int-key)_. - */ - @JvmStatic - @JsStatic - fun calculateHereTile(feature: NakshaFeature): Int { - val c = feature.referencePoint ?: feature.geometry?.calculateCentroid() - return if (c != null) HereTile(c.latitude, c.longitude).intKey else Fnv1a32.string(0, feature.id) - } - } - - // TODO: toByteArray - we have a binary encoding already in PgTupleLoader, move here - // fromByteArray - we have a binary encoding already in PgTupleLoader, move here - // maybe it is better to realize this with a MetadataByteArray (basically extract code from PgTupleLoader) -} - diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt index ecddfbccd..d218e7727 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt @@ -230,13 +230,13 @@ class Naksha private constructor() { } /** - * A regular expression to test if a string contains potentially a 63-bit unsigned integer (`0 .. 9,223,372,036,854,775,807`). + * A regular expression to test if a string contains potentially a 63-bit unsigned integer (`1 .. 9,223,372,036,854,775,807`). * @since 3.0 */ private val is63BitUnsigned = Regex("^[1-9][0-9]{0,18}$") /** - * A regular expression to test if a string contains potentially a 31-bit unsigned integer (`0 .. 2,147,483,647`). + * A regular expression to test if a string contains potentially a 31-bit unsigned integer (`1 .. 2,147,483,647`). * @since 3.0 */ private val is31BitUnsigned = Regex("^[1-9][0-9]{0,9}\$") @@ -461,195 +461,6 @@ class Naksha private constructor() { @JvmStatic fun alternativeInt32(number: Int): Int = (number + 1) or -2147483648 - /** - * Decode the [Naksha feature][NakshaFeature] from the given [tuple][Tuple]. - * - * This method will query the [cache] to get the [dictionary-manager][IDictManager]. - * - Throws [NakshaError.DICT_MANAGER_NOT_FOUND], if a [dictionary-manager][IDictManager] is needed to decode the [Tuple], but not available in [cache]. - * @param tuple the tuple to decode. - * @param dictionaryReader the dictionary reader to use, if _null_, then the storage or cache are used. - * @return the Naksha feature, _null_ if decoding failed or _null_ was given. - * @since 3.0 - */ - @JsStatic - @JvmStatic - @JvmOverloads - fun decodeTuple(tuple: Tuple, dictionaryReader: IDictReader? = null): NakshaFeature { - val sn = tuple.storageNumber - val dataEncodingStr = tuple.getStringMember(StandardMembers.DataEncoding) - val dataEncoding = if (dataEncodingStr.isNullOrEmpty()) DEFAULT_DATA_ENCODING else DataEncoding.fromString(dataEncodingStr) - val dictReader = dictionaryReader ?: getStorageByNumber(sn) ?: cache.getDictReader(sn) - val feature = decodeFeature(tuple.feature, dictReader) ?: NakshaFeature() - feature.properties.xyz = XyzNs.fromTuple(tuple) - val xyz = feature.properties.xyz - val tags = tuple.getTagList(StandardMembers.XyzTags) - if (tags != null) xyz.tags = tags - val geo = tuple.getByteArray(StandardMembers.Geometry) - if (geo != null) feature.geometry = decodeGeometry(geo) - return feature - } - - /** - * Encodes the given [NakshaFeature] into JBON2 bytes, extracting member values into the - * provided [membersBook] via a custom [IMemberEncoder]. - * - * The encoder walks the feature using [JbEncoder2]. When a value is encountered at a path - * that matches a member's [effectivePath][naksha.model.objects.Member.effectivePath], the - * member encoder: - * 1. Walks the feature to obtain the raw value (already available from the encoder) - * 2. Coerces the value to the expected [MemberType] - * 3. Invokes all registered [IMemberProcessor] instances for that member - * 4. Stores the final value in [membersBook] - * 5. Returns the member's index so the encoder writes a members-book reference - * - * If [collection.dataEncoding] is [DataEncoding.JBON2_GZIP] and the raw JBON2 bytes are - * at least 1000, the result is GZIP-compressed. However, if the compressed bytes are - * larger or equal to the source, the raw bytes are returned instead. - * - * @param feature the feature to encode. - * @param collection the collection that declares the members to extract. - * @param session the session for which to encode. - * @param membersBook the [HeapBook] to populate with extracted member values. - * @param globalBook the global book/dictionary to use for encoding; if any. - * @return the encoded feature bytes (JBON2, optionally GZIP-compressed). - * @since 3.0 - */ - @JsName("encodeFeatureWithMembers") - @JsStatic - @JvmStatic - fun encodeFeature( - feature: NakshaFeature, - collection: NakshaCollection, - session: IWriteSession, - membersBook: HeapBook, - globalBook: IBook? - ): ByteArray { - val members = collection.members?.toList() ?: StandardMembers.XYZ_MEMBERS - val processors = session.processors() - val rawTn = StandardMembers.Tn.read(collection) - val colTn: TupleNumber - if (rawTn is TupleNumber) { - colTn = rawTn - } else if (rawTn is String) { - colTn = TupleNumber.fromString(rawTn) - } else { - throw NakshaException(ILLEGAL_ARGUMENT, "The given collection does not have a valid tuple-number (uuid)") - } - if (colTn.featureNumber > Int.MAX_VALUE || colTn.featureNumber < Int.MIN_VALUE) { - throw NakshaException(ILLEGAL_ARGUMENT, "The given collection does have an invalid feature-number (uuid)") - } - // The feature is stored in the same database and catalog - val tn = TupleNumber( - // The feature is stored in the same database as the collection it is inserted into. - colTn.storageNumber, - // The feature is stored in the same catalog as the collection it is inserted into. - colTn.mapNumber, - // The feature-number of the collection is the collection-number of the feature stored in it. - colTn.featureNumber.toInt(), - // The feature-number of the actual feature. - feature.featureNumber, - // The version is the one of the transaction. - session.useTransaction().version - ) - - // Create the encoder with a custom member encoder. - val encoder = JbEncoder2(globalBook) - encoder.withMemberEncoder { path: Array, pathEnd: Int, value: Any? -> - // Build the current path key from the encoder's path. - members@ for (i in 0 until members.size) { - val member = members[i] ?: continue - val memberName = member.name - val memberPath = member.path - if (memberPath.size != pathEnd) continue - for (pi in 0 until pathEnd) { - if (path[pi] != memberPath[pi]) continue@members - } - // Path matches - var v = value - val procs = processors[memberName] - if (procs != null) { - for (proc in procs) { - v = proc.processMember(session, collection, feature, member, v) - } - } - // Coerce the value to the expected type. - v = FeatureMemberValues.coerce(value, member.dataType, feature.id, memberName) - - // Store in membersBook. - return@withMemberEncoder membersBook.put(memberName, v) - } - -1 - } - - // Add mandatory members. - membersBook.put("tn", ) - - // Encode the feature. - val raw = encoder.buildTupleFromMap(feature) - - // Optionally GZIP. - val encoding = collection.dataEncoding ?: Naksha.DEFAULT_DATA_ENCODING - if (encoding == DataEncoding.JBON2_GZIP && raw.size >= 1000) { - val compressed = gzipDeflate(raw) - if (compressed.size < raw.size) return compressed - } - return raw - } - - /** - * Decode a Naksha feature, auto-detecting the encoding from header bytes. - * - * 1. gzip magic (`1F 8B`) → gunzip, then re-inspect. - * 2. JBON2 magic (`@JB\x02`) → JBON2 decoder. - * 3. First byte in `{`, `[`, ` `, `\t`, `\n`, `\r` → parse as JSON. - * 4. Otherwise → legacy JBON1 decoder. - * - * @param bytes the bytes to decode. - * @param dictReader the dictionary manager to use for legacy JBON1 decoding; if any. - * @return the Naksha feature, or _null_ if the bytes are empty / undecodable. - * @since 3.0 - */ - @JsStatic - @JvmStatic - fun decodeFeature(bytes: ByteArray?, dictReader: IDictReader?): NakshaFeature? { - if (bytes == null || bytes.isEmpty()) return null - val raw = if (isGzipped(bytes)) gzipInflate(bytes) else bytes - if (raw.isEmpty()) return null - return when { - isJbon2(raw) -> { - // The current JBON2 encoder embeds a local book and uses no global string-refs, - // so a global dictionary is not required to decode the feature object. - val decoder = JbDecoder2(null) - decoder.mapBytes(raw) - decoder.toAnyObject().proxy(NakshaFeature::class) - } - isJson(raw) -> { - val decoded = fromJSON(raw.decodeToString()) - if (decoded is PlatformMap) decoded.proxy(NakshaFeature::class) else null - } - else -> { - val decoder = JbFeatureDecoder(dictReader) - decoder.mapBytes(raw) - decoder.toAnyObject().proxy(NakshaFeature::class) - } - } - } - - private fun isGzipped(bytes: ByteArray): Boolean = - bytes.size >= 2 && bytes[0] == 0x1F.toByte() && bytes[1] == 0x8B.toByte() - - private fun isJbon2(bytes: ByteArray): Boolean = - bytes.size >= 4 && - bytes[0] == JB2_MAGIC[0] && bytes[1] == JB2_MAGIC[1] && - bytes[2] == JB2_MAGIC[2] && bytes[3] == JB2_MAGIC[3] - - private fun isJson(bytes: ByteArray): Boolean { - val b = bytes[0] - return b == 0x7B.toByte() || b == 0x5B.toByte() || - b == 0x20.toByte() || b == 0x09.toByte() || - b == 0x0A.toByte() || b == 0x0D.toByte() - } - /** * Decode Naksha tags from their binary representation. * @param bytes the bytes to decode. diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/StorageTx.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/StorageTx.kt index 0b6e2d34a..e70dd911d 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/StorageTx.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/StorageTx.kt @@ -17,7 +17,6 @@ import kotlin.js.JsName * This class works without any real [storage][IStorage], however, storages can extend this class, and override certain behaviors with own better implementations, adjusted to the storage. * * @since 3.0 - * @see [Naksha.decodeTuple] */ @JsExport open class StorageTx private constructor( @@ -53,7 +52,7 @@ open class StorageTx private constructor( /** * The author _(user)_ that performs the modifications; if any. * - * If `null`, then the change is done by an application and the author and authorTs fields in [Tuple.members] are not modified _(they stay what they are right now)_. + * If `null`, then the change is done by an application and the author and authorTs fields in [Tuple.membersBook] are not modified _(they stay what they are right now)_. * @since 3.0 */ val author: String?, @@ -68,7 +67,7 @@ open class StorageTx private constructor( * The session to which this transaction is attached. * @since 3.0 */ - val session: ISession, + val session: IWriteSession, ) { @JsName("storageTxWithStorageNumber") @@ -78,7 +77,7 @@ open class StorageTx private constructor( appId: String, author: String?, dictReader: IDictReader?, - session: ISession, + session: IWriteSession, ): this(null, storageNumber, version, appId, author, dictReader, session) @JsName("storageTxWithStorage") @@ -88,7 +87,7 @@ open class StorageTx private constructor( appId: String, author: String?, dictReader: IDictReader?, - session: ISession, + session: IWriteSession, ): this(storage, storage.number, version, appId, author, dictReader, session) /** @@ -103,92 +102,4 @@ open class StorageTx private constructor( */ open val updatedAt: Int64 get() = transaction.time - - /** - * Builds the [Tuple] for the given write action. - * @param map the map in which the feature is being persisted. - * @param collection the collection in which the feature is being persisted. - * @param feature the feature. - * @param action the [action][Action] being performed. - * @return the encoded [Tuple]. - */ - private fun buildTuple( - map: NakshaMap, - collection: NakshaCollection, - feature: NakshaFeature, - action: Action, - ): Tuple { - val membersBook = HeapBook() - val globalBook = dictReader?.getEncodingDictionary(feature) - val featureBytes = Naksha.encodeFeature(feature, collection, session, membersBook, globalBook) - - val actionBits = Int64(action.intValue.toLong()) - val featureNumber = feature.featureNumber - val versionVal = version.txn and Int64(-4L) or actionBits - val nextVersion: Int64 = if (map.id == Naksha.ADMIN_MAP && collection.id == Naksha.TRANSACTIONS_COL) versionVal else Int64(-1L) - return Tuple( - storageNumber = storageNumber, - mapNumber = map.number, - collectionNumber = collection.number, - featureNumber = featureNumber, - version = Version(versionVal), - nextVersion = nextVersion, - members = membersBook, - feature = featureBytes - ) - } - - /** - * Convert the given [feature][NakshaFeature] into a [Tuple], when the feature was created. - * - * ### Note - * This method can be used for `upsert` as well, just that on-conflict the following values have to be updated from the already existing feature: - * - `created_at` - should be `created_at` of the existing version. - * - `cc` - _(change-count)_ should be set to the existing value + 1 - * - `author` - if the previous `author` is not the same as the current, then set to the current author _(author changed)_, otherwise set it to the previous one _(unchanged)_. Note: if either author is `null`, they are considered different. - * - `author_ts` - if the author changed, set to `null` _(same as `updatedAt`)_, otherwise set to the previous value. - * - `flags` - update `authorTs` flag accordingly; always set the `createdAt` flag; change the `action` to `UPDATED`. - * - * We need to beware, that in an `UPSERT` operation the [Tuple] changes, when we eventually perform the `UPDATE`, rather than the planned `INSERT`. So, we need to return the modified values in this case, therefore an `UPSERT` is a bit more complicated than an `INSERT` or `UPDATED`, but still we do not need to transfer all data forth and back, we can just create a new updated [Tuple]. - * - * **Therefore, do not store the [Tuple] in cache before being sure, that it really is persisted!** - * @param map the map in which the feature is going to be created. - * @param collection the collection in which the feature is going to be created. - * @param feature the feature that was created. - * @return the binary encoding of the [NakshaFeature] as [Tuple]. - */ - open fun created( - map: NakshaMap, - collection: NakshaCollection, - feature: NakshaFeature, - ): Tuple = buildTuple(map, collection, feature, Action.CREATED) - - /** - * Convert the given [feature][NakshaFeature] into a [Tuple], when the feature was updated. - * @param map the map in which the feature is going to be created. - * @param collection the collection in which the feature is going to be created. - * @param feature the feature that was created. - * @return the binary encoding of the [NakshaFeature] as [Tuple]. - */ - open fun updated( - map: NakshaMap, - collection: NakshaCollection, - feature: NakshaFeature, - ): Tuple = buildTuple(map, collection, feature, Action.UPDATED) - - /** - * Convert the given [feature][NakshaFeature] into a [Tuple], when the feature was deleted. - * - * ### Note - * There is no difference between purge and delete, except that on a purge, the [Tuple] is not persisted in shadow and/or history, which means that the [TupleNumber] of the purged feature potentially can't be load from storage or cache, so the [Tuple] is not available. - * @param map the map in which the feature is going to be created. - * @param collection the collection in which the feature is going to be created. - * @param feature the feature that was created. - * @return the binary encoding of the [NakshaFeature] as [Tuple]. - */ - open fun deleted( - map: NakshaMap, - collection: NakshaCollection, - feature: NakshaFeature, - ): Tuple = buildTuple(map, collection, feature, Action.DELETED) } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Tuple.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Tuple.kt index 2869daeac..2a60d7e74 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Tuple.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Tuple.kt @@ -2,74 +2,190 @@ package naksha.model +import naksha.base.AnyList import naksha.base.Int64 +import naksha.base.ListProxy +import naksha.base.MapProxy +import naksha.base.Platform.PlatformCompanion.gzipDeflate +import naksha.base.Platform.PlatformCompanion.gzipInflate import naksha.base.WeakRef import naksha.jbon.IBook import naksha.model.objects.Member import naksha.geo.SpGeometry +import naksha.jbon.BookType +import naksha.jbon.HeapBook +import naksha.jbon.JB2_MAGIC +import naksha.jbon.JbDecoder2 +import naksha.jbon.JbEncoder2 +import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_ARGUMENT +import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_STATE +import naksha.model.objects.MemberType +import naksha.model.objects.NakshaCollection import naksha.model.objects.NakshaFeature import naksha.model.objects.StandardMembers import kotlin.js.JsExport +import kotlin.js.JsStatic import kotlin.jvm.JvmField import kotlin.jvm.JvmOverloads +import kotlin.jvm.JvmStatic /** - * A tuple represents a specific immutable state of a feature. - * - * The tuple stores the address fields ([storageNumber], [mapNumber], [collectionNumber], [featureNumber], [version]) as primary constructor parameters, with an optional [members] dict for values stored in dedicated member slots of the storage. - * + * A tuple represents a specific immutable state of a feature in binary encoding. * @since 3.0 */ @JsExport -data class Tuple( +data class Tuple @JvmOverloads constructor( /** - * The storage-number (database-level identifier). + * Feature serialized with the encoding described by the collection's dataEncoding. * @since 3.0 */ - @JvmField val storageNumber: Int64, + @JvmField val jbonBytes: ByteArray, /** - * The map-number (catalog-level identifier). + * The members book provided by storage at read time. Contains dedicated member values, such as `id`, `tn`, etc. * @since 3.0 */ - @JvmField val mapNumber: Int, + @JvmField val membersBook: IBook, /** - * The collection-number. + * After encoding a [NakshaFeature] into a [Tuple] using [encodeFeature] method, the [previousTupleNumber] will be set by the encoder to the [TupleNumber] of the given feature; if it had any. + * + * This is metadata, it can as well be set manually, when a tuple is read from a storage. * @since 3.0 */ - @JvmField val collectionNumber: Int, + @JvmField var previousTupleNumber: TupleNumber? = null +) { - /** - * The feature-number. - * @since 3.0 - */ - @JvmField val featureNumber: Int64, + companion object Tuple_C { + /** + * Encodes the given [NakshaFeature] into JBON2 bytes and members book. + * + * @param feature the feature to encode. + * @param collection the collection for which to encode the feature; declares the members. + * @param action the action to apply. + * @param session the session for which to encode; declares the version. + * @param globalBook the global book to use for encoding; if any. + * @return the encoded feature bytes (JBON2, optionally GZIP-compressed). + * @since 3.0 + * @throws NakshaException if any fatal error happens when encoding. + */ + @JsStatic + @JvmStatic + fun encodeFeature( + feature: NakshaFeature, + collection: NakshaCollection, + action: Action, + session: IWriteSession, + globalBook: IBook? + ): Tuple { + val members = collection.useMembers() + val processors = session.processors() + + // Update the tuple-number. + val tnMember = collection.useMember(StandardMembers.Tn) + val colTn = tnMember.readTupleNumber(collection) ?: + throw NakshaException(ILLEGAL_ARGUMENT, "The given collection does not have a valid tuple-number (uuid)") + if (colTn.featureNumber > Int.MAX_VALUE || colTn.featureNumber < Int.MIN_VALUE) { + throw NakshaException(ILLEGAL_ARGUMENT, "The given collection does have an invalid feature-number (uuid)") + } + // Read the current tuple-number of the feature; if any. + val prevTn: TupleNumber? = collection.useMember(StandardMembers.Tn).readTupleNumber(feature) + val newTn: TupleNumber + if (prevTn != null) { + if (action != Action.VERSION && action == Action.CREATE) { + throw NakshaException(ILLEGAL_ARGUMENT, "Invalid action CREATE given, the feature exists already (has a uuid)") + } + newTn = TupleNumber.copy(prevTn, session.useTransaction().version.value) + } else { + if (action != Action.VERSION && action != Action.CREATE) { + throw NakshaException(ILLEGAL_ARGUMENT, "Invalid action $action given, the feature does not exist (missing uuid)") + } + newTn = TupleNumber( + // The feature is stored in the same database as the collection it is inserted into. + colTn.storageNumber, + // The feature is stored in the same catalog as the collection it is inserted into. + colTn.mapNumber, + // The feature-number of the collection is the collection-number of the feature we want to store in the collection. + colTn.featureNumber.toInt(), + // The feature-number of the actual feature. Will either be set explicit or calcualted. + feature.featureNumber, + // The version is the one of the transaction. + session.useTransaction().version.value + ) + } + // Update the feature with its new tuple-number. + tnMember.write(feature, newTn) + val globalBookTn: TupleNumber? + if (globalBook != null) { + if (newTn.storageNumber != globalBook.databaseNumber || globalBook.featureNumber == null) { + throw NakshaException(ILLEGAL_ARGUMENT, "The given global book is not located in the same storage as the feature") + } + globalBookTn = TupleNumber.copy(newTn, storageNumber = globalBook.databaseNumber, featureNumber = globalBook.featureNumber) + } else { + globalBookTn = null + } - /** - * The version (transaction-number with action bits). - * @since 3.0 - */ - @JvmField val version: Version, + // Create the encoder with a custom member encoder. + val encoder = JbEncoder2(globalBook) + val membersBook = HeapBook(BookType.MEMBER_BOOK) + encoder.withMemberEncoder { path: Array, pathEnd: Int, value: Any? -> + // Build the current path key from the encoder's path. + members@ for (i in 0 until members.size) { + val member = members[i] ?: continue + if (StandardMembers.GlobalBookFeatureNumber.isSameAs(member)) { + return@withMemberEncoder if (globalBookTn != null) membersBook.put(member.name, globalBookTn.featureNumber) else -1 + } + val memberName = member.name + val memberPath = member.path + if (memberPath.size != pathEnd) continue + for (pi in 0 until pathEnd) { + if (path[pi] != memberPath[pi]) continue@members + } + // Path matches + var v = value + val procs = processors[memberName] + if (procs != null) { + for (proc in procs) { + v = proc.processMember(session, collection, feature, member, v) + } + } + // Coerce the value to the expected type. + v = FeatureMemberValues.coerce(value, member.dataType, feature.id, memberName) + + // Store in membersBook. + return@withMemberEncoder membersBook.put(memberName, v) + } + -1 + } - /** - * The next-version at which this tuple was superseded. `NULL`-sentinel (`-1L`) indicates the tuple is the current (HEAD) state. - * @since 3.0 - */ - @JvmField var nextVersion: Int64 = Int64(-1L), + // Encode the feature. + val raw = encoder.buildTupleFromMap(feature) - /** - * Optional members dict provided by storage at read time. Contains metadata values such as `id`, `app_id`, `updated_at`, `data_encoding`, etc. - * @since 3.0 - */ - @JvmField val members: IBook? = null, + // Optionally GZIP. + if (raw.size >= 1000) { + val compressed = gzipDeflate(raw) + if (compressed.size < raw.size) { + return Tuple(compressed, membersBook, prevTn) + } + } + return Tuple(raw, membersBook, prevTn) + } - /** - * Feature serialized with the encoding described by the collection's dataEncoding. - * @since 3.0 - */ - @JvmField val feature: ByteArray? = null, -) { + private fun isGzipped(bytes: ByteArray): Boolean = + bytes.size >= 2 && bytes[0] == 0x1F.toByte() && bytes[1] == 0x8B.toByte() + + private fun isJbon2(bytes: ByteArray): Boolean = + bytes.size >= 4 && + bytes[0] == JB2_MAGIC[0] && bytes[1] == JB2_MAGIC[1] && + bytes[2] == JB2_MAGIC[2] && bytes[3] == JB2_MAGIC[3] + + private fun isJson(bytes: ByteArray): Boolean { + val b = bytes[0] + return b == 0x7B.toByte() || b == 0x5B.toByte() || + b == 0x20.toByte() || b == 0x09.toByte() || + b == 0x0A.toByte() || b == 0x0D.toByte() + } + } override fun equals(other: Any?): Boolean { if (this === other) return true @@ -94,84 +210,123 @@ data class Tuple( return ref } - private var _tupleNumber: TupleNumber? = null + /** + * The [TupleNumber] of the [Tuple]. + * @since 3.0 + */ + @JvmField + val tupleNumber: TupleNumber = membersBook.getByName(StandardMembers.Tn.name) as TupleNumber /** - * The [TupleNumber] of the [Tuple], lazily cached. + * The next-version at which this tuple was superseded. `NULL`-sentinel indicates the tuple is the current _([Version.HEAD])_ state. * @since 3.0 */ - val tupleNumber: TupleNumber - get() { - var tn = _tupleNumber - if (tn == null) { - tn = TupleNumber(storageNumber, mapNumber, collectionNumber, featureNumber, version) - _tupleNumber = tn + var nextVersion: Int64 + get() = membersBook.getByName(StandardMembers.NextVersion.name) as Int64 + set(version: Int64) { + val members = this.membersBook + if (members is HeapBook) { + members.put(StandardMembers.NextVersion.name, version) + } else { + throw NakshaException(ILLEGAL_STATE, "Members book is immutable, failed to set nextVersion") } - return tn } private var _nextTupleNumber: TupleNumber? = null /** - * The [TupleNumber] of the next version, if [nextVersion] is set (not `-1L`). - * Lazily cached. + * The [TupleNumber] of the next version.; `null` if [Version.HEAD]. * @since 3.0 */ val nextTupleNumber: TupleNumber? get() { - if (nextVersion == Int64(-1L)) return null - var tn = _nextTupleNumber - if (tn == null) { - tn = TupleNumber(storageNumber, mapNumber, collectionNumber, featureNumber, Version(nextVersion)) - _nextTupleNumber = tn + if (nextVersion >= Version.HEAD.value) return null + var nextTn = _nextTupleNumber + if (nextTn == null) { + nextTn = TupleNumber.copy(tupleNumber, version = nextVersion) + _nextTupleNumber = nextTn } - return tn + return nextTn } + private var _globalBookTn: TupleNumber? = null + /** - * The feature as deserialized [NakshaFeature]. Lazily cached. + * The [TupleNumber] of the global book needed to decode this [Tuple]. This [TupleNumber] can be used to load the book from the storage. If being `null`, then no global book is needed for decoding. * @since 3.0 */ - val nakshaFeature: NakshaFeature? - get() = _nakshaFeature - private var _nakshaFeature: NakshaFeature? = null + val globalBookTn: TupleNumber? + get() { + val globalBookTn = _globalBookTn + if (globalBookTn != null) return globalBookTn + val raw = getMember(StandardMembers.GlobalBookFeatureNumber) + if (raw is Int64 || raw is Long) { + TODO("Use the global book number as feature-number, combine with storage-number from tuple, and with admin-catalog, book-collection, version is always HEAD, books are immutable and can not be versioned") + } + _globalBookTn = globalBookTn + return globalBookTn + } private var _id: String? = null /** - * The feature identifier. - * If [featureNumber] is non-negative, returns the stringified feature-number. - * If [featureNumber] is negative, reads the custom identifier from [members]. - * Lazily cached. + * The custom feature identifier. + * - If the feature-number is non-negative, returns the stringified feature-number. + * - If the feature-number is negative, reads the custom identifier from [membersBook]. * @since 3.0 + * @throws NakshaException with error [ILLEGAL_STATE] if the feature number */ val id: String get() { - var result = _id - if (result == null) { - result = if (featureNumber >= 0) featureNumber.toString() else getStringMember(StandardMembers.Id) - ?: throw IllegalStateException("Missing 'id' member for tuple with negative feature-number: $tupleNumber") - _id = result + var id: String? = _id + if (id != null) return id + id = membersBook.getByName(StandardMembers.Id.name) as String? + if (id != null) { + _id = id + return id + } + val featureNumber = tupleNumber.featureNumber + id = if (featureNumber >= 0) featureNumber.toString() else { + throw NakshaException(ILLEGAL_STATE, "Missing 'id' member for tuple with negative feature-number: $tupleNumber") } - return result + _id = id + return id } /** - * Get a String member by name. - * Returns `null` if the member is missing, the value is `null`, or not a String. + * Tests if the `Tuple` contains the given member, optionally if it is of the desired type. + * @param member The member to query, only uses the [Member.name]. + * @param dataType The data-type to test for. + * @return `true` if the member exists and the value is of the correct type; `false` otherwise. * @since 3.0 */ - fun getStringMember(member: Member): String? = - members?.getByName(member.name)?.let { v -> if (v is String) v else null } + @JvmOverloads + fun hasMember(member: Member, dataType: MemberType? = null): Boolean { + val index = membersBook.indexOfName(member.name) + if (index < 0) return false + if (dataType == null) return true + val value = membersBook.get(index) + return dataType.isInstance(value) + } + + /** + * Get a String member. + * @param member The member to query, only uses the [Member.name]. + * @return the value from the [membersBook] book or `null`, if the member is missing, the value is `null`, or not the requested type. + * @since 3.0 + */ + fun getString(member: Member): String? = membersBook.getByName(member.name) as? String /** - * Get a long member by name with a default. - * Returns [alt] if the member is missing or not a long-compatible type. + * Get a long member. + * @param member The member to query, only uses the [Member.name]. + * @param alt The alternative to return. + * @return the value from the [membersBook] book or [alt], if the member is missing, the value is `null`, or not the requested type. * @since 3.0 */ @JvmOverloads - fun getLongMember(member: Member, alt: Int64 = Int64(0L)): Int64 = - members?.getByName(member.name)?.let { v -> + fun getLong(member: Member, alt: Int64 = Int64(0L)): Int64 = + membersBook.getByName(member.name)?.let { v -> when (v) { is Int64 -> v is Long -> Int64(v) @@ -181,13 +336,15 @@ data class Tuple( } ?: alt /** - * Get an int member by name with a default. - * Returns [alt] if the member is missing or not an int-compatible type. + * Get an integer member. + * @param member The member to query, only uses the [Member.name]. + * @param alt The alternative to return. + * @return the value from the [membersBook] book or [alt], if the member is missing, the value is `null`, or not the requested type. * @since 3.0 */ @JvmOverloads - fun getIntMember(member: Member, alt: Int = 0): Int = - members?.getByName(member.name)?.let { v -> + fun getInt(member: Member, alt: Int = 0): Int = + membersBook.getByName(member.name)?.let { v -> when (v) { is Int -> v is Number -> v.toInt() @@ -196,13 +353,15 @@ data class Tuple( } ?: alt /** - * Get a double member by name with a default. - * Returns [alt] if the member is missing or not a double-compatible type. + * Get a double member. + * @param member The member to query, only uses the [Member.name]. + * @param alt The alternative to return. + * @return the value from the [membersBook] book or [alt], if the member is missing, the value is `null`, or not the requested type. * @since 3.0 */ @JvmOverloads - fun getDoubleMember(member: Member, alt: Double = Double.NaN): Double = - members?.getByName(member.name)?.let { v -> + fun getDouble(member: Member, alt: Double = Double.NaN): Double = + membersBook.getByName(member.name)?.let { v -> when (v) { is Double -> v is Number -> v.toDouble() @@ -211,91 +370,94 @@ data class Tuple( } ?: alt /** - * Get a boolean member by name with a default. - * Returns [alt] if the member is missing or not a boolean. + * Get a boolean member. + * @param member The member to query, only uses the [Member.name]. + * @param alt The alternative to return. + * @return the value from the [membersBook] book or [alt], if the member is missing, the value is `null`, or not the requested type. * @since 3.0 */ @JvmOverloads - fun getBooleanMember(member: Member, alt: Boolean = false): Boolean = - members?.getByName(member.name)?.let { v -> if (v is Boolean) v else alt } ?: alt + fun getBoolean(member: Member, alt: Boolean = false): Boolean = membersBook.getByName(member.name) as? Boolean ?: alt - /** - * Get the raw value of a member by name. - * Returns `null` if the member is missing or the value is `null`. + /** + * Get the raw value of the member. + * @param member The member to query, only uses the [Member.name]. + * @return the value from the [membersBook] book or `null`, if the member is missing, the value is `null`, or not the requested type. * @since 3.0 */ - fun getMember(member: Member): Any? = members?.getByName(member.name) + fun getMember(member: Member): Any? = membersBook.getByName(member.name) /** - * Get a ByteArray member by name. - * Returns `null` if the member is missing, the value is `null`, or not a ByteArray. + * Get the byte-array value of the member. + * @param member The member to query, only uses the [Member.name]. + * @return the value from the [membersBook] book or `null`, if the member is missing, the value is `null`, or not the requested type. * @since 3.0 */ - fun getByteArray(member: Member): ByteArray? = - members?.getByName(member.name)?.let { v -> if (v is ByteArray) v else null } + fun getByteArray(member: Member): ByteArray? = membersBook.getByName(member.name) as ByteArray? /** - * Get a geometry member by name. - * Decodes the TWKB bytes into [SpGeometry]. - * Returns `null` if the member is missing, the value is `null`, or not a ByteArray. + * Get the [SpGeometry] value of the member. + * @param member The member to query, only uses the [Member.name]. + * @return the value from the [membersBook] book or `null`, if the member is missing, the value is `null`, or not the requested type. * @since 3.0 */ - fun getSpatialMember(member: Member): SpGeometry? { - val bytes = members?.getByName(member.name) as? ByteArray ?: return null - return try { Naksha.decodeGeometry(bytes) } catch (_: Exception) { null } + fun getSpatial(member: Member): SpGeometry? { + val raw = membersBook.getByName(member.name) + if (raw is SpGeometry) return raw + if (raw is ByteArray) return try { Naksha.decodeGeometry(raw) } catch (_: Exception) { null } + if (raw is MapProxy<*,*>) return raw.proxy(SpGeometry::class) + return null } /** - * Get a tags member by name. - * Decodes the JSON text into a [TagMap]. - * Returns `null` if the member is missing, the value is `null`, or not a String. + * Get the [TagMap] value of the member. + * @param member The member to query, only uses the [Member.name]. + * @return the value from the [membersBook] book or `null`, if the member is missing, the value is `null`, or not the requested type. * @since 3.0 */ fun getTags(member: Member): TagMap? { - val json = members?.getByName(member.name) as? String ?: return null - return try { Naksha.decodeTags(json) } catch (_: Exception) { null } + val raw = membersBook.getByName(member.name) + if (raw is TagMap) return raw + if (raw is MapProxy<*, *>) return raw.proxy(TagMap::class) + return null } /** - * Get a tags member by name as a [TagList]. - * Decodes the JSON text, supporting both persisted forms: a JSON array - * ([set][naksha.model.objects.MemberType.SET], the default — order preserved) and a JSON object - * ([naksha.model.objects.MemberType.TAGS_FROM_ARRAY] — re-flattened, order not guaranteed). - * Returns `null` if the member is missing, the value is `null`, or not a String. + * Get the [TagList] value of the member. + * @param member The member to query, only uses the [Member.name]. + * @return the value from the [membersBook] book or `null`, if the member is missing, the value is `null`, or not the requested type. * @since 3.0 */ fun getTagList(member: Member): TagList? { - val json = members?.getByName(member.name) as? String ?: return null - return try { Naksha.decodeTagList(json) } catch (_: Exception) { null } + val raw = membersBook.getByName(member.name) + if (raw is TagList) return raw + if (raw is ListProxy<*>) return raw.proxy(TagList::class) + return null } /** - * The [DataEncoding] of this tuple, read from [members]. - * Returns [Naksha.DEFAULT_DATA_ENCODING] if not set. + * Get the set value of the member. + * @param member The member to query, only uses the [Member.name]. + * @return the value from the [membersBook] book or `null`, if the member is missing, the value is `null`, or not the requested type. * @since 3.0 */ - val dataEncoding: DataEncoding - get() { - val str = getStringMember(StandardMembers.DataEncoding) ?: return Naksha.DEFAULT_DATA_ENCODING - return try { DataEncoding.fromString(str) } catch (_: Exception) { Naksha.DEFAULT_DATA_ENCODING } - } + fun getSet(member: Member): List<*>? { + val raw = membersBook.getByName(member.name) + if (raw is List<*>) return raw + return null + } /** - * Convert this [Tuple] into a [NakshaFeature] by decoding the feature blob and building the XYZ namespace. - * @return the decoded [NakshaFeature], or `null` if the tuple has no feature data. + * Decode this [Tuple] into a [NakshaFeature]. + * @param globalBook The global book to use to decode the [Tuple]. Need to be loaded from the storage, if [globalBookTn] is not `null`. + * @return the decoded [NakshaFeature]. * @since 3.0 + * @throws NakshaException if any error occurs. */ - fun toNakshaFeature(): NakshaFeature? { - val feature = Naksha.decodeFeature(this.feature, null) ?: return null - feature.properties.xyz = XyzNs.fromTuple(this) - val tags = getTagList(StandardMembers.XyzTags) - if (tags != null) { - feature.properties.xyz.tags = tags - } - val geoBytes = getByteArray(StandardMembers.Geometry) - if (geoBytes != null) { - feature.geometry = Naksha.decodeGeometry(geoBytes) - } - return feature + fun decodeFeature(globalBook: IBook?): NakshaFeature { + val rawBytes = if (isGzipped(jbonBytes)) gzipInflate(jbonBytes) else jbonBytes + val decoder = JbDecoder2(globalBook, membersBook) + decoder.mapBytes(rawBytes) + return decoder.toAnyObject().proxy(NakshaFeature::class) } } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumber.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumber.kt index 6bfa0e3c9..b04850d25 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumber.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumber.kt @@ -63,19 +63,12 @@ data class TupleNumber( /** * The version _(transaction)_ of which the [Tuple] is part of. - * The lower 2 bits of [Version.txn] encode the [Action]. + * The lower 2 bits of [Version.value] encode the [Action]. * @since 3.0 * @see [Version.HEAD] */ - @JvmField val version: Version, + @JvmField val version: Int64, ) : Comparable { - /** - * The transaction-number. - * @since 3.0 - */ - val txn: Int64 - get() = version.txn - /** * The partition-number of the [Tuple], a value between `0` and `65536` _(exclusive)_. * @since 3.0 @@ -85,11 +78,11 @@ data class TupleNumber( /** * The [Action] applied to generate the [Tuple] referred by this [TupleNumber]. - * Decoded from the lower 2 bits of [version.txn]. + * Decoded from the lower 2 bits of [version]. * @since 3.0 */ val action: Action - get() = Action.fromValue(version.txn.toInt() and 3) + get() = Action.fromValue(version.toInt() and 3) /** * Calculates the partition-index where this [Tuple] will be located. @@ -132,7 +125,7 @@ data class TupleNumber( && mapNumber == other.mapNumber && collectionNumber == other.collectionNumber && featureNumber == other.featureNumber - && version.txn == other.version.txn + && version == other.version } private lateinit var _string: String @@ -273,7 +266,7 @@ data class TupleNumber( dataview_set_int64(view, offset, featureNumber) offset += 8 } - dataview_set_int64(view, offset, version.txn) + dataview_set_int64(view, offset, version) return byteArray } @@ -307,7 +300,7 @@ data class TupleNumber( @JvmOverloads fun copy( tn: TupleNumber, - version: Version? = null, + version: Int64? = null, featureNumber: Int64? = null, collectionNumber: Int? = null, mapNumber: Int? = null, @@ -326,7 +319,7 @@ data class TupleNumber( * This happens for various reasons, for example when a [Tuple] is created in the client at runtime, and not yet persisted in any storage, therefore does not yet have a valid tuple-number. * @since 3.0 */ - val HEAD = TupleNumber(Int64(0), 0, 0, Int64(0), Version.HEAD) + val HEAD = TupleNumber(Int64(0), 0, 0, Int64(0), Version.HEAD.value) /** * Restore a [TupleNumber] from a binary encoding. @@ -408,8 +401,8 @@ data class TupleNumber( ?: throw illegalArg("Missing collection-number for given tuple-number variant $variant") val f_num = if (variant.encodeFeatureNumber()) binary.readInt64() else featureNumber ?: throw illegalArg("Missing collection-number for given tuple-number variant $variant") - val txn = binary.readInt64() - return TupleNumber(storage_num, map_num, col_num, f_num, Version(txn)) + val version = binary.readInt64() + return TupleNumber(storage_num, map_num, col_num, f_num, version) } /** @@ -454,7 +447,7 @@ data class TupleNumber( * - `map-number` _(32-bit integer)_ * - `collection-number` _(32-bit integer)_ * - `feature-number` _(64-bit integer)_ - * - `version` _(64-bit integer, the raw [Version.txn] value)_ + * - `version` _(64-bit integer, the raw [Version.value] value)_ * @param parts the string parts of the tuple-number. * @param offset the index in the given list where the `storage-number` is located, defaults to `0`. * @return the deserialized [TupleNumber]. @@ -470,7 +463,7 @@ data class TupleNumber( val mapNumber = parts[offset + MAP_NUMBER].toInt(10) val colNumber = parts[offset + COLLECTION_NUMBER].toInt(10) val featureNumber = Int64(parts[offset + FEATURE_NUMBER].toLong(10)) - val version = Version.fromString(parts[offset + VERSION]) + val version = Version.fromString(parts[offset + VERSION]).value return TupleNumber(storageNumber, mapNumber, colNumber, featureNumber, version) } } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumberBinaryArray.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumberBinaryArray.kt index 53a564d6e..2973e5531 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumberBinaryArray.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumberBinaryArray.kt @@ -214,7 +214,7 @@ data class TupleNumberBinaryArray( val collectionNumber = sharedCollectionNumber ?: dataview_get_int32(view, offset + collectionNumberOffset) val featureNumber = sharedFeatureNumber ?: dataview_get_int64(view, offset + featureNumberOffset) val txn = dataview_get_int64(view, offset + txnOffset) - val tupleNumber = TupleNumber(storageNumber, mapNumber, collectionNumber, featureNumber, Version(txn)) + val tupleNumber = TupleNumber(storageNumber, mapNumber, collectionNumber, featureNumber, txn) if (!disableCache) { var cache = tupleNumberCache if (index <= cache.size) { // Note: This only happens, when being EMPTY @@ -389,7 +389,7 @@ data class TupleNumberBinaryArray( && element.mapNumber == getMapNumber(i) && element.collectionNumber == getCollectionNumber(i) && element.featureNumber == getFeatureNumber(i) - && element.version.txn == getTxn(i)) return i + && element.version == getTxn(i)) return i } return -1 } @@ -401,7 +401,7 @@ data class TupleNumberBinaryArray( && element.mapNumber == getMapNumber(i) && element.collectionNumber == getCollectionNumber(i) && element.featureNumber == getFeatureNumber(i) - && element.version.txn == getTxn(i)) return i + && element.version == getTxn(i)) return i } return -1 } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumberList.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumberList.kt index 9836b90a1..bdeecedcf 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumberList.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumberList.kt @@ -168,7 +168,7 @@ class TupleNumberList : ListProxy(TupleNumber::class) { dataview_set_int64(view, i, tupleNumber.featureNumber) i += 8 } - dataview_set_int64(view, i, tupleNumber.txn) + dataview_set_int64(view, i, tupleNumber.version) i += 8 } check(i == SIZE) diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Version.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Version.kt index 7de53e788..f8c9031c0 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Version.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Version.kt @@ -11,6 +11,7 @@ import kotlin.jvm.JvmField import kotlin.jvm.JvmOverloads import kotlin.jvm.JvmStatic +// TODO: @AI: Fix the documentation, it does not match the actual one. /** * Wrapper for a version (transaction number), encoded as an unsigned 56-bit integer (the upper 8 bits are always zero). * @@ -41,15 +42,15 @@ import kotlin.jvm.JvmStatic * * ### String representation * - * [toString] returns the raw [txn] value as a plain decimal number, regardless of whether the + * [toString] returns the raw [value] value as a plain decimal number, regardless of whether the * version is dated or manual. [fromString] accepts both the decimal form and the legacy * `{year}:{month}:{day}:{seqWithAction}` form for backward-compatibility. * - * @property txn the raw 64-bit transaction number (upper 8 bits always zero). + * @property value the raw 53-bit version number (upper 11 bits are always zero). * @since 3.0 */ @JsExport -open class Version(@JvmField val txn: Int64) : Comparable { +open class Version(@JvmField val value: Int64) : Comparable { /** * Convert a transaction number given as [Long] into a version. @@ -86,7 +87,7 @@ open class Version(@JvmField val txn: Int64) : Comparable { * Creates a version from its string representation. * * Accepts either: - * - A pure decimal encoding of the 64-bit [txn] value. + * - A pure decimal encoding of the 64-bit [value] value. * - The human-readable form `{year}:{month}:{day}:{seq}` (seq is the 30-bit sequence, no action bits). * * Throws [NakshaError.ILLEGAL_ARGUMENT] if the string is invalid. @@ -125,13 +126,13 @@ open class Version(@JvmField val txn: Int64) : Comparable { * @param month month of the year; must be in 1..12. * @param day day of the month; must be in 1..31. * @param seq 30-bit sequence number within the day; must be in 0..1073741823 (0x3FFF_FFFF). - * @param action the [Action] to encode in the lower 2 bits; defaults to [Action.CREATED]. + * @param action the [Action] to encode in the lower 2 bits; defaults to [Action.CREATE]. * @since 3.0 */ @JvmStatic @JsStatic @JvmOverloads - fun auto(year: Int, month: Int, day: Int, seq: Int64, action: Action = Action.CREATED): Version { + fun auto(year: Int, month: Int, day: Int, seq: Int64, action: Action = Action.CREATE): Version { require(year in YEAR_MIN..YEAR_MAX) { "year must be in $YEAR_MIN..$YEAR_MAX, got $year" } @@ -151,20 +152,20 @@ open class Version(@JvmField val txn: Int64) : Comparable { /** * Constructs a **manual** version. * - * The resulting [txn] must have its upper 21 bits (63–43) all zero, which means the effective + * The resulting [value] must have its upper 21 bits (63–43) all zero, which means the effective * value fits in 43 bits. The [seq] therefore must be in 0..0x1FF_FFFF_FFFF (41 bits), since * the lower 2 bits are reserved for [action]. * * Throws [IllegalArgumentException] if [seq] is out of range. * * @param seq 41-bit sequence value; must be in 0..0x1FF_FFFF_FFFF. - * @param action the [Action] to encode in the lower 2 bits; defaults to [Action.CREATED]. + * @param action the [Action] to encode in the lower 2 bits; defaults to [Action.CREATE]. * @since 3.0 */ @JvmStatic @JsStatic @JvmOverloads - fun manual(seq: Int64, action: Action = Action.CREATED): Version { + fun manual(seq: Int64, action: Action = Action.CREATE): Version { require(seq >= Int64(0) && seq <= MANUAL_SEQ_MASK) { "seq for a manual version must be in 0..${MANUAL_SEQ_MASK.toLong()} (41-bit), got $seq" } @@ -175,13 +176,13 @@ open class Version(@JvmField val txn: Int64) : Comparable { * Creates a dated version for the current wall-clock time. * * @param seq 30-bit sequence number within the current day; must be in 0..1073741823. - * @param action the [Action] to encode; defaults to [Action.CREATED]. + * @param action the [Action] to encode; defaults to [Action.CREATE]. * @since 3.0 */ @JvmStatic @JsStatic @JvmOverloads - fun now(seq: Int64, action: Action = Action.CREATED): Version { + fun now(seq: Int64, action: Action = Action.CREATE): Version { val now = Timestamp.now() return auto(now.year, now.month, now.day, seq, action) } @@ -206,7 +207,7 @@ open class Version(@JvmField val txn: Int64) : Comparable { */ @JvmField @JsStatic - val MIN = auto(16, 1, 1, Int64(0), Action.CREATED) + val MIN = auto(16, 1, 1, Int64(0), Action.CREATE) // 0n + (0n << 2n) + (1n << 32n) + (1n << (32n+5n)) + (16n << (32n+5n+4n)) = 35326106009600n // bitwise: 0x0000_2021_0000_0000 @@ -238,7 +239,7 @@ open class Version(@JvmField val txn: Int64) : Comparable { val SEQ_MAX: Int64 = SEQ_30_MASK /** - * The raw increment to add to [txn] to advance the sequence counter by one while keeping the + * The raw increment to add to [value] to advance the sequence counter by one while keeping the * action bits unchanged. Equal to `1 shl 2` = `4`. * @since 3.0 */ @@ -256,7 +257,7 @@ open class Version(@JvmField val txn: Int64) : Comparable { */ val year: Int get() { - if (_year < 0) _year = (txn ushr 41).toInt() + if (_year < 0) _year = (value ushr 41).toInt() return _year } @@ -268,7 +269,7 @@ open class Version(@JvmField val txn: Int64) : Comparable { */ val month: Int get() { - if (_month < 0) _month = (txn ushr 37).toInt() and 0xF + if (_month < 0) _month = (value ushr 37).toInt() and 0xF return _month } @@ -280,7 +281,7 @@ open class Version(@JvmField val txn: Int64) : Comparable { */ val day: Int get() { - if (_day < 0) _day = (txn ushr 32).toInt() and 0x1F + if (_day < 0) _day = (value ushr 32).toInt() and 0x1F return _day } @@ -297,7 +298,7 @@ open class Version(@JvmField val txn: Int64) : Comparable { get() { var s = _seq if (s == null) { - s = (txn ushr 2) and SEQ_MAX + s = (value ushr 2) and SEQ_MAX _seq = s } return s @@ -307,7 +308,7 @@ open class Version(@JvmField val txn: Int64) : Comparable { * Returns `true` if this is a **dated** version, i.e. the year field (`txn ushr 41`) is ≥ 16. * @since 3.0 */ - fun isDated(): Boolean = (txn ushr 41).toInt() >= 16 + fun isDated(): Boolean = (value ushr 41).toInt() >= 16 /** * Returns `true` if this is a **manual** version, i.e. the year field is < 16 and the upper 21 bits are zero. @@ -317,28 +318,28 @@ open class Version(@JvmField val txn: Int64) : Comparable { fun isManualVersion(): Boolean = !isDated() /** - * Returns the [Action] encoded in the lower 2 bits of [txn]. + * Returns the [Action] encoded in the lower 2 bits of [value]. * @since 3.0 */ - fun action(): Action = Action.fromValue(txn.toInt() and 3) + fun action(): Action = Action.fromValue(value.toInt() and 3) private var _string: String? = null override fun equals(other: Any?): Boolean { - if (other is Int64) return txn eq other - if (other is Version) return txn eq other.txn + if (other is Int64) return value eq other + if (other is Version) return value eq other.value return false } override fun compareTo(other: Version): Int { - val diff = txn.minus(other.txn) + val diff = value.minus(other.value) return if (diff.eq(0)) 0 else if (diff < 0) -1 else 1 } - override fun hashCode(): Int = txn.hashCode() + override fun hashCode(): Int = value.hashCode() /** - * Returns the version as a plain decimal string of the raw [txn] value. + * Returns the version as a plain decimal string of the raw [value] value. * * This representation is lossless for all version types (dated and manual) and survives * a round-trip through [fromString]. The legacy `{year}:{month}:{day}:{seqWithAction}` @@ -348,7 +349,7 @@ open class Version(@JvmField val txn: Int64) : Comparable { override fun toString(): String { var s = _string if (s == null) { - s = txn.toLong().toString() + s = value.toLong().toString() _string = s } return s diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/XyzNs.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/XyzNs.kt index e47734cc2..6d7050a6a 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/XyzNs.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/XyzNs.kt @@ -4,6 +4,25 @@ package naksha.model import naksha.base.* import naksha.model.objects.StandardMembers +import naksha.model.objects.XyzMembers +import naksha.model.objects.XyzMembers.XyzMembers_C.XyzAppId +import naksha.model.objects.XyzMembers.XyzMembers_C.XyzAuthor +import naksha.model.objects.XyzMembers.XyzMembers_C.XyzAuthorTimestamp +import naksha.model.objects.XyzMembers.XyzMembers_C.XyzChangeCount +import naksha.model.objects.XyzMembers.XyzMembers_C.XyzCreatedAt +import naksha.model.objects.XyzMembers.XyzMembers_C.XyzCustomString0 +import naksha.model.objects.XyzMembers.XyzMembers_C.XyzCustomString1 +import naksha.model.objects.XyzMembers.XyzMembers_C.XyzCustomString2 +import naksha.model.objects.XyzMembers.XyzMembers_C.XyzCustomString3 +import naksha.model.objects.XyzMembers.XyzMembers_C.XyzCustomValue0 +import naksha.model.objects.XyzMembers.XyzMembers_C.XyzCustomValue1 +import naksha.model.objects.XyzMembers.XyzMembers_C.XyzCustomValue2 +import naksha.model.objects.XyzMembers.XyzMembers_C.XyzCustomValue3 +import naksha.model.objects.XyzMembers.XyzMembers_C.XyzHash +import naksha.model.objects.XyzMembers.XyzMembers_C.XyzHereTile +import naksha.model.objects.XyzMembers.XyzMembers_C.XyzOrigin +import naksha.model.objects.XyzMembers.XyzMembers_C.XyzTarget +import naksha.model.objects.XyzMembers.XyzMembers_C.XyzUpdatedAt import kotlin.DeprecationLevel.WARNING import kotlin.js.JsExport import kotlin.js.JsStatic @@ -19,8 +38,8 @@ import kotlin.jvm.JvmStatic * - **Create**: Clients should create features without an XYZ namespace, except for the [tags]. * - **Delete**: The content of the feature is ignored for deletes. * - **Update**: If the client wants to update a feature, it should read the feature, then modify it, and then send the modified feature back, without changing the XYZ namespace, except for the [tags]. When it does operate like this, the change is performed atomically safe, because the [uuid] will hint the server which version was modified by the client, and is expected to be current _HEAD_. If the feature was updated meanwhile by another client, the server can try to perform an auto-merge, otherwise it will respond with a conflict (which is what the low-level storage will do). - * - **Fork**: If the client reads a feature, and then writes it into another storage, map, or collection, or when the client modifies the ID of the feature, and then sends the feature to a service, without modifying the XYZ namespace, the storage will be able to detect that this is a **fork**. Forking means, that a feature is moved between storages, maps, or collections, or is re-identified. The storage will turn the action into [CREATED][Action.CREATED], and copy the [uuid] (which refers to the modified foreign state) into the [origin]. When the feature, that was forked, is modified later, it is possible to find all forks in all storages, maps, and collections, and to update them doing a [three-way-merge](https://en.wikipedia.org/wiki/Merge_(version_control)#Three-way_merge). This process is called rebase. - * - **Split**: If the client need to split a feature into parts, for example a Topology into two, it is required that it clones the original feature, and then modifies the copies, while deleting the original feature that was split. All features being part of the split will have the same [uuid]. The feature that was split is expected to be deleted with [action] set to [DELETED][Action.DELETED], and the new parts are created with [action] set to [CREATED][Action.CREATED]. + * - **Fork**: If the client reads a feature, and then writes it into another storage, map, or collection, or when the client modifies the ID of the feature, and then sends the feature to a service, without modifying the XYZ namespace, the storage will be able to detect that this is a **fork**. Forking means, that a feature is moved between storages, maps, or collections, or is re-identified. The storage will turn the action into [CREATED][Action.CREATE], and copy the [uuid] (which refers to the modified foreign state) into the [origin]. When the feature, that was forked, is modified later, it is possible to find all forks in all storages, maps, and collections, and to update them doing a [three-way-merge](https://en.wikipedia.org/wiki/Merge_(version_control)#Three-way_merge). This process is called rebase. + * - **Split**: If the client need to split a feature into parts, for example a Topology into two, it is required that it clones the original feature, and then modifies the copies, while deleting the original feature that was split. All features being part of the split will have the same [uuid]. The feature that was split is expected to be deleted with [action] set to [DELETED][Action.DELETE], and the new parts are created with [action] set to [CREATED][Action.CREATE]. * - **Join**: If the client need to join multiple features into a single one, it is required to create a new (_merged_) feature, and to delete all features joined into this new one. It is important that the client set the [target] of all features being part of the join to the [_HEAD_ Guid][Guid.headOf] of the _created_ (_new_) feature. The [_HEAD_ Guid][Guid.headOf] is simply the [Guid] without the [tuple-number][TupleNumber], so basically `urn:here:naksha:guid:{feature-id}`. * @since 3.0 */ @@ -175,7 +194,7 @@ class XyzNs : AnyObject() { */ const val CS3 = "cs3" - private val _ACTION = NotNullEnum(Action::class) { _, _ -> Action.CREATED } + private val _ACTION = NotNullEnum(Action::class) { _, _ -> Action.CREATE } private val _DATA_ENCODING_NULL = NullableEnum(DataEncoding::class) private val _APP_ID = NotNullProperty(String::class) { _, _ -> NakshaContext.appId() } private val _STRING_NULL = NullableProperty(String::class, autoRemove = true) @@ -196,108 +215,48 @@ class XyzNs : AnyObject() { @JsStatic fun fromTuple(tuple: Tuple): XyzNs { val tn = tuple.tupleNumber - val members = tuple.members - val id = members?.getByName("id") as? String ?: tuple.featureNumber.toString() - val guid = Guid(id, tn) - val updatedAt = tuple.getLongMember(StandardMembers.XyzUpdatedAt) - val createdAt = tuple.getLongMember(StandardMembers.CreatedAtXyz).let { - if (it == Int64(0L)) updatedAt else it - } - val authorTs = tuple.getLongMember(StandardMembers.XyzAuthorTimestamp)?.let { - if (it == Int64(0)) updatedAt else it - } ?: updatedAt - val nextVersion = tuple.nextVersion - val nextTn = if (nextVersion != Int64(-1L)) TupleNumber( - tn.storageNumber, tn.mapNumber, tn.collectionNumber, tn.featureNumber, Version(nextVersion) - ) else null - val base_tn = members?.getByName("base_tn")?.let { - if (it is ByteArray) TupleNumber.fromByteArray(it, 0, TupleNumberVariant.TupleNumberVariant_C.B128, - tn.storageNumber, tn.mapNumber, tn.collectionNumber) - else null - } + val id = tuple.id + val guid = Guid.fromTuple(tuple) + val updatedAt = tuple.getLong(XyzUpdatedAt, Platform.currentMillis()) + val createdAt = tuple.getLong(XyzCreatedAt, updatedAt) + val authorTs = tuple.getLong(XyzAuthorTimestamp,updatedAt) + val nextTn = tuple.nextTupleNumber return AnyObject().apply { setRaw(UUID, guid.toString()) if (nextTn != null) setRaw(NUUID, Guid(id, nextTn).toString()) - if (base_tn != null) setRaw(MUUID, Guid(id, base_tn).toString()) + setRaw(UPDATED_AT, updatedAt) if (createdAt != updatedAt) setRaw(CREATED_AT, createdAt) if (authorTs != updatedAt) setRaw(AUTHOR_TS, authorTs) - setRaw(UPDATED_AT, updatedAt) - setRaw(CHANGE_COUNT, tuple.getIntMember(StandardMembers.ChangeCountXyz)) - setRaw(APP_ID, tuple.getStringMember(StandardMembers.AppIdXyz)) - val author = tuple.getStringMember(StandardMembers.AuthorXyz) + setRaw(CHANGE_COUNT, tuple.getInt(XyzChangeCount, 1)) + setRaw(APP_ID, tuple.getString(XyzAppId)) + val author = tuple.getString(XyzAuthor) if (author != null) setRaw(AUTHOR, author) - setRaw(DATA_ENCODING, tuple.getStringMember(StandardMembers.DataEncoding)) setRaw(ACTION, tn.action.toString()) - setRaw(HASH, tuple.getIntMember(StandardMembers.Hash)) - setRaw(HERE_TILE, tuple.getIntMember(StandardMembers.HereTileXyz)) - val origin = tuple.getStringMember(StandardMembers.OriginXyz) + setRaw(HASH, tuple.getInt(XyzHash)) + setRaw(HERE_TILE, tuple.getInt(XyzHereTile)) + val origin = tuple.getString(XyzOrigin) if (origin != null) setRaw(ORIGIN, origin) - val target = tuple.getStringMember(StandardMembers.TargetXyz) + val target = tuple.getString(XyzTarget) if (target != null) setRaw(TARGET, target) - val cv0 = members?.getByName("cv0") - if (cv0 != null) setRaw(CV0, cv0 as? Double) - val cv1 = members?.getByName("cv1") - if (cv1 != null) setRaw(CV1, cv1 as? Double) - val cv2 = members?.getByName("cv2") - if (cv2 != null) setRaw(CV2, cv2 as? Double) - val cv3 = members?.getByName("cv3") - if (cv3 != null) setRaw(CV3, cv3 as? Double) - val cs0 = tuple.getStringMember(StandardMembers.XyzCustomString0) + val cv0: Double = tuple.getDouble(XyzCustomValue0) + if (cv0 == cv0) setRaw(CV0, cv0) + val cv1: Double = tuple.getDouble(XyzCustomValue1) + if (cv1 == cv1) setRaw(CV1, cv1) + val cv2: Double = tuple.getDouble(XyzCustomValue2) + if (cv2 == cv2) setRaw(CV2, cv2) + val cv3: Double = tuple.getDouble(XyzCustomValue3) + if (cv3 == cv3) setRaw(CV3, cv3) + val cs0 = tuple.getString(XyzCustomString0) if (cs0 != null) setRaw(CS0, cs0) - val cs1 = tuple.getStringMember(StandardMembers.XyzCustomString1) + val cs1 = tuple.getString(XyzCustomString1) if (cs1 != null) setRaw(CS1, cs1) - val cs2 = tuple.getStringMember(StandardMembers.CustomString2) + val cs2 = tuple.getString(XyzCustomString2) if (cs2 != null) setRaw(CS2, cs2) - val cs3 = tuple.getStringMember(StandardMembers.XyzCustomString3) + val cs3 = tuple.getString(XyzCustomString3) if (cs3 != null) setRaw(CS3, cs3) }.proxy(XyzNs::class) } - /** - * Create the XYZ-namespace from the given [Metadata]. - * @param meta the [Metadata] - * @return the [XYZ namespace][XyzNs]. - * @see [Metadata.fromXyzNs] - * @deprecated Use [fromTuple] instead. - */ - @Deprecated("Use fromTuple instead", ReplaceWith("fromTuple(tuple)")) - @JvmStatic - @JsStatic - fun fromMetadata(meta: Metadata): XyzNs { - val tn = meta.tupleNumber - val guid = Guid(meta.id, tn) - val nextVersion = meta.nextVersion - val nextTn = if (nextVersion != null) TupleNumber( - tn.storageNumber, tn.mapNumber, tn.collectionNumber, tn.featureNumber, Version(nextVersion) - ) else null - val base_tn = meta.baseTupleNumber - return AnyObject().apply { - setRaw(UUID, guid.toString()) - if (nextTn != null) setRaw(NUUID, Guid(meta.id, nextTn).toString()) - if (base_tn != null) setRaw(MUUID, Guid(meta.id, base_tn).toString()) - if (meta.createdAt != meta.updatedAt) setRaw(CREATED_AT, meta.createdAt) - if (meta.authorTs != meta.updatedAt) setRaw(AUTHOR_TS, meta.authorTs) - setRaw(UPDATED_AT, meta.updatedAt) - setRaw(CHANGE_COUNT, meta.changeCount) - setRaw(APP_ID, meta.appId) - if (meta.author != null) setRaw(AUTHOR, meta.author) - setRaw(DATA_ENCODING, meta.dataEncoding.toString()) - setRaw(ACTION, meta.action().toString()) - setRaw(HASH, meta.hash) - setRaw(HERE_TILE, meta.hereTile) - if (meta.origin != null) setRaw(ORIGIN, meta.origin) - if (meta.target != null) setRaw(TARGET, meta.target) - if (meta.cv0 != null) setRaw(CV0, meta.cv0) - if (meta.cv1 != null) setRaw(CV1, meta.cv1) - if (meta.cv2 != null) setRaw(CV2, meta.cv2) - if (meta.cv3 != null) setRaw(CV3, meta.cv3) - if (meta.cs0 != null) setRaw(CS0, meta.cs0) - if (meta.cs1 != null) setRaw(CS1, meta.cs1) - if (meta.cs2 != null) setRaw(CS2, meta.cs2) - if (meta.cs3 != null) setRaw(CS3, meta.cs3) - }.proxy(XyzNs::class) - } - /** * A method to normalize a list of tags. * @@ -559,7 +518,8 @@ class XyzNs : AnyObject() { // Downward compatibility hack. val raw = getRaw("version") if (raw is Int64 && raw >= Version.MIN) return Version(raw) - return guid?.tupleNumber?.version + val version = guid?.tupleNumber?.version + return if (version != null) Version(version) else null } /** @@ -567,7 +527,7 @@ class XyzNs : AnyObject() { * @since 2.0 */ val txn: Int64? - get() = guid?.tupleNumber?.txn + get() = guid?.tupleNumber?.version /** * The action of the [Tuple], encoded as the lower 2 bits of the transaction number. diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt index cc717fcdb..3aa25c346 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt @@ -3,9 +3,15 @@ package naksha.model.objects import naksha.base.AnyObject +import naksha.base.Int64 +import naksha.base.MapProxy import naksha.base.NotNullEnum import naksha.base.NotNullProperty import naksha.base.Proxy +import naksha.geo.SpGeometry +import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_STATE +import naksha.model.NakshaException +import naksha.model.TupleNumber import kotlin.js.JsExport import kotlin.js.JsName @@ -143,6 +149,134 @@ class Member() : AnyObject() { return this } + /** + * Ensures that the given `found` member is the same as the `expected` member, allow different path. + * @param other The member to compare this member with. + * @param comparePath If the path must be the same as well, defaults to _false_. + * @return _true_ if the two members are the same; _false_ otherwise. + */ + fun isSameAs(other: Member?, comparePath: Boolean = false): Boolean { + if (other == null) return false + if (this === other) return true + // We require same name and data-type, but not same JSON path. + if (name != other.name) return false + if (dataType != other.dataType) return false + if (comparePath && !path.contentDeepEquals(other.path)) return false + return true + } + + /** + * Ensures that the given `other` member is the same as this. + * @param other The member to compare this with. + * @param comparePath If the path must be the same as well, defaults to _false_. + * @return The `other` member, if it is the same as this. + * @throws NakshaException with error [ILLEGAL_STATE], when the given `other` members does not match this member. + */ + fun asSame(other: Member?, comparePath: Boolean = false): Member { + if (other == null) throw NakshaException(ILLEGAL_STATE, "The other member is NULL") + if (other === this) return other + // We require same name and data-type, but not same JSON path. + if (name != other.name) { + throw NakshaException(ILLEGAL_STATE, "The other member has different name: '${other.name}', expected: '${name}'") + } + if (dataType != other.dataType) { + throw NakshaException(ILLEGAL_STATE, "The other member has wrong data type: '${other.dataType}', expected '${dataType}'") + } + if (comparePath && !path.contentDeepEquals(other.path)) { + throw NakshaException(ILLEGAL_STATE, "The other member has a different path: '${other.path.joinToString("->")}', expected: '${path.joinToString("->")}'") + } + return other + } + + /** + * Helper to read a [TupleNumber] form the given feature. + * @param feature The feature to read from. + * @return the read value or `null`, if the feature does not store a valid value at the member path. + */ + fun readTupleNumber(feature: MapProxy<*,*>): TupleNumber? { + val raw = feature.getPath(path) + if (raw is TupleNumber) return raw + if (raw is String) return TupleNumber.fromString(raw) + return null + } + + /** + * Helper to read a string form the given feature. + * @param feature The feature to read from. + * @return the read value or `null`, if the feature does not store a valid value at the member path. + */ + fun readBoolean(feature: MapProxy<*,*>): Boolean? { + val raw = feature.getPath(path) + if (raw is Boolean) return raw + return null + } + + /** + * Helper to read a string form the given feature. + * @param feature The feature to read from. + * @return the read value or `null`, if the feature does not store a valid value at the member path. + */ + fun readString(feature: MapProxy<*,*>): String? { + val raw = feature.getPath(path) + if (raw is String) return raw + return null + } + + /** + * Helper to read a 64-bit integer form the given feature. + * @param feature The feature to read from. + * @return the read value or `null`, if the feature does not store a valid value at the member path. + */ + fun readLong(feature: MapProxy<*,*>): Int64? { + val raw = feature.getPath(path) + if (raw is Int64) return raw + if (raw is Long) return Int64(raw) + if (raw is Number) return Int64(raw.toLong()) + return null + } + + /** + * Helper to read a 64-bit floating point number form the given feature. + * @param feature The feature to read from. + * @return the read value or `null`, if the feature does not store a valid value at the member path. + */ + fun readDouble(feature: MapProxy<*,*>): Double? { + val raw = feature.getPath(path) + if (raw is Double) return raw + if (raw is Number) return raw.toDouble() + return null + } + + /** + * Helper to read a 64-bit floating point number form the given feature. + * @param feature The feature to read from. + * @return the read value or `null`, if the feature does not store a valid value at the member path. + */ + fun readGeometry(feature: MapProxy<*,*>): SpGeometry? { + val raw = feature.getPath(path) + if (raw is SpGeometry) return raw + return null + } + + /** + * Helper to read a 64-bit floating point number form the given feature. + * @param feature The feature to read from. + * @return the read value or `null`, if the feature does not store a valid value at the member path. + */ + fun readByteArray(feature: MapProxy<*,*>): ByteArray? { + val raw = feature.getPath(path) + if (raw is ByteArray) return raw + return null + } + + /** + * Helper to write a member value to the given feature. + * @param feature The feature to write to. + * @return the previous value. + * @throws RuntimeException If the given feature has a broken path, so the path requires an array, but an object exists already. + */ + fun write(feature: MapProxy<*,*>, value: Any?): Any? = feature.setPath(value, path) + companion object Member_C { private val NAME = NotNullProperty(String::class) { _, _ -> "" } private val DATA_TYPE = NotNullEnum(MemberType::class) { _, _ -> MemberType.STRING } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberType.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberType.kt index 860cc6ad9..d0d2c8d8b 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberType.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberType.kt @@ -2,7 +2,12 @@ package naksha.model.objects +import naksha.base.Int64 import naksha.base.JsEnum +import naksha.geo.SpGeometry +import naksha.model.TagList +import naksha.model.TagMap +import naksha.model.TupleNumber import kotlin.js.JsExport import kotlin.jvm.JvmField import kotlin.reflect.KClass @@ -162,4 +167,29 @@ class MemberType : JsEnum() { @JvmField val SET = defIgnoreCase(MemberType::class, "set") } + + /** + * Tests if the given value is of this type. + * @param value the value to test. + * @return `true` if the value is of this type; false otherwise. + */ + fun isInstance(value: Any?): Boolean { + if (value == null) return false + return when (this) { + BOOLEAN -> value is Boolean + INT8 -> value is Byte + INT16 -> value is Byte || value is Short + INT32 -> value is Byte || value is Short || value is Int + INT64 -> value is Byte || value is Short || value is Int || value is Long || value is Int64 + FLOAT32 -> value is Float + FLOAT64 -> value is Float || value is Double + STRING -> value is String + BYTE_ARRAY -> value is ByteArray + TUPLE_NUMBER -> value is TupleNumber + SPATIAL -> value is SpGeometry + TAGS, TAGS_FROM_ARRAY -> value is TagMap + SET -> value is List<*> + else -> false + } + } } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt index 6953d7682..a15f859d8 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt @@ -9,8 +9,6 @@ import naksha.geo.SpPoint import naksha.model.DataEncoding import naksha.model.Naksha import naksha.model.NakshaError -import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_ARGUMENT -import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_STATE import naksha.model.NakshaException import kotlin.js.JsExport import kotlin.js.JsName @@ -345,7 +343,7 @@ open class NakshaCollection() : NakshaFeature() { * @since 3.0 */ open fun useMembers(): MemberList { - var write: Boolean = false + var write = false var list = this.members if (list == null) { list = MemberList(XyzMembers.ALL) @@ -357,13 +355,7 @@ open class NakshaCollection() : NakshaFeature() { for (mandatory in StandardMembers.MANDATORY) { val found: Member? = list.get(mandatory.name) if (found != null) { - // We require same name and data-type, but not same JSON path. - if (mandatory.dataType != found.dataType) { - throw NakshaException( - ILLEGAL_STATE, - "Member '${mandatory.name}' has different wrong data type: '${found.dataType}', expected '${mandatory.name}'" - ) - } + mandatory.asSame(found) } else { list.add(mandatory) } @@ -372,6 +364,29 @@ open class NakshaCollection() : NakshaFeature() { return list } + /** + * Search for the given member in this collection. + * @param member The member to search for, compares name and type. + * @param comparePath If _true_, then the path is as well compared; otherwise any path is accepted. + * @return the found member. + * @throws NakshaException If no such member was found. + */ + @JvmOverloads + open fun useMember(member: Member, comparePath: Boolean = false): Member = member.asSame(useMembers().get(member.name), comparePath) + + /** + * Search for the given member in this collection. + * @param member The member to search for, compares name and type. + * @param comparePath If _true_, then the path is as well compared; otherwise any path is accepted. + * @return the found member or `null`, if no such member is declared in this collection. + */ + @JvmOverloads + open fun findMember(member: Member, comparePath: Boolean = false): Member? { + val found = useMembers().get(member.name) + if (member.isSameAs(found, comparePath)) return found + return null + } + /** * The indices to maintain on this collection. * diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaFeature.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaFeature.kt index 525313a12..6fb868d54 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaFeature.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaFeature.kt @@ -69,7 +69,6 @@ open class NakshaFeature() : AnyObject() { private val PROPERTIES = NotNullProperty(NakshaProperties::class) private val TITLE_NULL = NullableProperty(String::class) private val DESCRIPTION_NULL = NullableProperty(String::class) - private val ATTACHMENT_NULL = NullableProperty(ByteArray::class) } /** @@ -129,7 +128,8 @@ open class NakshaFeature() : AnyObject() { val cachedId = this.cachedId var cachedFeatureNumber = this.cachedFeatureNumber - // If the user changed the id. + // If the user changed the id (we by intention compare the reference, not the value!). + @Suppress("StringReferentialEquality") if (id === cachedId && cachedFeatureNumber != null) return cachedFeatureNumber // If the feature exists already, and the `id` was not changed, return existing feature number. diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaTx.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaTx.kt index bbbcc6afd..1aabb915b 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaTx.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaTx.kt @@ -155,7 +155,7 @@ open class NakshaTx : NakshaFeature() { * @since 3.0 */ val txn: Int64 - get() = version.txn + get() = version.value /** * Number of features modified in the transaction - total number of features from all touched collections. diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaTxCollection.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaTxCollection.kt index 799ff3f4b..fe384e9a0 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaTxCollection.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaTxCollection.kt @@ -6,9 +6,9 @@ import naksha.base.NotNullProperty import naksha.base.AnyObject import naksha.base.NullableProperty import naksha.model.Action -import naksha.model.Action.Action_C.CREATED -import naksha.model.Action.Action_C.DELETED -import naksha.model.Action.Action_C.UPDATED +import naksha.model.Action.Action_C.CREATE +import naksha.model.Action.Action_C.DELETE +import naksha.model.Action.Action_C.UPDATE import naksha.model.NakshaError import naksha.model.NakshaException import naksha.model.TupleNumber @@ -107,9 +107,9 @@ class NakshaTxCollection() : AnyObject() { */ fun add(tupleNumber: TupleNumber, partitions: Int? = null): NakshaTxCollection { when (tupleNumber.action) { - CREATED -> this.created += 1 - UPDATED -> this.updated += 1 - DELETED -> this.deleted += 1 + CREATE -> this.created += 1 + UPDATE -> this.updated += 1 + DELETE -> this.deleted += 1 } if (partitions != null && partitions > 1) { var featuresByPartition = this.featuresByPartition diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt index 87ed9e765..9a2edecac 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt @@ -54,7 +54,7 @@ class StandardMembers private constructor() { * @since 3.0 */ @JvmField @JsStatic - val GlobalBookNumber = Member("~gbn", MemberType.INT64, JsonPath("gbn")) + val GlobalBookFeatureNumber = Member("~gbn", MemberType.INT64, JsonPath("gbn")) /** * `feature` — **Serialised feature** (`BYTE_ARRAY`). The encoded feature blob. The encoding is controlled by [NakshaCollection.dataEncoding]. Mandatory, storage-managed. The feature member is special in that it represents the feature itself, therefore the path is an empty list! @@ -79,7 +79,7 @@ class StandardMembers private constructor() { * @since 3.0 */ @JvmField @JsStatic - val MANDATORY: List = listOf(Tn, NextVersion, Feature, Id, GlobalBookNumber) + val MANDATORY: List = listOf(Tn, NextVersion, Feature, Id, GlobalBookFeatureNumber) // ------------------------------------------------------------------------- // Optional members. diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzMembers.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzMembers.kt index 320c5ff06..f704f011f 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzMembers.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzMembers.kt @@ -30,11 +30,11 @@ class XyzMembers private constructor() { val XyzNextVersion = Member("~nv", MemberType.INT64, JsonPath("properties", "@ns:com:here:xyz", "nextVersion")) /** - * The same as [StandardMembers.GlobalBookNumber], but with a Data-Hub compatible path. + * The same as [StandardMembers.GlobalBookFeatureNumber], but with a Data-Hub compatible path. * @since 3.0 */ @JvmField @JsStatic - val XyzGlobalBookNumber = Member("~gbn", MemberType.INT64, JsonPath("properties", "@ns:com:here:xyz", "gbn")) + val XyzGlobalBookFeatureNumber = Member("~gbfn", MemberType.INT64, JsonPath("properties", "@ns:com:here:xyz", "globalBookFn")) /** * The same as [StandardMembers.Feature]. @@ -237,7 +237,7 @@ class XyzMembers private constructor() { */ @JvmField @JsStatic val ALL: List = listOf( - XyzTn, XyzNextVersion, XyzGlobalBookNumber, XyzFeature, XyzId, XyzGeometry, + XyzTn, XyzNextVersion, XyzGlobalBookFeatureNumber, XyzFeature, XyzId, XyzGeometry, // Optional members XyzUpdatedAt, XyzCreatedAt, XyzAuthorTimestamp, XyzHash, XyzHereTile, XyzChangeCount, XyzBaseTn, diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/FeatureTuple.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/FeatureTuple.kt index 91aef4f54..ba27fb6cb 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/FeatureTuple.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/FeatureTuple.kt @@ -56,7 +56,7 @@ open class FeatureTuple( */ val id: String? get() { - val member = tuple?.getStringMember(StandardMembers.Id) + val member = tuple?.getString(StandardMembers.Id) if (member != null) return member return feature?.id } @@ -73,14 +73,14 @@ open class FeatureTuple( * - Setting the value to an explicit [NakshaFeature] will disable the automatic cache updates, when the [tuple] is modified. * - **Beware**: If the returned feature is modified, this will as well modify the cached version. * @since 3.0 - * @see [Tuple.toNakshaFeature] + * @see [Tuple.decodeFeature] */ open var feature: NakshaFeature? get() { var feature = cachedFeature val tuple = this.tuple if (tuple != null && tuple !== cachedTuple && !doNotAutoUpdate) { - feature = tuple.toNakshaFeature() + feature = tuple.decodeFeature() cachedFeature = feature cachedJson = null } @@ -116,5 +116,5 @@ open class FeatureTuple( * @return a new copy of the tuple converted into a feature. * @since 3.0 */ - open fun newFeature(): NakshaFeature? = tuple?.toNakshaFeature() + open fun newFeature(): NakshaFeature? = tuple?.decodeFeature() } \ No newline at end of file diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/PropertyFilter.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/PropertyFilter.kt index 620b60f14..537b6002e 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/PropertyFilter.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/PropertyFilter.kt @@ -24,7 +24,7 @@ class PropertyFilter(val req: ReadFeatures) : ResultFilter { val tuple = featureTuple.tuple ?: return null val sn = tuple.storageNumber val dictReader = getStorageByNumber(sn) ?: cache.getDictReader(sn) - val feature = Naksha.decodeFeature(tuple.feature, dictReader) ?: return null + val feature = Naksha.decodeFeature(tuple.jbonBytes, dictReader) ?: return null return if (resolvePropsQueryOnFeature(pSearch, feature)) featureTuple else null } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/Write.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/Write.kt index b8ffce915..f9a8125bc 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/Write.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/Write.kt @@ -212,7 +212,7 @@ open class Write : AnyObject() { set(value) { if (value == null) removeRaw("version") else { versionValue = value - versionRaw = value.txn + versionRaw = value.value setRaw("version", versionRaw) } } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MetaColumn.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MetaColumn.kt index ecea02e70..a3e194eb3 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MetaColumn.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MetaColumn.kt @@ -585,7 +585,7 @@ open class MetaColumn() : AnyObject() { fun cs3(): MetaColumn = MetaColumn(CS3) /** - * The name of the virtual columns that stores the [feature][naksha.model.Tuple.feature]. + * The name of the virtual columns that stores the [feature][naksha.model.Tuple.jbonBytes]. * * This can only be queried using a special [property query][IPropertyQuery]. */ diff --git a/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/TupleNumberTest.kt b/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/TupleNumberTest.kt index a1bbed192..19d945cd9 100644 --- a/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/TupleNumberTest.kt +++ b/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/TupleNumberTest.kt @@ -27,25 +27,25 @@ class TupleNumberTest { @Test fun actionCreatedEncodedInTxn() { - val t = tn(Action.CREATED) - assertEquals(Action.CREATED, t.action) + val t = tn(Action.CREATE) + assertEquals(Action.CREATE, t.action) } @Test fun actionUpdatedEncodedInTxn() { - val t = tn(Action.UPDATED) - assertEquals(Action.UPDATED, t.action) + val t = tn(Action.UPDATE) + assertEquals(Action.UPDATE, t.action) } @Test fun actionDeletedEncodedInTxn() { - val t = tn(Action.DELETED) - assertEquals(Action.DELETED, t.action) + val t = tn(Action.DELETE) + assertEquals(Action.DELETE, t.action) } @Test fun binaryRoundTripB64() { - val t = tn(Action.CREATED) + val t = tn(Action.CREATE) val bytes = t.toByteArray(B64) assertEquals(8, bytes.size) val restored = TupleNumber.fromB64(bytes, storageNumber, mapNumber, collectionNumber, featureNumber) @@ -55,7 +55,7 @@ class TupleNumberTest { @Test fun binaryRoundTripB128() { - val t = tn(Action.UPDATED) + val t = tn(Action.UPDATE) val bytes = t.toByteArray(B128) assertEquals(16, bytes.size) val restored = TupleNumber.fromB128(bytes, storageNumber, mapNumber, collectionNumber) @@ -66,7 +66,7 @@ class TupleNumberTest { @Test fun binaryRoundTripB160() { - val t = tn(Action.DELETED) + val t = tn(Action.DELETE) val bytes = t.toByteArray(B160) assertEquals(20, bytes.size) val restored = TupleNumber.fromB160(bytes, storageNumber, mapNumber) @@ -78,7 +78,7 @@ class TupleNumberTest { @Test fun binaryRoundTripB192() { - val t = tn(Action.CREATED) + val t = tn(Action.CREATE) val bytes = t.toByteArray(B192) assertEquals(24, bytes.size) val restored = TupleNumber.fromB192(bytes, storageNumber) @@ -90,7 +90,7 @@ class TupleNumberTest { @Test fun binaryRoundTripB256() { - val t = tn(Action.UPDATED) + val t = tn(Action.UPDATE) val bytes = t.toByteArray(B256) assertEquals(32, bytes.size) val restored = TupleNumber.fromB256(bytes) @@ -103,7 +103,7 @@ class TupleNumberTest { @Test fun stringRoundTrip() { - val t = tn(Action.CREATED) + val t = tn(Action.CREATE) val s = t.toString() val parts = s.split(":") // 5 parts: storageNumber, mapNumber, collectionNumber, featureNumber, version diff --git a/here-naksha-lib-model/src/jvmTest/kotlin/naksha/model/PropertyFilterTest.kt b/here-naksha-lib-model/src/jvmTest/kotlin/naksha/model/PropertyFilterTest.kt index 5461e1fe6..5d06849de 100644 --- a/here-naksha-lib-model/src/jvmTest/kotlin/naksha/model/PropertyFilterTest.kt +++ b/here-naksha-lib-model/src/jvmTest/kotlin/naksha/model/PropertyFilterTest.kt @@ -482,8 +482,8 @@ class PropertyFilterTest { collectionNumber = collectionNumber, featureNumber = featureNumber(feature.id), version = version, - members = members, - feature = featureBytes + membersBook = members, + jbonBytes = featureBytes ) return FeatureTuple(tupleNumber, tuple) } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgAdminMap.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgAdminMap.kt index ba0f88561..dadbfc0c8 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgAdminMap.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgAdminMap.kt @@ -423,7 +423,7 @@ SELECT basics.*, procs.* FROM basics, procs; logger.info("Transaction counter is still at wrong day, rollover to next day") // Rollover, we update sequence of the day. Start at seq=1 (auto() encodes action in bits 1-0). version = Version.auto(txDate.year, txDate.monthNumber, txDate.dayOfMonth, Int64(1)) - txn = version.txn + txn = version.value conn.execute("SELECT setval($1, $2)", arrayOf(txnSequenceOid, txn + 4)).close() } logger.info("Release advisory lock") diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumnRows.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumnRows.kt index 271f303f4..2b57f1907 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumnRows.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumnRows.kt @@ -2,13 +2,8 @@ package naksha.psql import naksha.base.Int64 import naksha.base.Platform.PlatformCompanion.toJSON -import naksha.jbon.DictEntry import naksha.jbon.IBook -import naksha.jbon.JbDictionary import naksha.model.* -import naksha.model.TupleNumberVariant.TupleNumberVariant_C.B128 -import naksha.model.TupleNumberVariant.TupleNumberVariant_C.B64 -import naksha.model.objects.StandardMembers import naksha.psql.PgColumn.PgColumnCompanion.allColumns /** @@ -41,12 +36,12 @@ internal class PgRowDict( return col.values.getOrNull(row) } - override fun indexOf(string: String): Int { + override fun indexOfString(string: String): Int { val col = rows.columnByName[string] return col?.index ?: -1 } - override fun stringAt(index: Int): String? { + override fun getStringAt(index: Int): String? { val col = rows.columns.getOrNull(index) ?: return null val v = col.values.getOrNull(row) return if (v is String) v else null @@ -54,7 +49,7 @@ internal class PgRowDict( override fun hasNames(): Boolean = true - override fun getIndexOf(name: String): Int = indexOf(name) + override fun indexOfName(name: String): Int = indexOfString(name) override fun getNameAt(index: Int): String? = rows.columns.getOrNull(index)?.name @@ -77,7 +72,7 @@ internal class PgRowDict( } } - override fun find(hash: Int): List = emptyList() + override fun getAllWithHash(hash: Int): List = emptyList() } /** @@ -314,8 +309,8 @@ internal class PgColumnRows { featureNumber = fn, version = naksha.model.Version(version), nextVersion = nextVersion ?: Int64(-1L), - members = members, - feature = getByteArray(row, PgColumn.feature) + membersBook = members, + jbonBytes = getByteArray(row, PgColumn.feature) ) } @@ -383,7 +378,7 @@ internal class PgColumnRows { operator fun set(row: Int, tuple: Tuple) { withMinSize(row) - val members = tuple.members ?: return + val members = tuple.membersBook ?: return set(row, PgColumn.updated_at, members.getByName("updated_at") as? Int64) set(row, PgColumn.created_at, members.getByName("created_at") as? Int64) set(row, PgColumn.author_ts, members.getByName("author_ts") as? Int64) @@ -411,7 +406,7 @@ internal class PgColumnRows { set(row, PgColumn.cs3, members.getByName("cs3") as? String) set(row, PgColumn.tags, members.getByName("tags") as? String) set(row, PgColumn.ref_point, members.getByName("ref_point") as? ByteArray) - set(row, PgColumn.feature, tuple.feature) + set(row, PgColumn.feature, tuple.jbonBytes) set(row, PgColumn.geo, members.getByName("geo") as? ByteArray) set(row, PgColumn.attachment, members.getByName("attachment") as? ByteArray) } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt index 8158d80e0..72e09450c 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt @@ -76,7 +76,7 @@ internal class PgQueryWhereBuilder(private val request: ReadFeatures) { val versions = arrayOfNulls(tupleNumbers.size) for (i in tupleNumbers.indices) { fns[i] = tupleNumbers[i].featureNumber - versions[i] = tupleNumbers[i].version.txn + versions[i] = tupleNumbers[i].version.value } val fnPlaceholder = placeholderForArg(fns, PgType.INT64_ARRAY) val versionPlaceholder = placeholderForArg(versions, PgType.INT64_ARRAY) @@ -88,12 +88,12 @@ internal class PgQueryWhereBuilder(private val request: ReadFeatures) { val txn = request.version if (txn != null) { if (where.isNotEmpty()) where.append(" AND ") - where.append("${PgColumn.version} <= ${txn.txn}") + where.append("${PgColumn.version} <= ${txn.value}") } val min_txn = request.minVersion if (min_txn != null) { if (where.isNotEmpty()) where.append(" AND ") - where.append("${PgColumn.version} >= ${min_txn.txn}") + where.append("${PgColumn.version} >= ${min_txn.value}") } } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWrite.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWrite.kt index 4c9c20e24..b04d49f56 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWrite.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWrite.kt @@ -45,9 +45,9 @@ internal data class PgWrite(val original: Write, val i: Int) { * @since 3.0 */ var action: Action = when (original.op) { - WriteOp.CREATE, WriteOp.UPSERT -> Action.CREATED - WriteOp.UPDATE -> Action.UPDATED - WriteOp.DELETE, WriteOp.PURGE -> Action.DELETED + WriteOp.CREATE, WriteOp.UPSERT -> Action.CREATE + WriteOp.UPDATE -> Action.UPDATE + WriteOp.DELETE, WriteOp.PURGE -> Action.DELETE else -> Action.VERSION } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterDelete.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterDelete.kt index ee7603759..2e46a16d1 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterDelete.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterDelete.kt @@ -34,7 +34,7 @@ internal class PgWriterDelete(writer: PgWriter, collection: PgCollection, partit val row = e.index val write = e.value inRows.set(row, "id", write.id) - inRows.set(row, "expected_version", write.version?.txn) + inRows.set(row, "expected_version", write.version?.value) } } @@ -43,7 +43,7 @@ internal class PgWriterDelete(writer: PgWriter, collection: PgCollection, partit val insert_into_history = if (historyTable != null && collection.head.storeHistory == StoreMode.ON) historyTable else null // The new version with action bits set to DELETED (2). - val deleted_version = "(${tx.version.txn}::int8 | 2)" + val deleted_version = "(${tx.version.value}::int8 | 2)" // All input provided by client, `id` and optionally `expected_version` val query = """WITH query AS ( diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt index abfe58809..521408f27 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt @@ -30,7 +30,7 @@ internal class PgWriterUpdate(writer: PgWriter, collection: PgCollection, partit if (tuple != null) { writeById[write.id] = write inRows[i] = tuple - inRows.set(i, "expected_version", write.version?.txn) + inRows.set(i, "expected_version", write.version?.value) inRows.setCustomMembers(i, write.feature, members) i++ } @@ -184,7 +184,7 @@ LEFT JOIN inserted ON inserted.id = new_row.id // (sentinel "undefined" causes the DB to retain the existing value). val geo = if (PgColumn.geo in keepableByteCols) rows.getByteArray(rowNum, PgColumn.geo.name) else tuple.getByteArray(StandardMembers.Geometry) val referencePoint = if (PgColumn.ref_point in keepableByteCols) rows.getByteArray(rowNum, PgColumn.ref_point.name) else tuple.getByteArray(StandardMembers.ReferencePoint) - val tags = tuple.getStringMember(StandardMembers.XyzTags) + val tags = tuple.getString(StandardMembers.XyzTags) val attachment = if (PgColumn.attachment in keepableByteCols) rows.getByteArray(rowNum, PgColumn.attachment.name) else tuple.getByteArray(StandardMembers.XyzAttachment) val oldGeo = tuple.getByteArray(StandardMembers.Geometry) val oldRefPoint = tuple.getByteArray(StandardMembers.ReferencePoint) @@ -193,7 +193,7 @@ LEFT JOIN inserted ON inserted.id = new_row.id || (oldRefPoint == null || !oldRefPoint.contentEquals(referencePoint ?: ByteArray(0))) || (oldAttachment == null || !oldAttachment.contentEquals(attachment ?: ByteArray(0))) if (needsPatch) { - val m = tuple.members + val m = tuple.membersBook val newMembers = if (m is naksha.jbon.HeapBook) { val dict = m.copy() dict.put(StandardMembers.Geometry.name, geo) @@ -202,7 +202,7 @@ LEFT JOIN inserted ON inserted.id = new_row.id dict.put(StandardMembers.XyzAttachment.name, attachment) dict } else m - write.tuple = tuple.copy(members = newMembers) + write.tuple = tuple.copy(membersBook = newMembers) } } } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt index 4d6fb4269..598c78af9 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt @@ -209,10 +209,10 @@ ${if (head_to_history.isNotEmpty()) "LEFT JOIN head_to_history ON head_to_histor // with the existing value, so the in-memory tuple must reflect the final stored state. val geo = if (PgColumn.geo in keepableByteCols) outRows.getByteArray(row, PgColumn.geo.name) else tuple.getByteArray(StandardMembers.Geometry) val referencePoint = if (PgColumn.ref_point in keepableByteCols) outRows.getByteArray(row, PgColumn.ref_point.name) else tuple.getByteArray(StandardMembers.ReferencePoint) - val tags = tuple.getStringMember(StandardMembers.XyzTags) + val tags = tuple.getString(StandardMembers.XyzTags) val attachment = if (PgColumn.attachment in keepableByteCols) outRows.getByteArray(row, PgColumn.attachment.name) else tuple.getByteArray(StandardMembers.XyzAttachment) write.tupleNumber = updated_tn - val m = tuple.members + val m = tuple.membersBook val newMembers = if (m is naksha.jbon.HeapBook) { val dict = m.copy() dict.put(StandardMembers.Geometry.name, geo) @@ -223,9 +223,9 @@ ${if (head_to_history.isNotEmpty()) "LEFT JOIN head_to_history ON head_to_histor } else m write.tuple = tuple.copy( version = updated_tn.version, - members = newMembers + membersBook = newMembers ) - write.action = Action.UPDATED + write.action = Action.UPDATE } } } diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/AttachmentTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/AttachmentTest.kt index ed1eeaf50..2ebbe7fd0 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/AttachmentTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/AttachmentTest.kt @@ -28,7 +28,7 @@ class AttachmentTest : PgTestBase() { assertEquals(1, features.size) val feature = assertNotNull(features.first()) assertEquals(featureToCreate.id, feature.id) - assertEquals(Action.CREATED, feature.properties.xyz.guid?.tupleNumber?.action) + assertEquals(Action.CREATE, feature.properties.xyz.guid?.tupleNumber?.action) val featureTupleList = this.featureTupleList assertEquals(1, featureTupleList.size) @@ -49,7 +49,7 @@ class AttachmentTest : PgTestBase() { assertEquals(1, features.size) val feature = assertNotNull(features.first()) assertEquals(featureToCreate.id, feature.id) - assertEquals(Action.CREATED, feature.properties.xyz.guid?.tupleNumber?.action) + assertEquals(Action.CREATE, feature.properties.xyz.guid?.tupleNumber?.action) val featureTupleList = this.featureTupleList assertEquals(1, featureTupleList.size) @@ -82,7 +82,7 @@ class AttachmentTest : PgTestBase() { val feature = assertNotNull(features.first()) assertEquals(featureToCreate.id, feature.id) assertEquals("start", feature.properties["test"]) - assertEquals(Action.CREATED, feature.properties.xyz.guid?.tupleNumber?.action) + assertEquals(Action.CREATE, feature.properties.xyz.guid?.tupleNumber?.action) val featureTupleList = this.featureTupleList assertEquals(1, featureTupleList.size) @@ -105,7 +105,7 @@ class AttachmentTest : PgTestBase() { val feature = assertNotNull(features.first()) assertEquals(featureToCreate.id, feature.id) assertEquals("start", feature.properties["test"]) - assertEquals(Action.CREATED, feature.properties.xyz.guid?.tupleNumber?.action) + assertEquals(Action.CREATE, feature.properties.xyz.guid?.tupleNumber?.action) val featureTupleList = this.featureTupleList assertEquals(1, featureTupleList.size) @@ -138,7 +138,7 @@ class AttachmentTest : PgTestBase() { val feature = assertNotNull(features.first()) assertEquals(featureToCreate.id, feature.id) assertEquals("end", feature.properties["test"]) - assertEquals(Action.UPDATED, feature.properties.xyz.guid?.tupleNumber?.action) + assertEquals(Action.UPDATE, feature.properties.xyz.guid?.tupleNumber?.action) val featureTupleList = this.featureTupleList assertEquals(1, featureTupleList.size) @@ -170,7 +170,7 @@ class AttachmentTest : PgTestBase() { assertEquals(1, features.size) val feature = assertNotNull(features.first()) assertEquals(featureToCreate.id, feature.id) - assertEquals(Action.CREATED, feature.properties.xyz.guid?.tupleNumber?.action) + assertEquals(Action.CREATE, feature.properties.xyz.guid?.tupleNumber?.action) assertEquals("start", feature.properties["test"]) val featureTupleList = this.featureTupleList @@ -193,7 +193,7 @@ class AttachmentTest : PgTestBase() { assertEquals(1, features.size) val feature = assertNotNull(features.first()) assertEquals(featureToCreate.id, feature.id) - assertEquals(Action.CREATED, feature.properties.xyz.guid?.tupleNumber?.action) + assertEquals(Action.CREATE, feature.properties.xyz.guid?.tupleNumber?.action) assertEquals("start", feature.properties["test"]) val featureTupleList = this.featureTupleList @@ -227,7 +227,7 @@ class AttachmentTest : PgTestBase() { val feature = assertNotNull(features.first()) assertEquals(featureToCreate.id, feature.id) assertEquals("end", feature.properties["test"]) - assertEquals(Action.UPDATED, feature.properties.xyz.guid?.tupleNumber?.action) + assertEquals(Action.UPDATE, feature.properties.xyz.guid?.tupleNumber?.action) val featureTupleList = this.featureTupleList assertEquals(1, featureTupleList.size) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/DeleteFeatureBase.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/DeleteFeatureBase.kt index bc5f2bbc7..ad56ded2d 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/DeleteFeatureBase.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/DeleteFeatureBase.kt @@ -68,7 +68,7 @@ abstract class DeleteFeatureBase( }).apply { // this = SuccessResponse assertEquals(1, features.size) val firstTuple = featureTupleList[0]?.tuple - assertSame(Action.CREATED, Action.fromValue((firstTuple?.getLongMember(naksha.model.objects.StandardMembers.Version)?.toInt() ?: -1) and 3)) + assertSame(Action.CREATE, Action.fromValue((firstTuple?.getLong(naksha.model.objects.StandardMembers.Version)?.toInt() ?: -1) and 3)) } // verify if delete table contains element @@ -81,7 +81,7 @@ abstract class DeleteFeatureBase( assertEquals(1, features.size) val deletedFeature = assertNotNull(features[0]) assertEquals(initialFeature.id, deletedFeature.id) - assertEquals(Action.DELETED, deletedFeature.properties.xyz.action) + assertEquals(Action.DELETE, deletedFeature.properties.xyz.action) } } @@ -96,7 +96,7 @@ abstract class DeleteFeatureBase( executeWrite(WriteRequest().add(Write().createFeature(collection, NakshaFeature(featureId)))) .features.first()!!.properties.xyz.guid?.tupleNumber ) - val createTxn = createdTn.version.txn + val createTxn = createdTn.version.value assertEquals(0L, (createTxn and ACTION_MASK).toLong(), "CREATE version must have action bits = 0 (CREATED)") @@ -105,7 +105,7 @@ abstract class DeleteFeatureBase( executeWrite(WriteRequest().add(Write().updateFeature(collection, NakshaFeature(featureId), false))) .features.first()!!.properties.xyz.guid?.tupleNumber ) - val updateTxn = updatedTn.version.txn + val updateTxn = updatedTn.version.value assertEquals(1L, (updateTxn and ACTION_MASK).toLong(), "UPDATE version must have action bits = 1 (UPDATED)") assertTrue((updateTxn and ACTION_CLEAR) > (createTxn and ACTION_CLEAR), @@ -116,7 +116,7 @@ abstract class DeleteFeatureBase( executeWrite(WriteRequest().add(Write().deleteFeatureById(collection, featureId))) .features.first()!!.properties.xyz.guid?.tupleNumber ) - val deleteTxn = deletedTn.version.txn + val deleteTxn = deletedTn.version.value // Lower 2 bits must be 2 (DELETED action). assertEquals(2L, (deleteTxn and ACTION_MASK).toLong(), @@ -143,9 +143,9 @@ abstract class DeleteFeatureBase( }).apply { assertEquals(1, features.size) val tombstone = assertNotNull(features[0]) - assertEquals(Action.DELETED, tombstone.properties.xyz.action) + assertEquals(Action.DELETE, tombstone.properties.xyz.action) // The raw version from HEAD must match exactly what the DELETE write returned. - assertEquals(deleteTxn, tombstone.properties.xyz.guid?.tupleNumber?.version?.txn) + assertEquals(deleteTxn, tombstone.properties.xyz.guid?.tupleNumber?.version?.value) } } diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/HistoryUuidTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/HistoryUuidTest.kt index 8af9763c1..ceabbfa8d 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/HistoryUuidTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/HistoryUuidTest.kt @@ -11,7 +11,6 @@ import naksha.model.request.WriteRequest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull -import kotlin.test.assertNull class HistoryUuidTest: PgTestBase(NakshaCollection( id = "history_puuid_test_collection", @@ -50,9 +49,9 @@ class HistoryUuidTest: PgTestBase(NakshaCollection( // Then: assertEquals(3, featureVersions.size) - val retrievedCreatedFeature = featureVersions.find { it.properties.xyz.action == Action.CREATED }!! - val retrievedUpdatedFeature = featureVersions.find { it.properties.xyz.action == Action.UPDATED }!! - val retrievedDeletedFeature = featureVersions.find { it.properties.xyz.action == Action.DELETED }!! + val retrievedCreatedFeature = featureVersions.find { it.properties.xyz.action == Action.CREATE }!! + val retrievedUpdatedFeature = featureVersions.find { it.properties.xyz.action == Action.UPDATE }!! + val retrievedDeletedFeature = featureVersions.find { it.properties.xyz.action == Action.DELETE }!! // And: assertNotNull(retrievedCreatedFeature.properties.xyz.uuid) @@ -97,9 +96,9 @@ class HistoryUuidTest: PgTestBase(NakshaCollection( // Then: assertEquals(3, featureVersions.size) - val retrievedCreatedFeature = featureVersions.find { it.properties.xyz.action == Action.CREATED }!! - val retrievedUpsertedFeature = featureVersions.find { it.properties.xyz.action == Action.UPDATED }!! - val retrievedDeletedFeature = featureVersions.find { it.properties.xyz.action == Action.DELETED }!! + val retrievedCreatedFeature = featureVersions.find { it.properties.xyz.action == Action.CREATE }!! + val retrievedUpsertedFeature = featureVersions.find { it.properties.xyz.action == Action.UPDATE }!! + val retrievedDeletedFeature = featureVersions.find { it.properties.xyz.action == Action.DELETE }!! // And: assertNotNull(retrievedCreatedFeature.properties.xyz.uuid) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/InsertFeatureTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/InsertFeatureTest.kt index e0db8d232..89e43290f 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/InsertFeatureTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/InsertFeatureTest.kt @@ -54,7 +54,7 @@ class InsertFeatureTest : PgTestBase() { retrievedXyz .hasProperty("appId", PgTest.TEST_APP_ID) .hasProperty("author", PgTest.TEST_APP_AUTHOR) - .hasProperty("action", Action.CREATED.text) + .hasProperty("action", Action.CREATE.text) } .hasTags(TagList("wicked")) } @@ -129,7 +129,7 @@ class InsertFeatureTest : PgTestBase() { retrievedXyz .hasProperty("appId", PgTest.TEST_APP_ID) .hasProperty("author", PgTest.TEST_APP_AUTHOR) - .hasProperty("action", Action.CREATED.text) + .hasProperty("action", Action.CREATE.text) } .hasTags(TagList("wicked")) } @@ -229,7 +229,7 @@ class InsertFeatureTest : PgTestBase() { retrievedXyz .hasProperty("appId", PgTest.TEST_APP_ID) .hasProperty("author", PgTest.TEST_APP_AUTHOR) - .hasProperty("action", Action.CREATED.text) + .hasProperty("action", Action.CREATE.text) } } } diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesAll.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesAll.kt index 40421a105..fcc462c37 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesAll.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesAll.kt @@ -1,11 +1,9 @@ package naksha.psql import naksha.model.Action -import naksha.model.objects.NakshaCollection import naksha.model.objects.NakshaFeature import naksha.model.request.* import naksha.model.RandomFeatures.RandomFeatures_C.randomFeatures -import naksha.psql.PgTest.PgTest_C.TEST_MAP_ID import kotlin.test.* class ReadFeaturesAll : PgTestBase() { @@ -29,7 +27,7 @@ class ReadFeaturesAll : PgTestBase() { for (feature in writeFeaturesResp.features) { assertNotNull(feature) assertNull(allFeatures[feature.id]) - assertEquals(Action.CREATED, feature.properties.xyz.action) + assertEquals(Action.CREATE, feature.properties.xyz.action) allFeatures[feature.id] = feature } } diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByMetadataTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByMetadataTest.kt index c0fbf7fb8..4aaece5ea 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByMetadataTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByMetadataTest.kt @@ -426,7 +426,7 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { queryHistory = true queryDeleted = true query = RequestQuery().apply { - metadata = MetaQuery(MetaColumn.action(), DoubleOp.NE, Action.CREATED.intValue) + metadata = MetaQuery(MetaColumn.action(), DoubleOp.NE, Action.CREATE.intValue) } } val response = executeRead(getHistoryWithoutUpdates) @@ -435,7 +435,7 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { // Then: We only got DELETED state assertEquals(1, retrievedFeatures.size) val singleRetrievedHistoryFeature = retrievedFeatures[0]!! - assertEquals(Action.DELETED, singleRetrievedHistoryFeature.properties.xyz.action) + assertEquals(Action.DELETE, singleRetrievedHistoryFeature.properties.xyz.action) } private fun insertFeatureAndGetXyz(feature: NakshaFeature): XyzNs { diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByOtherTns.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByOtherTns.kt index 12ce04631..7cf2ac783 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByOtherTns.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByOtherTns.kt @@ -40,7 +40,7 @@ class ReadFeaturesByOtherTns : PgTestBase( val updateResp = executeWrite(update) // And: the shared `next_version` of all updated features (all 5 updates ran in one transaction). - val updatedVersion: Int64 = updateResp.features[0]!!.tupleNumber.version.txn + val updatedVersion: Int64 = updateResp.features[0]!!.tupleNumber.version.value // When: querying for features whose `next_version` matches that version val nextVersionQuery = MetaQuery( diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadHistoryTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadHistoryTest.kt index 8983f2882..0819fc387 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadHistoryTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadHistoryTest.kt @@ -31,7 +31,7 @@ class ReadHistoryTest : PgTestBase() { for (feature in writeFeaturesResp.features) { assertNotNull(feature) assertNull(allFeatures[feature.id]) - assertEquals(Action.CREATED, feature.properties.xyz.action) + assertEquals(Action.CREATE, feature.properties.xyz.action) allFeatures[feature.id] = feature } } @@ -40,7 +40,7 @@ class ReadHistoryTest : PgTestBase() { fun checkSingleFeatureHistory() { // Pick one feature val createdFeature = allFeatures.firstNotNullOf { it.value } - assertEquals(Action.CREATED, createdFeature.properties.xyz.guid?.tupleNumber?.action) + assertEquals(Action.CREATE, createdFeature.properties.xyz.guid?.tupleNumber?.action) val featureId = createdFeature.id // Update it. @@ -52,7 +52,7 @@ class ReadHistoryTest : PgTestBase() { assertEquals(1, features.size) updatedFeature1 = assertNotNull(features.first()) assertEquals(featureId, updatedFeature1.id) - assertEquals(Action.UPDATED, updatedFeature1.properties.xyz.guid?.tupleNumber?.action) + assertEquals(Action.UPDATE, updatedFeature1.properties.xyz.guid?.tupleNumber?.action) } // Update it a second time. @@ -64,7 +64,7 @@ class ReadHistoryTest : PgTestBase() { assertEquals(1, features.size) updatedFeature2 = assertNotNull(features.first()) assertEquals(featureId, updatedFeature2.id) - assertEquals(Action.UPDATED, updatedFeature2.properties.xyz.guid?.tupleNumber?.action) + assertEquals(Action.UPDATE, updatedFeature2.properties.xyz.guid?.tupleNumber?.action) } // Delete it. @@ -75,7 +75,7 @@ class ReadHistoryTest : PgTestBase() { assertEquals(1, features.size) deletedFeature = assertNotNull(features.first()) assertEquals(featureId, deletedFeature.id) - assertEquals(Action.DELETED, deletedFeature.properties.xyz.guid?.tupleNumber?.action) + assertEquals(Action.DELETE, deletedFeature.properties.xyz.guid?.tupleNumber?.action) } // Clear cache, and read the history of the feature. @@ -97,25 +97,25 @@ class ReadHistoryTest : PgTestBase() { val create = assertNotNull(features[3]) assertEquals(featureId, delete.id) - assertEquals(Action.DELETED, delete.properties.xyz.action) - assertEquals(Action.DELETED, delete.properties.xyz.guid?.tupleNumber?.action) + assertEquals(Action.DELETE, delete.properties.xyz.action) + assertEquals(Action.DELETE, delete.properties.xyz.guid?.tupleNumber?.action) assertEquals(delete.properties.xyz.nguid, delete.properties.xyz.guid) assertEquals(featureId, update2.id) - assertEquals(Action.UPDATED, update2.properties.xyz.action) - assertEquals(Action.UPDATED, update2.properties.xyz.guid?.tupleNumber?.action) + assertEquals(Action.UPDATE, update2.properties.xyz.action) + assertEquals(Action.UPDATE, update2.properties.xyz.guid?.tupleNumber?.action) assertEquals("second_update", update2.properties[ALIAS]) assertEquals(update2.properties.xyz.nguid, delete.properties.xyz.guid) assertEquals(featureId, update1.id) - assertEquals(Action.UPDATED, update1.properties.xyz.action) - assertEquals(Action.UPDATED, update1.properties.xyz.guid?.tupleNumber?.action) + assertEquals(Action.UPDATE, update1.properties.xyz.action) + assertEquals(Action.UPDATE, update1.properties.xyz.guid?.tupleNumber?.action) assertEquals("first_update", update1.properties[ALIAS]) assertEquals(update1.properties.xyz.nguid, update2.properties.xyz.guid) assertEquals(featureId, create.id) - assertEquals(Action.CREATED, create.properties.xyz.action) - assertEquals(Action.CREATED, create.properties.xyz.guid?.tupleNumber?.action) + assertEquals(Action.CREATE, create.properties.xyz.action) + assertEquals(Action.CREATE, create.properties.xyz.guid?.tupleNumber?.action) assertNull(create.properties[ALIAS]) assertEquals(create.properties.xyz.nguid, update1.properties.xyz.guid) } @@ -136,10 +136,10 @@ class ReadHistoryTest : PgTestBase() { val update2 = assertNotNull(features[1]) assertEquals(featureId, delete.id) - assertEquals(Action.DELETED, delete.properties.xyz.action) + assertEquals(Action.DELETE, delete.properties.xyz.action) assertEquals(delete.properties.xyz.nguid, delete.properties.xyz.guid) - assertEquals(Action.UPDATED, update2.properties.xyz.action) + assertEquals(Action.UPDATE, update2.properties.xyz.action) } executeRead(ReadFeatures().apply { @@ -158,10 +158,10 @@ class ReadHistoryTest : PgTestBase() { val update1 = assertNotNull(features[1]) assertEquals(featureId, update1.id) - assertEquals(Action.UPDATED, update1.properties.xyz.action) + assertEquals(Action.UPDATE, update1.properties.xyz.action) assertEquals(featureId, update2.id) - assertEquals(Action.UPDATED, update2.properties.xyz.action) + assertEquals(Action.UPDATE, update2.properties.xyz.action) assertEquals(update2.guid, update1.properties.xyz.nguid) } diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadOrderedTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadOrderedTest.kt index 0a8ac66b0..9c8198f71 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadOrderedTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadOrderedTest.kt @@ -1,7 +1,6 @@ package naksha.psql import naksha.model.Action -import naksha.model.objects.NakshaCollection import naksha.model.objects.NakshaFeature import naksha.model.request.OrderBy import naksha.model.request.ReadFeatures @@ -37,7 +36,7 @@ class ReadOrderedTest : PgTestBase() { for (feature in writeFeaturesResp.features) { assertNotNull(feature) assertNull(allFeatures[feature.id]) - assertEquals(Action.CREATED, feature.properties.xyz.action) + assertEquals(Action.CREATE, feature.properties.xyz.action) allFeatures[feature.id] = feature allFeaturesOrderedByIdAsc.add(feature) allFeaturesOrderedByIdDesc.add(feature) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/RecreateAfterDeleteTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/RecreateAfterDeleteTest.kt index 8969ab924..c9458273e 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/RecreateAfterDeleteTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/RecreateAfterDeleteTest.kt @@ -8,7 +8,6 @@ import naksha.model.request.Write import naksha.model.request.WriteRequest import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertNotNull /** * Verifies the auto-purge behaviour when a feature is re-created after deletion. @@ -31,7 +30,7 @@ class RecreateAfterDeleteTest : PgTestBase() { WriteRequest().add(Write().createFeature(collection, NakshaFeature(featureId))) ).features.first()!! assertEquals(featureId, created.id) - assertEquals(Action.CREATED, created.properties.xyz.action) + assertEquals(Action.CREATE, created.properties.xyz.action) assertEquals(1, created.properties.xyz.changeCount) // Step 2: UPDATE — build on the returned created feature so cc is correct @@ -46,7 +45,7 @@ class RecreateAfterDeleteTest : PgTestBase() { featureIds += featureId }).features.first()!! assertEquals(featureId, updated.id) - assertEquals(Action.UPDATED, updated.properties.xyz.action) + assertEquals(Action.UPDATE, updated.properties.xyz.action) // Step 3: DELETE — tombstone now lives in HEAD executeWrite( @@ -61,7 +60,7 @@ class RecreateAfterDeleteTest : PgTestBase() { queryDeleted = true }).features.first()!! assertEquals(featureId, deleted.id) - assertEquals(Action.DELETED, deleted.properties.xyz.action) + assertEquals(Action.DELETE, deleted.properties.xyz.action) // Confirm feature is invisible in a normal read Naksha.cache.clear() @@ -79,7 +78,7 @@ class RecreateAfterDeleteTest : PgTestBase() { })) ).features.first()!! assertEquals(featureId, recreated.id) - assertEquals(Action.CREATED, recreated.properties.xyz.action) + assertEquals(Action.CREATE, recreated.properties.xyz.action) // cc resets to 1 for the new lifecycle assertEquals(1, recreated.properties.xyz.changeCount) @@ -91,7 +90,7 @@ class RecreateAfterDeleteTest : PgTestBase() { featureIds += featureId }) assertEquals(1, found.features.size) - assertEquals(Action.CREATED, found.features[0]!!.properties.xyz.action) + assertEquals(Action.CREATE, found.features[0]!!.properties.xyz.action) // queryHistory=true returns current HEAD (live CREATED) + all history entries. // History after auto-purge: DELETED (archived tombstone), UPDATED, CREATED (old lifecycle). @@ -104,10 +103,10 @@ class RecreateAfterDeleteTest : PgTestBase() { versions = 10 }) assertEquals(4, historyOnly.features.size) - assertEquals(Action.CREATED, historyOnly.features[0]!!.properties.xyz.action) // new HEAD - assertEquals(Action.DELETED, historyOnly.features[1]!!.properties.xyz.action) // archived tombstone - assertEquals(Action.UPDATED, historyOnly.features[2]!!.properties.xyz.action) - assertEquals(Action.CREATED, historyOnly.features[3]!!.properties.xyz.action) + assertEquals(Action.CREATE, historyOnly.features[0]!!.properties.xyz.action) // new HEAD + assertEquals(Action.DELETE, historyOnly.features[1]!!.properties.xyz.action) // archived tombstone + assertEquals(Action.UPDATE, historyOnly.features[2]!!.properties.xyz.action) + assertEquals(Action.CREATE, historyOnly.features[3]!!.properties.xyz.action) // queryHistory + queryDeleted: same result — no tombstone in HEAD (was auto-purged), // so queryDeleted=true adds nothing here. @@ -120,9 +119,9 @@ class RecreateAfterDeleteTest : PgTestBase() { versions = 10 }) assertEquals(4, full.features.size) - assertEquals(Action.CREATED, full.features[0]!!.properties.xyz.action) - assertEquals(Action.DELETED, full.features[1]!!.properties.xyz.action) - assertEquals(Action.UPDATED, full.features[2]!!.properties.xyz.action) - assertEquals(Action.CREATED, full.features[3]!!.properties.xyz.action) + assertEquals(Action.CREATE, full.features[0]!!.properties.xyz.action) + assertEquals(Action.DELETE, full.features[1]!!.properties.xyz.action) + assertEquals(Action.UPDATE, full.features[2]!!.properties.xyz.action) + assertEquals(Action.CREATE, full.features[3]!!.properties.xyz.action) } } diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/TupleNumberPersistenceTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/TupleNumberPersistenceTest.kt index 4714792d7..0f6329f19 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/TupleNumberPersistenceTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/TupleNumberPersistenceTest.kt @@ -104,8 +104,8 @@ class TupleNumberPersistenceTest : PgTestBase(collection = null, mapId = "") { featureTuples.filterNotNull().forEach { featureTuple -> val tuple = featureTuple.tuple assertNotNull(tuple) - assertEquals(featuresById[featureTuple.id]?.id, tuple.getStringMember(naksha.model.objects.StandardMembers.Id)) - assertEquals(Action.CREATED, featureTuple.tupleNumber.action) + assertEquals(featuresById[featureTuple.id]?.id, tuple.getString(naksha.model.objects.StandardMembers.Id)) + assertEquals(Action.CREATE, featureTuple.tupleNumber.action) } } } \ No newline at end of file diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/UpdateFeatureTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/UpdateFeatureTest.kt index 0f7b02fae..b6491eaa4 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/UpdateFeatureTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/UpdateFeatureTest.kt @@ -57,7 +57,7 @@ class UpdateFeatureTest : PgTestBase(collection = null, mapId = "") { retrievedXyz .hasProperty("appId", PgTest.TEST_APP_ID) .hasProperty("author", PgTest.TEST_APP_AUTHOR) - .hasProperty("action", Action.UPDATED.text) + .hasProperty("action", Action.UPDATE.text) .hasProperty("changeCount", 2) } } @@ -103,31 +103,31 @@ class UpdateFeatureTest : PgTestBase(collection = null, mapId = "") { assertNotNull(retrievedTuples) assertEquals(2, retrievedTuples.size) - val createdTuple = retrievedTuples.first { Action.fromValue((it.getLongMember(naksha.model.objects.StandardMembers.Version).toInt() and 3) ?: -1) == Action.CREATED } - val updatedTuple = retrievedTuples.first { Action.fromValue((it.getLongMember(naksha.model.objects.StandardMembers.Version).toInt() and 3) ?: -1) == Action.UPDATED } + val createdTuple = retrievedTuples.first { Action.fromValue((it.getLong(naksha.model.objects.StandardMembers.Version).toInt() and 3) ?: -1) == Action.CREATE } + val updatedTuple = retrievedTuples.first { Action.fromValue((it.getLong(naksha.model.objects.StandardMembers.Version).toInt() and 3) ?: -1) == Action.UPDATE } // Then assertNotEquals(updatedTuple.tupleNumber.version, createdTuple.tupleNumber.version) - assertEquals(createdTuple.getLongMember(naksha.model.objects.StandardMembers.NextVersion), updatedTuple.tupleNumber.version.txn) - assertNull(updatedTuple.getLongMember(naksha.model.objects.StandardMembers.NextVersion, Int64(-1L)).let { if (it == Int64(-1L)) null else it }) + assertEquals(createdTuple.getLong(naksha.model.objects.StandardMembers.NextVersion), updatedTuple.tupleNumber.version.value) + assertNull(updatedTuple.getLong(naksha.model.objects.StandardMembers.NextVersion, Int64(-1L)).let { if (it == Int64(-1L)) null else it }) // Both tuples share the collection's feature encoding (action lives in version bits). assertEquals(createdTuple.dataEncoding, updatedTuple.dataEncoding) - assertEquals(1, createdTuple.getIntMember(naksha.model.objects.StandardMembers.ChangeCountXyz)) - assertEquals(2, updatedTuple.getIntMember(naksha.model.objects.StandardMembers.ChangeCountXyz)) + assertEquals(1, createdTuple.getInt(naksha.model.objects.StandardMembers.ChangeCountXyz)) + assertEquals(2, updatedTuple.getInt(naksha.model.objects.StandardMembers.ChangeCountXyz)) assertEquals(createdTuple.getByteArray(naksha.model.objects.StandardMembers.Geometry), updatedTuple.getByteArray(naksha.model.objects.StandardMembers.Geometry)) - assertEquals(createdTuple.getStringMember(naksha.model.objects.StandardMembers.XyzTags), updatedTuple.getStringMember(naksha.model.objects.StandardMembers.XyzTags)) - assertNotEquals(createdTuple.feature, updatedTuple.feature) + assertEquals(createdTuple.getString(naksha.model.objects.StandardMembers.XyzTags), updatedTuple.getString(naksha.model.objects.StandardMembers.XyzTags)) + assertNotEquals(createdTuple.jbonBytes, updatedTuple.jbonBytes) assertEquals(createdTuple.getByteArray(naksha.model.objects.StandardMembers.ReferencePoint), updatedTuple.getByteArray(naksha.model.objects.StandardMembers.ReferencePoint)) - assertNull(createdTuple.toNakshaFeature()?.properties["new_attr"]) - assertEquals("some_value", updatedTuple.toNakshaFeature()?.properties["new_attr"]) - assertEquals(createdTuple.getLongMember(naksha.model.objects.StandardMembers.CreatedAtXyz)?.let { if (it == Int64(0L)) null else it } ?: createdTuple.getLongMember(naksha.model.objects.StandardMembers.XyzUpdatedAt), updatedTuple.getLongMember(naksha.model.objects.StandardMembers.CreatedAtXyz)?.let { if (it == Int64(0L)) null else it }) - assertNotEquals(updatedTuple.getLongMember(naksha.model.objects.StandardMembers.CreatedAtXyz), updatedTuple.getLongMember(naksha.model.objects.StandardMembers.XyzUpdatedAt)) - assertNull(createdTuple.getLongMember(naksha.model.objects.StandardMembers.CreatedAtXyz)?.let { if (it == Int64(0L)) null else it }) - assertNotNull(createdTuple.getLongMember(naksha.model.objects.StandardMembers.XyzUpdatedAt)) - assertEquals(createdTuple.getIntMember(naksha.model.objects.StandardMembers.HereTileXyz), updatedTuple.getIntMember(naksha.model.objects.StandardMembers.HereTileXyz)) - assertEquals(Action.UPDATED, updatedTuple.tupleNumber.action) - assertEquals(Action.CREATED, createdTuple.tupleNumber.action) - assertNotEquals(createdTuple.getLongMember(naksha.model.objects.StandardMembers.XyzAuthorTimestamp), updatedTuple.getLongMember(naksha.model.objects.StandardMembers.XyzAuthorTimestamp)) + assertNull(createdTuple.decodeFeature()?.properties["new_attr"]) + assertEquals("some_value", updatedTuple.decodeFeature()?.properties["new_attr"]) + assertEquals(createdTuple.getLong(naksha.model.objects.StandardMembers.CreatedAtXyz)?.let { if (it == Int64(0L)) null else it } ?: createdTuple.getLong(naksha.model.objects.StandardMembers.XyzUpdatedAt), updatedTuple.getLong(naksha.model.objects.StandardMembers.CreatedAtXyz)?.let { if (it == Int64(0L)) null else it }) + assertNotEquals(updatedTuple.getLong(naksha.model.objects.StandardMembers.CreatedAtXyz), updatedTuple.getLong(naksha.model.objects.StandardMembers.XyzUpdatedAt)) + assertNull(createdTuple.getLong(naksha.model.objects.StandardMembers.CreatedAtXyz)?.let { if (it == Int64(0L)) null else it }) + assertNotNull(createdTuple.getLong(naksha.model.objects.StandardMembers.XyzUpdatedAt)) + assertEquals(createdTuple.getInt(naksha.model.objects.StandardMembers.HereTileXyz), updatedTuple.getInt(naksha.model.objects.StandardMembers.HereTileXyz)) + assertEquals(Action.UPDATE, updatedTuple.tupleNumber.action) + assertEquals(Action.CREATE, createdTuple.tupleNumber.action) + assertNotEquals(createdTuple.getLong(naksha.model.objects.StandardMembers.XyzAuthorTimestamp), updatedTuple.getLong(naksha.model.objects.StandardMembers.XyzAuthorTimestamp)) } @Test diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/UpsertFeatureTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/UpsertFeatureTest.kt index 758df1671..abb709e4f 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/UpsertFeatureTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/UpsertFeatureTest.kt @@ -1,13 +1,11 @@ package naksha.psql import naksha.model.Action -import naksha.model.objects.NakshaCollection import naksha.model.objects.NakshaFeature import naksha.model.request.ReadFeatures import naksha.model.request.SuccessResponse import naksha.model.request.Write import naksha.model.request.WriteRequest -import naksha.psql.PgTest.PgTest_C.TEST_MAP_ID import naksha.psql.assertions.NakshaFeatureFluidAssertions.Companion.assertThatFeature import kotlin.test.Test import kotlin.test.assertEquals @@ -41,7 +39,7 @@ class UpsertFeatureTest : PgTestBase() { collectionIds += collection.id featureIds += initialFeature.id queryHistory = true - }).features.sortedBy { it!!.properties.xyz.version!!.txn.toLong() } + }).features.sortedBy { it!!.properties.xyz.version!!.value.toLong() } // Then assertThatFeature(retrievedFeatures[0]!!) @@ -54,7 +52,7 @@ class UpsertFeatureTest : PgTestBase() { .hasFeatureType(initialFeature.properties.featureType) .hasXyzThat { retrievedXyz -> retrievedXyz - .hasProperty("action", Action.CREATED.text) + .hasProperty("action", Action.CREATE.text) .hasProperty("changeCount", 1) } } @@ -69,7 +67,7 @@ class UpsertFeatureTest : PgTestBase() { .hasFeatureType(initialFeature.properties.featureType) .hasXyzThat { retrievedXyz -> retrievedXyz - .hasProperty("action", Action.UPDATED.text) + .hasProperty("action", Action.UPDATE.text) .hasProperty("changeCount", 2) } } @@ -105,7 +103,7 @@ class UpsertFeatureTest : PgTestBase() { // TODO: only the first one is updated response.features.forEach { feature -> assertNotNull(feature) - assertEquals(Action.UPDATED, feature.properties.xyz.action) + assertEquals(Action.UPDATE, feature.properties.xyz.action) } } } \ No newline at end of file diff --git a/here-naksha-lib-view/src/jvmTest/java/com/here/naksha/lib/view/ViewTest.java b/here-naksha-lib-view/src/jvmTest/java/com/here/naksha/lib/view/ViewTest.java index 911cc63fe..c3ed90831 100644 --- a/here-naksha-lib-view/src/jvmTest/java/com/here/naksha/lib/view/ViewTest.java +++ b/here-naksha-lib-view/src/jvmTest/java/com/here/naksha/lib/view/ViewTest.java @@ -58,7 +58,6 @@ import naksha.base.StringList; import naksha.model.Action; import naksha.model.IReadSession; -import naksha.model.ISession; import naksha.model.IStorage; import naksha.model.IWriteSession; import naksha.model.NakshaContext; @@ -78,7 +77,6 @@ import naksha.model.request.query.PQuery; import naksha.model.request.query.Property; import naksha.model.request.query.StringOp; -import naksha.model.util.CustomStoragePropertiesUtil; import naksha.model.util.RequestHelper; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -148,14 +146,14 @@ void testWriteApiNotation() { request.add(write.createFeature(TEST_MAP_ID, "", feature)); // when(storage.tupleToFeature(any())).thenReturn(feature); - Response success = new SuccessResponse(sampleXyzWriteResponse(1, Action.CREATED)); + Response success = new SuccessResponse(sampleXyzWriteResponse(1, Action.CREATE)); when(session.execute(request)).thenReturn(success); ViewWriteSession writeSession = view.newWriteSession(sessionOptions); Response response = writeSession.execute(request); assertInstanceOf(SuccessResponse.class, response); SuccessResponse successResponse = (SuccessResponse) response; assertEquals(feature.getId(), successResponse.getFeatures().get(0).getId()); - assertEquals(Action.CREATED, successResponse.getFeatureTupleList().get(0).getFeature().getProperties().getXyz().getAction()); + assertEquals(Action.CREATE, successResponse.getFeatureTupleList().get(0).getFeature().getProperties().getXyz().getAction()); writeSession.commit(); } @@ -173,7 +171,7 @@ void testDeleteApiNotation() { final WriteRequest request = new WriteRequest(); final NakshaFeature feature = new NakshaFeature("0"); request.add(write.deleteFeatureById(topologiesDS.getMapId(), topologiesDS.getCollectionId(), feature.getId())); - SuccessResponse successResponse1 = new SuccessResponse(sampleXyzWriteResponse(1, Action.DELETED)); + SuccessResponse successResponse1 = new SuccessResponse(sampleXyzWriteResponse(1, Action.DELETE)); when(session.execute(request)).thenReturn(successResponse1); ViewWriteSession writeSession = view.newWriteSession(sessionOptions); @@ -181,7 +179,7 @@ void testDeleteApiNotation() { assertInstanceOf(SuccessResponse.class, response); SuccessResponse successResponse = (SuccessResponse) response; assertEquals(feature.getId(), successResponse.getFeatureTupleList().get(0).getId()); - assertEquals(Action.DELETED, successResponse.getFeatureTupleList().get(0).getFeature().getProperties().getXyz().getAction()); + assertEquals(Action.DELETE, successResponse.getFeatureTupleList().get(0).getFeature().getProperties().getXyz().getAction()); writeSession.commit(); } diff --git a/here-naksha-lib-view/src/jvmTest/java/com/here/naksha/lib/view/ViewWriteSessionTests.java b/here-naksha-lib-view/src/jvmTest/java/com/here/naksha/lib/view/ViewWriteSessionTests.java index 61b01b785..e349eef86 100644 --- a/here-naksha-lib-view/src/jvmTest/java/com/here/naksha/lib/view/ViewWriteSessionTests.java +++ b/here-naksha-lib-view/src/jvmTest/java/com/here/naksha/lib/view/ViewWriteSessionTests.java @@ -111,7 +111,7 @@ void readAndWrite_UsingViewWriteSession() { assertEquals(1d, ((PointCoord) feature.getGeometry().getCoordinates()).getLongitude()); assertTrue(feature.getProperties().containsKey("testProperty")); assertEquals("test", feature.getProperties().getPath("testProperty").toString()); - assertSame(Action.UPDATED, response1.getFeatureTupleList().get(0).tuple.version.action()); + assertSame(Action.UPDATE, response1.getFeatureTupleList().get(0).tuple.version.action()); writeSession.commit(); @@ -153,7 +153,7 @@ void updateNonExistentFeatureCreatesFeature() { SuccessResponse response = (SuccessResponse) writeSession.execute(writeRequest); //THEN assertNotNull(response.getFeatureTupleList().get(0)); - assertSame(Action.CREATED, response.getFeatureTupleList().get(0).tuple.version.action()); + assertSame(Action.CREATE, response.getFeatureTupleList().get(0).tuple.version.action()); writeSession.commit(); //GIVEN Verify the feature was actually created in the top layer (collection_0) @@ -208,7 +208,7 @@ void writeFeatureOnSelectedLayer() { SuccessResponse response = (SuccessResponse) writeSession.execute(writeRequest); assertNotNull(response.getFeatureTupleList().get(0)); - assertSame(Action.CREATED, response.getFeatureTupleList().get(0).tuple.version.action()); + assertSame(Action.CREATE, response.getFeatureTupleList().get(0).tuple.version.action()); writeSession.commit(); //check if the newly added feature found on layer @@ -250,7 +250,7 @@ void deleteFeatureFromTopLayer() { Tuple tuple = featureTuple.tuple; assertNotNull(tuple); - assertSame(Action.DELETED, tuple.version.action()); + assertSame(Action.DELETE, tuple.version.action()); assertEquals("feature_id_view1", featureTuple.getId()); // TODO: Ones we have the loadTuples available, do: From 64abbf4cdd2a37e3ac372962d8f2df19aab14fde Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Fri, 12 Jun 2026 14:08:33 +0200 Subject: [PATCH 12/57] Fix compilation errors that were the result of the previous modifications, still some left. Signed-off-by: Alexander Lowey-Weber --- .../cli/copy/service/CopyServiceTest.java | 6 +- .../lib/handlers/DefaultStorageHandler.java | 2 +- .../lib/handlers/util/RequestTypesUtil.java | 4 +- .../handlers/DefaultStorageHandlerTest.java | 24 ++--- .../lib/hub/mock/NHAdminWriterMock.java | 2 +- .../storages/NHSpaceStorageWriterTest.java | 11 +-- .../commonMain/kotlin/naksha/model/Action.kt | 6 +- .../kotlin/naksha/model/IStorage.kt | 4 +- .../commonMain/kotlin/naksha/model/Naksha.kt | 67 +++++++------- .../commonMain/kotlin/naksha/model/Tuple.kt | 13 ++- .../kotlin/naksha/model/TupleCache.kt | 6 +- .../kotlin/naksha/model/TupleNumber.kt | 34 +++---- .../naksha/model/TupleNumberBinaryArray.kt | 4 +- .../kotlin/naksha/model/TupleNumberList.kt | 7 +- .../commonMain/kotlin/naksha/model/Version.kt | 81 +++++++++-------- .../kotlin/naksha/model/objects/IndexList.kt | 22 ++++- .../naksha/model/objects/NakshaCollection.kt | 52 ++++++++++- .../naksha/model/objects/NakshaFeature.kt | 2 +- .../kotlin/naksha/model/objects/NakshaMap.kt | 12 +-- .../kotlin/naksha/model/objects/NakshaTx.kt | 2 +- .../kotlin/naksha/model/objects/XyzIndices.kt | 3 + .../naksha/model/request/FeatureTuple.kt | 5 +- .../naksha/model/request/PropertyFilter.kt | 5 +- .../naksha/model/request/ReadCollections.kt | 3 +- .../kotlin/naksha/model/request/ReadMaps.kt | 4 +- .../naksha/model/request/ReadTransactions.kt | 4 +- .../kotlin/naksha/model/request/Write.kt | 91 ++++++++++--------- .../naksha/model/TupleNumberQueryTest.kt | 2 +- .../kotlin/naksha/model/TupleNumberTest.kt | 2 +- .../kotlin/naksha/model/TupleHeapCache.jvm.kt | 4 +- .../commonMain/kotlin/naksha/psql/LibPsql.kt | 6 +- .../kotlin/naksha/psql/PgAdminMap.kt | 45 +++++---- .../kotlin/naksha/psql/PgCollection.kt | 6 +- .../kotlin/naksha/psql/PgColumnRows.kt | 84 ++--------------- .../commonMain/kotlin/naksha/psql/PgCursor.kt | 6 ++ .../commonMain/kotlin/naksha/psql/PgMap.kt | 58 +++++++----- .../kotlin/naksha/psql/PgNakshaBooks.kt | 4 +- .../kotlin/naksha/psql/PgNakshaCatalogs.kt | 4 +- .../kotlin/naksha/psql/PgNakshaCollections.kt | 2 +- .../naksha/psql/PgNakshaTransactions.kt | 8 +- .../kotlin/naksha/psql/PgQueryWhereBuilder.kt | 4 +- .../kotlin/naksha/psql/PgSession.kt | 2 +- .../kotlin/naksha/psql/PgStorage.kt | 2 + .../commonMain/kotlin/naksha/psql/PgUtil.kt | 20 ++-- .../commonMain/kotlin/naksha/psql/PgWrite.kt | 10 +- .../kotlin/naksha/psql/PgWriterDelete.kt | 4 +- .../kotlin/naksha/psql/PgWriterUpdate.kt | 2 +- .../kotlin/naksha/psql/AttachmentTest.kt | 4 +- .../kotlin/naksha/psql/CollectionTests.kt | 5 +- .../naksha/psql/TupleNumberPersistenceTest.kt | 2 +- .../kotlin/naksha/psql/UpsertFeatureTest.kt | 2 +- .../jsMain/kotlin/naksha/psql/Plv8Cursor.kt | 4 + .../jvmMain/kotlin/naksha/psql/PsqlCursor.kt | 2 +- .../src/jvmMain/kotlin/naksha/psql/PsqlMap.kt | 5 - 54 files changed, 396 insertions(+), 379 deletions(-) diff --git a/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/copy/service/CopyServiceTest.java b/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/copy/service/CopyServiceTest.java index d43c9a1f8..36a63df69 100644 --- a/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/copy/service/CopyServiceTest.java +++ b/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/copy/service/CopyServiceTest.java @@ -839,8 +839,8 @@ private StorageProvider createStorageProvider(IStorage srcStorage, IStorage targ } private void assertCreateMapWrite(Write write) { - assertEquals(Naksha.ADMIN_MAP, write.getMapId()); - assertEquals(Naksha.CATALOGS_COL, write.getCollectionId()); + assertEquals(Naksha.ADMIN_CATALOG_ID, write.getMapId()); + assertEquals(Naksha.CATALOGS_COL_ID, write.getCollectionId()); assertEquals(WriteOp.CREATE, write.getOp()); assertNotNull(write.getFeature()); assertEquals(targetCopyElement.getMapId(), write.getFeature().getId()); @@ -848,7 +848,7 @@ private void assertCreateMapWrite(Write write) { private void assertCreateCollectionWrite(Write write) { assertEquals(targetCopyElement.getMapId(), write.getMapId()); - assertEquals(Naksha.COLLECTIONS_COL, write.getCollectionId()); + assertEquals(Naksha.ADMIN_COL_ID, write.getCollectionId()); assertEquals(WriteOp.CREATE, write.getOp()); assertNotNull(write.getFeature()); assertEquals(targetCopyElement.getCollectionId(), write.getFeature().getId()); diff --git a/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/DefaultStorageHandler.java b/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/DefaultStorageHandler.java index ce16f058b..624a6ae48 100644 --- a/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/DefaultStorageHandler.java +++ b/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/DefaultStorageHandler.java @@ -535,7 +535,7 @@ private void applyMapIdAndCollectionId( collectionFromRequest.setId(collectionId); }); } - String finalCollectionId = isOnlyWriteCollections(wr) ? Naksha.COLLECTIONS_COL : collectionId; + String finalCollectionId = isOnlyWriteCollections(wr) ? Naksha.ADMIN_COL_ID : collectionId; wr.getWrites().forEach(write -> { write.setMapId(mapId); write.setCollectionId(finalCollectionId); diff --git a/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/util/RequestTypesUtil.java b/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/util/RequestTypesUtil.java index ed42b8899..3406fae47 100644 --- a/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/util/RequestTypesUtil.java +++ b/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/util/RequestTypesUtil.java @@ -38,7 +38,7 @@ public static boolean isOnlyWriteFeatures(@NotNull Request request) { for (Write write : ((WriteRequest) request).getWrites()) { // A Write operation onto the virtual "naksha~collections" means that it is a write request for // NakshaCollection - if (Naksha.COLLECTIONS_COL.equals(write.getCollectionId())) return false; + if (Naksha.ADMIN_COL_ID.equals(write.getCollectionId())) return false; } return true; } @@ -53,7 +53,7 @@ public static boolean isOnlyWriteCollections(Request request) { for (Write write : writes) { // A Write operation onto the virtual "naksha~collections" means that it is a write request for // NakshaCollection - if (!Naksha.COLLECTIONS_COL.equals(write.getCollectionId())) return false; + if (!Naksha.ADMIN_COL_ID.equals(write.getCollectionId())) return false; } return true; } diff --git a/here-naksha-lib-handlers/src/jvmTest/java/com/here/naksha/lib/handlers/DefaultStorageHandlerTest.java b/here-naksha-lib-handlers/src/jvmTest/java/com/here/naksha/lib/handlers/DefaultStorageHandlerTest.java index 801ab0ecd..e1c7d4ce8 100644 --- a/here-naksha-lib-handlers/src/jvmTest/java/com/here/naksha/lib/handlers/DefaultStorageHandlerTest.java +++ b/here-naksha-lib-handlers/src/jvmTest/java/com/here/naksha/lib/handlers/DefaultStorageHandlerTest.java @@ -180,7 +180,7 @@ void shouldCreateMissingCollectionRespectingPriority(CollectionPriorityTestCase Write writeCollection = findSingleCreateCollectionWrite(capturedWrites); assertEquals(WriteOp.CREATE, writeCollection.getOp()); assertEquals(testCase.correctCollection().getId(), writeCollection.getId()); - assertEquals(Naksha.COLLECTIONS_COL, writeCollection.getCollectionId()); + assertEquals(Naksha.ADMIN_COL_ID, writeCollection.getCollectionId()); // And: write features related to the same feature in correct collection List featureWrites = getSingularWritesToCollection(capturedWrites, testCase.correctCollection().getId()); @@ -321,8 +321,8 @@ void shouldCreateMapIfMissing() { assertEquals(1, secondRequestWrites.size()); Write mapWrite = secondRequestWrites.get(0); assertEquals(WriteOp.CREATE, mapWrite.getOp()); - assertEquals(Naksha.ADMIN_MAP, mapWrite.getMapId()); - assertEquals(Naksha.CATALOGS_COL, mapWrite.getCollectionId()); + assertEquals(Naksha.ADMIN_CATALOG_ID, mapWrite.getMapId()); + assertEquals(Naksha.CATALOGS_COL_ID, mapWrite.getCollectionId()); assertEquals(mapId, mapWrite.getId()); } @@ -393,18 +393,18 @@ void shouldReattemptWriteCollectionsAfterMissingMapByCreatingMap() { // 1st and 3rd are WriteCollections against COLLECTIONS_COL with map from storage props assertTrue(RequestTypesUtil.isOnlyWriteCollections(calls.get(0))); assertTrue(RequestTypesUtil.isOnlyWriteCollections(calls.get(2))); - assertEquals(Naksha.COLLECTIONS_COL, calls.get(0).getWrites().get(0).getCollectionId()); + assertEquals(Naksha.ADMIN_COL_ID, calls.get(0).getWrites().get(0).getCollectionId()); assertEquals(mapIdFromStorageProps, calls.get(0).getWrites().get(0).getMapId()); assertEquals("target_collection", calls.get(0).getWrites().get(0).getFeature().getId()); - assertEquals(Naksha.COLLECTIONS_COL, calls.get(2).getWrites().get(0).getCollectionId()); + assertEquals(Naksha.ADMIN_COL_ID, calls.get(2).getWrites().get(0).getCollectionId()); assertEquals(mapIdFromStorageProps, calls.get(2).getWrites().get(0).getMapId()); assertEquals("target_collection", calls.get(2).getWrites().get(0).getFeature().getId()); // 2nd call is map creation in admin map / maps collection Write mapCreate = calls.get(1).getWrites().get(0); assertEquals(WriteOp.CREATE, mapCreate.getOp()); - assertEquals(Naksha.ADMIN_MAP, mapCreate.getMapId()); - assertEquals(Naksha.CATALOGS_COL, mapCreate.getCollectionId()); + assertEquals(Naksha.ADMIN_CATALOG_ID, mapCreate.getMapId()); + assertEquals(Naksha.CATALOGS_COL_ID, mapCreate.getCollectionId()); assertEquals(mapIdFromStorageProps, mapCreate.getId()); } @@ -461,7 +461,7 @@ void shouldApplyMapIdAndCollectionsColForWriteCollections() { verify(storageWriteSession).execute(captor.capture()); Write write = captor.getValue().getWrites().get(0); assertTrue(RequestTypesUtil.isOnlyWriteCollections(captor.getValue())); - assertEquals(Naksha.COLLECTIONS_COL, write.getCollectionId(), "WriteCollections must target naksa~collections collection"); + assertEquals(Naksha.ADMIN_COL_ID, write.getCollectionId(), "WriteCollections must target naksa~collections collection"); assertEquals(mapIdFromStorageProps, write.getMapId(), "MapId must be taken from storage props"); assertEquals("apply_col", write.getFeature().getId()); } @@ -540,7 +540,7 @@ private IStorage registeredStorageWithConfig(NakshaStorage config) { } private static Write findSingleCreateCollectionWrite(List writeRequests) { - List collectionWrites = getSingularWritesToCollection(writeRequests, Naksha.COLLECTIONS_COL); + List collectionWrites = getSingularWritesToCollection(writeRequests, Naksha.ADMIN_COL_ID); assertEquals(1, collectionWrites.size(), "Expected single collection write"); return collectionWrites.get(0); } @@ -560,7 +560,7 @@ private static Stream flattenSingularWriteRequest(List writ private static ArgumentMatcher matchesCreateCollectionRequest() { return writeRequest -> { WriteList writes = writeRequest.getWrites(); - return writes.size() == 1 && Naksha.COLLECTIONS_COL.equals(writes.get(0).getCollectionId()); + return writes.size() == 1 && Naksha.ADMIN_COL_ID.equals(writes.get(0).getCollectionId()); }; } @@ -753,8 +753,8 @@ private static ArgumentMatcher matchesCreateMapRequest(String mapI } Write w = writes.get(0); return WriteOp.CREATE.equals(w.getOp()) - && Naksha.ADMIN_MAP.equals(w.getMapId()) - && Naksha.CATALOGS_COL.equals(w.getCollectionId()) + && Naksha.ADMIN_CATALOG_ID.equals(w.getMapId()) + && Naksha.CATALOGS_COL_ID.equals(w.getCollectionId()) && mapId.equals(w.getId()); }; } diff --git a/here-naksha-lib-hub/src/jvmTest/java/com/here/naksha/lib/hub/mock/NHAdminWriterMock.java b/here-naksha-lib-hub/src/jvmTest/java/com/here/naksha/lib/hub/mock/NHAdminWriterMock.java index 21e4683a0..37d2140d3 100644 --- a/here-naksha-lib-hub/src/jvmTest/java/com/here/naksha/lib/hub/mock/NHAdminWriterMock.java +++ b/here-naksha-lib-hub/src/jvmTest/java/com/here/naksha/lib/hub/mock/NHAdminWriterMock.java @@ -51,7 +51,7 @@ public NHAdminWriterMock(final @NotNull Map write .hasOp(WriteOp.DELETE) - .hasCollectionId(Naksha.COLLECTIONS_COL) + .hasCollectionId(Naksha.ADMIN_COL_ID) .hasId(CUSTOM_SPACE) ); @@ -132,7 +129,7 @@ void shouldNotTriggerSpaceEntryDeletionWhenPurgingFailed() { assertThatWriteRequest(requestsPassedToPipeline.get(0)) .hasSingleWriteThat(write -> write .hasOp(WriteOp.DELETE) - .hasCollectionId(Naksha.COLLECTIONS_COL) + .hasCollectionId(Naksha.ADMIN_COL_ID) .hasId(CUSTOM_SPACE) ); @@ -162,7 +159,7 @@ void shouldFailWhenSpaceEntryDeletionFailed() { assertThatWriteRequest(requestsPassedToPipeline.get(0)) .hasSingleWriteThat(write -> write .hasOp(WriteOp.DELETE) - .hasCollectionId(Naksha.COLLECTIONS_COL) + .hasCollectionId(Naksha.ADMIN_COL_ID) .hasId(CUSTOM_SPACE) ); @@ -203,7 +200,7 @@ private EventPipeline eventPipelineFailingOn(ArgumentMatcher faili private ArgumentMatcher writeCollectionRequest() { return writeRequest -> { List writes = writeRequest.getWrites(); - return writes.size() == 1 && writes.get(0).getCollectionId().equals(Naksha.COLLECTIONS_COL); + return writes.size() == 1 && writes.get(0).getCollectionId().equals(Naksha.ADMIN_COL_ID); }; } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Action.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Action.kt index 2a3b44659..b1a7cb856 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Action.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Action.kt @@ -10,11 +10,11 @@ import kotlin.reflect.KClass /** * An enumeration about the action that actually was performed for a feature in a storage, being [CREATE], [UPDATE], or [DELETE]. * - * The numeric [intValue] corresponds to the lower two bits of a [Version.value]: + * The numeric [intValue] corresponds to the lower two bits of a [Version.number]: * - `0` ([CREATE]) — the feature was created in this version. * - `1` ([UPDATE]) — the feature was updated in this version. * - `2` ([DELETE]) — the feature was deleted in this version. - * - `3` ([VERSION]) — both action bits are set; used as a sentinel to indicate that the [Version.value] value itself + * - `3` ([VERSION]) — both action bits are set; used as a sentinel to indicate that the [Version.number] value itself * is being used as a version reference rather than encoding a state-change action. * * @since 1.0.0 @@ -79,7 +79,7 @@ class Action : JsEnum() { } /** - * Both action bits are set (`3`). Used as a sentinel to signal that the [Version.value] value + * Both action bits are set (`3`). Used as a sentinel to signal that the [Version.number] value * is a version reference rather than a state-change action. Also returned by [fromValue] for * any unrecognised integer value. * @since 1.0.0 diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/IStorage.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/IStorage.kt index be78135d7..7e8963112 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/IStorage.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/IStorage.kt @@ -13,9 +13,9 @@ import kotlin.js.JsExport /** * Any entity implementing the [IStorage] interface represents some data-sink, and comes with an implementation that grants access to the data. The storage normally is a singleton that opens many sessions in parallel. * - * Storages operate on maps. All storages do have an [administrative map][Naksha.ADMIN_MAP], which can be virtual or real, implementation dependent. In this admin-map the storage exposes and manages the custom maps it stores, the transaction-logs of the storage, and the global dictionaries needed for the [JBON](https://github.com/heremaps/naksha/blob/v3/docs/JBON.md), plus optional implementation specific information. + * Storages operate on maps. All storages do have an [administrative map][Naksha.ADMIN_CATALOG_ID], which can be virtual or real, implementation dependent. In this admin-map the storage exposes and manages the custom maps it stores, the transaction-logs of the storage, and the global dictionaries needed for the [JBON](https://github.com/heremaps/naksha/blob/v3/docs/JBON.md), plus optional implementation specific information. * - * All other maps are custom maps, which are isolated data sinks within the same storage (like an own database schema, an own S3 bucket, an own SQLite database, an own directory or file, aso.). Each custom map is a fully separated storage entity. Within each custom map one [virtual admin collection][Naksha.COLLECTIONS_COL] is exposed, which can be used to manage the collections in the map. Some storages allow to access multiple maps from one session, others may limit a session to a single map, and will reject cross map operations with [NakshaError.UNSUPPORTED_OPERATION]. + * All other maps are custom maps, which are isolated data sinks within the same storage (like an own database schema, an own S3 bucket, an own SQLite database, an own directory or file, aso.). Each custom map is a fully separated storage entity. Within each custom map one [virtual admin collection][Naksha.ADMIN_COL_ID] is exposed, which can be used to manage the collections in the map. Some storages allow to access multiple maps from one session, others may limit a session to a single map, and will reject cross map operations with [NakshaError.UNSUPPORTED_OPERATION]. * * The storage will cache the dictionaries to avoid that just for [Tuple] decoding a new session need to be opened, which would require object creation for every single feature being decoded, therefore every storage implements the [IDictReader] interface, which internally should be attached to a storage local cache, that is automatically kept up-to-date. The same cache can be accessed from every [session][ISession], because every [session][ISession] implements as well the [dictionary-reader interface][IDictReader]. * diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt index d218e7727..75356f95c 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt @@ -4,26 +4,19 @@ package naksha.model import naksha.base.* import naksha.base.Platform.PlatformCompanion.fromJSON -import naksha.base.Platform.PlatformCompanion.gzipDeflate -import naksha.base.Platform.PlatformCompanion.gzipInflate import naksha.base.Platform.PlatformCompanion.md5 import naksha.base.Platform.PlatformCompanion.toJSON import naksha.geo.GeoUtil.GeoUtil_C.fromTWKB -import naksha.model.objects.StandardMembers import naksha.geo.GeoUtil.GeoUtil_C.toTWKB import naksha.geo.SpGeometry -import naksha.jbon.* import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_ARGUMENT import naksha.model.NakshaError.NakshaErrorCompanion.STORAGE_NOT_FOUND import naksha.model.NakshaVersion.Companion.CURRENT -import naksha.model.objects.NakshaCollection -import naksha.model.objects.NakshaFeature import naksha.model.objects.NakshaStorage import kotlin.js.JsExport import kotlin.js.JsName import kotlin.js.JsStatic import kotlin.jvm.JvmField -import kotlin.jvm.JvmOverloads import kotlin.jvm.JvmStatic /** @@ -41,66 +34,70 @@ class Naksha private constructor() { const val INTERNAL_PREFIX = "naksha~" /** - * The identifier of the administration map _(`naksha~admin`)_. + * The identifier of the administration catalog, fixed to `naksha~admin`. It can be found in any database under Naksha control. + * + * **Note**: The feature of the administration catalog is an immutable feature needed to bootstrap a Naksha controlled database, therefore it is not peristed anywhere. * @since 3.0 */ - const val ADMIN_MAP = "naksha~admin" + const val ADMIN_CATALOG_ID = "naksha~admin" /** - * The number of the administration map _(`0`)_. + * The identifier of the administration catalog, fixed to `0`. It can be found in any database under Naksha control. * @since 3.0 */ - const val ADMIN_MAP_NUMBER = 0 + const val ADMIN_CATALOG_FN = 0 /** - * The identifier of the virtual collection in which the collections of a map are managed, located within each map _(`naksha~collections`)_. + * The identifier of the admin-collection in which the collection-features of each catalog are persisted. + * + * This collection exists in every catalog under Naksha management. The identifier of the collection itself is fixed to `naksha~collections`. The feature of the administration collection is an immutable feature. It is needed to bootstrap a new catalog, therefore it is not persisted anywhere. * @since 3.0 */ - const val COLLECTIONS_COL = "naksha~collections" + const val ADMIN_COL_ID = "naksha~collections" /** - * The collection-number of the virtual collection in which the collections of a map are managed, located within each map _(`0`)_ . + * The collection-number of the admin-collection in which the collection-features of each catalog are persisted, it has the fixed feature-number _(`0`)_. * @since 3.0 */ - const val COLLECTIONS_COL_NUMBER = 0 + const val ADMIN_COL_FN = 0 /** - * The identifier of the collection in which transactions are stored, located in the [admin-map][ADMIN_MAP] _(`naksha~transactions`)_. + * The identifier of the collection in which transactions are stored, located in the [admin-map][ADMIN_CATALOG_ID] _(`naksha~transactions`)_. * @since 3.0 * @see [naksha.model.objects.NakshaTx] */ - const val TRANSACTIONS_COL = "naksha~transactions" + const val TRANSACTIONS_COL_ID = "naksha~transactions" /** - * The collection-number of the collection in which transactions are stored, located in the [admin-map][ADMIN_MAP] _(`1`)_. + * The collection-number of the collection in which transactions are stored, located in the [admin-catalog][ADMIN_CATALOG_ID]. The feature-number of this collection is fixed to `1`. * @since 3.0 */ - const val TRANSACTIONS_COL_NUMBER = 1 + const val TRANSACTIONS_COL_FN = 1 /** - * The identifier of the collection in which catalogs (maps) are stored, located only within the [admin-map][ADMIN_MAP] _(`naksha~catalogs`)_. + * The identifier of the collection in which catalogs (maps) are stored, located only within the [admin-map][ADMIN_CATALOG_ID] _(`naksha~catalogs`)_. * @see [naksha.model.objects.NakshaMap] * @since 3.0 */ - const val CATALOGS_COL = "naksha~catalogs" + const val CATALOGS_COL_ID = "naksha~catalogs" /** - * The collection-number of the collection in which catalogs (maps) are stored, located in the [admin-map][ADMIN_MAP] _(`2`)_. + * The collection-number of the collection in which catalogs (maps) are stored, located in the [admin-map][ADMIN_CATALOG_ID] _(`2`)_. * @since 3.0 */ - const val CATALOGS_COL_NUMBER = 2 + const val CATALOGS_COL_FN = 2 /** - * The identifier of the collection in which books (global JBON2 dictionaries) are stored, located in the [admin-map][ADMIN_MAP] _(`naksha~books`)_. + * The identifier of the collection in which books (global JBON2 dictionaries) are stored, located in the [admin-map][ADMIN_CATALOG_ID] _(`naksha~books`)_. * @since 3.0 */ - const val BOOKS_COL = "naksha~books" + const val BOOKS_COL_ID = "naksha~books" /** - * The collection-number of the collection in which books (global JBON2 dictionaries) are stored, located in the [admin-map][ADMIN_MAP] _(`3`)_. + * The collection-number of the collection in which books (global JBON2 dictionaries) are stored, located in the [admin-map][ADMIN_CATALOG_ID] _(`3`)_. * @since 3.0 */ - const val BOOKS_COL_NUMBER = 3 + const val BOOKS_COL_FN = 3 /** * The maximum length of identifiers _(`42`)_ . @@ -115,11 +112,11 @@ class Naksha private constructor() { @JsStatic @JvmStatic val internalIdToNumber = mapOf( - Pair(ADMIN_MAP, ADMIN_MAP_NUMBER), - Pair(COLLECTIONS_COL, COLLECTIONS_COL_NUMBER), - Pair(TRANSACTIONS_COL, TRANSACTIONS_COL_NUMBER), - Pair(CATALOGS_COL, CATALOGS_COL_NUMBER), - Pair(BOOKS_COL, BOOKS_COL_NUMBER), + Pair(ADMIN_CATALOG_ID, ADMIN_CATALOG_FN), + Pair(ADMIN_COL_ID, ADMIN_COL_FN), + Pair(TRANSACTIONS_COL_ID, TRANSACTIONS_COL_FN), + Pair(CATALOGS_COL_ID, CATALOGS_COL_FN), + Pair(BOOKS_COL_ID, BOOKS_COL_FN), ) /** @@ -272,7 +269,7 @@ class Naksha private constructor() { @JsStatic @JvmStatic fun mapNumber(id: String): Int { - if (id == ADMIN_MAP) return ADMIN_MAP_NUMBER + if (id == ADMIN_CATALOG_ID) return ADMIN_CATALOG_FN if (id == "0" || is31BitUnsigned.matches(id)) { try { return id.toUInt(10).toInt() @@ -294,7 +291,7 @@ class Naksha private constructor() { @JvmStatic fun collectionNumber(id: String): Int { val internalNumber = internalIdToNumber[id] - if (id != ADMIN_MAP && internalNumber != null) return internalNumber + if (id != ADMIN_CATALOG_ID && internalNumber != null) return internalNumber if (id == "0" || is31BitUnsigned.matches(id)) { try { return id.toUInt(10).toInt() @@ -619,7 +616,7 @@ class Naksha private constructor() { */ @JvmStatic @JsStatic - fun getStorageByTupleNumber(tupleNumber: TupleNumber): IStorage? = storagesByNumber[tupleNumber.storageNumber] + fun getStorageByTupleNumber(tupleNumber: TupleNumber): IStorage? = storagesByNumber[tupleNumber.databaseNumber] /** * Set up the storage with the given configuration, enforces an [initStorage][AbstractStorage.initStorage] invocation that is forced to `create` or `upgrade` the storage. diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Tuple.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Tuple.kt index 2a60d7e74..0c1c70069 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Tuple.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Tuple.kt @@ -2,7 +2,6 @@ package naksha.model -import naksha.base.AnyList import naksha.base.Int64 import naksha.base.ListProxy import naksha.base.MapProxy @@ -95,14 +94,14 @@ data class Tuple @JvmOverloads constructor( if (action != Action.VERSION && action == Action.CREATE) { throw NakshaException(ILLEGAL_ARGUMENT, "Invalid action CREATE given, the feature exists already (has a uuid)") } - newTn = TupleNumber.copy(prevTn, session.useTransaction().version.value) + newTn = TupleNumber.copy(prevTn, session.useTransaction().version.number) } else { if (action != Action.VERSION && action != Action.CREATE) { throw NakshaException(ILLEGAL_ARGUMENT, "Invalid action $action given, the feature does not exist (missing uuid)") } newTn = TupleNumber( // The feature is stored in the same database as the collection it is inserted into. - colTn.storageNumber, + colTn.databaseNumber, // The feature is stored in the same catalog as the collection it is inserted into. colTn.mapNumber, // The feature-number of the collection is the collection-number of the feature we want to store in the collection. @@ -110,14 +109,14 @@ data class Tuple @JvmOverloads constructor( // The feature-number of the actual feature. Will either be set explicit or calcualted. feature.featureNumber, // The version is the one of the transaction. - session.useTransaction().version.value + session.useTransaction().version.number ) } // Update the feature with its new tuple-number. tnMember.write(feature, newTn) val globalBookTn: TupleNumber? if (globalBook != null) { - if (newTn.storageNumber != globalBook.databaseNumber || globalBook.featureNumber == null) { + if (newTn.databaseNumber != globalBook.databaseNumber || globalBook.featureNumber == null) { throw NakshaException(ILLEGAL_ARGUMENT, "The given global book is not located in the same storage as the feature") } globalBookTn = TupleNumber.copy(newTn, storageNumber = globalBook.databaseNumber, featureNumber = globalBook.featureNumber) @@ -240,7 +239,7 @@ data class Tuple @JvmOverloads constructor( */ val nextTupleNumber: TupleNumber? get() { - if (nextVersion >= Version.HEAD.value) return null + if (nextVersion >= Version.HEAD.number) return null var nextTn = _nextTupleNumber if (nextTn == null) { nextTn = TupleNumber.copy(tupleNumber, version = nextVersion) @@ -454,7 +453,7 @@ data class Tuple @JvmOverloads constructor( * @since 3.0 * @throws NakshaException if any error occurs. */ - fun decodeFeature(globalBook: IBook?): NakshaFeature { + fun decodeFeature(globalBook: IBook?): NakshaFeature { // TODO: Java: After switching back to Java, we can allow arbitrary return types. val rawBytes = if (isGzipped(jbonBytes)) gzipInflate(jbonBytes) else jbonBytes val decoder = JbDecoder2(globalBook, membersBook) decoder.mapBytes(rawBytes) diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleCache.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleCache.kt index fbdb8aafa..2e8db727b 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleCache.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleCache.kt @@ -198,7 +198,7 @@ class TupleCache internal constructor() { it != null && it.tuple == null && it.feature == null else it != null && it.tuple == null - }.filterNotNull().groupBy { it.tupleNumber.storageNumber } + }.filterNotNull().groupBy { it.tupleNumber.databaseNumber } for (entry in byStorage) { val storageNumber = entry.key val toLoad = entry.value @@ -317,7 +317,7 @@ class TupleCache internal constructor() { */ @JsName("getDictReaderForTuple") fun getDictReader(tuple: Tuple): IDictReader? - = getDictReader(tuple.tupleNumber.storageNumber) + = getDictReader(tuple.tupleNumber.databaseNumber) /** * A method to query for a dictionary reader. @@ -327,7 +327,7 @@ class TupleCache internal constructor() { @JsName("getDictReaderForFeatureTuple") fun getDictReader(featureTuple: FeatureTuple): IDictReader? { val tuple = featureTuple.tuple ?: return null - return getDictReader(tuple.tupleNumber.storageNumber) + return getDictReader(tuple.tupleNumber.databaseNumber) } /** diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumber.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumber.kt index b04850d25..074121c95 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumber.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumber.kt @@ -22,13 +22,13 @@ import kotlin.jvm.JvmOverloads import kotlin.jvm.JvmStatic /** - * The in-memory representation of the unique [Tuple] identifier. + * The in-memory representation of the unique address of a [Tuple]. * - * The full qualified [Tuple] identifier is a 256-bit value _(32 byte)_, persisting out of the storage-number, map-number, collection-number, feature-number, and [transaction-number][Version]. Note that the lower two bits of `version.txn` encode the [action][Action]. + * The full qualified [Tuple] address is a 256-bit value _(32 byte)_, persisting out of the database-number, catalog-number, collection-number, feature-number, and version. Note that the lower two bits of version encode the [action][Action]. * - * The tuple-number is stringified into: + * The tuple-number is stringified either into URN: * ``` - * {storage-number}:{map-number}:{collection-number}:{feature-number}:{version} + * urn:naksha:tn:{database-number}:{catalog-number}:{collection-number}:{feature-number}:{version} * ``` * * - There are no two [tuples][Tuple] with the same [tuple-number][TupleNumber]; world-wide. @@ -37,10 +37,10 @@ import kotlin.jvm.JvmStatic @JsExport data class TupleNumber( /** - * The storage-number, uniquely identifies the storage where the tuple is stored. + * The database-number, uniquely identifies the storage where the tuple is stored. * @since 3.0 */ - @JvmField val storageNumber: Int64, + @JvmField val databaseNumber: Int64, /** * The map-number of the map in which the tuple is stored within the storage. @@ -63,7 +63,7 @@ data class TupleNumber( /** * The version _(transaction)_ of which the [Tuple] is part of. - * The lower 2 bits of [Version.value] encode the [Action]. + * The lower 2 bits of [Version.number] encode the [Action]. * @since 3.0 * @see [Version.HEAD] */ @@ -97,7 +97,7 @@ data class TupleNumber( override fun hashCode(): Int = version.hashCode() override fun compareTo(other: TupleNumber): Int { - var i64_diff = storageNumber - other.storageNumber + var i64_diff = databaseNumber - other.databaseNumber if (i64_diff < 0) return -1 if (i64_diff > 1) return 1 var i32_diff = mapNumber - other.mapNumber @@ -121,7 +121,7 @@ data class TupleNumber( override fun equals(other: Any?): Boolean { if (this === other) return true return other is TupleNumber - && storageNumber == other.storageNumber + && databaseNumber == other.databaseNumber && mapNumber == other.mapNumber && collectionNumber == other.collectionNumber && featureNumber == other.featureNumber @@ -140,7 +140,7 @@ data class TupleNumber( */ override fun toString(): String { if (!this::_string.isInitialized) { - _string = "$storageNumber:$mapNumber:$collectionNumber:$featureNumber:$version" + _string = "$databaseNumber:$mapNumber:$collectionNumber:$featureNumber:$version" } return _string } @@ -161,7 +161,7 @@ data class TupleNumber( val fn = this.featureNumber if (fn >= 0) throw NakshaException(ILLEGAL_STATE, "The feature-number is not auto-generated, failed to calculate alternative") val new_fn = Naksha.alternativeInt64(fn) - return TupleNumber(storageNumber, mapNumber, collectionNumber, new_fn, version) + return TupleNumber(databaseNumber, mapNumber, collectionNumber, new_fn, version) } private var _urn: String? = null @@ -251,7 +251,7 @@ data class TupleNumber( val view = Platform.newDataView(byteArray) var offset = 0 if (variant.encodeStorageNumber()) { - dataview_set_int64(view, offset, storageNumber) + dataview_set_int64(view, offset, databaseNumber) offset += 8 } if (variant.encodeMapNumber()) { @@ -306,7 +306,7 @@ data class TupleNumber( mapNumber: Int? = null, storageNumber: Int64? = null, ) = TupleNumber( - storageNumber ?: tn.storageNumber, + storageNumber ?: tn.databaseNumber, mapNumber ?: tn.mapNumber, collectionNumber ?: tn.collectionNumber, featureNumber ?: tn.featureNumber, @@ -319,7 +319,7 @@ data class TupleNumber( * This happens for various reasons, for example when a [Tuple] is created in the client at runtime, and not yet persisted in any storage, therefore does not yet have a valid tuple-number. * @since 3.0 */ - val HEAD = TupleNumber(Int64(0), 0, 0, Int64(0), Version.HEAD.value) + val HEAD = TupleNumber(Int64(0), 0, 0, Int64(0), Version.HEAD.number) /** * Restore a [TupleNumber] from a binary encoding. @@ -336,7 +336,7 @@ data class TupleNumber( offset: Int, variant: TupleNumberVariant, tn: TupleNumber - ): TupleNumber = fromBinary(Binary(bytes, offset), variant, tn.storageNumber, tn.mapNumber, tn.collectionNumber, tn.featureNumber) + ): TupleNumber = fromBinary(Binary(bytes, offset), variant, tn.databaseNumber, tn.mapNumber, tn.collectionNumber, tn.featureNumber) fun fromB256(bytes: ByteArray) = fromByteArray(bytes, 0, B256) @@ -447,7 +447,7 @@ data class TupleNumber( * - `map-number` _(32-bit integer)_ * - `collection-number` _(32-bit integer)_ * - `feature-number` _(64-bit integer)_ - * - `version` _(64-bit integer, the raw [Version.value] value)_ + * - `version` _(64-bit integer, the raw [Version.number] value)_ * @param parts the string parts of the tuple-number. * @param offset the index in the given list where the `storage-number` is located, defaults to `0`. * @return the deserialized [TupleNumber]. @@ -463,7 +463,7 @@ data class TupleNumber( val mapNumber = parts[offset + MAP_NUMBER].toInt(10) val colNumber = parts[offset + COLLECTION_NUMBER].toInt(10) val featureNumber = Int64(parts[offset + FEATURE_NUMBER].toLong(10)) - val version = Version.fromString(parts[offset + VERSION]).value + val version = Version.fromString(parts[offset + VERSION]).number return TupleNumber(storageNumber, mapNumber, colNumber, featureNumber, version) } } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumberBinaryArray.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumberBinaryArray.kt index 2973e5531..c4dde2591 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumberBinaryArray.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumberBinaryArray.kt @@ -385,7 +385,7 @@ data class TupleNumberBinaryArray( override fun lastIndexOf(element: TupleNumber?): Int { if (element == null) return -1 for (i in size - 1 downTo 0) { - if (element.storageNumber == getStorageNumber(i) + if (element.databaseNumber == getStorageNumber(i) && element.mapNumber == getMapNumber(i) && element.collectionNumber == getCollectionNumber(i) && element.featureNumber == getFeatureNumber(i) @@ -397,7 +397,7 @@ data class TupleNumberBinaryArray( override fun indexOf(element: TupleNumber?): Int { if (element == null) return -1 for (i in 0 until size) { - if (element.storageNumber == getStorageNumber(i) + if (element.databaseNumber == getStorageNumber(i) && element.mapNumber == getMapNumber(i) && element.collectionNumber == getCollectionNumber(i) && element.featureNumber == getFeatureNumber(i) diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumberList.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumberList.kt index bdeecedcf..a03fda4bf 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumberList.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumberList.kt @@ -9,7 +9,6 @@ import naksha.base.PlatformDataViewApi.PlatformDataViewApiCompanion.dataview_set import naksha.base.PlatformDataViewApi.PlatformDataViewApiCompanion.dataview_set_int64 import naksha.model.BinaryUtil.BinaryUtil_C.TYPE_TUPLE_NUMBER_ARRAY import naksha.model.BinaryUtil.BinaryUtil_C.writeSimpleHeader -import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_STATE import naksha.model.TupleNumberVariant.TupleNumberVariant_C.B128 import naksha.model.TupleNumberVariant.TupleNumberVariant_C.B160 import naksha.model.TupleNumberVariant.TupleNumberVariant_C.B192 @@ -78,13 +77,13 @@ class TupleNumberList : ListProxy(TupleNumber::class) { if (variant == null) { // We found a first tuple, we hope that each tuple can be encoded in 64-bit only. variant = B64 - storageNumber = tupleNumber.storageNumber + storageNumber = tupleNumber.databaseNumber mapNumber = tupleNumber.mapNumber collectionNumber = tupleNumber.collectionNumber featureNumber = tupleNumber.featureNumber continue } - if (storageNumber != tupleNumber.storageNumber) { + if (storageNumber != tupleNumber.databaseNumber) { // We need to encode all values individually variant = B256 storageNumber = null @@ -153,7 +152,7 @@ class TupleNumberList : ListProxy(TupleNumber::class) { for (tupleNumber in this) { if (tupleNumber == null) continue if (variant.encodeStorageNumber()) { - dataview_set_int64(view, i, tupleNumber.storageNumber) + dataview_set_int64(view, i, tupleNumber.databaseNumber) i += 8 } if (variant.encodeMapNumber()) { diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Version.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Version.kt index f8c9031c0..1086a77c5 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Version.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Version.kt @@ -4,6 +4,7 @@ package naksha.model import naksha.base.Int64 import naksha.base.Timestamp +import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_ARGUMENT import kotlin.js.JsExport import kotlin.js.JsName import kotlin.js.JsStatic @@ -42,24 +43,39 @@ import kotlin.jvm.JvmStatic * * ### String representation * - * [toString] returns the raw [value] value as a plain decimal number, regardless of whether the + * [toString] returns the raw [number] value as a plain decimal number, regardless of whether the * version is dated or manual. [fromString] accepts both the decimal form and the legacy * `{year}:{month}:{day}:{seqWithAction}` form for backward-compatibility. * - * @property value the raw 53-bit version number (upper 11 bits are always zero). + * @property number the raw 53-bit version number (upper 11 bits are always zero). * @since 3.0 */ @JsExport -open class Version(@JvmField val value: Int64) : Comparable { +open class Version(@JvmField val number: Int64) : Comparable { + // TODO: When we move nack to Java, we can extend Number, so that we're basically like a Long. + init { + if ((number and HEAD.number) != number) { + throw NakshaException(ILLEGAL_ARGUMENT, "$number is not a valid version") + } + } /** - * Convert a transaction number given as [Long] into a version. - * @param txn the transaction number. + * Convert a [Long] into a [Int64] version. + * @param value the transaction number. * @since 3.0 */ @Suppress("NON_EXPORTABLE_TYPE") @JsName("fromLong") - constructor(txn: Long) : this(Int64(txn)) + constructor(value: Long) : this(Int64(value)) + + /** + * Convert a stringified version. + * @param value the stringified version as decimal number. + * @since 3.0 + * @throws NumberFormatException if the given string is no valid version. + */ + @JsName("fromString") + constructor(value: String) : this(Int64(value.toLong())) companion object VersionCompanion { @@ -87,7 +103,7 @@ open class Version(@JvmField val value: Int64) : Comparable { * Creates a version from its string representation. * * Accepts either: - * - A pure decimal encoding of the 64-bit [value] value. + * - A pure decimal encoding of the 64-bit [number] value. * - The human-readable form `{year}:{month}:{day}:{seq}` (seq is the 30-bit sequence, no action bits). * * Throws [NakshaError.ILLEGAL_ARGUMENT] if the string is invalid. @@ -98,21 +114,9 @@ open class Version(@JvmField val value: Int64) : Comparable { @JvmStatic fun fromString(s: String): Version { try { - if (s.indexOf(':') >= 0) { - val parts = s.split(':') - if (parts.size != 4) throw Exception("Expected 4 colon-separated parts") - // The 4th field carries the raw lower 32 bits of the txn (including action bits in 1-0). - val year = parts[0].toInt() - val month = parts[1].toInt() - val day = parts[2].toInt() - val seqRaw = Int64(parts[3].toLong()) // raw lower 32 bits, includes action in bits 1-0 - val txn = (Int64(year) shl 41) or (Int64(month) shl 37) or (Int64(day) shl 32) or seqRaw - return Version(txn) - } else { - return Version(Int64(s.toLong())) - } + return Version(Int64(s.toLong())) } catch (_: Exception) { - throw NakshaException(NakshaError.ILLEGAL_ARGUMENT, "Invalid version string: $s") + throw NakshaException(ILLEGAL_ARGUMENT, "Invalid version string: $s") } } @@ -152,7 +156,7 @@ open class Version(@JvmField val value: Int64) : Comparable { /** * Constructs a **manual** version. * - * The resulting [value] must have its upper 21 bits (63–43) all zero, which means the effective + * The resulting [number] must have its upper 21 bits (63–43) all zero, which means the effective * value fits in 43 bits. The [seq] therefore must be in 0..0x1FF_FFFF_FFFF (41 bits), since * the lower 2 bits are reserved for [action]. * @@ -188,10 +192,9 @@ open class Version(@JvmField val value: Int64) : Comparable { } /** - * The _HEAD_ sentinel version _(9_007_199_254_740_991L aka `2^53-1`)_. + * The _HEAD_ sentinel version _(9_007_199_254_740_991L aka `2^53-1`)_. Can be used as well to mask version to ensure valid range. * - * When a [Tuple] is the current HEAD state its `nextVersion` is synthesised as this value - * (the column is not physically stored in HEAD tables). + * When a [Tuple] is the current HEAD state its `nextVersion` is synthesised as this value. * @since 3.0 */ @JvmField @@ -239,7 +242,7 @@ open class Version(@JvmField val value: Int64) : Comparable { val SEQ_MAX: Int64 = SEQ_30_MASK /** - * The raw increment to add to [value] to advance the sequence counter by one while keeping the + * The raw increment to add to [number] to advance the sequence counter by one while keeping the * action bits unchanged. Equal to `1 shl 2` = `4`. * @since 3.0 */ @@ -257,7 +260,7 @@ open class Version(@JvmField val value: Int64) : Comparable { */ val year: Int get() { - if (_year < 0) _year = (value ushr 41).toInt() + if (_year < 0) _year = (number ushr 41).toInt() return _year } @@ -269,7 +272,7 @@ open class Version(@JvmField val value: Int64) : Comparable { */ val month: Int get() { - if (_month < 0) _month = (value ushr 37).toInt() and 0xF + if (_month < 0) _month = (number ushr 37).toInt() and 0xF return _month } @@ -281,7 +284,7 @@ open class Version(@JvmField val value: Int64) : Comparable { */ val day: Int get() { - if (_day < 0) _day = (value ushr 32).toInt() and 0x1F + if (_day < 0) _day = (number ushr 32).toInt() and 0x1F return _day } @@ -298,7 +301,7 @@ open class Version(@JvmField val value: Int64) : Comparable { get() { var s = _seq if (s == null) { - s = (value ushr 2) and SEQ_MAX + s = (number ushr 2) and SEQ_MAX _seq = s } return s @@ -308,7 +311,7 @@ open class Version(@JvmField val value: Int64) : Comparable { * Returns `true` if this is a **dated** version, i.e. the year field (`txn ushr 41`) is ≥ 16. * @since 3.0 */ - fun isDated(): Boolean = (value ushr 41).toInt() >= 16 + fun isDated(): Boolean = (number ushr 41).toInt() >= 16 /** * Returns `true` if this is a **manual** version, i.e. the year field is < 16 and the upper 21 bits are zero. @@ -318,28 +321,28 @@ open class Version(@JvmField val value: Int64) : Comparable { fun isManualVersion(): Boolean = !isDated() /** - * Returns the [Action] encoded in the lower 2 bits of [value]. + * Returns the [Action] encoded in the lower 2 bits of [number]. * @since 3.0 */ - fun action(): Action = Action.fromValue(value.toInt() and 3) + fun action(): Action = Action.fromValue(number.toInt() and 3) private var _string: String? = null override fun equals(other: Any?): Boolean { - if (other is Int64) return value eq other - if (other is Version) return value eq other.value + if (other is Int64) return number eq other + if (other is Version) return number eq other.number return false } override fun compareTo(other: Version): Int { - val diff = value.minus(other.value) + val diff = number.minus(other.number) return if (diff.eq(0)) 0 else if (diff < 0) -1 else 1 } - override fun hashCode(): Int = value.hashCode() + override fun hashCode(): Int = number.hashCode() /** - * Returns the version as a plain decimal string of the raw [value] value. + * Returns the version as a plain decimal string of the raw [number] value. * * This representation is lossless for all version types (dated and manual) and survives * a round-trip through [fromString]. The legacy `{year}:{month}:{day}:{seqWithAction}` @@ -349,7 +352,7 @@ open class Version(@JvmField val value: Int64) : Comparable { override fun toString(): String { var s = _string if (s == null) { - s = value.toLong().toString() + s = number.toLong().toString() _string = s } return s diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/IndexList.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/IndexList.kt index 4ce392e92..01ddf16c6 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/IndexList.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/IndexList.kt @@ -18,9 +18,27 @@ open class IndexList() : ListProxy(Index::class) { * Construct a list from a vararg of indexes. * @since 3.0 */ - @JsName("fromIndexes") + @JsName("of") constructor(vararg indexes: Index) : this() { - addAll(indexes.toList()) + for (index in indexes) add(index) + } + + /** + * Construct a list from a vararg of indexes. + * @since 3.0 + */ + @JsName("fromArray") + constructor(indexes: Array) : this() { + for (index in indexes) add(index) + } + + /** + * Construct a list from a vararg of indexes. + * @since 3.0 + */ + @JsName("fromList") + constructor(indexes: List) : this() { + addAll(indexes) } companion object IndexList_C { diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt index a15f859d8..d11a6debb 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt @@ -76,11 +76,11 @@ open class NakshaCollection() : NakshaFeature() { /** * Always return `0`, because all collections are always stored in `naksha~collections` collection. * @since 3.0 - * @see [Naksha.COLLECTIONS_COL] - * @see [Naksha.COLLECTIONS_COL_NUMBER] + * @see [Naksha.ADMIN_COL_ID] + * @see [Naksha.ADMIN_COL_FN] */ override val collectionNumber: Int - get() = Naksha.COLLECTIONS_COL_NUMBER + get() = Naksha.ADMIN_COL_FN /** * The map-id of the map in which the collection is located; `null` if not yet known. @@ -337,6 +337,30 @@ open class NakshaCollection() : NakshaFeature() { return this } + // TODO: Add a removeMember method. + + /** + * Initializes the [members] to the bare minimum, therefore [mandatory members][StandardMembers.MANDATORY]. + * + * The method should be called, if next to the minimal members additional proprietary members should be added. + * @since 3.0 + */ + fun withMinimalMembers(): NakshaCollection { + members = MemberList(StandardMembers.MANDATORY) + return this + } + + /** + * Initializes the [members] to the [standard XYZ members][XyzMembers.ALL]. + * + * The method should be called, if next to the standard XYZ members, additional proprietary members should be added. + * @since 3.0 + */ + fun withXyzMembers(): NakshaCollection { + members = MemberList(XyzMembers.ALL) + return this + } + /** * Returns the members list. If the member list is currently `null`, it creates it from [XyzMembers.ALL]. If the list does not contain the mandatory members, they will be added. * @return the members list of this collection. @@ -397,6 +421,28 @@ open class NakshaCollection() : NakshaFeature() { */ var indices: IndexList? by INDICES + /** + * Initializes the [indices] to the bare minimum, therefore [mandatory indices][StandardIndices.MANDATORY]. + * + * The method should be called, if next to the minimal indices additional proprietary indices should be added. + * @since 3.0 + */ + fun withMinimalIndices(): NakshaCollection { + indices = IndexList(StandardIndices.MANDATORY) + return this + } + + /** + * Initializes the [indices] to the [standard XYZ indices][XyzIndices.ALL]. + * + * The method should be called, if next to the standard XYZ indices, additional proprietary indices should be added. + * @since 3.0 + */ + fun withXyzIndices(): NakshaCollection { + indices = IndexList(XyzIndices.ALL) + return this + } + /** * @see [indices] */ diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaFeature.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaFeature.kt index 6fb868d54..ab3f4ff12 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaFeature.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaFeature.kt @@ -190,7 +190,7 @@ open class NakshaFeature() : AnyObject() { * @since 3.0 */ open val storageNumber: Int64? - get() = guid?.tupleNumber?.storageNumber + get() = guid?.tupleNumber?.databaseNumber /** * The type of the feature, to be [GeoJSON](https://datatracker.ietf.org/doc/html/rfc7946) compatible, one of the following is expected: diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaMap.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaMap.kt index b6b0ab844..d3bee639f 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaMap.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaMap.kt @@ -72,20 +72,20 @@ open class NakshaMap() : NakshaFeature() { /** * Always return `2`, because all catalogs (maps) are always stored in `naksha~catalogs` collection. * @since 3.0 - * @see [Naksha.CATALOGS_COL] - * @see [Naksha.CATALOGS_COL_NUMBER] + * @see [Naksha.CATALOGS_COL_ID] + * @see [Naksha.CATALOGS_COL_FN] */ override val collectionNumber: Int - get() = Naksha.CATALOGS_COL_NUMBER + get() = Naksha.CATALOGS_COL_FN /** * Always return `0`, because all maps are always stored in `naksha~admin` map. * @since 3.0 - * @see [Naksha.ADMIN_MAP] - * @see [Naksha.ADMIN_MAP_NUMBER] + * @see [Naksha.ADMIN_CATALOG_ID] + * @see [Naksha.ADMIN_CATALOG_FN] */ override val mapNumber: Int - get() = Naksha.ADMIN_MAP_NUMBER + get() = Naksha.ADMIN_CATALOG_FN /** * The storage-id of the storage in which the map is located; `null` if not yet known. diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaTx.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaTx.kt index 1aabb915b..0db53fea9 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaTx.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaTx.kt @@ -155,7 +155,7 @@ open class NakshaTx : NakshaFeature() { * @since 3.0 */ val txn: Int64 - get() = version.value + get() = version.number /** * Number of features modified in the transaction - total number of features from all touched collections. diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzIndices.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzIndices.kt index ee1c8fe9f..e990231bc 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzIndices.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzIndices.kt @@ -14,5 +14,8 @@ import kotlin.jvm.JvmField class XyzIndices private constructor() { companion object XyzIndices_C { + + // TODO: Please fix me, we need an own listOf(...)! + @JvmField val ALL: List = StandardIndices.ALL } } \ No newline at end of file diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/FeatureTuple.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/FeatureTuple.kt index ba27fb6cb..6efd3c122 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/FeatureTuple.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/FeatureTuple.kt @@ -80,7 +80,8 @@ open class FeatureTuple( var feature = cachedFeature val tuple = this.tuple if (tuple != null && tuple !== cachedTuple && !doNotAutoUpdate) { - feature = tuple.decodeFeature() + // TODO: We need a global book for decoding, we should make Tuples explicit for clients! + feature = tuple.decodeFeature(null) cachedFeature = feature cachedJson = null } @@ -116,5 +117,5 @@ open class FeatureTuple( * @return a new copy of the tuple converted into a feature. * @since 3.0 */ - open fun newFeature(): NakshaFeature? = tuple?.decodeFeature() + open fun newFeature(): NakshaFeature? = tuple?.decodeFeature(null) // TODO: Same as above, we need to change this generally! } \ No newline at end of file diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/PropertyFilter.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/PropertyFilter.kt index 537b6002e..e7f459886 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/PropertyFilter.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/PropertyFilter.kt @@ -21,10 +21,7 @@ class PropertyFilter(val req: ReadFeatures) : ResultFilter { */ override fun filter(featureTuple: FeatureTuple): FeatureTuple? { val pSearch = req.query.properties ?: return featureTuple - val tuple = featureTuple.tuple ?: return null - val sn = tuple.storageNumber - val dictReader = getStorageByNumber(sn) ?: cache.getDictReader(sn) - val feature = Naksha.decodeFeature(tuple.jbonBytes, dictReader) ?: return null + val feature = featureTuple.feature ?: return null return if (resolvePropsQueryOnFeature(pSearch, feature)) featureTuple else null } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadCollections.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadCollections.kt index d490264d9..8ec3ed428 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadCollections.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadCollections.kt @@ -8,7 +8,6 @@ import naksha.base.StringList import naksha.model.Naksha import kotlin.js.ExperimentalJsExport import kotlin.js.JsExport -import kotlin.js.JsName /** * A request to read [collection features][naksha.model.objects.NakshaCollection] from a map of the storage. @@ -82,7 +81,7 @@ open class ReadCollections : ReadRequest() { fun toReadFeatures(): ReadFeatures { val req = ReadFeatures() req.mapId = mapId - req.collectionIds.add(Naksha.COLLECTIONS_COL) + req.collectionIds.add(Naksha.ADMIN_COL_ID) req.featureIds.addAll(collectionIds) return req } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadMaps.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadMaps.kt index 2b3f3dbd2..3e1a76776 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadMaps.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadMaps.kt @@ -71,8 +71,8 @@ open class ReadMaps() : ReadRequest() { */ fun toReadFeatures(): ReadFeatures { val req = ReadFeatures() - req.mapId = Naksha.ADMIN_MAP - req.collectionIds.add(Naksha.CATALOGS_COL) + req.mapId = Naksha.ADMIN_CATALOG_ID + req.collectionIds.add(Naksha.CATALOGS_COL_ID) req.featureIds.addAll(mapIds) return req } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadTransactions.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadTransactions.kt index 5224afab3..f33e8eb49 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadTransactions.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadTransactions.kt @@ -13,8 +13,8 @@ import kotlin.js.JsExport @JsExport open class ReadTransactions : ReadFeatures() { init { - mapId = Naksha.ADMIN_MAP - collectionIds.add(Naksha.TRANSACTIONS_COL) + mapId = Naksha.ADMIN_CATALOG_ID + collectionIds.add(Naksha.TRANSACTIONS_COL_ID) } /** diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/Write.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/Write.kt index f9a8125bc..8a6f49062 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/Write.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/Write.kt @@ -4,10 +4,10 @@ package naksha.model.request import naksha.base.* import naksha.model.* -import naksha.model.Naksha.NakshaCompanion.ADMIN_MAP -import naksha.model.Naksha.NakshaCompanion.COLLECTIONS_COL -import naksha.model.Naksha.NakshaCompanion.BOOKS_COL -import naksha.model.Naksha.NakshaCompanion.CATALOGS_COL +import naksha.model.Naksha.NakshaCompanion.ADMIN_CATALOG_ID +import naksha.model.Naksha.NakshaCompanion.ADMIN_COL_ID +import naksha.model.Naksha.NakshaCompanion.BOOKS_COL_ID +import naksha.model.Naksha.NakshaCompanion.CATALOGS_COL_ID import naksha.model.Naksha.NakshaCompanion.featureNumber import naksha.model.Naksha.NakshaCompanion.isInternalId import naksha.model.Naksha.NakshaCompanion.partitionNumber @@ -61,19 +61,19 @@ open class Write : AnyObject() { private fun compareMapIds(a: String, b: String): Int { if (a == b) return 0 // We order all modifications done in admin-map first. - if (ADMIN_MAP == a) return -1 - if (ADMIN_MAP == b) return 1 + if (ADMIN_CATALOG_ID == a) return -1 + if (ADMIN_CATALOG_ID == b) return 1 return a.compareTo(b) } private fun compareCollectionIds(a: String, b: String): Int { if (a == b) return 0 // We order all modifications done in map's collection first (create maps first). - if (CATALOGS_COL == a) return -1 - if (CATALOGS_COL == b) return 1 + if (CATALOGS_COL_ID == a) return -1 + if (CATALOGS_COL_ID == b) return 1 // We order all modifications done in internal collection's-collection second. - if (COLLECTIONS_COL == a) return -1 - if (COLLECTIONS_COL == b) return 1 + if (ADMIN_COL_ID == a) return -1 + if (ADMIN_COL_ID == b) return 1 // Rest by id return a.compareTo(b) } @@ -149,7 +149,7 @@ open class Write : AnyObject() { /** * The identifier of the map to access; if `null` then the map-id is read from the [NakshaContext]. * - * If a [map][NakshaMap] or [dictionary][NakshaDictionary] should be modified, then use [Naksha.ADMIN_MAP]. + * If a [map][NakshaMap] or [dictionary][NakshaDictionary] should be modified, then use [Naksha.ADMIN_CATALOG_ID]. * @since 3.0 */ var mapId by MAP_ID @@ -165,9 +165,9 @@ open class Write : AnyObject() { /** * The identifier of the collection to modify; must not be `null` then the map-id is read from the [NakshaContext]. * - * - If a [map][NakshaMap] should be modified, then [Naksha.CATALOGS_COL] should be used, within [Naksha.ADMIN_MAP]. - * - If a [dictionary][NakshaDictionary] should be modified, the [Naksha.BOOKS_COL] should be used, within [Naksha.ADMIN_MAP]. - * - If a [collection][NakshaCollection] should be modified, then [Naksha.COLLECTIONS_COL] should be used, must not be used together with [Naksha.ADMIN_MAP], because the admin-map does not allow collection modification, it is internally managed. + * - If a [map][NakshaMap] should be modified, then [Naksha.CATALOGS_COL_ID] should be used, within [Naksha.ADMIN_CATALOG_ID]. + * - If a [dictionary][NakshaDictionary] should be modified, the [Naksha.BOOKS_COL_ID] should be used, within [Naksha.ADMIN_CATALOG_ID]. + * - If a [collection][NakshaCollection] should be modified, then [Naksha.ADMIN_COL_ID] should be used, must not be used together with [Naksha.ADMIN_CATALOG_ID], because the admin-map does not allow collection modification, it is internally managed. * - If a [feature][NakshaFeature] should be created, then the [NakshaCollection] in which the feature should be stored is required. * - Throws [ILLEGAL_STATE], if the collection-id is read, before being set. * @since 3.0 @@ -207,12 +207,13 @@ open class Write : AnyObject() { versionValue = Version(raw) return versionValue } - return feature?.properties?.xyz?.guid?.tupleNumber?.version + val versionNumber = feature?.properties?.xyz?.guid?.tupleNumber?.version + return if (versionNumber != null) Version(versionNumber) else null } set(value) { if (value == null) removeRaw("version") else { versionValue = value - versionRaw = value.value + versionRaw = value.number setRaw("version", versionRaw) } } @@ -471,8 +472,8 @@ open class Write : AnyObject() { * @since 3.0 */ fun createDictionary(dict: NakshaDictionary): Write { - this.mapId = ADMIN_MAP - this.collectionId = BOOKS_COL + this.mapId = ADMIN_CATALOG_ID + this.collectionId = BOOKS_COL_ID this.op = WriteOp.CREATE this.feature = dict return this @@ -486,8 +487,8 @@ open class Write : AnyObject() { * @since 3.0 */ fun updateDictionary(dict: NakshaDictionary, atomic: Boolean): Write { - this.mapId = ADMIN_MAP - this.collectionId = BOOKS_COL + this.mapId = ADMIN_CATALOG_ID + this.collectionId = BOOKS_COL_ID this.op = WriteOp.UPDATE this.feature = dict this.atomic = atomic @@ -501,8 +502,8 @@ open class Write : AnyObject() { * @since 3.0 */ fun upsertDictionary(dict: NakshaDictionary): Write { - this.mapId = ADMIN_MAP - this.collectionId = BOOKS_COL + this.mapId = ADMIN_CATALOG_ID + this.collectionId = BOOKS_COL_ID this.op = WriteOp.UPSERT this.feature = dict return this @@ -516,8 +517,8 @@ open class Write : AnyObject() { * @since 3.0 */ fun deleteDictionary(dict: NakshaDictionary, atomic: Boolean): Write { - this.mapId = ADMIN_MAP - this.collectionId = BOOKS_COL + this.mapId = ADMIN_CATALOG_ID + this.collectionId = BOOKS_COL_ID this.op = WriteOp.DELETE this.feature = dict this.atomic = atomic @@ -533,8 +534,8 @@ open class Write : AnyObject() { */ @JvmOverloads fun deleteDictionaryById(dictId: String, version: Version? = null): Write { - this.mapId = ADMIN_MAP - this.collectionId = BOOKS_COL + this.mapId = ADMIN_CATALOG_ID + this.collectionId = BOOKS_COL_ID this.op = WriteOp.DELETE this.id = dictId this.version = version @@ -549,8 +550,8 @@ open class Write : AnyObject() { * @since 3.0 */ fun createMap(map: NakshaMap): Write { - this.mapId = ADMIN_MAP - this.collectionId = CATALOGS_COL + this.mapId = ADMIN_CATALOG_ID + this.collectionId = CATALOGS_COL_ID this.op = WriteOp.CREATE this.feature = map return this @@ -564,8 +565,8 @@ open class Write : AnyObject() { * @since 3.0 */ fun updateMap(map: NakshaMap, atomic: Boolean): Write { - this.mapId = ADMIN_MAP - this.collectionId = CATALOGS_COL + this.mapId = ADMIN_CATALOG_ID + this.collectionId = CATALOGS_COL_ID this.op = WriteOp.UPDATE this.feature = map this.atomic = atomic @@ -580,8 +581,8 @@ open class Write : AnyObject() { * @since 3.0 */ fun upsertMap(map: NakshaMap, atomic: Boolean): Write { - this.mapId = ADMIN_MAP - this.collectionId = CATALOGS_COL + this.mapId = ADMIN_CATALOG_ID + this.collectionId = CATALOGS_COL_ID this.op = WriteOp.UPSERT this.feature = map this.atomic = atomic @@ -596,8 +597,8 @@ open class Write : AnyObject() { * @since 3.0 */ fun deleteMap(map: NakshaMap, atomic: Boolean): Write { - this.mapId = ADMIN_MAP - this.collectionId = CATALOGS_COL + this.mapId = ADMIN_CATALOG_ID + this.collectionId = CATALOGS_COL_ID this.op = WriteOp.DELETE this.feature = map this.atomic = atomic @@ -613,8 +614,8 @@ open class Write : AnyObject() { */ @JvmOverloads fun deleteMapById(id: String, version: Version? = null): Write { - this.mapId = ADMIN_MAP - this.collectionId = CATALOGS_COL + this.mapId = ADMIN_CATALOG_ID + this.collectionId = CATALOGS_COL_ID this.op = WriteOp.DELETE this.id = id this.version = version @@ -629,7 +630,7 @@ open class Write : AnyObject() { */ fun createCollection(collection: NakshaCollection): Write { this.mapId = collection.mapId - this.collectionId = COLLECTIONS_COL + this.collectionId = ADMIN_COL_ID this.op = WriteOp.CREATE this.feature = collection return this @@ -643,7 +644,7 @@ open class Write : AnyObject() { */ fun updateCollection(collection: NakshaCollection, atomic: Boolean): Write { this.mapId = collection.mapId - this.collectionId = COLLECTIONS_COL + this.collectionId = ADMIN_COL_ID this.op = WriteOp.UPDATE this.feature = collection this.atomic = atomic @@ -657,7 +658,7 @@ open class Write : AnyObject() { */ fun upsertCollection(collection: NakshaCollection): Write { this.mapId = collection.mapId - this.collectionId = COLLECTIONS_COL + this.collectionId = ADMIN_COL_ID this.op = WriteOp.UPSERT this.feature = collection return this @@ -671,7 +672,7 @@ open class Write : AnyObject() { */ fun deleteCollection(collection: NakshaCollection, atomic: Boolean): Write { this.mapId = collection.mapId - this.collectionId = COLLECTIONS_COL + this.collectionId = ADMIN_COL_ID this.op = WriteOp.DELETE this.feature = collection this.atomic = atomic @@ -688,7 +689,7 @@ open class Write : AnyObject() { @JvmOverloads fun deleteCollectionById(mapId: String? = null, collectionId: String, version: Version? = null): Write { this.mapId = mapId - this.collectionId = COLLECTIONS_COL + this.collectionId = ADMIN_COL_ID this.op = WriteOp.DELETE this.id = collectionId this.version = version @@ -914,7 +915,7 @@ open class Write : AnyObject() { * @return `true` if this write modifies a dictionary; `false` otherwise. * @since 3.0 */ - fun isDictionaryModification(): Boolean = mapId == ADMIN_MAP && collectionId == BOOKS_COL + fun isDictionaryModification(): Boolean = mapId == ADMIN_CATALOG_ID && collectionId == BOOKS_COL_ID /** * Tests if this write modifies a map. @@ -922,7 +923,7 @@ open class Write : AnyObject() { * @return `true` if this write modifies a map; `false` otherwise. * @since 3.0 */ - fun isMapModification(): Boolean = mapId == ADMIN_MAP && collectionId == CATALOGS_COL + fun isMapModification(): Boolean = mapId == ADMIN_CATALOG_ID && collectionId == CATALOGS_COL_ID /** * Tests if this write modifies a collection. @@ -930,7 +931,7 @@ open class Write : AnyObject() { * @return `true` if this write modifies a collection; `false` otherwise. * @since 3.0 */ - fun isCollectionModification(): Boolean = collectionId == COLLECTIONS_COL + fun isCollectionModification(): Boolean = collectionId == ADMIN_COL_ID /** * Tests if this write modifies a feature within a collection. @@ -953,7 +954,7 @@ open class Write : AnyObject() { * @see [WriteOp] */ fun validate(): Write { - if (mapId == ADMIN_MAP || collectionId == COLLECTIONS_COL) { + if (mapId == ADMIN_CATALOG_ID || collectionId == ADMIN_COL_ID) { if (isInternalId(id)) { throw NakshaException(ILLEGAL_STATE, "Modification of internal features forbidden: '$id'") } diff --git a/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/TupleNumberQueryTest.kt b/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/TupleNumberQueryTest.kt index f1a54d687..eba0f3129 100644 --- a/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/TupleNumberQueryTest.kt +++ b/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/TupleNumberQueryTest.kt @@ -29,7 +29,7 @@ class TupleNumberQueryTest { private fun randomTupleNumber() = TupleNumber( - storageNumber = Int64(random.nextInt(10)), + databaseNumber = Int64(random.nextInt(10)), mapNumber = random.nextInt(10), collectionNumber = random.nextInt(10), featureNumber = Int64(random.nextInt(10)), diff --git a/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/TupleNumberTest.kt b/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/TupleNumberTest.kt index 19d945cd9..23368b755 100644 --- a/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/TupleNumberTest.kt +++ b/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/TupleNumberTest.kt @@ -94,7 +94,7 @@ class TupleNumberTest { val bytes = t.toByteArray(B256) assertEquals(32, bytes.size) val restored = TupleNumber.fromB256(bytes) - assertEquals(t.storageNumber, restored.storageNumber) + assertEquals(t.databaseNumber, restored.databaseNumber) assertEquals(t.mapNumber, restored.mapNumber) assertEquals(t.collectionNumber, restored.collectionNumber) assertEquals(t.featureNumber, restored.featureNumber) diff --git a/here-naksha-lib-model/src/jvmMain/kotlin/naksha/model/TupleHeapCache.jvm.kt b/here-naksha-lib-model/src/jvmMain/kotlin/naksha/model/TupleHeapCache.jvm.kt index d0f3147fc..b8c63310e 100644 --- a/here-naksha-lib-model/src/jvmMain/kotlin/naksha/model/TupleHeapCache.jvm.kt +++ b/here-naksha-lib-model/src/jvmMain/kotlin/naksha/model/TupleHeapCache.jvm.kt @@ -24,7 +24,7 @@ actual class TupleHeapCache : ITupleCache { private var tuplesByStorage = AtomicMap>>() actual override fun get(tupleNumber: TupleNumber): Tuple? - = tuplesByStorage[tupleNumber.storageNumber]?.get(tupleNumber)?.deref() + = tuplesByStorage[tupleNumber.databaseNumber]?.get(tupleNumber)?.deref() actual override fun load(featureTuples: List, from: Int, to: Int, acceptFeature: Boolean): Int { var loaded = 0 @@ -47,7 +47,7 @@ actual class TupleHeapCache : ITupleCache { actual override fun put(tuple: Tuple) { val tupleNumber = tuple.tupleNumber - val storageNumber = tupleNumber.storageNumber + val storageNumber = tupleNumber.databaseNumber var storageTuples = tuplesByStorage[storageNumber] if (storageTuples == null) { storageTuples = AtomicMap() diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/LibPsql.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/LibPsql.kt index 4859a87b2..82ef287bc 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/LibPsql.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/LibPsql.kt @@ -106,10 +106,10 @@ internal const val NAKSHA_TXN_SEQ = "naksha_txn_seq" internal const val MAX_POSTGRES_TOAST_TUPLE_TARGET = 32736 internal const val MIN_POSTGRES_TOAST_TUPLE_TARGET = 2048 -internal const val TRANSACTIONS_COL = Naksha.TRANSACTIONS_COL +internal const val TRANSACTIONS_COL = Naksha.TRANSACTIONS_COL_ID -internal const val NKC_TABLE = Naksha.TRANSACTIONS_COL -internal const val NKC_TABLE_ESC = "\"${Naksha.TRANSACTIONS_COL}\"" +internal const val NKC_TABLE = Naksha.TRANSACTIONS_COL_ID +internal const val NKC_TABLE_ESC = "\"${Naksha.TRANSACTIONS_COL_ID}\"" internal const val NKC_PARTITION_COUNT = "partitionCount" internal const val NKC_ID = "id" internal const val NKC_GEO_INDEX = "geoIndex" diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgAdminMap.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgAdminMap.kt index dadbfc0c8..2234794d7 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgAdminMap.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgAdminMap.kt @@ -13,13 +13,17 @@ import naksha.base.Platform.PlatformCompanion.logger import naksha.jbon.IDictReader import naksha.jbon.JbDictionary import naksha.model.* -import naksha.model.Naksha.NakshaCompanion.ADMIN_MAP -import naksha.model.Naksha.NakshaCompanion.ADMIN_MAP_NUMBER -import naksha.model.Naksha.NakshaCompanion.CATALOGS_COL_NUMBER +import naksha.model.Naksha.NakshaCompanion.ADMIN_CATALOG_ID +import naksha.model.Naksha.NakshaCompanion.ADMIN_CATALOG_FN +import naksha.model.Naksha.NakshaCompanion.ADMIN_COL_ID +import naksha.model.Naksha.NakshaCompanion.CATALOGS_COL_FN import naksha.model.NakshaError.NakshaErrorCompanion.EXCEPTION import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_ARGUMENT import naksha.model.NakshaError.NakshaErrorCompanion.STORAGE_ID_MISMATCH +import naksha.model.objects.MemberList +import naksha.model.objects.NakshaCollection import naksha.model.objects.NakshaMap +import naksha.model.objects.StandardMembers import naksha.psql.PgColumn.PgColumnCompanion.headColumns import kotlin.js.ExperimentalJsExport import kotlin.js.JsExport @@ -55,7 +59,14 @@ abstract class PgAdminMap internal constructor( * @since 3.0.0 */ upgrade: Boolean? -) : PgMap(storage, NakshaMap().withStorageId(storage.id).withId(ADMIN_MAP)), IDictReader { +) : PgMap(storage, NakshaMap().withStorageId(storage.id).withId(ADMIN_CATALOG_ID)), IDictReader { + + /** + * This collection does not exist. It is only + */ + internal val adminMapCollection = NakshaCollection(ADMIN_COL_ID, ADMIN_CATALOG_ID) + .withMinimalMembers() + .withMinimalIndices() /** * The page-size of the database (`current_setting('block_size')`). @@ -265,7 +276,7 @@ SELECT basics.*, procs.* FROM basics, procs; var installed_version: NakshaVersion var installed_storage_id: String var installed_storage_number: Int64 - conn.execute("SELECT \"${ADMIN_MAP}\".naksha_version() AS v, \"${ADMIN_MAP}\".naksha_storage_id() AS id, \"${ADMIN_MAP}\".naksha_storage_number() AS n").fetch().use { cursor -> + conn.execute("SELECT \"${ADMIN_CATALOG_ID}\".naksha_version() AS v, \"${ADMIN_CATALOG_ID}\".naksha_storage_id() AS id, \"${ADMIN_CATALOG_ID}\".naksha_storage_number() AS n").fetch().use { cursor -> try { val v: Int64 = cursor["v"] installed_version = NakshaVersion(v) @@ -423,7 +434,7 @@ SELECT basics.*, procs.* FROM basics, procs; logger.info("Transaction counter is still at wrong day, rollover to next day") // Rollover, we update sequence of the day. Start at seq=1 (auto() encodes action in bits 1-0). version = Version.auto(txDate.year, txDate.monthNumber, txDate.dayOfMonth, Int64(1)) - txn = version.value + txn = version.number conn.execute("SELECT setval($1, $2)", arrayOf(txnSequenceOid, txn + 4)).close() } logger.info("Release advisory lock") @@ -535,7 +546,7 @@ SELECT basics.*, procs.* FROM basics, procs; * @since 3.0.0 */ fun getPgMapById(conn: PgConnection?, id: String): PgMap? { - if (ADMIN_MAP == id) return this + if (ADMIN_CATALOG_ID == id) return this val number = mapNumberById[id] val existing = if (number != null) mapCache[number] else null if (existing != null) return existing @@ -544,21 +555,21 @@ SELECT basics.*, procs.* FROM basics, procs; // Read from database val outRows = PgColumnRows() .withStorageNumber(storage.number) - .withMapNumber(ADMIN_MAP_NUMBER) - .withCollectionNumber(CATALOGS_COL_NUMBER) + .withMapNumber(ADMIN_CATALOG_FN) + .withCollectionNumber(CATALOGS_COL_FN) .withDefaultDataEncoding(Naksha.DEFAULT_DATA_ENCODING) .addColumns(headColumns) - val SQL = """SELECT ${outRows.names()} + val SQL = """SELECT * FROM "naksha~admin".${catalogs.headTable.quotedName} WHERE id = $1 AND (version & 3) < 2""" val plan = conn.prepare(SQL, arrayOf(PgType.STRING.text)) - plan.execute(arrayOf(id)).fetch().use { - outRows.addAll(cursor = it) + plan.execute(arrayOf(id)).fetch().use { cursor: PgCursor -> + } if (outRows.size == 0) return null val tuple = outRows[0] ?: return null Naksha.cache.store(tuple) - val nakshaMap = Naksha.decodeTuple(tuple).proxy(NakshaMap::class) + val nakshaMap = tuple.decodeFeature(null).proxy(NakshaMap::class) val pgMap = PgMap(storage, nakshaMap) storeMap(pgMap) return pgMap @@ -572,7 +583,7 @@ WHERE id = $1 AND (version & 3) < 2""" * @since 3.0.0 */ fun getPgMapByNumber(conn: PgConnection?, number: Int): PgMap? { - if (ADMIN_MAP_NUMBER == number) return this + if (ADMIN_CATALOG_FN == number) return this val existing = mapCache[number] if (existing != null) return existing if (conn == null) return null @@ -580,8 +591,8 @@ WHERE id = $1 AND (version & 3) < 2""" // Read from database val outRows = PgColumnRows() .withStorageNumber(storage.number) - .withMapNumber(ADMIN_MAP_NUMBER) - .withCollectionNumber(CATALOGS_COL_NUMBER) + .withMapNumber(ADMIN_CATALOG_FN) + .withCollectionNumber(CATALOGS_COL_FN) .withDefaultDataEncoding(Naksha.DEFAULT_DATA_ENCODING) .addColumns(headColumns) val SQL = """ @@ -597,7 +608,7 @@ WHERE id = $1 AND (version & 3) < 2""" if (outRows.size == 0) return null val tuple = outRows[0] ?: return null Naksha.cache.store(tuple) - val nakshaMap = Naksha.decodeTuple(tuple).proxy(NakshaMap::class) + val nakshaMap = tuple.decodeFeature(null).proxy(NakshaMap::class) val pgMap = PgMap(storage, nakshaMap) storeMap(pgMap) return pgMap diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt index ff9a3d8aa..85beaa434 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt @@ -307,8 +307,8 @@ FOR EACH ROW EXECUTE FUNCTION naksha_trigger_after();""" val prevByName = mutableMapOf() val nextByName = mutableMapOf() // Internal (mandatory) indices are always managed by the storage; skip them in the diff. - if (prevIdx != null) for (ci in prevIdx) if (ci != null && !ci.internal) prevByName[ci.name] = ci - if (nextIdx != null) for (ci in nextIdx) if (ci != null && !ci.internal) nextByName[ci.name] = ci + if (prevIdx != null) for (ci in prevIdx) if (ci != null && !ci.isInternal()) prevByName[ci.name] = ci + if (nextIdx != null) for (ci in nextIdx) if (ci != null && !ci.isInternal()) nextByName[ci.name] = ci // Removed (or changed): drop on every root. for ((name, pi) in prevByName) { @@ -330,7 +330,7 @@ FOR EACH ROW EXECUTE FUNCTION naksha_trigger_after();""" private fun customIndexEquals(a: naksha.model.objects.Index, b: naksha.model.objects.Index): Boolean { if (a.name != b.name) return false if (a.type != b.type) return false - if (a.unique != b.unique) return false + if (a.isUnique() != b.isUnique()) return false if (!listsEqual(a.on, b.on)) return false if (!listsEqual(a.include, b.include)) return false return true diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumnRows.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumnRows.kt index 2b57f1907..a5c4a7bc5 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumnRows.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumnRows.kt @@ -2,84 +2,17 @@ package naksha.psql import naksha.base.Int64 import naksha.base.Platform.PlatformCompanion.toJSON -import naksha.jbon.IBook +import naksha.jbon.BookType +import naksha.jbon.HeapBook import naksha.model.* +import naksha.model.objects.NakshaCollection import naksha.psql.PgColumn.PgColumnCompanion.allColumns -/** -* Thin [IBook] wrapper that maps column names to row values from [PgColumnRows]. - * - * This implements [IBook] by delegating index/name lookups to the underlying [rows] - * instance so that [Tuple] getters can resolve members at read time. - * @since 3.0 - */ -internal class PgRowDict( - val row: Int, - private val rows: PgColumnRows, - val fn: Int64, - val dataEncoding: DataEncoding, -) : IBook { - - override val id: String? - get() { - if (fn >= Int64(0)) { - return rows.getString(row, PgColumn.id) - } - return rows.getString(row, PgColumn.id) - } - - override val length: Int - get() = rows.columns.size - - override fun get(index: Int): Any? { - val col = rows.columns.getOrNull(index) ?: return null - return col.values.getOrNull(row) - } - - override fun indexOfString(string: String): Int { - val col = rows.columnByName[string] - return col?.index ?: -1 - } - - override fun getStringAt(index: Int): String? { - val col = rows.columns.getOrNull(index) ?: return null - val v = col.values.getOrNull(row) - return if (v is String) v else null - } - - override fun hasNames(): Boolean = true - - override fun indexOfName(name: String): Int = indexOfString(name) - - override fun getNameAt(index: Int): String? = rows.columns.getOrNull(index)?.name - - override fun namesLength(): Int = rows.columns.size - - override fun getByName(name: String): Any? { - val col = rows.columnByName[name] ?: return null - return col.values.getOrNull(row) - } - - /** - * Returns the value of a `jsonb` column as raw JSON text. - */ - private fun getJsonText(column: PgColumn): String? { - val value = rows.getAny(row, column) - return when (value) { - null -> null - is String -> value - else -> toJSON(value) - } - } - - override fun getAllWithHash(hash: Int): List = emptyList() -} - /** * Helper class to convert rows into arrays of column and vice versa. The main purpose is to read and write full tuples, but it supports basically as well virtual columns. * @since 3.0 */ -internal class PgColumnRows { +internal class PgColumnRows(val collection: NakshaCollection) { /** * All columns being added already. * @since 3.0 @@ -287,6 +220,7 @@ internal class PgColumnRows { null } } + fun getTuple(row: Int, storageNumber: Int64, mapNumber: Int, collectionNumber: Int): Tuple? { if (row < 0 || row >= size) return null val fn = getInt64(row, PgColumn.fn) ?: return null @@ -296,12 +230,8 @@ internal class PgColumnRows { "PgColumnRows.defaultDataEncoding was not set before calling getTuple(); " + "set it from the collection's dataEncoding via withDefaultDataEncoding(...)." ) - val members = PgRowDict( - row = row, - rows = this, - fn = fn, - dataEncoding = encoding - ) + val members = HeapBook(BookType.MEMBER_BOOK) + return Tuple( storageNumber = storageNumber, mapNumber = mapNumber, diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCursor.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCursor.kt index f086d3e40..44f18c5e6 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCursor.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCursor.kt @@ -44,6 +44,12 @@ interface PgCursor : AutoCloseable { */ fun rowNumber(): Int + /** + * Returns the names of the selected columns. + * @return the names of the selected columns. + */ + fun columnNames(): Array + /** * Tests if the current row has the given colum. * @param name the name of the column diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMap.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMap.kt index e833bebce..4e674d93f 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMap.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMap.kt @@ -5,27 +5,27 @@ package naksha.psql import naksha.base.* import naksha.base.Platform.PlatformCompanion.logger import naksha.model.Naksha -import naksha.model.Naksha.NakshaCompanion.COLLECTIONS_COL -import naksha.model.Naksha.NakshaCompanion.COLLECTIONS_COL_NUMBER -import naksha.model.Naksha.NakshaCompanion.BOOKS_COL -import naksha.model.Naksha.NakshaCompanion.BOOKS_COL_NUMBER -import naksha.model.Naksha.NakshaCompanion.CATALOGS_COL -import naksha.model.Naksha.NakshaCompanion.CATALOGS_COL_NUMBER -import naksha.model.Naksha.NakshaCompanion.TRANSACTIONS_COL -import naksha.model.Naksha.NakshaCompanion.TRANSACTIONS_COL_NUMBER -import naksha.model.NakshaError +import naksha.model.Naksha.NakshaCompanion.ADMIN_CATALOG_ID +import naksha.model.Naksha.NakshaCompanion.ADMIN_COL_ID +import naksha.model.Naksha.NakshaCompanion.ADMIN_COL_FN +import naksha.model.Naksha.NakshaCompanion.BOOKS_COL_ID +import naksha.model.Naksha.NakshaCompanion.BOOKS_COL_FN +import naksha.model.Naksha.NakshaCompanion.CATALOGS_COL_ID +import naksha.model.Naksha.NakshaCompanion.CATALOGS_COL_FN +import naksha.model.Naksha.NakshaCompanion.TRANSACTIONS_COL_ID +import naksha.model.Naksha.NakshaCompanion.TRANSACTIONS_COL_FN import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_STATE import naksha.model.NakshaException import naksha.model.objects.NakshaCollection import naksha.model.objects.NakshaMap import naksha.psql.PgColumn.PgColumnCompanion.headColumns import naksha.psql.PgUtil.PgUtilCompanion.quoteIdent -import naksha.psql.PgUtil.PgUtilCompanion.quoteLiteral import kotlin.js.JsExport import kotlin.jvm.JvmField /** - * A map stores collections. + * A map _(aka catalog)_ contains collections. + * @since 3.0 */ @JsExport open class PgMap internal constructor( @@ -53,6 +53,16 @@ open class PgMap internal constructor( */ val number: Int = nakshaMap.number ) { + /** + * The admin-collection, aka `naksha~collections`, that exist in every catalog. + * + * The admin-collection contains the [NakshaCollection] features of all collections being part of the catalog. The admin-collection itself does not exist as dedicated feature, therefore is hardcoded here. Currently, there is no way to modify the members of it. However, in the future we at least want to allow clients to specify the members and indices when creating a new database. So the storage need to read the admin-collection and admin-catalog from some internal private location within the storage to get the member->JSON mapping, and to understand members and indices. + * @since 3.0 + */ + internal val adminCollection = NakshaCollection(ADMIN_COL_ID, ADMIN_CATALOG_ID) + .withXyzMembers() + .withXyzIndices() + /** * The map-identifier quoted optionally in double quotes. * @since 3.0 @@ -91,7 +101,7 @@ open class PgMap internal constructor( get() { var c = _collections if (c == null) { - c = PgCollection(this, NakshaCollection().withMapId(id).withId(COLLECTIONS_COL)) + c = PgCollection(this, NakshaCollection().withMapId(id).withId(ADMIN_COL_ID)) _collections = c } return c @@ -320,14 +330,14 @@ open class PgMap internal constructor( fun getPgCollectionById(conn: PgConnection?, id: String): PgCollection? { if (this is PgAdminMap) { return when (id) { - COLLECTIONS_COL -> collections - TRANSACTIONS_COL -> transactions - CATALOGS_COL -> catalogs - BOOKS_COL -> books + ADMIN_COL_ID -> collections + TRANSACTIONS_COL_ID -> transactions + CATALOGS_COL_ID -> catalogs + BOOKS_COL_ID -> books else -> null } } - if (id == COLLECTIONS_COL) return collections + if (id == ADMIN_COL_ID) return collections val number = collectionNumberById[id] val existing = if (number != null) collectionCache[number] else null if (existing != null || conn == null) return existing @@ -336,7 +346,7 @@ open class PgMap internal constructor( val outRows = PgColumnRows() .withStorageNumber(storage.number) .withMapNumber(this.number) - .withCollectionNumber(COLLECTIONS_COL_NUMBER) + .withCollectionNumber(ADMIN_COL_FN) .withDefaultDataEncoding(Naksha.DEFAULT_DATA_ENCODING) .addColumns(headColumns) setSearchPath(conn) @@ -366,14 +376,14 @@ WHERE id = $1 AND (version & 3) < 2""" fun getPgCollectionByNumber(conn: PgConnection?, number: Int): PgCollection? { if (this is PgAdminMap) { return when (number) { - COLLECTIONS_COL_NUMBER -> collections - TRANSACTIONS_COL_NUMBER -> transactions - CATALOGS_COL_NUMBER -> catalogs - BOOKS_COL_NUMBER -> books + ADMIN_COL_FN -> collections + TRANSACTIONS_COL_FN -> transactions + CATALOGS_COL_FN -> catalogs + BOOKS_COL_FN -> books else -> null } } - if (number == COLLECTIONS_COL_NUMBER) return collections + if (number == ADMIN_COL_FN) return collections val existing = collectionCache[number] if (existing != null || conn == null) return existing @@ -381,7 +391,7 @@ WHERE id = $1 AND (version & 3) < 2""" val outRows = PgColumnRows() .withStorageNumber(storage.number) .withMapNumber(this.number) - .withCollectionNumber(COLLECTIONS_COL_NUMBER) + .withCollectionNumber(ADMIN_COL_FN) .withDefaultDataEncoding(Naksha.DEFAULT_DATA_ENCODING) .addColumns(headColumns) setSearchPath(conn) diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaBooks.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaBooks.kt index 3fe3c3765..00e202664 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaBooks.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaBooks.kt @@ -13,8 +13,8 @@ import kotlin.js.JsExport */ @JsExport class PgNakshaBooks internal constructor(adminMap: PgAdminMap) : PgCollection(adminMap, NakshaCollection() - .withMapId(Naksha.ADMIN_MAP) - .withId(Naksha.BOOKS_COL) + .withMapId(Naksha.ADMIN_CATALOG_ID) + .withId(Naksha.BOOKS_COL_ID) ), PgInternalCollection, IDictManager { override fun putDictionary(dict: JbDictionary) { diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaCatalogs.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaCatalogs.kt index fe62e35ab..537412acc 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaCatalogs.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaCatalogs.kt @@ -12,6 +12,6 @@ import kotlin.js.JsExport */ @JsExport class PgNakshaCatalogs internal constructor(adminMap: PgAdminMap) : PgCollection(adminMap, NakshaCollection() - .withMapId(Naksha.ADMIN_MAP) - .withId(Naksha.CATALOGS_COL) + .withMapId(Naksha.ADMIN_CATALOG_ID) + .withId(Naksha.CATALOGS_COL_ID) ), PgInternalCollection diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaCollections.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaCollections.kt index a30c05eea..32dc49b5b 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaCollections.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaCollections.kt @@ -12,5 +12,5 @@ import kotlin.js.JsExport @JsExport class PgNakshaCollections internal constructor(map: PgMap) : PgCollection(map, NakshaCollection() .withMapId(map.id) - .withId(Naksha.COLLECTIONS_COL) + .withId(Naksha.ADMIN_COL_ID) ), PgInternalCollection diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaTransactions.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaTransactions.kt index d50e9d404..c3c64ab48 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaTransactions.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaTransactions.kt @@ -2,8 +2,8 @@ package naksha.psql -import naksha.model.Naksha.NakshaCompanion.ADMIN_MAP -import naksha.model.Naksha.NakshaCompanion.TRANSACTIONS_COL +import naksha.model.Naksha.NakshaCompanion.ADMIN_CATALOG_ID +import naksha.model.Naksha.NakshaCompanion.TRANSACTIONS_COL_ID import naksha.model.objects.NakshaCollection import naksha.model.objects.StandardIndices import naksha.model.objects.StandardMembers @@ -28,8 +28,8 @@ import kotlin.js.JsExport */ @JsExport class PgNakshaTransactions internal constructor(adminMap: PgAdminMap) : PgCollection(adminMap, NakshaCollection() - .withMapId(ADMIN_MAP) - .withId(TRANSACTIONS_COL) + .withMapId(ADMIN_CATALOG_ID) + .withId(TRANSACTIONS_COL_ID) .withStoreDeleted(StoreMode.OFF) .withStoreHistory(StoreMode.ON) .withStoreMeta(StoreMode.OFF) diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt index 72e09450c..384ce8eea 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt @@ -88,12 +88,12 @@ internal class PgQueryWhereBuilder(private val request: ReadFeatures) { val txn = request.version if (txn != null) { if (where.isNotEmpty()) where.append(" AND ") - where.append("${PgColumn.version} <= ${txn.value}") + where.append("${PgColumn.version} <= ${txn.number}") } val min_txn = request.minVersion if (min_txn != null) { if (where.isNotEmpty()) where.append(" AND ") - where.append("${PgColumn.version} >= ${min_txn.value}") + where.append("${PgColumn.version} >= ${min_txn.number}") } } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgSession.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgSession.kt index 32e585827..75d10d3f1 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgSession.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgSession.kt @@ -307,7 +307,7 @@ open class PgSession( if (tx != null) { try { val transaction = tx.transaction - val writeTx = Write().createFeature(Naksha.ADMIN_MAP, TRANSACTIONS_COL, transaction) + val writeTx = Write().createFeature(Naksha.ADMIN_CATALOG_ID, TRANSACTIONS_COL, transaction) val writeRequest = WriteRequest().add(writeTx) // TODO: Should we use a savepoint here? val writer = PgWriter(this, false) diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgStorage.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgStorage.kt index 34814796d..8f1f3ca8a 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgStorage.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgStorage.kt @@ -4,7 +4,9 @@ package naksha.psql import naksha.base.fn.Fx2 import naksha.model.* +import naksha.model.Naksha.NakshaCompanion.ADMIN_COL_ID import naksha.model.NakshaError.NakshaErrorCompanion.UNINITIALIZED +import naksha.model.objects.NakshaCollection import kotlin.js.JsExport // TODO: Create "naksha~admin" map with map-number 0 diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgUtil.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgUtil.kt index 2a3f48a26..3b340a30e 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgUtil.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgUtil.kt @@ -6,11 +6,11 @@ import naksha.base.* import naksha.geo.SpGeometry import naksha.jbon.* import naksha.model.* -import naksha.model.Naksha.NakshaCompanion.ADMIN_MAP -import naksha.model.Naksha.NakshaCompanion.COLLECTIONS_COL -import naksha.model.Naksha.NakshaCompanion.BOOKS_COL -import naksha.model.Naksha.NakshaCompanion.CATALOGS_COL -import naksha.model.Naksha.NakshaCompanion.TRANSACTIONS_COL +import naksha.model.Naksha.NakshaCompanion.ADMIN_CATALOG_ID +import naksha.model.Naksha.NakshaCompanion.ADMIN_COL_ID +import naksha.model.Naksha.NakshaCompanion.BOOKS_COL_ID +import naksha.model.Naksha.NakshaCompanion.CATALOGS_COL_ID +import naksha.model.Naksha.NakshaCompanion.TRANSACTIONS_COL_ID import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_ARGUMENT import naksha.model.objects.NakshaFeature import naksha.psql.PgPlatform.PgPlatformCompanion.quote_ident @@ -32,7 +32,7 @@ class PgUtil private constructor() { */ @JvmField @JsStatic - val ADMIN_MAP_QUOTED = quoteIdent(ADMIN_MAP) + val ADMIN_MAP_QUOTED = quoteIdent(ADMIN_CATALOG_ID) /** * The quoted identifier of the collection in which transactions are stored. @@ -40,7 +40,7 @@ class PgUtil private constructor() { */ @JvmField @JsStatic - val ADMIN_TRANSACTIONS_COL_QUOTED = quoteIdent(TRANSACTIONS_COL) + val ADMIN_TRANSACTIONS_COL_QUOTED = quoteIdent(TRANSACTIONS_COL_ID) /** * The quoted identifier of the virtual catalogs collection to be used in queries. @@ -48,7 +48,7 @@ class PgUtil private constructor() { */ @JvmField @JsStatic - val ADMIN_CATALOGS_COL_QUOTED = quoteIdent(CATALOGS_COL) + val ADMIN_CATALOGS_COL_QUOTED = quoteIdent(CATALOGS_COL_ID) /** * The quoted identifier of the virtual collection in which the books (global JBON2 dictionaries) are stored. @@ -56,7 +56,7 @@ class PgUtil private constructor() { */ @JvmField @JsStatic - val ADMIN_BOOKS_COL_QUOTED = quoteIdent(BOOKS_COL) + val ADMIN_BOOKS_COL_QUOTED = quoteIdent(BOOKS_COL_ID) /** * The quoted identifier of the virtual collections collection to be used in queries. @@ -64,7 +64,7 @@ class PgUtil private constructor() { */ @JvmField @JsStatic - val COLLECTIONS_COL_QUOTED = quoteIdent(COLLECTIONS_COL) + val COLLECTIONS_COL_QUOTED = quoteIdent(ADMIN_COL_ID) /** * Array to query the partition name from the partition number (resolves 0 to "000", 1 to "001", ..., 255 to "256"). diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWrite.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWrite.kt index b04d49f56..70a09c4b1 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWrite.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWrite.kt @@ -18,8 +18,8 @@ internal data class PgWrite(val original: Write, val i: Int) { /** * The map into which to write. * - * - If a map is modified, this is [Naksha.ADMIN_MAP][naksha.model.Naksha.ADMIN_MAP], [asPgMap] and [asNakshaMap] will be set. - * - If a collection is modified, this is the map in which [Naksha.COLLECTIONS_COL][naksha.model.Naksha.COLLECTIONS_COL] is located, [asPgCollection] and [asNakshaCollection] will be set. + * - If a map is modified, this is [Naksha.ADMIN_MAP][naksha.model.Naksha.ADMIN_CATALOG_ID], [asPgMap] and [asNakshaMap] will be set. + * - If a collection is modified, this is the map in which [Naksha.COLLECTIONS_COL][naksha.model.Naksha.ADMIN_COL_ID] is located, [asPgCollection] and [asNakshaCollection] will be set. * @since 3.0 */ lateinit var map: PgMap @@ -27,8 +27,8 @@ internal data class PgWrite(val original: Write, val i: Int) { /** * The collection into which to write. * - * - If a map is modified, this is [Naksha.CATALOGS_COL][naksha.model.Naksha.CATALOGS_COL], [asPgMap] and [asNakshaMap] will be set. - * - If a collection is modified, this is [Naksha.COLLECTIONS_COL][naksha.model.Naksha.COLLECTIONS_COL], [asPgCollection] and [asNakshaCollection] will be set. + * - If a map is modified, this is [Naksha.CATALOGS_COL][naksha.model.Naksha.CATALOGS_COL_ID], [asPgMap] and [asNakshaMap] will be set. + * - If a collection is modified, this is [Naksha.COLLECTIONS_COL][naksha.model.Naksha.ADMIN_COL_ID], [asPgCollection] and [asNakshaCollection] will be set. * @since 3.0 */ lateinit var collection: PgCollection @@ -120,7 +120,7 @@ internal data class PgWrite(val original: Write, val i: Int) { val isCollectionModification: Boolean get() = original.isCollectionModification() val isTransactionModification: Boolean - get() = Naksha.ADMIN_MAP == map.id && Naksha.TRANSACTIONS_COL == collection.id + get() = Naksha.ADMIN_CATALOG_ID == map.id && Naksha.TRANSACTIONS_COL_ID == collection.id // This variant differs from write.isFeatureModification, because it includes dictionaries, which are just features for us! val isFeatureModification: Boolean get() = !isTransactionModification && !isMapModification && !isCollectionModification diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterDelete.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterDelete.kt index 2e46a16d1..1d384f1e2 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterDelete.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterDelete.kt @@ -34,7 +34,7 @@ internal class PgWriterDelete(writer: PgWriter, collection: PgCollection, partit val row = e.index val write = e.value inRows.set(row, "id", write.id) - inRows.set(row, "expected_version", write.version?.value) + inRows.set(row, "expected_version", write.version?.number) } } @@ -43,7 +43,7 @@ internal class PgWriterDelete(writer: PgWriter, collection: PgCollection, partit val insert_into_history = if (historyTable != null && collection.head.storeHistory == StoreMode.ON) historyTable else null // The new version with action bits set to DELETED (2). - val deleted_version = "(${tx.version.value}::int8 | 2)" + val deleted_version = "(${tx.version.number}::int8 | 2)" // All input provided by client, `id` and optionally `expected_version` val query = """WITH query AS ( diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt index 521408f27..bdfb6854d 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt @@ -30,7 +30,7 @@ internal class PgWriterUpdate(writer: PgWriter, collection: PgCollection, partit if (tuple != null) { writeById[write.id] = write inRows[i] = tuple - inRows.set(i, "expected_version", write.version?.value) + inRows.set(i, "expected_version", write.version?.number) inRows.setCustomMembers(i, write.feature, members) i++ } diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/AttachmentTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/AttachmentTest.kt index 2ebbe7fd0..f4b5ba725 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/AttachmentTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/AttachmentTest.kt @@ -119,7 +119,7 @@ class AttachmentTest : PgTestBase() { val insertedFeatureGuid = readFeature.properties.xyz.guid assertNotNull(insertedFeatureGuid) assertEquals(featureId, insertedFeatureGuid.id) - assertEquals(storage.number, insertedFeatureGuid.tupleNumber.storageNumber) + assertEquals(storage.number, insertedFeatureGuid.tupleNumber.databaseNumber) assertEquals(map.number, insertedFeatureGuid.tupleNumber.mapNumber) assertEquals(collection.number, insertedFeatureGuid.tupleNumber.collectionNumber) @@ -208,7 +208,7 @@ class AttachmentTest : PgTestBase() { val insertedFeatureGuid = readFeature.properties.xyz.guid assertNotNull(insertedFeatureGuid) assertEquals(featureId, insertedFeatureGuid.id) - assertEquals(storage.number, insertedFeatureGuid.tupleNumber.storageNumber) + assertEquals(storage.number, insertedFeatureGuid.tupleNumber.databaseNumber) assertEquals(map.number, insertedFeatureGuid.tupleNumber.mapNumber) assertEquals(collection.number, insertedFeatureGuid.tupleNumber.collectionNumber) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/CollectionTests.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/CollectionTests.kt index 2b848dc50..4102f36e8 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/CollectionTests.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/CollectionTests.kt @@ -35,7 +35,6 @@ import naksha.model.request.ReadFeatures import naksha.model.request.Write import naksha.model.request.WriteRequest import kotlin.test.* -import naksha.psql.PgType class CollectionTests : PgTestBase(collection = null, mapId = "") { @@ -62,7 +61,7 @@ class CollectionTests : PgTestBase(collection = null, mapId = "") { // And: Virtual Collections contain the created collection val selectCollectionFromVirt = ReadFeatures().apply { mapId = collection.mapId - collectionIds += Naksha.COLLECTIONS_COL + collectionIds += Naksha.ADMIN_COL_ID featureIds += collection.id } val virtBeforeDelete = executeRead(selectCollectionFromVirt) @@ -332,7 +331,7 @@ class CollectionTests : PgTestBase(collection = null, mapId = "") { assertEquals(StoreMode.SUSPEND, responseCollection.storeDeleted) val selectCollectionFromVirt = ReadFeatures().apply { mapId = map.id - collectionIds += Naksha.COLLECTIONS_COL + collectionIds += Naksha.ADMIN_COL_ID featureIds += collection.id } val colRead = assertNotNull(executeRead(selectCollectionFromVirt).features[0]).proxy(NakshaCollection::class) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/TupleNumberPersistenceTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/TupleNumberPersistenceTest.kt index 0f6329f19..ba0c51ffd 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/TupleNumberPersistenceTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/TupleNumberPersistenceTest.kt @@ -71,7 +71,7 @@ class TupleNumberPersistenceTest : PgTestBase(collection = null, mapId = "") { require(pgMap != null) { "Missing map ${collection.mapId}" } val pgCollection = pgMap.getPgCollectionById(conn, collection.id) require(pgCollection != null) { "Missing collection ${collection.id}" } - assertEquals(storage.number, persistedTuple.tupleNumber.storageNumber) + assertEquals(storage.number, persistedTuple.tupleNumber.databaseNumber) assertEquals(pgMap.number, persistedTuple.tupleNumber.mapNumber) assertEquals(pgCollection.number, persistedTuple.tupleNumber.collectionNumber) assertEquals(featureNumber(feature.id), persistedTuple.tupleNumber.featureNumber) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/UpsertFeatureTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/UpsertFeatureTest.kt index abb709e4f..b3d9af008 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/UpsertFeatureTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/UpsertFeatureTest.kt @@ -39,7 +39,7 @@ class UpsertFeatureTest : PgTestBase() { collectionIds += collection.id featureIds += initialFeature.id queryHistory = true - }).features.sortedBy { it!!.properties.xyz.version!!.value.toLong() } + }).features.sortedBy { it!!.properties.xyz.version!!.number.toLong() } // Then assertThatFeature(retrievedFeatures[0]!!) diff --git a/here-naksha-lib-psql/src/jsMain/kotlin/naksha/psql/Plv8Cursor.kt b/here-naksha-lib-psql/src/jsMain/kotlin/naksha/psql/Plv8Cursor.kt index fd0865d2f..0bfcd809f 100644 --- a/here-naksha-lib-psql/src/jsMain/kotlin/naksha/psql/Plv8Cursor.kt +++ b/here-naksha-lib-psql/src/jsMain/kotlin/naksha/psql/Plv8Cursor.kt @@ -27,6 +27,10 @@ class Plv8Cursor: PgCursor { TODO("Not yet implemented") } + override fun columnNames(): Array { + TODO("Not yet implemented") + } + override fun contains(name: String): Boolean { TODO("Not yet implemented") } diff --git a/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlCursor.kt b/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlCursor.kt index 8e5bf2662..26a1d0208 100644 --- a/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlCursor.kt +++ b/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlCursor.kt @@ -97,7 +97,7 @@ class PsqlCursor internal constructor(private val stmt: Statement, private val c /** * The names of the columns, indexed from 0, while in a SQL statement the index starts with 1. */ - private fun columnNames(): Array = + override fun columnNames(): Array = columnNames ?: throw IllegalStateException("Initialization error: Missing column names array") /** diff --git a/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlMap.kt b/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlMap.kt index e96c1ce56..f2e5d3c68 100644 --- a/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlMap.kt +++ b/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlMap.kt @@ -2,12 +2,7 @@ package naksha.psql import com.github.benmanes.caffeine.cache.* import naksha.base.* -import naksha.model.Naksha -import naksha.model.Naksha.NakshaCompanion.ADMIN_MAP_NUMBER -import naksha.model.Naksha.NakshaCompanion.CATALOGS_COL_NUMBER import naksha.model.illegalArg -import naksha.model.objects.NakshaMap -import naksha.psql.PgColumn.PgColumnCompanion.allColumns import java.util.concurrent.TimeUnit /** From 7879c57877941b34c72eac63edb0aa7c46180a33 Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Fri, 12 Jun 2026 17:12:42 +0200 Subject: [PATCH 13/57] Latest state, work in progress. Signed-off-by: Alexander Lowey-Weber --- .../cli/copy/service/CopyServiceTest.java | 2 +- .../lib/handlers/DefaultStorageHandler.java | 2 +- .../lib/handlers/util/RequestTypesUtil.java | 4 +- .../handlers/DefaultStorageHandlerTest.java | 12 +- .../lib/hub/mock/NHAdminWriterMock.java | 2 +- .../storages/NHSpaceStorageWriterTest.java | 8 +- .../kotlin/naksha/model/IStorage.kt | 2 +- .../commonMain/kotlin/naksha/model/Naksha.kt | 14 +- .../commonMain/kotlin/naksha/model/Tuple.kt | 2 +- .../kotlin/naksha/model/TupleNumber.kt | 20 +- .../naksha/model/TupleNumberBinaryArray.kt | 4 +- .../kotlin/naksha/model/TupleNumberList.kt | 6 +- .../kotlin/naksha/model/objects/IndexList.kt | 9 - .../kotlin/naksha/model/objects/Member.kt | 4 +- .../kotlin/naksha/model/objects/MemberType.kt | 49 ++- .../naksha/model/objects/NakshaCollection.kt | 8 +- .../naksha/model/objects/NakshaFeature.kt | 2 +- .../naksha/model/objects/StandardMembers.kt | 2 +- .../naksha/model/request/ReadCollections.kt | 2 +- .../kotlin/naksha/model/request/Write.kt | 22 +- .../naksha/model/TupleNumberQueryTest.kt | 2 +- .../kotlin/naksha/model/TupleNumberTest.kt | 4 +- .../kotlin/naksha/psql/PgAdminMap.kt | 21 +- .../kotlin/naksha/psql/PgCollection.kt | 7 +- .../kotlin/naksha/psql/PgColumnEntry.kt | 2 - .../kotlin/naksha/psql/PgColumnRows.kt | 78 +--- .../naksha/psql/PgCustomMemberValues.kt | 343 ---------------- .../commonMain/kotlin/naksha/psql/PgMap.kt | 42 +- .../kotlin/naksha/psql/PgMemberHelper.kt | 382 ++++++++++++++++++ .../kotlin/naksha/psql/PgNakshaCollections.kt | 2 +- .../commonMain/kotlin/naksha/psql/PgRead.kt | 6 +- .../kotlin/naksha/psql/PgStorage.kt | 2 - .../commonMain/kotlin/naksha/psql/PgTable.kt | 12 +- .../commonMain/kotlin/naksha/psql/PgType.kt | 29 +- .../commonMain/kotlin/naksha/psql/PgUtil.kt | 4 +- .../commonMain/kotlin/naksha/psql/PgWrite.kt | 4 +- .../commonMain/kotlin/naksha/psql/PgWriter.kt | 4 +- .../kotlin/naksha/psql/PgWriterBase.kt | 1 - .../kotlin/naksha/psql/AttachmentTest.kt | 4 +- .../kotlin/naksha/psql/CollectionTests.kt | 8 +- .../naksha/psql/TupleNumberPersistenceTest.kt | 2 +- 41 files changed, 569 insertions(+), 566 deletions(-) delete mode 100644 here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCustomMemberValues.kt create mode 100644 here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMemberHelper.kt diff --git a/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/copy/service/CopyServiceTest.java b/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/copy/service/CopyServiceTest.java index 36a63df69..9c31061be 100644 --- a/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/copy/service/CopyServiceTest.java +++ b/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/copy/service/CopyServiceTest.java @@ -848,7 +848,7 @@ private void assertCreateMapWrite(Write write) { private void assertCreateCollectionWrite(Write write) { assertEquals(targetCopyElement.getMapId(), write.getMapId()); - assertEquals(Naksha.ADMIN_COL_ID, write.getCollectionId()); + assertEquals(Naksha.COLLECTIONS_COL_ID, write.getCollectionId()); assertEquals(WriteOp.CREATE, write.getOp()); assertNotNull(write.getFeature()); assertEquals(targetCopyElement.getCollectionId(), write.getFeature().getId()); diff --git a/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/DefaultStorageHandler.java b/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/DefaultStorageHandler.java index 624a6ae48..4f8e0efd3 100644 --- a/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/DefaultStorageHandler.java +++ b/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/DefaultStorageHandler.java @@ -535,7 +535,7 @@ private void applyMapIdAndCollectionId( collectionFromRequest.setId(collectionId); }); } - String finalCollectionId = isOnlyWriteCollections(wr) ? Naksha.ADMIN_COL_ID : collectionId; + String finalCollectionId = isOnlyWriteCollections(wr) ? Naksha.COLLECTIONS_COL_ID : collectionId; wr.getWrites().forEach(write -> { write.setMapId(mapId); write.setCollectionId(finalCollectionId); diff --git a/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/util/RequestTypesUtil.java b/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/util/RequestTypesUtil.java index 3406fae47..19b6b476b 100644 --- a/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/util/RequestTypesUtil.java +++ b/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/util/RequestTypesUtil.java @@ -38,7 +38,7 @@ public static boolean isOnlyWriteFeatures(@NotNull Request request) { for (Write write : ((WriteRequest) request).getWrites()) { // A Write operation onto the virtual "naksha~collections" means that it is a write request for // NakshaCollection - if (Naksha.ADMIN_COL_ID.equals(write.getCollectionId())) return false; + if (Naksha.COLLECTIONS_COL_ID.equals(write.getCollectionId())) return false; } return true; } @@ -53,7 +53,7 @@ public static boolean isOnlyWriteCollections(Request request) { for (Write write : writes) { // A Write operation onto the virtual "naksha~collections" means that it is a write request for // NakshaCollection - if (!Naksha.ADMIN_COL_ID.equals(write.getCollectionId())) return false; + if (!Naksha.COLLECTIONS_COL_ID.equals(write.getCollectionId())) return false; } return true; } diff --git a/here-naksha-lib-handlers/src/jvmTest/java/com/here/naksha/lib/handlers/DefaultStorageHandlerTest.java b/here-naksha-lib-handlers/src/jvmTest/java/com/here/naksha/lib/handlers/DefaultStorageHandlerTest.java index e1c7d4ce8..3e2f2cf8a 100644 --- a/here-naksha-lib-handlers/src/jvmTest/java/com/here/naksha/lib/handlers/DefaultStorageHandlerTest.java +++ b/here-naksha-lib-handlers/src/jvmTest/java/com/here/naksha/lib/handlers/DefaultStorageHandlerTest.java @@ -180,7 +180,7 @@ void shouldCreateMissingCollectionRespectingPriority(CollectionPriorityTestCase Write writeCollection = findSingleCreateCollectionWrite(capturedWrites); assertEquals(WriteOp.CREATE, writeCollection.getOp()); assertEquals(testCase.correctCollection().getId(), writeCollection.getId()); - assertEquals(Naksha.ADMIN_COL_ID, writeCollection.getCollectionId()); + assertEquals(Naksha.COLLECTIONS_COL_ID, writeCollection.getCollectionId()); // And: write features related to the same feature in correct collection List featureWrites = getSingularWritesToCollection(capturedWrites, testCase.correctCollection().getId()); @@ -393,10 +393,10 @@ void shouldReattemptWriteCollectionsAfterMissingMapByCreatingMap() { // 1st and 3rd are WriteCollections against COLLECTIONS_COL with map from storage props assertTrue(RequestTypesUtil.isOnlyWriteCollections(calls.get(0))); assertTrue(RequestTypesUtil.isOnlyWriteCollections(calls.get(2))); - assertEquals(Naksha.ADMIN_COL_ID, calls.get(0).getWrites().get(0).getCollectionId()); + assertEquals(Naksha.COLLECTIONS_COL_ID, calls.get(0).getWrites().get(0).getCollectionId()); assertEquals(mapIdFromStorageProps, calls.get(0).getWrites().get(0).getMapId()); assertEquals("target_collection", calls.get(0).getWrites().get(0).getFeature().getId()); - assertEquals(Naksha.ADMIN_COL_ID, calls.get(2).getWrites().get(0).getCollectionId()); + assertEquals(Naksha.COLLECTIONS_COL_ID, calls.get(2).getWrites().get(0).getCollectionId()); assertEquals(mapIdFromStorageProps, calls.get(2).getWrites().get(0).getMapId()); assertEquals("target_collection", calls.get(2).getWrites().get(0).getFeature().getId()); @@ -461,7 +461,7 @@ void shouldApplyMapIdAndCollectionsColForWriteCollections() { verify(storageWriteSession).execute(captor.capture()); Write write = captor.getValue().getWrites().get(0); assertTrue(RequestTypesUtil.isOnlyWriteCollections(captor.getValue())); - assertEquals(Naksha.ADMIN_COL_ID, write.getCollectionId(), "WriteCollections must target naksa~collections collection"); + assertEquals(Naksha.COLLECTIONS_COL_ID, write.getCollectionId(), "WriteCollections must target naksa~collections collection"); assertEquals(mapIdFromStorageProps, write.getMapId(), "MapId must be taken from storage props"); assertEquals("apply_col", write.getFeature().getId()); } @@ -540,7 +540,7 @@ private IStorage registeredStorageWithConfig(NakshaStorage config) { } private static Write findSingleCreateCollectionWrite(List writeRequests) { - List collectionWrites = getSingularWritesToCollection(writeRequests, Naksha.ADMIN_COL_ID); + List collectionWrites = getSingularWritesToCollection(writeRequests, Naksha.COLLECTIONS_COL_ID); assertEquals(1, collectionWrites.size(), "Expected single collection write"); return collectionWrites.get(0); } @@ -560,7 +560,7 @@ private static Stream flattenSingularWriteRequest(List writ private static ArgumentMatcher matchesCreateCollectionRequest() { return writeRequest -> { WriteList writes = writeRequest.getWrites(); - return writes.size() == 1 && Naksha.ADMIN_COL_ID.equals(writes.get(0).getCollectionId()); + return writes.size() == 1 && Naksha.COLLECTIONS_COL_ID.equals(writes.get(0).getCollectionId()); }; } diff --git a/here-naksha-lib-hub/src/jvmTest/java/com/here/naksha/lib/hub/mock/NHAdminWriterMock.java b/here-naksha-lib-hub/src/jvmTest/java/com/here/naksha/lib/hub/mock/NHAdminWriterMock.java index 37d2140d3..2aae972d9 100644 --- a/here-naksha-lib-hub/src/jvmTest/java/com/here/naksha/lib/hub/mock/NHAdminWriterMock.java +++ b/here-naksha-lib-hub/src/jvmTest/java/com/here/naksha/lib/hub/mock/NHAdminWriterMock.java @@ -51,7 +51,7 @@ public NHAdminWriterMock(final @NotNull Map write .hasOp(WriteOp.DELETE) - .hasCollectionId(Naksha.ADMIN_COL_ID) + .hasCollectionId(Naksha.COLLECTIONS_COL_ID) .hasId(CUSTOM_SPACE) ); @@ -129,7 +129,7 @@ void shouldNotTriggerSpaceEntryDeletionWhenPurgingFailed() { assertThatWriteRequest(requestsPassedToPipeline.get(0)) .hasSingleWriteThat(write -> write .hasOp(WriteOp.DELETE) - .hasCollectionId(Naksha.ADMIN_COL_ID) + .hasCollectionId(Naksha.COLLECTIONS_COL_ID) .hasId(CUSTOM_SPACE) ); @@ -159,7 +159,7 @@ void shouldFailWhenSpaceEntryDeletionFailed() { assertThatWriteRequest(requestsPassedToPipeline.get(0)) .hasSingleWriteThat(write -> write .hasOp(WriteOp.DELETE) - .hasCollectionId(Naksha.ADMIN_COL_ID) + .hasCollectionId(Naksha.COLLECTIONS_COL_ID) .hasId(CUSTOM_SPACE) ); @@ -200,7 +200,7 @@ private EventPipeline eventPipelineFailingOn(ArgumentMatcher faili private ArgumentMatcher writeCollectionRequest() { return writeRequest -> { List writes = writeRequest.getWrites(); - return writes.size() == 1 && writes.get(0).getCollectionId().equals(Naksha.ADMIN_COL_ID); + return writes.size() == 1 && writes.get(0).getCollectionId().equals(Naksha.COLLECTIONS_COL_ID); }; } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/IStorage.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/IStorage.kt index 7e8963112..c2e00e3e9 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/IStorage.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/IStorage.kt @@ -15,7 +15,7 @@ import kotlin.js.JsExport * * Storages operate on maps. All storages do have an [administrative map][Naksha.ADMIN_CATALOG_ID], which can be virtual or real, implementation dependent. In this admin-map the storage exposes and manages the custom maps it stores, the transaction-logs of the storage, and the global dictionaries needed for the [JBON](https://github.com/heremaps/naksha/blob/v3/docs/JBON.md), plus optional implementation specific information. * - * All other maps are custom maps, which are isolated data sinks within the same storage (like an own database schema, an own S3 bucket, an own SQLite database, an own directory or file, aso.). Each custom map is a fully separated storage entity. Within each custom map one [virtual admin collection][Naksha.ADMIN_COL_ID] is exposed, which can be used to manage the collections in the map. Some storages allow to access multiple maps from one session, others may limit a session to a single map, and will reject cross map operations with [NakshaError.UNSUPPORTED_OPERATION]. + * All other maps are custom maps, which are isolated data sinks within the same storage (like an own database schema, an own S3 bucket, an own SQLite database, an own directory or file, aso.). Each custom map is a fully separated storage entity. Within each custom map one [virtual admin collection][Naksha.COLLECTIONS_COL_ID] is exposed, which can be used to manage the collections in the map. Some storages allow to access multiple maps from one session, others may limit a session to a single map, and will reject cross map operations with [NakshaError.UNSUPPORTED_OPERATION]. * * The storage will cache the dictionaries to avoid that just for [Tuple] decoding a new session need to be opened, which would require object creation for every single feature being decoded, therefore every storage implements the [IDictReader] interface, which internally should be attached to a storage local cache, that is automatically kept up-to-date. The same cache can be accessed from every [session][ISession], because every [session][ISession] implements as well the [dictionary-reader interface][IDictReader]. * diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt index 75356f95c..12b9fd8b1 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt @@ -12,6 +12,8 @@ import naksha.geo.SpGeometry import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_ARGUMENT import naksha.model.NakshaError.NakshaErrorCompanion.STORAGE_NOT_FOUND import naksha.model.NakshaVersion.Companion.CURRENT +import naksha.model.objects.NakshaCollection +import naksha.model.objects.NakshaMap import naksha.model.objects.NakshaStorage import kotlin.js.JsExport import kotlin.js.JsName @@ -48,18 +50,18 @@ class Naksha private constructor() { const val ADMIN_CATALOG_FN = 0 /** - * The identifier of the admin-collection in which the collection-features of each catalog are persisted. + * The identifier of the collections-collection, the collection in which the collection-features of each catalog are persisted. * - * This collection exists in every catalog under Naksha management. The identifier of the collection itself is fixed to `naksha~collections`. The feature of the administration collection is an immutable feature. It is needed to bootstrap a new catalog, therefore it is not persisted anywhere. + * This collection exists in every catalog under Naksha management. The identifier of the collection itself is fixed to `naksha~collections`. The feature of the collections-collection is an immutable feature. It is needed to bootstrap a new catalog. * @since 3.0 */ - const val ADMIN_COL_ID = "naksha~collections" + const val COLLECTIONS_COL_ID = "naksha~collections" /** - * The collection-number of the admin-collection in which the collection-features of each catalog are persisted, it has the fixed feature-number _(`0`)_. + * The collection-number of the collections-collection in which the collection-features of each catalog are persisted, it has the fixed feature-number _(`0`)_. * @since 3.0 */ - const val ADMIN_COL_FN = 0 + const val COLLECTIONS_COL_FN = 0 /** * The identifier of the collection in which transactions are stored, located in the [admin-map][ADMIN_CATALOG_ID] _(`naksha~transactions`)_. @@ -113,7 +115,7 @@ class Naksha private constructor() { @JvmStatic val internalIdToNumber = mapOf( Pair(ADMIN_CATALOG_ID, ADMIN_CATALOG_FN), - Pair(ADMIN_COL_ID, ADMIN_COL_FN), + Pair(COLLECTIONS_COL_ID, COLLECTIONS_COL_FN), Pair(TRANSACTIONS_COL_ID, TRANSACTIONS_COL_FN), Pair(CATALOGS_COL_ID, CATALOGS_COL_FN), Pair(BOOKS_COL_ID, BOOKS_COL_FN), diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Tuple.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Tuple.kt index 0c1c70069..f05c8d27d 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Tuple.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Tuple.kt @@ -103,7 +103,7 @@ data class Tuple @JvmOverloads constructor( // The feature is stored in the same database as the collection it is inserted into. colTn.databaseNumber, // The feature is stored in the same catalog as the collection it is inserted into. - colTn.mapNumber, + colTn.catalogNumber, // The feature-number of the collection is the collection-number of the feature we want to store in the collection. colTn.featureNumber.toInt(), // The feature-number of the actual feature. Will either be set explicit or calcualted. diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumber.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumber.kt index 074121c95..e7b9904f1 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumber.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumber.kt @@ -31,7 +31,7 @@ import kotlin.jvm.JvmStatic * urn:naksha:tn:{database-number}:{catalog-number}:{collection-number}:{feature-number}:{version} * ``` * - * - There are no two [tuples][Tuple] with the same [tuple-number][TupleNumber]; world-wide. + * - There are no two [tuples][Tuple] with the same [tuple-number][TupleNumber]; world-wide.\ * @since 3.0 */ @JsExport @@ -43,10 +43,10 @@ data class TupleNumber( @JvmField val databaseNumber: Int64, /** - * The map-number of the map in which the tuple is stored within the storage. + * The catalog-number of the map in which the tuple is stored within the storage. * @since 3.0.0 */ - @JvmField val mapNumber: Int, + @JvmField val catalogNumber: Int, /** * The collection-number of the collection in which the tuple is stored within the storage. @@ -100,7 +100,7 @@ data class TupleNumber( var i64_diff = databaseNumber - other.databaseNumber if (i64_diff < 0) return -1 if (i64_diff > 1) return 1 - var i32_diff = mapNumber - other.mapNumber + var i32_diff = catalogNumber - other.catalogNumber if (i32_diff < 0) return -1 if (i32_diff > 1) return 1 i32_diff = collectionNumber - other.collectionNumber @@ -122,7 +122,7 @@ data class TupleNumber( if (this === other) return true return other is TupleNumber && databaseNumber == other.databaseNumber - && mapNumber == other.mapNumber + && catalogNumber == other.catalogNumber && collectionNumber == other.collectionNumber && featureNumber == other.featureNumber && version == other.version @@ -140,7 +140,7 @@ data class TupleNumber( */ override fun toString(): String { if (!this::_string.isInitialized) { - _string = "$databaseNumber:$mapNumber:$collectionNumber:$featureNumber:$version" + _string = "$databaseNumber:$catalogNumber:$collectionNumber:$featureNumber:$version" } return _string } @@ -161,7 +161,7 @@ data class TupleNumber( val fn = this.featureNumber if (fn >= 0) throw NakshaException(ILLEGAL_STATE, "The feature-number is not auto-generated, failed to calculate alternative") val new_fn = Naksha.alternativeInt64(fn) - return TupleNumber(databaseNumber, mapNumber, collectionNumber, new_fn, version) + return TupleNumber(databaseNumber, catalogNumber, collectionNumber, new_fn, version) } private var _urn: String? = null @@ -255,7 +255,7 @@ data class TupleNumber( offset += 8 } if (variant.encodeMapNumber()) { - dataview_set_int32(view, offset, mapNumber) + dataview_set_int32(view, offset, catalogNumber) offset += 4 } if (variant.encodeCollectionNumber()) { @@ -307,7 +307,7 @@ data class TupleNumber( storageNumber: Int64? = null, ) = TupleNumber( storageNumber ?: tn.databaseNumber, - mapNumber ?: tn.mapNumber, + mapNumber ?: tn.catalogNumber, collectionNumber ?: tn.collectionNumber, featureNumber ?: tn.featureNumber, version ?: tn.version, @@ -336,7 +336,7 @@ data class TupleNumber( offset: Int, variant: TupleNumberVariant, tn: TupleNumber - ): TupleNumber = fromBinary(Binary(bytes, offset), variant, tn.databaseNumber, tn.mapNumber, tn.collectionNumber, tn.featureNumber) + ): TupleNumber = fromBinary(Binary(bytes, offset), variant, tn.databaseNumber, tn.catalogNumber, tn.collectionNumber, tn.featureNumber) fun fromB256(bytes: ByteArray) = fromByteArray(bytes, 0, B256) diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumberBinaryArray.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumberBinaryArray.kt index c4dde2591..d7f72a00d 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumberBinaryArray.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumberBinaryArray.kt @@ -386,7 +386,7 @@ data class TupleNumberBinaryArray( if (element == null) return -1 for (i in size - 1 downTo 0) { if (element.databaseNumber == getStorageNumber(i) - && element.mapNumber == getMapNumber(i) + && element.catalogNumber == getMapNumber(i) && element.collectionNumber == getCollectionNumber(i) && element.featureNumber == getFeatureNumber(i) && element.version == getTxn(i)) return i @@ -398,7 +398,7 @@ data class TupleNumberBinaryArray( if (element == null) return -1 for (i in 0 until size) { if (element.databaseNumber == getStorageNumber(i) - && element.mapNumber == getMapNumber(i) + && element.catalogNumber == getMapNumber(i) && element.collectionNumber == getCollectionNumber(i) && element.featureNumber == getFeatureNumber(i) && element.version == getTxn(i)) return i diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumberList.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumberList.kt index a03fda4bf..9881cf304 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumberList.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumberList.kt @@ -78,7 +78,7 @@ class TupleNumberList : ListProxy(TupleNumber::class) { // We found a first tuple, we hope that each tuple can be encoded in 64-bit only. variant = B64 storageNumber = tupleNumber.databaseNumber - mapNumber = tupleNumber.mapNumber + mapNumber = tupleNumber.catalogNumber collectionNumber = tupleNumber.collectionNumber featureNumber = tupleNumber.featureNumber continue @@ -93,7 +93,7 @@ class TupleNumberList : ListProxy(TupleNumber::class) { break } if (variant === B192) continue - if (mapNumber != tupleNumber.mapNumber) { + if (mapNumber != tupleNumber.catalogNumber) { // We need to encode individual map-, collection-, and feature-numbers variant = B192 mapNumber = null @@ -156,7 +156,7 @@ class TupleNumberList : ListProxy(TupleNumber::class) { i += 8 } if (variant.encodeMapNumber()) { - dataview_set_int32(view, i, tupleNumber.mapNumber) + dataview_set_int32(view, i, tupleNumber.catalogNumber) i += 4 } if (variant.encodeCollectionNumber()) { diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/IndexList.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/IndexList.kt index 01ddf16c6..92e1c8135 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/IndexList.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/IndexList.kt @@ -23,15 +23,6 @@ open class IndexList() : ListProxy(Index::class) { for (index in indexes) add(index) } - /** - * Construct a list from a vararg of indexes. - * @since 3.0 - */ - @JsName("fromArray") - constructor(indexes: Array) : this() { - for (index in indexes) add(index) - } - /** * Construct a list from a vararg of indexes. * @since 3.0 diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt index 3aa25c346..9ff2b7c0e 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt @@ -31,7 +31,7 @@ import kotlin.js.JsName * @since 3.0 */ @JsExport -class Member() : AnyObject() { +class Member() : AnyObject(), Comparator { /** * Construct a member with a name and the given data type. @@ -277,6 +277,8 @@ class Member() : AnyObject() { */ fun write(feature: MapProxy<*,*>, value: Any?): Any? = feature.setPath(value, path) + override fun compare(a: Member, b: Member): Int = a.dataType.sortOrder - b.dataType.sortOrder + companion object Member_C { private val NAME = NotNullProperty(String::class) { _, _ -> "" } private val DATA_TYPE = NotNullEnum(MemberType::class) { _, _ -> MemberType.STRING } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberType.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberType.kt index d0d2c8d8b..b4914c5cb 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberType.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberType.kt @@ -5,7 +5,8 @@ package naksha.model.objects import naksha.base.Int64 import naksha.base.JsEnum import naksha.geo.SpGeometry -import naksha.model.TagList +import naksha.model.NakshaError.NakshaErrorCompanion.INITIALIZATION_FAILED +import naksha.model.NakshaException import naksha.model.TagMap import naksha.model.TupleNumber import kotlin.js.JsExport @@ -48,70 +49,70 @@ class MemberType : JsEnum() { * @since 3.0 */ @JvmField - val BOOLEAN = defIgnoreCase(MemberType::class, "boolean") + val BOOLEAN = defIgnoreCase(MemberType::class, "boolean") { self -> self.sortOrder = 6 } /** * 8-bit signed integer in storage, but when reading from book, decode as long. * @since 3.0 */ @JvmField - val INT8 = defIgnoreCase(MemberType::class, "int8") + val INT8 = defIgnoreCase(MemberType::class, "int8") { self -> self.sortOrder = 5 } /** * 16-bit signed integer in storage, but when reading from book, decode as long. * @since 3.0 */ @JvmField - val INT16 = defIgnoreCase(MemberType::class, "int16") + val INT16 = defIgnoreCase(MemberType::class, "int16") { self -> self.sortOrder = 4 } /** * 32-bit signed integer in storage, but when reading from book, decode as long. * @since 3.0 */ @JvmField - val INT32 = defIgnoreCase(MemberType::class, "int32") + val INT32 = defIgnoreCase(MemberType::class, "int32") { self -> self.sortOrder = 2 } /** * 64-bit signed integer. * @since 3.0 */ @JvmField - val INT64 = defIgnoreCase(MemberType::class, "int64") + val INT64 = defIgnoreCase(MemberType::class, "int64") { self -> self.sortOrder = 0 } /** * 32-bit IEEE-754 floating point in storage, but when reading from book, decode as double. * @since 3.0 */ @JvmField - val FLOAT32 = defIgnoreCase(MemberType::class, "float32") + val FLOAT32 = defIgnoreCase(MemberType::class, "float32"){ self -> self.sortOrder = 3 } /** * 64-bit IEEE-754 floating point. * @since 3.0 */ @JvmField - val FLOAT64 = defIgnoreCase(MemberType::class, "float64") + val FLOAT64 = defIgnoreCase(MemberType::class, "float64") { self -> self.sortOrder = 1 } /** * Variable-length string. * @since 3.0 */ @JvmField - val STRING = defIgnoreCase(MemberType::class, "string") + val STRING = defIgnoreCase(MemberType::class, "string") { self -> self.sortOrder = 7 } /** * Raw byte array. * @since 3.0 */ @JvmField - val BYTE_ARRAY = defIgnoreCase(MemberType::class, "byte_array") + val BYTE_ARRAY = defIgnoreCase(MemberType::class, "byte_array") { self -> self.sortOrder = 13 } /** * A tuple-number, can be encoded as string or byte-array _(storage decides)_. To be used with [IndexType.BTREE]. * @since 3.0 */ @JvmField - val TUPLE_NUMBER = defIgnoreCase(MemberType::class, "tuple_number") + val TUPLE_NUMBER = defIgnoreCase(MemberType::class, "tuple_number") { self -> self.sortOrder = 12 } /** * A geometry stored as raw [TWKB](https://github.com/nicowillis/twkb) bytes. @@ -123,7 +124,7 @@ class MemberType : JsEnum() { * @since 3.0 */ @JvmField - val SPATIAL = defIgnoreCase(MemberType::class, "spatial") + val SPATIAL = defIgnoreCase(MemberType::class, "spatial") { self -> self.sortOrder = 11 } /** * A map whose keys are strings and values are primitives, following the JBON2 tag-map @@ -134,7 +135,7 @@ class MemberType : JsEnum() { * @since 3.0 */ @JvmField - val TAGS = defIgnoreCase(MemberType::class, "tags") + val TAGS = defIgnoreCase(MemberType::class, "tags") { self -> self.sortOrder = 8 } /** * A string-array using Naksha tag syntax that is expanded into a [TAGS] map at write time. @@ -148,7 +149,7 @@ class MemberType : JsEnum() { * @since 3.0 */ @JvmField - val TAGS_FROM_ARRAY = defIgnoreCase(MemberType::class, "tags_from_array") + val TAGS_FROM_ARRAY = defIgnoreCase(MemberType::class, "tags_from_array") { self -> self.sortOrder = 9 } /** * A JSON array of unique primitive values (booleans, numbers, strings), following the JBON2 @@ -165,7 +166,7 @@ class MemberType : JsEnum() { * @since 3.0 */ @JvmField - val SET = defIgnoreCase(MemberType::class, "set") + val SET = defIgnoreCase(MemberType::class, "set") { self -> self.sortOrder = 10 } } /** @@ -192,4 +193,22 @@ class MemberType : JsEnum() { else -> false } } + + var sortOrder: Int = -1 + private set(value) { + val it: Iterator = iterate(MemberType::class) + var biggestSortOrder = -1 + while (it.hasNext()) { + val memberType = it.next() + if (memberType.sortOrder == value) { + throw NakshaException(INITIALIZATION_FAILED, "Found duplicate sortOrder, member $this == $memberType") + } + if (memberType.sortOrder > biggestSortOrder) biggestSortOrder = memberType.sortOrder + } + var expected = biggestSortOrder+1 + if (value != biggestSortOrder) { + throw NakshaException(INITIALIZATION_FAILED, "Member $this is expected to have sortOrder $expected, but has $value") + } + field = value + } } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt index d11a6debb..7a0c59533 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt @@ -22,7 +22,7 @@ import kotlin.jvm.JvmStatic */ @JsExport open class NakshaCollection() : NakshaFeature() { - +\ /** * Create a Naksha collection with settings. * @param id the collection-identifier. @@ -76,11 +76,11 @@ open class NakshaCollection() : NakshaFeature() { /** * Always return `0`, because all collections are always stored in `naksha~collections` collection. * @since 3.0 - * @see [Naksha.ADMIN_COL_ID] - * @see [Naksha.ADMIN_COL_FN] + * @see [Naksha.COLLECTIONS_COL_ID] + * @see [Naksha.COLLECTIONS_COL_FN] */ override val collectionNumber: Int - get() = Naksha.ADMIN_COL_FN + get() = Naksha.COLLECTIONS_COL_FN /** * The map-id of the map in which the collection is located; `null` if not yet known. diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaFeature.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaFeature.kt index ab3f4ff12..216c9244a 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaFeature.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaFeature.kt @@ -183,7 +183,7 @@ open class NakshaFeature() : AnyObject() { * @since 3.0 */ open val mapNumber: Int? - get() = guid?.tupleNumber?.mapNumber + get() = guid?.tupleNumber?.catalogNumber /** * Returns the storage-number of the storage in which the feature is currently persisted; `null` if the feature is not yet persisted. diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt index 9a2edecac..836cce0c5 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt @@ -33,7 +33,7 @@ class StandardMembers private constructor() { @JvmField @JsStatic val Tn = Member("~tn", MemberType.TUPLE_NUMBER, JsonPath("tn")) - /** + /** * `nv` — **next-version** (`INT64`). The version at which this tuple was superseded * by the next state. Present only in history; in head the value is intrinsically the current * HEAD sentinel and is not stored as a physical member. Mandatory, storage-managed. diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadCollections.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadCollections.kt index 8ec3ed428..25aec5298 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadCollections.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadCollections.kt @@ -81,7 +81,7 @@ open class ReadCollections : ReadRequest() { fun toReadFeatures(): ReadFeatures { val req = ReadFeatures() req.mapId = mapId - req.collectionIds.add(Naksha.ADMIN_COL_ID) + req.collectionIds.add(Naksha.COLLECTIONS_COL_ID) req.featureIds.addAll(collectionIds) return req } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/Write.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/Write.kt index 8a6f49062..f833211cf 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/Write.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/Write.kt @@ -5,7 +5,7 @@ package naksha.model.request import naksha.base.* import naksha.model.* import naksha.model.Naksha.NakshaCompanion.ADMIN_CATALOG_ID -import naksha.model.Naksha.NakshaCompanion.ADMIN_COL_ID +import naksha.model.Naksha.NakshaCompanion.COLLECTIONS_COL_ID import naksha.model.Naksha.NakshaCompanion.BOOKS_COL_ID import naksha.model.Naksha.NakshaCompanion.CATALOGS_COL_ID import naksha.model.Naksha.NakshaCompanion.featureNumber @@ -72,8 +72,8 @@ open class Write : AnyObject() { if (CATALOGS_COL_ID == a) return -1 if (CATALOGS_COL_ID == b) return 1 // We order all modifications done in internal collection's-collection second. - if (ADMIN_COL_ID == a) return -1 - if (ADMIN_COL_ID == b) return 1 + if (COLLECTIONS_COL_ID == a) return -1 + if (COLLECTIONS_COL_ID == b) return 1 // Rest by id return a.compareTo(b) } @@ -167,7 +167,7 @@ open class Write : AnyObject() { * * - If a [map][NakshaMap] should be modified, then [Naksha.CATALOGS_COL_ID] should be used, within [Naksha.ADMIN_CATALOG_ID]. * - If a [dictionary][NakshaDictionary] should be modified, the [Naksha.BOOKS_COL_ID] should be used, within [Naksha.ADMIN_CATALOG_ID]. - * - If a [collection][NakshaCollection] should be modified, then [Naksha.ADMIN_COL_ID] should be used, must not be used together with [Naksha.ADMIN_CATALOG_ID], because the admin-map does not allow collection modification, it is internally managed. + * - If a [collection][NakshaCollection] should be modified, then [Naksha.COLLECTIONS_COL_ID] should be used, must not be used together with [Naksha.ADMIN_CATALOG_ID], because the admin-map does not allow collection modification, it is internally managed. * - If a [feature][NakshaFeature] should be created, then the [NakshaCollection] in which the feature should be stored is required. * - Throws [ILLEGAL_STATE], if the collection-id is read, before being set. * @since 3.0 @@ -630,7 +630,7 @@ open class Write : AnyObject() { */ fun createCollection(collection: NakshaCollection): Write { this.mapId = collection.mapId - this.collectionId = ADMIN_COL_ID + this.collectionId = COLLECTIONS_COL_ID this.op = WriteOp.CREATE this.feature = collection return this @@ -644,7 +644,7 @@ open class Write : AnyObject() { */ fun updateCollection(collection: NakshaCollection, atomic: Boolean): Write { this.mapId = collection.mapId - this.collectionId = ADMIN_COL_ID + this.collectionId = COLLECTIONS_COL_ID this.op = WriteOp.UPDATE this.feature = collection this.atomic = atomic @@ -658,7 +658,7 @@ open class Write : AnyObject() { */ fun upsertCollection(collection: NakshaCollection): Write { this.mapId = collection.mapId - this.collectionId = ADMIN_COL_ID + this.collectionId = COLLECTIONS_COL_ID this.op = WriteOp.UPSERT this.feature = collection return this @@ -672,7 +672,7 @@ open class Write : AnyObject() { */ fun deleteCollection(collection: NakshaCollection, atomic: Boolean): Write { this.mapId = collection.mapId - this.collectionId = ADMIN_COL_ID + this.collectionId = COLLECTIONS_COL_ID this.op = WriteOp.DELETE this.feature = collection this.atomic = atomic @@ -689,7 +689,7 @@ open class Write : AnyObject() { @JvmOverloads fun deleteCollectionById(mapId: String? = null, collectionId: String, version: Version? = null): Write { this.mapId = mapId - this.collectionId = ADMIN_COL_ID + this.collectionId = COLLECTIONS_COL_ID this.op = WriteOp.DELETE this.id = collectionId this.version = version @@ -931,7 +931,7 @@ open class Write : AnyObject() { * @return `true` if this write modifies a collection; `false` otherwise. * @since 3.0 */ - fun isCollectionModification(): Boolean = collectionId == ADMIN_COL_ID + fun isCollectionModification(): Boolean = collectionId == COLLECTIONS_COL_ID /** * Tests if this write modifies a feature within a collection. @@ -954,7 +954,7 @@ open class Write : AnyObject() { * @see [WriteOp] */ fun validate(): Write { - if (mapId == ADMIN_CATALOG_ID || collectionId == ADMIN_COL_ID) { + if (mapId == ADMIN_CATALOG_ID || collectionId == COLLECTIONS_COL_ID) { if (isInternalId(id)) { throw NakshaException(ILLEGAL_STATE, "Modification of internal features forbidden: '$id'") } diff --git a/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/TupleNumberQueryTest.kt b/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/TupleNumberQueryTest.kt index eba0f3129..6ab6f833a 100644 --- a/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/TupleNumberQueryTest.kt +++ b/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/TupleNumberQueryTest.kt @@ -30,7 +30,7 @@ class TupleNumberQueryTest { private fun randomTupleNumber() = TupleNumber( databaseNumber = Int64(random.nextInt(10)), - mapNumber = random.nextInt(10), + catalogNumber = random.nextInt(10), collectionNumber = random.nextInt(10), featureNumber = Int64(random.nextInt(10)), version = Version(Int64(random.nextInt(10))) diff --git a/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/TupleNumberTest.kt b/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/TupleNumberTest.kt index 23368b755..f37ace49e 100644 --- a/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/TupleNumberTest.kt +++ b/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/TupleNumberTest.kt @@ -82,7 +82,7 @@ class TupleNumberTest { val bytes = t.toByteArray(B192) assertEquals(24, bytes.size) val restored = TupleNumber.fromB192(bytes, storageNumber) - assertEquals(t.mapNumber, restored.mapNumber) + assertEquals(t.catalogNumber, restored.catalogNumber) assertEquals(t.collectionNumber, restored.collectionNumber) assertEquals(t.featureNumber, restored.featureNumber) assertEquals(t.version, restored.version) @@ -95,7 +95,7 @@ class TupleNumberTest { assertEquals(32, bytes.size) val restored = TupleNumber.fromB256(bytes) assertEquals(t.databaseNumber, restored.databaseNumber) - assertEquals(t.mapNumber, restored.mapNumber) + assertEquals(t.catalogNumber, restored.catalogNumber) assertEquals(t.collectionNumber, restored.collectionNumber) assertEquals(t.featureNumber, restored.featureNumber) assertEquals(t.version, restored.version) diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgAdminMap.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgAdminMap.kt index 2234794d7..07acb8879 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgAdminMap.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgAdminMap.kt @@ -15,15 +15,13 @@ import naksha.jbon.JbDictionary import naksha.model.* import naksha.model.Naksha.NakshaCompanion.ADMIN_CATALOG_ID import naksha.model.Naksha.NakshaCompanion.ADMIN_CATALOG_FN -import naksha.model.Naksha.NakshaCompanion.ADMIN_COL_ID +import naksha.model.Naksha.NakshaCompanion.COLLECTIONS_COL_ID import naksha.model.Naksha.NakshaCompanion.CATALOGS_COL_FN import naksha.model.NakshaError.NakshaErrorCompanion.EXCEPTION import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_ARGUMENT import naksha.model.NakshaError.NakshaErrorCompanion.STORAGE_ID_MISMATCH -import naksha.model.objects.MemberList import naksha.model.objects.NakshaCollection import naksha.model.objects.NakshaMap -import naksha.model.objects.StandardMembers import naksha.psql.PgColumn.PgColumnCompanion.headColumns import kotlin.js.ExperimentalJsExport import kotlin.js.JsExport @@ -60,14 +58,6 @@ abstract class PgAdminMap internal constructor( */ upgrade: Boolean? ) : PgMap(storage, NakshaMap().withStorageId(storage.id).withId(ADMIN_CATALOG_ID)), IDictReader { - - /** - * This collection does not exist. It is only - */ - internal val adminMapCollection = NakshaCollection(ADMIN_COL_ID, ADMIN_CATALOG_ID) - .withMinimalMembers() - .withMinimalIndices() - /** * The page-size of the database (`current_setting('block_size')`). * @since 3.0.0 @@ -552,14 +542,12 @@ SELECT basics.*, procs.* FROM basics, procs; if (existing != null) return existing if (conn == null) return null - // Read from database - val outRows = PgColumnRows() + val outRows = PgColumnRows(catalogs.head) .withStorageNumber(storage.number) .withMapNumber(ADMIN_CATALOG_FN) .withCollectionNumber(CATALOGS_COL_FN) - .withDefaultDataEncoding(Naksha.DEFAULT_DATA_ENCODING) .addColumns(headColumns) - val SQL = """SELECT * + val SQL = """SELECT ${outRows.names()} FROM "naksha~admin".${catalogs.headTable.quotedName} WHERE id = $1 AND (version & 3) < 2""" val plan = conn.prepare(SQL, arrayOf(PgType.STRING.text)) @@ -589,11 +577,10 @@ WHERE id = $1 AND (version & 3) < 2""" if (conn == null) return null // Read from database - val outRows = PgColumnRows() + val outRows = PgColumnRows(catalogs.head) .withStorageNumber(storage.number) .withMapNumber(ADMIN_CATALOG_FN) .withCollectionNumber(CATALOGS_COL_FN) - .withDefaultDataEncoding(Naksha.DEFAULT_DATA_ENCODING) .addColumns(headColumns) val SQL = """ SELECT ${outRows.names()} diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt index 85beaa434..e795e0393 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt @@ -6,7 +6,6 @@ import naksha.model.objects.NakshaCollection import naksha.model.objects.StoreMode import naksha.psql.PgUtil.PgUtilCompanion.quoteIdent import kotlin.js.JsExport -import kotlin.jvm.JvmField /** * A collection is a set of database tables, that together form a logical feature store. This lower level implementation supports methods to create the collection physically (so the whole set of tables), to refresh the information about the collection, drop the tables, to add, or remove indices at runtime, aso. @@ -276,8 +275,8 @@ FOR EACH ROW EXECUTE FUNCTION naksha_trigger_after();""" // Added. for ((name, nm) in nextByName) { if (prevByName.containsKey(name)) continue - val pgIdent = "\"${PgCustomMemberValues.pgColumnName(name)}\"" - val pgType = PgCustomMemberValues.pgSqlTypeFor(nm.dataType) + val pgIdent = "\"${PgMemberHelper.pgColumnName(name)}\"" + val pgType = PgMemberHelper.pgSqlTypeFor(nm.dataType) for (root in mutableRootTables()) { val sql = "ALTER TABLE ${root.quotedName} ADD COLUMN IF NOT EXISTS $pgIdent $pgType" conn.execute(sql).close() @@ -293,7 +292,7 @@ FOR EACH ROW EXECUTE FUNCTION naksha_trigger_after();""" "Set Write.force = true to allow ALTER TABLE DROP COLUMN." ) } - val pgIdent = "\"${PgCustomMemberValues.pgColumnName(name)}\"" + val pgIdent = "\"${PgMemberHelper.pgColumnName(name)}\"" for (root in mutableRootTables()) { val sql = "ALTER TABLE ${root.quotedName} DROP COLUMN IF EXISTS $pgIdent" conn.execute(sql).close() diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumnEntry.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumnEntry.kt index b2520c3f5..9e6092387 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumnEntry.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumnEntry.kt @@ -10,8 +10,6 @@ internal data class PgColumnEntry( val type: PgType, val values: AnyList = AnyList() ) { - constructor(column: PgColumn, index: Int = column.i) : this(index, column.name, column.type) - fun withSize(size: Int): PgColumnEntry { values.size = size return this diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumnRows.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumnRows.kt index a5c4a7bc5..01a87595e 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumnRows.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumnRows.kt @@ -1,15 +1,13 @@ package naksha.psql import naksha.base.Int64 -import naksha.base.Platform.PlatformCompanion.toJSON import naksha.jbon.BookType import naksha.jbon.HeapBook import naksha.model.* import naksha.model.objects.NakshaCollection -import naksha.psql.PgColumn.PgColumnCompanion.allColumns /** - * Helper class to convert rows into arrays of column and vice versa. The main purpose is to read and write full tuples, but it supports basically as well virtual columns. + * Helper class to convert rows into arrays of column-data and vice versa. The main purpose is to read and write full tuples, but it supports basically as well virtual columns. * @since 3.0 */ internal class PgColumnRows(val collection: NakshaCollection) { @@ -18,6 +16,13 @@ internal class PgColumnRows(val collection: NakshaCollection) { * @since 3.0 */ val columns = mutableListOf() + init { + val members = collection.useMembers() + for (i in 0 until members.size) { + val member = members[i] ?: continue + columns.add(PgColumnEntry(i, member.name, PgType.ofMemberType(member.dataType))) + } + } internal val columnByName = mutableMapOf() private var isComplete: Boolean? = null private var names: String? = null @@ -86,28 +91,6 @@ internal class PgColumnRows(val collection: NakshaCollection) { */ var collectionNumber: Int? = null - /** - * The feature encoding that applies to every tuple in this result set. - * - * The encoding is no longer persisted as a per-row column; it is a per-collection setting - * looked up by callers (e.g. from [PgCollection.head.dataEncoding]) and passed here so the - * synthesized [Tuple.dataEncoding] tells decoders how to unpack the feature bytes. - * - * **MUST be set via [withDefaultDataEncoding] before any call to [getTuple] / [get]** — - * leaving it unset and then materializing a tuple would silently decode features under the - * wrong encoding, so [getTuple] throws instead of guessing. - * @since 3.0 - */ - var defaultDataEncoding: DataEncoding? = null - - /** - * @see [defaultDataEncoding] - */ - fun withDefaultDataEncoding(value: DataEncoding): PgColumnRows { - defaultDataEncoding = value - return this - } - /** * @see [collectionNumber] */ @@ -135,8 +118,6 @@ internal class PgColumnRows(val collection: NakshaCollection) { return detected } - fun addColumn(col: PgColumn): PgColumnRows = addColumn(col.name, col.type) - fun addColumn(name: String, type: PgType): PgColumnRows { clearCache() val existing = columnByName[name] @@ -162,41 +143,32 @@ internal class PgColumnRows(val collection: NakshaCollection) { return this } fun getColumn(name: String): PgColumnEntry? = columnByName[name] - fun getColumn(col: PgColumn): PgColumnEntry? = columnByName[col.name] fun getColumn(index: Int): PgColumnEntry? = if (index in 0 until columns.size) columns[index] else null fun hasColumn(name: String): Boolean = getColumn(name) != null - fun hasColumn(col: PgColumn): Boolean = getColumn(col) != null fun hasColumn(index: Int): Boolean = getColumn(index) != null fun getAny(row: Int, columnName: String): Any? = columnByName[columnName]?.values?.get(row) - fun getAny(row: Int, column: PgColumn): Any? = getAny(row, column.name) fun getInt(row: Int, columnName: String): Int? { val value = getAny(row, columnName) return if (value is Int) value else null } - fun getInt(row: Int, column: PgColumn): Int? = getInt(row, column.name) fun getInt64(row: Int, columnName: String): Int64? { val value = getAny(row, columnName) return if (value is Int64) value else null } - fun getInt64(row: Int, column: PgColumn): Int64? = getInt64(row, column.name) fun getDouble(row: Int, columnName: String): Double? { val value = getAny(row, columnName) return if (value is Double) value else null } - fun getDouble(row: Int, column: PgColumn): Double? = getDouble(row, column.name) fun getString(row: Int, columnName: String): String? { val value = getAny(row, columnName) return if (value is String) value else null } - fun getString(row: Int, column: PgColumn): String? = getString(row, column.name) fun getByteArray(row: Int, columnName: String): ByteArray? { val value = getAny(row, columnName) return if (value is ByteArray) value else null } - fun getByteArray(row: Int, column: PgColumn): ByteArray? = getByteArray(row, column.name) - fun getB64(row: Int, column: PgColumn, featureNumber: Int64): TupleNumber? = getB64(row, column.name, featureNumber) fun getB64(row: Int, columnName: String, featureNumber: Int64): TupleNumber? { val raw = getByteArray(row, columnName) ?: return null val storageNumber = this.storageNumber ?: return null @@ -208,7 +180,6 @@ internal class PgColumnRows(val collection: NakshaCollection) { null } } - fun getB128(row: Int, column: PgColumn): TupleNumber? = getB128(row, column.name) fun getB128(row: Int, columnName: String): TupleNumber? { val raw = getByteArray(row, columnName) ?: return null val storageNumber = this.storageNumber ?: return null @@ -226,10 +197,6 @@ internal class PgColumnRows(val collection: NakshaCollection) { val fn = getInt64(row, PgColumn.fn) ?: return null val version = getInt64(row, PgColumn.version) ?: return null val nextVersion = getInt64(row, PgColumn.next_version) - val encoding = defaultDataEncoding ?: throw illegalState( - "PgColumnRows.defaultDataEncoding was not set before calling getTuple(); " + - "set it from the collection's dataEncoding via withDefaultDataEncoding(...)." - ) val members = HeapBook(BookType.MEMBER_BOOK) return Tuple( @@ -244,22 +211,6 @@ internal class PgColumnRows(val collection: NakshaCollection) { ) } - /** - * Returns the value of a `jsonb` column as raw JSON text. - * - * `jsonb` columns come back from the cursor pre-parsed (a [naksha.base.PlatformMap] / - * [naksha.base.PlatformList]) since most callers want a typed object. For carriers that hold - * the raw JSON text (like tags) we round-trip through [toJSON] to recover it. - */ - private fun getJsonText(row: Int, column: PgColumn): String? { - val value = getAny(row, column) - return when (value) { - null -> null - is String -> value - else -> toJSON(value) - } - } - operator fun get(row: Int): Tuple? { val storage_num = storageNumber ?: return null val map_num = mapNumber ?: return null @@ -276,7 +227,7 @@ internal class PgColumnRows(val collection: NakshaCollection) { } return false } - fun set(row: Int, column: PgColumn, value: Any?): Boolean = set(row, column.name, value) + /** * Adds one [PgColumnEntry] per declared [naksha.model.objects.Member]. * @@ -286,7 +237,7 @@ internal class PgColumnRows(val collection: NakshaCollection) { if (members == null) return this for (m in members) { if (m == null) continue - addColumn(PgCustomMemberValues.pgColumnName(m.name), PgCustomMemberValues.pgTypeFor(m.dataType)) + addColumn(PgMemberHelper.pgColumnName(m.name), PgMemberHelper.pgTypeFor(m.dataType)) } return this } @@ -300,15 +251,16 @@ internal class PgColumnRows(val collection: NakshaCollection) { if (feature == null || members == null) return for (m in members) { if (m == null) continue - val raw = PgCustomMemberValues.walkFeature(feature, m.effectivePath()) - val coerced = PgCustomMemberValues.coerce(raw, m.dataType, feature.id, m.name) - set(row, PgCustomMemberValues.pgColumnName(m.name), coerced) + val raw = PgMemberHelper.walkFeature(feature, m.effectivePath()) + val coerced = PgMemberHelper.coerce(raw, m.dataType, feature.id, m.name) + set(row, PgMemberHelper.pgColumnName(m.name), coerced) } } operator fun set(row: Int, tuple: Tuple) { withMinSize(row) - val members = tuple.membersBook ?: return + val members = tuple.membersBook + set(row, PgColumn.updated_at, members.getByName("updated_at") as? Int64) set(row, PgColumn.created_at, members.getByName("created_at") as? Int64) set(row, PgColumn.author_ts, members.getByName("author_ts") as? Int64) diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCustomMemberValues.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCustomMemberValues.kt deleted file mode 100644 index f6aeb03ec..000000000 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCustomMemberValues.kt +++ /dev/null @@ -1,343 +0,0 @@ -@file:Suppress("OPT_IN_USAGE") - -package naksha.psql - -import naksha.model.FeatureMemberValues -import naksha.base.AnyList -import naksha.base.AnyObject -import naksha.base.Int64 -import naksha.base.ListProxy -import naksha.base.PlatformList -import naksha.base.Platform.PlatformCompanion.logger -import naksha.base.Platform.PlatformCompanion.toJSON -import naksha.model.NakshaError -import naksha.model.NakshaException -import naksha.model.TagList -import naksha.model.objects.Member -import naksha.model.objects.MemberList -import naksha.model.objects.MemberType -import naksha.model.objects.NakshaFeature - -/** - * Helpers to map [CustomMember] values from a [NakshaFeature] into a [PgColumnRows] row. - * - * - [walkFeature]: descend a [NakshaFeature] using the member's path; returns _null_ if the path is missing. - * - [coerce]: coerce a raw value to the type of the member; returns _null_ and logs a warning on mismatch. - * - [pgTypeFor]: maps a [CustomMemberType] to the [PgType] used for prepared-statement binding. - * - [pgSqlTypeFor]: returns the PostgreSQL DDL type for `CREATE TABLE` / `ALTER TABLE ADD COLUMN`. - * - [pgColumnName]: returns the physical column name (same as [CustomMember.name]) used in Postgres. - */ -internal object PgCustomMemberValues { - - /** - * The set of all reserved column names — any name that belongs to a built-in [PgColumn]. - * Custom members must not use any of these names; [validateMemberNames] enforces this. - */ - private val reservedColumnNames: Set by lazy { - PgColumn.allColumns.map { it.name }.toSet() - } - - /** - * Returns the physical Postgres column name for the given member name. - * The name is used as-is; collision with built-in columns is prevented by [validateMemberNames]. - */ - fun pgColumnName(memberName: String): String = memberName - - /** - * Validates that none of the members in [members] use a reserved built-in column name. - * Throws [NakshaException] with [NakshaError.ILLEGAL_ARGUMENT] on the first conflict found. - * Must be called before creating a new collection. - */ - fun validateMemberNames(members: MemberList) { - for (member in members) { - if (member != null && member.name in reservedColumnNames) { - throw NakshaException( - NakshaError.ILLEGAL_ARGUMENT, - "Custom member name '${member.name}' conflicts with a built-in column name" - ) - } - } - } - - fun pgTypeFor(type: MemberType): PgType = when (type) { - MemberType.BOOLEAN -> PgType.BOOLEAN - MemberType.INT8 -> PgType.SHORT - MemberType.INT16 -> PgType.SHORT - MemberType.INT32 -> PgType.INT - MemberType.INT64 -> PgType.INT64 - MemberType.FLOAT32 -> PgType.FLOAT - MemberType.FLOAT64 -> PgType.DOUBLE - MemberType.STRING -> PgType.STRING - MemberType.BYTE_ARRAY -> PgType.BYTE_ARRAY - MemberType.SPATIAL -> PgType.BYTE_ARRAY - MemberType.TAGS -> PgType.JSONB - MemberType.TAGS_FROM_ARRAY -> PgType.JSONB - MemberType.SET -> PgType.JSONB - else -> PgType.STRING - } - - /** - * Returns the PostgreSQL DDL type string for the given member type, used inside `CREATE TABLE` / `ALTER TABLE ADD COLUMN`. - * Note: there is no 1-byte signed integer type in PostgreSQL, so [MemberType.INT8] is materialized as `smallint`; - * the storage enforces the 8-bit range on coercion. - * [MemberType.TAGS], [MemberType.TAGS_FROM_ARRAY], and [MemberType.SET] all use `jsonb STORAGE MAIN` — - * compressed inline, only TOASTed as a last resort. The only difference is the JSON shape: - * TAGS and TAGS_FROM_ARRAY persist a JSON object, SET persists a JSON array. - */ - fun pgSqlTypeFor(type: MemberType): String = when (type) { - MemberType.BOOLEAN -> "boolean" - MemberType.INT8 -> "smallint" - MemberType.INT16 -> "smallint" - MemberType.INT32 -> "integer" - MemberType.INT64 -> "bigint" - MemberType.FLOAT32 -> "real" - MemberType.FLOAT64 -> "double precision" - MemberType.STRING -> "text COLLATE \"C\"" - MemberType.BYTE_ARRAY -> "bytea" - MemberType.SPATIAL -> "bytea STORAGE EXTERNAL" - MemberType.TAGS -> "jsonb STORAGE MAIN" - MemberType.TAGS_FROM_ARRAY -> "jsonb STORAGE MAIN" - MemberType.SET -> "jsonb STORAGE MAIN" - else -> "text" - } - - /** - * Returns the SQL fragment for the [Member.name] / [Member.dataType] used in `CREATE TABLE`. Example: `"age" smallint`. - */ - fun sqlDefinitionFor(member: Member): String = - "\"${pgColumnName(member.name)}\" ${pgSqlTypeFor(member.dataType)}" - - fun walkFeature(feature: NakshaFeature, path: List): Any? { - var current: Any? = feature - for (segment in path) { - if (current == null) return null - current = when (current) { - is AnyObject -> current.getRaw(segment) - else -> return null - } - } - return current - } - - fun coerce(value: Any?, type: MemberType, featureId: String, memberName: String): Any? { - if (value == null) return null - return when (type) { - MemberType.BOOLEAN -> coerceBoolean(value, featureId, memberName) - MemberType.INT8 -> coerceInt8(value, featureId, memberName) - MemberType.INT16 -> coerceInt16(value, featureId, memberName) - MemberType.INT32 -> coerceInt32(value, featureId, memberName) - MemberType.INT64 -> coerceInt64(value, featureId, memberName) - MemberType.FLOAT32 -> coerceFloat32(value, featureId, memberName) - MemberType.FLOAT64 -> coerceFloat64(value, featureId, memberName) - MemberType.STRING -> coerceString(value, featureId, memberName) - MemberType.BYTE_ARRAY -> coerceByteArray(value, featureId, memberName) - MemberType.SPATIAL -> coerceByteArray(value, featureId, memberName) - MemberType.TAGS -> coerceTags(value, featureId, memberName) - MemberType.TAGS_FROM_ARRAY -> coerceTagsFromArray(value, featureId, memberName) - MemberType.SET -> coerceSet(value, featureId, memberName) - else -> { - warnMismatch(featureId, memberName, type.toString(), value) - null - } - } - } - - private fun coerceBoolean(value: Any, featureId: String, memberName: String): Boolean? = when (value) { - is Boolean -> value - else -> { warnMismatch(featureId, memberName, "boolean", value); null } - } - - private fun coerceInt8(value: Any, featureId: String, memberName: String): Short? { - val asLong = numberToLongOrNull(value) ?: return null.also { warnMismatch(featureId, memberName, "int8", value) } - if (asLong !in Byte.MIN_VALUE.toLong()..Byte.MAX_VALUE.toLong()) { - warnMismatch(featureId, memberName, "int8 (out of range)", value) - return null - } - return asLong.toShort() - } - - private fun coerceInt16(value: Any, featureId: String, memberName: String): Short? { - val asLong = numberToLongOrNull(value) ?: return null.also { warnMismatch(featureId, memberName, "int16", value) } - if (asLong !in Short.MIN_VALUE.toLong()..Short.MAX_VALUE.toLong()) { - warnMismatch(featureId, memberName, "int16 (out of range)", value) - return null - } - return asLong.toShort() - } - - private fun coerceInt32(value: Any, featureId: String, memberName: String): Int? { - val asLong = numberToLongOrNull(value) ?: return null.also { warnMismatch(featureId, memberName, "int32", value) } - if (asLong !in Int.MIN_VALUE.toLong()..Int.MAX_VALUE.toLong()) { - warnMismatch(featureId, memberName, "int32 (out of range)", value) - return null - } - return asLong.toInt() - } - - private fun coerceInt64(value: Any, featureId: String, memberName: String): Int64? = when (value) { - is Int64 -> value - is Int -> Int64(value.toLong()) - is Long -> Int64(value) - is Short -> Int64(value.toLong()) - is Byte -> Int64(value.toLong()) - is Double -> if (value.isFinite() && value == value.toLong().toDouble()) Int64(value.toLong()) else { warnMismatch(featureId, memberName, "int64", value); null } - is Float -> if (value.isFinite() && value == value.toLong().toFloat()) Int64(value.toLong()) else { warnMismatch(featureId, memberName, "int64", value); null } - else -> { warnMismatch(featureId, memberName, "int64", value); null } - } - - private fun coerceFloat32(value: Any, featureId: String, memberName: String): Float? = when (value) { - is Float -> value - is Double -> value.toFloat() - is Int -> value.toFloat() - is Long -> value.toFloat() - is Int64 -> value.toLong().toFloat() - is Short -> value.toFloat() - is Byte -> value.toFloat() - else -> { warnMismatch(featureId, memberName, "float32", value); null } - } - - private fun coerceFloat64(value: Any, featureId: String, memberName: String): Double? = when (value) { - is Double -> value - is Float -> value.toDouble() - is Int -> value.toDouble() - is Long -> value.toDouble() - is Int64 -> value.toLong().toDouble() - is Short -> value.toDouble() - is Byte -> value.toDouble() - else -> { warnMismatch(featureId, memberName, "float64", value); null } - } - - private fun coerceString(value: Any, featureId: String, memberName: String): String? = when (value) { - is String -> value - else -> { warnMismatch(featureId, memberName, "string", value); null } - } - - private fun coerceByteArray(value: Any, featureId: String, memberName: String): ByteArray? = when (value) { - is ByteArray -> value - else -> { warnMismatch(featureId, memberName, "byte_array", value); null } - } - - private fun coerceTags(value: Any, featureId: String, memberName: String): String? { - if (value !is AnyObject) { - warnMismatch(featureId, memberName, "tags", value) - return null - } - return try { toJSON(value) } catch (_: Exception) { warnMismatch(featureId, memberName, "tags", value); null } - } - - private fun coerceTagsFromArray(value: Any, featureId: String, memberName: String): String? { - val tagList = when (value) { - is TagList -> value - is AnyObject -> value.proxy(TagList::class) - else -> { warnMismatch(featureId, memberName, "tags_from_array", value); return null } - } - val tagMap = tagList.toTagMap() - return try { toJSON(tagMap) } catch (_: Exception) { warnMismatch(featureId, memberName, "tags_from_array", value); null } - } - - /** - * Coerces a [MemberType.SET] value: a JSON array of unique primitives (booleans, numbers, strings). - * The array is persisted unmodified (element order preserved). Entries that are `null`, non-primitive, - * or duplicates violate the set contract; the value is then not materialized (warning + NULL column). - */ - private fun coerceSet(value: Any, featureId: String, memberName: String): String? { - val list: ListProxy<*> = when (value) { - is ListProxy<*> -> value - is PlatformList -> value.proxy(AnyList::class) - else -> { warnMismatch(featureId, memberName, "set", value); return null } - } - val seen = HashSet() - for (e in list) { - if (e == null) { - warnMismatch(featureId, memberName, "set (entries must not be null)", "null") - return null - } - if (!isPrimitive(e)) { - warnMismatch(featureId, memberName, "set (entries must be primitives)", e) - return null - } - if (!seen.add(e)) { - warnMismatch(featureId, memberName, "set (entries must be unique)", e) - return null - } - } - return try { toJSON(list) } catch (_: Exception) { warnMismatch(featureId, memberName, "set", list); null } - } - - private fun isPrimitive(value: Any): Boolean = when (value) { - is String, is Boolean, is Byte, is Short, is Int, is Long, is Int64, is Float, is Double -> true - else -> false - } - - private fun numberToLongOrNull(value: Any): Long? = when (value) { - is Byte -> value.toLong() - is Short -> value.toLong() - is Int -> value.toLong() - is Long -> value - is Int64 -> value.toLong() - is Float -> if (value.isFinite() && value == value.toLong().toFloat()) value.toLong() else null - is Double -> if (value.isFinite() && value == value.toLong().toDouble()) value.toLong() else null - else -> null - } - - /** - * Returns the sort priority for a [MemberType] based on PostgreSQL alignment size, to minimise - * tuple padding when columns are laid out in declaration order: - * 1. 8-byte types ([INT64], [FLOAT64]) — first, to get 8-byte alignment right away - * 2. 4-byte types ([INT32], [FLOAT32]) — next, still fixed-width - * 3. 1/2-byte types ([INT16], [INT8], [BOOLEAN]) — small fixed-width - * 4. Variable-length text ([STRING]) — variable but human-readable - * 5. Opaque variable-length ([BYTE_ARRAY], [SPATIAL], [TAGS], [TAGS_FROM_ARRAY], [SET]) — last - */ - fun columnSortOrder(type: MemberType): Int = when (type) { - MemberType.INT64 -> 0 - MemberType.FLOAT64 -> 1 - MemberType.INT32 -> 2 - MemberType.FLOAT32 -> 3 - MemberType.INT16 -> 4 - MemberType.INT8 -> 5 - MemberType.BOOLEAN -> 6 - MemberType.STRING -> 7 - MemberType.BYTE_ARRAY -> 8 - MemberType.SPATIAL -> 8 - MemberType.TAGS -> 9 - MemberType.TAGS_FROM_ARRAY -> 10 - MemberType.SET -> 11 - else -> 12 - } - - /** - * The set of member names that correspond to pre-defined optional [PgColumn]s (e.g. `geo`, `tags`, `cc`). - * When two members have the same type sort-order, pre-defined members are placed before user-invented ones. - */ - private val predefinedMemberNames: Set by lazy { - val mandatory = PgColumn.mandatoryColumns.map { it.name }.toSet() - PgColumn.headColumns.map { it.name }.filter { it !in mandatory }.toSet() - } - - /** - * Sorts [members] in-place for optimal PostgreSQL column layout. - * - * Ordering rules (applied only at **collection-creation** time; never on updates): - * 1. Primary: type alignment group ([columnSortOrder]) - * 2. Secondary: pre-defined members (matching a built-in optional column name) before user-invented ones - * 3. Tertiary: member name lexicographically ascending - * - * The sort is stable within each group so that the caller's intent is preserved as a tie-breaker. - */ - fun sortMembersForStorage(members: MemberList) { - if (members.size <= 1) return - val snapshot = (0 until members.size).map { members[it]!! } - val sorted = snapshot.sortedWith(compareBy( - { columnSortOrder(it.dataType) }, - { if (it.name in predefinedMemberNames) 0 else 1 }, - { it.name } - )) - members.clear() - members.addAll(sorted) - } - - private fun warnMismatch(featureId: String, memberName: String, expected: String, value: Any) { - logger.warn("Custom member '$memberName' on feature '$featureId': expected $expected, got ${value::class.simpleName}") - } -} diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMap.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMap.kt index 4e674d93f..62f3e4061 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMap.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMap.kt @@ -5,9 +5,8 @@ package naksha.psql import naksha.base.* import naksha.base.Platform.PlatformCompanion.logger import naksha.model.Naksha -import naksha.model.Naksha.NakshaCompanion.ADMIN_CATALOG_ID -import naksha.model.Naksha.NakshaCompanion.ADMIN_COL_ID -import naksha.model.Naksha.NakshaCompanion.ADMIN_COL_FN +import naksha.model.Naksha.NakshaCompanion.COLLECTIONS_COL_ID +import naksha.model.Naksha.NakshaCompanion.COLLECTIONS_COL_FN import naksha.model.Naksha.NakshaCompanion.BOOKS_COL_ID import naksha.model.Naksha.NakshaCompanion.BOOKS_COL_FN import naksha.model.Naksha.NakshaCompanion.CATALOGS_COL_ID @@ -53,16 +52,6 @@ open class PgMap internal constructor( */ val number: Int = nakshaMap.number ) { - /** - * The admin-collection, aka `naksha~collections`, that exist in every catalog. - * - * The admin-collection contains the [NakshaCollection] features of all collections being part of the catalog. The admin-collection itself does not exist as dedicated feature, therefore is hardcoded here. Currently, there is no way to modify the members of it. However, in the future we at least want to allow clients to specify the members and indices when creating a new database. So the storage need to read the admin-collection and admin-catalog from some internal private location within the storage to get the member->JSON mapping, and to understand members and indices. - * @since 3.0 - */ - internal val adminCollection = NakshaCollection(ADMIN_COL_ID, ADMIN_CATALOG_ID) - .withXyzMembers() - .withXyzIndices() - /** * The map-identifier quoted optionally in double quotes. * @since 3.0 @@ -101,7 +90,10 @@ open class PgMap internal constructor( get() { var c = _collections if (c == null) { - c = PgCollection(this, NakshaCollection().withMapId(id).withId(ADMIN_COL_ID)) + val nakshaCollection = NakshaCollection(COLLECTIONS_COL_ID, id) + .withXyzMembers() + .withXyzIndices() + c = PgCollection(this, nakshaCollection) _collections = c } return c @@ -330,24 +322,23 @@ open class PgMap internal constructor( fun getPgCollectionById(conn: PgConnection?, id: String): PgCollection? { if (this is PgAdminMap) { return when (id) { - ADMIN_COL_ID -> collections + COLLECTIONS_COL_ID -> collections TRANSACTIONS_COL_ID -> transactions CATALOGS_COL_ID -> catalogs BOOKS_COL_ID -> books else -> null } } - if (id == ADMIN_COL_ID) return collections + if (id == COLLECTIONS_COL_ID) return collections val number = collectionNumberById[id] val existing = if (number != null) collectionCache[number] else null if (existing != null || conn == null) return existing // Read from database - val outRows = PgColumnRows() + val outRows = PgColumnRows(collections.head) .withStorageNumber(storage.number) .withMapNumber(this.number) - .withCollectionNumber(ADMIN_COL_FN) - .withDefaultDataEncoding(Naksha.DEFAULT_DATA_ENCODING) + .withCollectionNumber(COLLECTIONS_COL_FN) .addColumns(headColumns) setSearchPath(conn) val SQL = """SELECT ${outRows.names()} @@ -360,7 +351,7 @@ WHERE id = $1 AND (version & 3) < 2""" if (outRows.size == 0) return null val tuple = outRows[0] ?: return null Naksha.cache.store(tuple) - val nakshaCollection = Naksha.decodeTuple(tuple).proxy(NakshaCollection::class) + val nakshaCollection = tuple.decodeFeature(null).proxy(NakshaCollection::class) val pgCollection = PgCollection(this, nakshaCollection) storeCollection(pgCollection) return pgCollection @@ -376,23 +367,22 @@ WHERE id = $1 AND (version & 3) < 2""" fun getPgCollectionByNumber(conn: PgConnection?, number: Int): PgCollection? { if (this is PgAdminMap) { return when (number) { - ADMIN_COL_FN -> collections + COLLECTIONS_COL_FN -> collections TRANSACTIONS_COL_FN -> transactions CATALOGS_COL_FN -> catalogs BOOKS_COL_FN -> books else -> null } } - if (number == ADMIN_COL_FN) return collections + if (number == COLLECTIONS_COL_FN) return collections val existing = collectionCache[number] if (existing != null || conn == null) return existing // Read from database - val outRows = PgColumnRows() + val outRows = PgColumnRows(collections.head) .withStorageNumber(storage.number) .withMapNumber(this.number) - .withCollectionNumber(ADMIN_COL_FN) - .withDefaultDataEncoding(Naksha.DEFAULT_DATA_ENCODING) + .withCollectionNumber(COLLECTIONS_COL_FN) .addColumns(headColumns) setSearchPath(conn) val SQL = """SELECT ${outRows.names()} @@ -405,7 +395,7 @@ WHERE fn = $1 AND (version & 3) < 2""" if (outRows.size == 0) return null val tuple = outRows[0] ?: return null Naksha.cache.store(tuple) - val nakshaCollection = Naksha.decodeTuple(tuple).proxy(NakshaCollection::class) + val nakshaCollection = tuple.decodeFeature(null).proxy(NakshaCollection::class) val pgCollection = PgCollection(this, nakshaCollection) storeCollection(pgCollection) return pgCollection diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMemberHelper.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMemberHelper.kt new file mode 100644 index 000000000..285b8ffd1 --- /dev/null +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMemberHelper.kt @@ -0,0 +1,382 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.psql + +import naksha.base.AnyList +import naksha.base.AnyObject +import naksha.base.Int64 +import naksha.base.ListProxy +import naksha.base.PlatformList +import naksha.base.Platform.PlatformCompanion.logger +import naksha.base.Platform.PlatformCompanion.toJSON +import naksha.model.NakshaError +import naksha.model.NakshaException +import naksha.model.TagList +import naksha.model.objects.Member +import naksha.model.objects.MemberList +import naksha.model.objects.MemberType +import naksha.model.objects.NakshaFeature + +/** + * Helpers to map [CustomMember] values from a [NakshaFeature] into a [PgColumnRows] row. + * + * - [walkFeature]: descend a [NakshaFeature] using the member's path; returns _null_ if the path is missing. + * - [coerce]: coerce a raw value to the type of the member; returns _null_ and logs a warning on mismatch. + * - [pgTypeFor]: maps a [CustomMemberType] to the [PgType] used for prepared-statement binding. + * - [pgSqlTypeFor]: returns the PostgreSQL DDL type for `CREATE TABLE` / `ALTER TABLE ADD COLUMN`. + * - [pgColumnName]: returns the physical column name (same as [CustomMember.name]) used in Postgres. + */ +class PgMemberHelper private constructor() { + + companion object PgMemberHelper_C { + + /** + * The set of all reserved column names — any name that belongs to a built-in [PgColumn]. + * Custom members must not use any of these names; [validateMemberNames] enforces this. + */ + private val reservedColumnNames: Set by lazy { + PgColumn.allColumns.map { it.name }.toSet() + } + + /** + * Returns the physical Postgres column name for the given member name. + * The name is used as-is; collision with built-in columns is prevented by [validateMemberNames]. + */ + fun pgColumnName(memberName: String): String = memberName + + /** + * Validates that none of the members in [members] use a reserved built-in column name. + * Throws [NakshaException] with [NakshaError.ILLEGAL_ARGUMENT] on the first conflict found. + * Must be called before creating a new collection. + */ + fun validateMemberNames(members: MemberList) { + for (member in members) { + if (member != null && member.name in reservedColumnNames) { + throw NakshaException( + NakshaError.ILLEGAL_ARGUMENT, + "Custom member name '${member.name}' conflicts with a built-in column name" + ) + } + } + } + + fun pgTypeFor(type: MemberType): PgType = when (type) { + MemberType.BOOLEAN -> PgType.BOOLEAN + MemberType.INT8 -> PgType.SHORT + MemberType.INT16 -> PgType.SHORT + MemberType.INT32 -> PgType.INT + MemberType.INT64 -> PgType.INT64 + MemberType.FLOAT32 -> PgType.FLOAT + MemberType.FLOAT64 -> PgType.DOUBLE + MemberType.STRING -> PgType.STRING + MemberType.BYTE_ARRAY -> PgType.BYTE_ARRAY + MemberType.SPATIAL -> PgType.BYTE_ARRAY + MemberType.TAGS -> PgType.JSONB + MemberType.TAGS_FROM_ARRAY -> PgType.JSONB + MemberType.SET -> PgType.JSONB + else -> PgType.STRING + } + + /** + * Returns the PostgreSQL DDL type string for the given member type, used inside `CREATE TABLE` / `ALTER TABLE ADD COLUMN`. + * Note: there is no 1-byte signed integer type in PostgreSQL, so [MemberType.INT8] is materialized as `smallint`; + * the storage enforces the 8-bit range on coercion. + * [MemberType.TAGS], [MemberType.TAGS_FROM_ARRAY], and [MemberType.SET] all use `jsonb STORAGE MAIN` — + * compressed inline, only TOASTed as a last resort. The only difference is the JSON shape: + * TAGS and TAGS_FROM_ARRAY persist a JSON object, SET persists a JSON array. + */ + fun pgSqlTypeFor(type: MemberType): String = when (type) { + MemberType.BOOLEAN -> "boolean" + MemberType.INT8 -> "smallint" + MemberType.INT16 -> "smallint" + MemberType.INT32 -> "integer" + MemberType.INT64 -> "bigint" + MemberType.FLOAT32 -> "real" + MemberType.FLOAT64 -> "double precision" + MemberType.STRING -> "text COLLATE \"C\"" + MemberType.BYTE_ARRAY -> "bytea" + MemberType.SPATIAL -> "bytea STORAGE EXTERNAL" + MemberType.TAGS -> "jsonb STORAGE MAIN" + MemberType.TAGS_FROM_ARRAY -> "jsonb STORAGE MAIN" + MemberType.SET -> "jsonb STORAGE MAIN" + else -> "text" + } + + /** + * Returns the SQL fragment for the [Member.name] / [Member.dataType] used in `CREATE TABLE`. Example: `"age" smallint`. + */ + fun sqlDefinitionFor(member: Member): String = + "\"${pgColumnName(member.name)}\" ${pgSqlTypeFor(member.dataType)}" + + fun walkFeature(feature: NakshaFeature, path: List): Any? { + var current: Any? = feature + for (segment in path) { + if (current == null) return null + current = when (current) { + is AnyObject -> current.getRaw(segment) + else -> return null + } + } + return current + } + + fun coerce(value: Any?, type: MemberType, featureId: String, memberName: String): Any? { + if (value == null) return null + return when (type) { + MemberType.BOOLEAN -> coerceBoolean(value, featureId, memberName) + MemberType.INT8 -> coerceInt8(value, featureId, memberName) + MemberType.INT16 -> coerceInt16(value, featureId, memberName) + MemberType.INT32 -> coerceInt32(value, featureId, memberName) + MemberType.INT64 -> coerceInt64(value, featureId, memberName) + MemberType.FLOAT32 -> coerceFloat32(value, featureId, memberName) + MemberType.FLOAT64 -> coerceFloat64(value, featureId, memberName) + MemberType.STRING -> coerceString(value, featureId, memberName) + MemberType.BYTE_ARRAY -> coerceByteArray(value, featureId, memberName) + MemberType.SPATIAL -> coerceByteArray(value, featureId, memberName) + MemberType.TAGS -> coerceTags(value, featureId, memberName) + MemberType.TAGS_FROM_ARRAY -> coerceTagsFromArray(value, featureId, memberName) + MemberType.SET -> coerceSet(value, featureId, memberName) + else -> { + warnMismatch(featureId, memberName, type.toString(), value) + null + } + } + } + + private fun coerceBoolean(value: Any, featureId: String, memberName: String): Boolean? = when (value) { + is Boolean -> value + else -> { + warnMismatch(featureId, memberName, "boolean", value); null + } + } + + private fun coerceInt8(value: Any, featureId: String, memberName: String): Short? { + val asLong = numberToLongOrNull(value) ?: return null.also { warnMismatch(featureId, memberName, "int8", value) } + if (asLong !in Byte.MIN_VALUE.toLong()..Byte.MAX_VALUE.toLong()) { + warnMismatch(featureId, memberName, "int8 (out of range)", value) + return null + } + return asLong.toShort() + } + + private fun coerceInt16(value: Any, featureId: String, memberName: String): Short? { + val asLong = numberToLongOrNull(value) ?: return null.also { warnMismatch(featureId, memberName, "int16", value) } + if (asLong !in Short.MIN_VALUE.toLong()..Short.MAX_VALUE.toLong()) { + warnMismatch(featureId, memberName, "int16 (out of range)", value) + return null + } + return asLong.toShort() + } + + private fun coerceInt32(value: Any, featureId: String, memberName: String): Int? { + val asLong = numberToLongOrNull(value) ?: return null.also { warnMismatch(featureId, memberName, "int32", value) } + if (asLong !in Int.MIN_VALUE.toLong()..Int.MAX_VALUE.toLong()) { + warnMismatch(featureId, memberName, "int32 (out of range)", value) + return null + } + return asLong.toInt() + } + + private fun coerceInt64(value: Any, featureId: String, memberName: String): Int64? = when (value) { + is Int64 -> value + is Int -> Int64(value.toLong()) + is Long -> Int64(value) + is Short -> Int64(value.toLong()) + is Byte -> Int64(value.toLong()) + is Double -> if (value.isFinite() && value == value.toLong().toDouble()) Int64(value.toLong()) else { + warnMismatch(featureId, memberName, "int64", value); null + } + + is Float -> if (value.isFinite() && value == value.toLong().toFloat()) Int64(value.toLong()) else { + warnMismatch(featureId, memberName, "int64", value); null + } + + else -> { + warnMismatch(featureId, memberName, "int64", value); null + } + } + + private fun coerceFloat32(value: Any, featureId: String, memberName: String): Float? = when (value) { + is Float -> value + is Double -> value.toFloat() + is Int -> value.toFloat() + is Long -> value.toFloat() + is Int64 -> value.toLong().toFloat() + is Short -> value.toFloat() + is Byte -> value.toFloat() + else -> { + warnMismatch(featureId, memberName, "float32", value); null + } + } + + private fun coerceFloat64(value: Any, featureId: String, memberName: String): Double? = when (value) { + is Double -> value + is Float -> value.toDouble() + is Int -> value.toDouble() + is Long -> value.toDouble() + is Int64 -> value.toLong().toDouble() + is Short -> value.toDouble() + is Byte -> value.toDouble() + else -> { + warnMismatch(featureId, memberName, "float64", value); null + } + } + + private fun coerceString(value: Any, featureId: String, memberName: String): String? = when (value) { + is String -> value + else -> { + warnMismatch(featureId, memberName, "string", value); null + } + } + + private fun coerceByteArray(value: Any, featureId: String, memberName: String): ByteArray? = when (value) { + is ByteArray -> value + else -> { + warnMismatch(featureId, memberName, "byte_array", value); null + } + } + + private fun coerceTags(value: Any, featureId: String, memberName: String): String? { + if (value !is AnyObject) { + warnMismatch(featureId, memberName, "tags", value) + return null + } + return try { + toJSON(value) + } catch (_: Exception) { + warnMismatch(featureId, memberName, "tags", value); null + } + } + + private fun coerceTagsFromArray(value: Any, featureId: String, memberName: String): String? { + val tagList = when (value) { + is TagList -> value + is AnyObject -> value.proxy(TagList::class) + else -> { + warnMismatch(featureId, memberName, "tags_from_array", value); return null + } + } + val tagMap = tagList.toTagMap() + return try { + toJSON(tagMap) + } catch (_: Exception) { + warnMismatch(featureId, memberName, "tags_from_array", value); null + } + } + + /** + * Coerces a [MemberType.SET] value: a JSON array of unique primitives (booleans, numbers, strings). + * The array is persisted unmodified (element order preserved). Entries that are `null`, non-primitive, + * or duplicates violate the set contract; the value is then not materialized (warning + NULL column). + */ + private fun coerceSet(value: Any, featureId: String, memberName: String): String? { + val list: ListProxy<*> = when (value) { + is ListProxy<*> -> value + is PlatformList -> value.proxy(AnyList::class) + else -> { + warnMismatch(featureId, memberName, "set", value); return null + } + } + val seen = HashSet() + for (e in list) { + if (e == null) { + warnMismatch(featureId, memberName, "set (entries must not be null)", "null") + return null + } + if (!isPrimitive(e)) { + warnMismatch(featureId, memberName, "set (entries must be primitives)", e) + return null + } + if (!seen.add(e)) { + warnMismatch(featureId, memberName, "set (entries must be unique)", e) + return null + } + } + return try { + toJSON(list) + } catch (_: Exception) { + warnMismatch(featureId, memberName, "set", list); null + } + } + + private fun isPrimitive(value: Any): Boolean = when (value) { + is String, is Boolean, is Byte, is Short, is Int, is Long, is Int64, is Float, is Double -> true + else -> false + } + + private fun numberToLongOrNull(value: Any): Long? = when (value) { + is Byte -> value.toLong() + is Short -> value.toLong() + is Int -> value.toLong() + is Long -> value + is Int64 -> value.toLong() + is Float -> if (value.isFinite() && value == value.toLong().toFloat()) value.toLong() else null + is Double -> if (value.isFinite() && value == value.toLong().toDouble()) value.toLong() else null + else -> null + } + + /** + * Returns the sort priority for a [MemberType] based on PostgreSQL alignment size, to minimise + * tuple padding when columns are laid out in declaration order: + * 1. 8-byte types ([INT64], [FLOAT64]) — first, to get 8-byte alignment right away + * 2. 4-byte types ([INT32], [FLOAT32]) — next, still fixed-width + * 3. 1/2-byte types ([INT16], [INT8], [BOOLEAN]) — small fixed-width + * 4. Variable-length text ([STRING]) — variable but human-readable + * 5. Opaque variable-length ([BYTE_ARRAY], [SPATIAL], [TAGS], [TAGS_FROM_ARRAY], [SET]) — last + */ + fun columnSortOrder(type: MemberType): Int = when (type) { + MemberType.INT64 -> 0 + MemberType.FLOAT64 -> 1 + MemberType.INT32 -> 2 + MemberType.FLOAT32 -> 3 + MemberType.INT16 -> 4 + MemberType.INT8 -> 5 + MemberType.BOOLEAN -> 6 + MemberType.STRING -> 7 + MemberType.BYTE_ARRAY -> 8 + MemberType.SPATIAL -> 8 + MemberType.TAGS -> 9 + MemberType.TAGS_FROM_ARRAY -> 10 + MemberType.SET -> 11 + else -> 12 + } + + /** + * The set of member names that correspond to pre-defined optional [PgColumn]s (e.g. `geo`, `tags`, `cc`). + * When two members have the same type sort-order, pre-defined members are placed before user-invented ones. + */ + private val predefinedMemberNames: Set by lazy { + val mandatory = PgColumn.mandatoryColumns.map { it.name }.toSet() + PgColumn.headColumns.map { it.name }.filter { it !in mandatory }.toSet() + } + + /** + * Sorts [members] in-place for optimal PostgreSQL column layout. + * + * Ordering rules (applied only at **collection-creation** time; never on updates): + * 1. Primary: type alignment group ([columnSortOrder]) + * 2. Secondary: pre-defined members (matching a built-in optional column name) before user-invented ones + * 3. Tertiary: member name lexicographically ascending + * + * The sort is stable within each group so that the caller's intent is preserved as a tie-breaker. + */ + fun sortMembersForStorage(members: MemberList) { + // TODO: Move this into MemberList as `sortForStorage()` + // Use: members.sortedBy { it?.dataType?.sortOrder ?: Int.MAX_VALUE } + if (members.size <= 1) return + val snapshot = (0 until members.size).map { members[it]!! } + val sorted = snapshot.sortedWith( + compareBy( + { columnSortOrder(it.dataType) }, + { if (it.name in predefinedMemberNames) 0 else 1 }, + { it.name } + )) + members.clear() + members.addAll(sorted) + } + + private fun warnMismatch(featureId: String, memberName: String, expected: String, value: Any) { + logger.warn("Custom member '$memberName' on feature '$featureId': expected $expected, got ${value::class.simpleName}") + } + } +} diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaCollections.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaCollections.kt index 32dc49b5b..e920bfbba 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaCollections.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaCollections.kt @@ -12,5 +12,5 @@ import kotlin.js.JsExport @JsExport class PgNakshaCollections internal constructor(map: PgMap) : PgCollection(map, NakshaCollection() .withMapId(map.id) - .withId(Naksha.ADMIN_COL_ID) + .withId(Naksha.COLLECTIONS_COL_ID) ), PgInternalCollection diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRead.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRead.kt index bc30dfdd7..b4721e63a 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRead.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRead.kt @@ -77,9 +77,9 @@ internal data class PgRead( * @param tupleNumber the tuple-number of the tuple to read. */ constructor(conn: PgConnection, adminMap: PgAdminMap, tupleNumber: TupleNumber) : this( - adminMap.getPgMapByNumber(conn, tupleNumber.mapNumber) - ?: throw mapNotFound("The map for map-number ${tupleNumber.mapNumber} not found"), - adminMap.getPgMapByNumber(conn, tupleNumber.mapNumber)?.getPgCollectionByNumber(conn, tupleNumber.collectionNumber) + adminMap.getPgMapByNumber(conn, tupleNumber.catalogNumber) + ?: throw mapNotFound("The map for map-number ${tupleNumber.catalogNumber} not found"), + adminMap.getPgMapByNumber(conn, tupleNumber.catalogNumber)?.getPgCollectionByNumber(conn, tupleNumber.collectionNumber) ?: throw collectionNotFound("The collection for collection-number ${tupleNumber.collectionNumber} not found"), tupleNumber, null diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgStorage.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgStorage.kt index 8f1f3ca8a..34814796d 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgStorage.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgStorage.kt @@ -4,9 +4,7 @@ package naksha.psql import naksha.base.fn.Fx2 import naksha.model.* -import naksha.model.Naksha.NakshaCompanion.ADMIN_COL_ID import naksha.model.NakshaError.NakshaErrorCompanion.UNINITIALIZED -import naksha.model.objects.NakshaCollection import kotlin.js.JsExport // TODO: Create "naksha~admin" map with map-number 0 diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgTable.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgTable.kt index 1109e9c99..f26579775 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgTable.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgTable.kt @@ -511,7 +511,7 @@ $TABLESPACE""" val colDef = when { knownPgCol != null && knownPgCol !in standardSet -> knownPgCol.sqlDefinition knownPgCol != null -> continue // already in headColumns loop above - else -> PgCustomMemberValues.sqlDefinitionFor(m) + else -> PgMemberHelper.sqlDefinitionFor(m) } conn.execute("ALTER TABLE $quotedName ADD COLUMN IF NOT EXISTS $colDef").close() } @@ -581,7 +581,7 @@ $TABLESPACE""" // Standard head columns are already in baseColumns; skip to avoid duplicates. knownPgCol != null -> continue // Truly custom member: generate from type mapping. - else -> PgCustomMemberValues.sqlDefinitionFor(member) + else -> PgMemberHelper.sqlDefinitionFor(member) } sb.append(",\n").append(colDef) } @@ -589,7 +589,7 @@ $TABLESPACE""" } /** - * Maps a [PgType] to the same sort-order bucket used by [PgCustomMemberValues.columnSortOrder], + * Maps a [PgType] to the same sort-order bucket used by [PgMemberHelper_C.columnSortOrder], * so that standard and custom columns can be interleaved into the correct type-alignment group. */ private fun pgTypeSortOrder(type: PgType): Int = when (type) { @@ -636,9 +636,9 @@ $TABLESPACE""" val colDef = when { knownPgCol != null && knownPgCol !in standardSet -> knownPgCol.sqlDefinition knownPgCol != null -> continue // already in baseColumns — skip - else -> PgCustomMemberValues.sqlDefinitionFor(m) + else -> PgMemberHelper.sqlDefinitionFor(m) } - val bucket = PgCustomMemberValues.columnSortOrder(m.dataType) + val bucket = PgMemberHelper.columnSortOrder(m.dataType) extrasByBucket.getOrPut(bucket) { mutableListOf() }.add(colDef) } @@ -776,7 +776,7 @@ ${TABLESPACE};""".trim() private fun physicalColumnName(name: String, members: MemberList?): String { if (members != null) { for (m in members) { - if (m != null && m.name == name) return PgCustomMemberValues.pgColumnName(name) + if (m != null && m.name == name) return PgMemberHelper.pgColumnName(name) } } return name diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgType.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgType.kt index e16ef15a4..48a3fc2ce 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgType.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgType.kt @@ -3,7 +3,8 @@ package naksha.psql import naksha.base.JsEnum -import naksha.base.PlatformUtil +import naksha.model.objects.Member +import naksha.model.objects.MemberType import kotlin.js.JsExport import kotlin.js.JsStatic import kotlin.jvm.JvmField @@ -154,6 +155,32 @@ class PgType : JsEnum() { @JsStatic @JvmStatic fun of(name: String?): PgType? = getDefined(name, PgType::class) + + /** + * Returns the database column type to be used for a specific [MemberType]. + * @param member the [MemberType] to lookup. + * @return the database column type to be used for a specific [MemberType]. + * @since 3.0 + */ + @JsStatic + @JvmStatic + fun ofMemberType(member: Member): PgType = when (member.dataType) { + MemberType.BOOLEAN -> BOOLEAN + MemberType.INT8 -> SHORT + MemberType.INT16 -> SHORT + MemberType.INT32 -> INT + MemberType.INT64 -> INT64 + MemberType.FLOAT32 -> FLOAT + MemberType.FLOAT64 -> DOUBLE + MemberType.STRING -> STRING + // MemberType.BYTE_ARRAY -> BYTE_ARRAY + // MemberType.TUPLE_NUMBER -> BYTE_ARRAY + // MemberType.SPATIAL -> BYTE_ARRAY + MemberType.TAGS -> JSONB + MemberType.TAGS_FROM_ARRAY -> JSONB + MemberType.SET -> JSONB + else -> BYTE_ARRAY + } } @Suppress("NON_EXPORTABLE_TYPE") diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgUtil.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgUtil.kt index 3b340a30e..6b8e295c0 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgUtil.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgUtil.kt @@ -7,7 +7,7 @@ import naksha.geo.SpGeometry import naksha.jbon.* import naksha.model.* import naksha.model.Naksha.NakshaCompanion.ADMIN_CATALOG_ID -import naksha.model.Naksha.NakshaCompanion.ADMIN_COL_ID +import naksha.model.Naksha.NakshaCompanion.COLLECTIONS_COL_ID import naksha.model.Naksha.NakshaCompanion.BOOKS_COL_ID import naksha.model.Naksha.NakshaCompanion.CATALOGS_COL_ID import naksha.model.Naksha.NakshaCompanion.TRANSACTIONS_COL_ID @@ -64,7 +64,7 @@ class PgUtil private constructor() { */ @JvmField @JsStatic - val COLLECTIONS_COL_QUOTED = quoteIdent(ADMIN_COL_ID) + val COLLECTIONS_COL_QUOTED = quoteIdent(COLLECTIONS_COL_ID) /** * Array to query the partition name from the partition number (resolves 0 to "000", 1 to "001", ..., 255 to "256"). diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWrite.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWrite.kt index 70a09c4b1..5263fade4 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWrite.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWrite.kt @@ -19,7 +19,7 @@ internal data class PgWrite(val original: Write, val i: Int) { * The map into which to write. * * - If a map is modified, this is [Naksha.ADMIN_MAP][naksha.model.Naksha.ADMIN_CATALOG_ID], [asPgMap] and [asNakshaMap] will be set. - * - If a collection is modified, this is the map in which [Naksha.COLLECTIONS_COL][naksha.model.Naksha.ADMIN_COL_ID] is located, [asPgCollection] and [asNakshaCollection] will be set. + * - If a collection is modified, this is the map in which [Naksha.COLLECTIONS_COL][naksha.model.Naksha.COLLECTIONS_COL_ID] is located, [asPgCollection] and [asNakshaCollection] will be set. * @since 3.0 */ lateinit var map: PgMap @@ -28,7 +28,7 @@ internal data class PgWrite(val original: Write, val i: Int) { * The collection into which to write. * * - If a map is modified, this is [Naksha.CATALOGS_COL][naksha.model.Naksha.CATALOGS_COL_ID], [asPgMap] and [asNakshaMap] will be set. - * - If a collection is modified, this is [Naksha.COLLECTIONS_COL][naksha.model.Naksha.ADMIN_COL_ID], [asPgCollection] and [asNakshaCollection] will be set. + * - If a collection is modified, this is [Naksha.COLLECTIONS_COL][naksha.model.Naksha.COLLECTIONS_COL_ID], [asPgCollection] and [asNakshaCollection] will be set. * @since 3.0 */ lateinit var collection: PgCollection diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt index d75111a4f..f9b74f6f5 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt @@ -462,8 +462,8 @@ open class PgWriter internal constructor( normalizedMembers.add(m) } } - PgCustomMemberValues.validateMemberNames(normalizedMembers) - PgCustomMemberValues.sortMembersForStorage(normalizedMembers) + PgMemberHelper.validateMemberNames(normalizedMembers) + PgMemberHelper.sortMembersForStorage(normalizedMembers) collection.members = normalizedMembers } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterBase.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterBase.kt index c4757af72..6d2d67bdb 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterBase.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterBase.kt @@ -76,7 +76,6 @@ internal abstract class PgWriterBase protected constructor( .withStorageNumber(storageNumber) .withMapNumber(mapNumber) .withCollectionNumber(collectionNumber) - .withDefaultDataEncoding(collection.head.dataEncoding ?: naksha.model.Naksha.DEFAULT_DATA_ENCODING) .withMinSize(writes.size) /** diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/AttachmentTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/AttachmentTest.kt index f4b5ba725..1766eff3a 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/AttachmentTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/AttachmentTest.kt @@ -120,7 +120,7 @@ class AttachmentTest : PgTestBase() { assertNotNull(insertedFeatureGuid) assertEquals(featureId, insertedFeatureGuid.id) assertEquals(storage.number, insertedFeatureGuid.tupleNumber.databaseNumber) - assertEquals(map.number, insertedFeatureGuid.tupleNumber.mapNumber) + assertEquals(map.number, insertedFeatureGuid.tupleNumber.catalogNumber) assertEquals(collection.number, insertedFeatureGuid.tupleNumber.collectionNumber) // Now, update the feature, leave the attachment as it is. @@ -209,7 +209,7 @@ class AttachmentTest : PgTestBase() { assertNotNull(insertedFeatureGuid) assertEquals(featureId, insertedFeatureGuid.id) assertEquals(storage.number, insertedFeatureGuid.tupleNumber.databaseNumber) - assertEquals(map.number, insertedFeatureGuid.tupleNumber.mapNumber) + assertEquals(map.number, insertedFeatureGuid.tupleNumber.catalogNumber) assertEquals(collection.number, insertedFeatureGuid.tupleNumber.collectionNumber) // Now, update the feature, leave the attachment as it is. diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/CollectionTests.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/CollectionTests.kt index 4102f36e8..11cfe095e 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/CollectionTests.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/CollectionTests.kt @@ -61,7 +61,7 @@ class CollectionTests : PgTestBase(collection = null, mapId = "") { // And: Virtual Collections contain the created collection val selectCollectionFromVirt = ReadFeatures().apply { mapId = collection.mapId - collectionIds += Naksha.ADMIN_COL_ID + collectionIds += Naksha.COLLECTIONS_COL_ID featureIds += collection.id } val virtBeforeDelete = executeRead(selectCollectionFromVirt) @@ -331,7 +331,7 @@ class CollectionTests : PgTestBase(collection = null, mapId = "") { assertEquals(StoreMode.SUSPEND, responseCollection.storeDeleted) val selectCollectionFromVirt = ReadFeatures().apply { mapId = map.id - collectionIds += Naksha.ADMIN_COL_ID + collectionIds += Naksha.COLLECTIONS_COL_ID featureIds += collection.id } val colRead = assertNotNull(executeRead(selectCollectionFromVirt).features[0]).proxy(NakshaCollection::class) @@ -524,7 +524,7 @@ class CollectionTests : PgTestBase(collection = null, mapId = "") { assertEquals(expectedCount, columns.size, "Expected ${PgColumn.headColumns.size} head columns + 1 custom column, got: $columns") assertTrue(PgColumn.headColumns.all { it.name in columns }) - val customColName = PgCustomMemberValues.pgColumnName("score") + val customColName = PgMemberHelper.pgColumnName("score") assertTrue(customColName in columns, "Custom column '$customColName' not found in: $columns") // Indices: no default optional indices; only the declared custom index must be present. @@ -563,7 +563,7 @@ class CollectionTests : PgTestBase(collection = null, mapId = "") { var dataType: String? = null conn.execute( "SELECT data_type FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2 AND column_name = $3", - arrayOf(map.id, collection.id, PgCustomMemberValues.pgColumnName("labels")) + arrayOf(map.id, collection.id, PgMemberHelper.pgColumnName("labels")) ).use { cursor -> if (cursor.next()) dataType = cursor["data_type"] } assertEquals("jsonb", dataType, "SET member 'labels' must be materialized as jsonb") diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/TupleNumberPersistenceTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/TupleNumberPersistenceTest.kt index ba0c51ffd..756ef6270 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/TupleNumberPersistenceTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/TupleNumberPersistenceTest.kt @@ -72,7 +72,7 @@ class TupleNumberPersistenceTest : PgTestBase(collection = null, mapId = "") { val pgCollection = pgMap.getPgCollectionById(conn, collection.id) require(pgCollection != null) { "Missing collection ${collection.id}" } assertEquals(storage.number, persistedTuple.tupleNumber.databaseNumber) - assertEquals(pgMap.number, persistedTuple.tupleNumber.mapNumber) + assertEquals(pgMap.number, persistedTuple.tupleNumber.catalogNumber) assertEquals(pgCollection.number, persistedTuple.tupleNumber.collectionNumber) assertEquals(featureNumber(feature.id), persistedTuple.tupleNumber.featureNumber) assertEquals(partitionNumber(featureNumber(feature.id)), persistedTuple.tupleNumber.partitionNumber) From 2cac66d616198166f87a643ab76823169992bdcf Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Mon, 15 Jun 2026 10:51:59 +0200 Subject: [PATCH 14/57] Fix minor issue in JBON2 spec. Signed-off-by: Alexander Lowey-Weber --- docs/latest/JBON2.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/latest/JBON2.md b/docs/latest/JBON2.md index 56fa693d5..3c2abd7d9 100644 --- a/docs/latest/JBON2.md +++ b/docs/latest/JBON2.md @@ -870,13 +870,13 @@ The `members` [book] is per-tuple and travels with the tuple. This means, the st Some `elements` of the `members` [book] have a pre-defined meaning: -| Name | Path | Type | Description | -|------------------|------------------------------------|---------------|------------------------------------------------------------------------------------------------------------------------------------------| -| `tn` | `properties->@ns:com:here:xyz->tn` | [TupleNumber] | The [Tuple-Number] of this tuple. | -| `global_book_fn` | `properties->@ns:com:here:xyz->gb` | [int]? | The _optional_ feature-number of the `global` [book] needed to decode; `null` if no global book is needed. | -| `next_version` | `properties->@ns:com:here:xyz->nv` | [uint56] | The next version of the tuple; if the tuple is in _HEAD_ state the value will be `9_007_199_254_740_991L`. | -| `id` | `id` | [String]? | The _optional_ identifier of this tuple; a string when the feature-number is negative; `null` when the feature-number is positive (≥ 0). | -| ... | ... | [indexable]? | All custom members appended starting here, types **MUST** be [indexable]. | +| Name | Path | Type | Description | +|------------------|--------|---------------|------------------------------------------------------------------------------------------------------------------------------------------| +| `tn` | `tn` | [TupleNumber] | The [Tuple-Number] of this tuple. | +| `global_book_fn` | `gbfn` | [int]? | The _optional_ feature-number of the `global` [book] needed to decode; `null` if no global book is needed. | +| `next_version` | `nv` | [uint56] | The next version of the tuple; if the tuple is in _HEAD_ state the value will be `9_007_199_254_740_991L`. | +| `id` | `id` | [String]? | The _optional_ identifier of this tuple; a string when the feature-number is negative; `null` when the feature-number is positive (≥ 0). | +| ... | ... | [indexable]? | All custom members appended starting here, types **MUST** be [indexable]. | The `next_version` MUST be encoded as [uint56] _(**lead-in** `0000_1101`)_, so it can be patched in place without changing the byte size of the tuple. From c51252921b489cf0aa5422f750e5b647105b04b3 Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Mon, 15 Jun 2026 10:52:19 +0200 Subject: [PATCH 15/57] Next bunch of fixes about the members not being hardcoded. Signed-off-by: Alexander Lowey-Weber --- .../naksha/cli/copy/service/CopyService.java | 6 +- .../cli/storages/GeneratingSession.java | 10 +- .../cli/copy/service/psql/PsqlCopyTest.java | 4 +- .../commonMain/kotlin/naksha/geo/SpType.kt | 31 ++- .../lib/handlers/DefaultStorageHandler.java | 6 +- .../handlers/DefaultStorageHandlerTest.java | 8 +- .../internal/IntHandlerForSpacesTest.java | 3 +- .../com/here/naksha/lib/hub/NakshaHub.java | 4 +- .../hub/storages/NHAdminStorageReader.java | 10 +- .../hub/storages/NHSpaceStorageReader.java | 10 +- .../lib/hub/mock/NHAdminReaderMock.java | 11 +- .../commonMain/kotlin/naksha/jbon/HeapBook.kt | 2 +- .../commonMain/kotlin/naksha/jbon/IBook.kt | 31 +-- .../kotlin/naksha/model/AbstractStorage.kt | 2 +- .../kotlin/naksha/model/FetchMode.kt | 6 +- .../kotlin/naksha/model/ISession.kt | 10 +- .../kotlin/naksha/model/IStorage.kt | 2 +- .../kotlin/naksha/model/LibModel.kt | 2 +- .../commonMain/kotlin/naksha/model/Naksha.kt | 26 +- .../kotlin/naksha/model/NakshaError.kt | 8 + .../commonMain/kotlin/naksha/model/Tuple.kt | 89 +++++-- .../kotlin/naksha/model/objects/Member.kt | 24 +- .../kotlin/naksha/model/objects/MemberList.kt | 63 ++++- .../kotlin/naksha/model/objects/MemberType.kt | 4 + .../naksha/model/objects/NakshaCatalog.kt | 83 +++++++ .../naksha/model/objects/NakshaCollection.kt | 112 ++++----- .../naksha/model/objects/NakshaDictionary.kt | 1 - .../naksha/model/objects/NakshaFeature.kt | 95 -------- .../kotlin/naksha/model/objects/NakshaMap.kt | 104 -------- .../naksha/model/objects/NakshaStorage.kt | 22 +- .../kotlin/naksha/model/objects/NakshaTx.kt | 1 - .../naksha/model/objects/StandardMembers.kt | 2 +- .../naksha/model/request/FeatureTuple.kt | 18 +- .../kotlin/naksha/model/request/ReadMaps.kt | 4 +- .../naksha/model/request/SuccessResponse.kt | 8 +- .../kotlin/naksha/model/request/Write.kt | 47 ++-- .../naksha/model/request/query/MetaColumn.kt | 2 +- .../kotlin/naksha/model/PropertyFilterTest.kt | 2 +- .../kotlin/naksha/psql/PgAdminMap.kt | 22 +- .../kotlin/naksha/psql/PgColumnRows.kt | 227 ++++++++---------- .../commonMain/kotlin/naksha/psql/PgMap.kt | 14 +- .../kotlin/naksha/psql/PgNakshaBooks.kt | 2 +- .../kotlin/naksha/psql/PgNakshaCatalogs.kt | 2 +- .../kotlin/naksha/psql/PgNakshaCollections.kt | 2 +- .../naksha/psql/PgNakshaTransactions.kt | 2 +- .../kotlin/naksha/psql/PgSession.kt | 14 +- .../commonMain/kotlin/naksha/psql/PgWrite.kt | 6 +- .../commonMain/kotlin/naksha/psql/PgWriter.kt | 10 +- .../kotlin/naksha/psql/PgWriterBase.kt | 4 +- .../kotlin/naksha/psql/PgWriterDelete.kt | 4 +- .../kotlin/naksha/psql/PgWriterUpdate.kt | 4 +- .../kotlin/naksha/psql/PgWriterUpsert.kt | 4 +- .../kotlin/naksha/psql/AttachmentTest.kt | 14 +- .../kotlin/naksha/psql/ChainCollectionTest.kt | 19 +- .../kotlin/naksha/psql/CollectionTests.kt | 6 +- .../kotlin/naksha/psql/DeleteFeatureBase.kt | 14 +- .../kotlin/naksha/psql/HistoryUuidTest.kt | 4 +- .../kotlin/naksha/psql/InsertFeatureTest.kt | 28 +-- .../kotlin/naksha/psql/PartitioningTest.kt | 3 +- .../naksha/psql/PgPropertyFilterTest.kt | 8 +- .../kotlin/naksha/psql/PgTestBase.kt | 34 +-- .../kotlin/naksha/psql/ReadFeaturesAll.kt | 4 +- .../naksha/psql/ReadFeaturesByGeometryTest.kt | 6 +- .../naksha/psql/ReadFeaturesByGuuidTest.kt | 2 +- .../naksha/psql/ReadFeaturesByMetadataTest.kt | 10 +- .../naksha/psql/ReadFeaturesByOtherTns.kt | 2 +- .../naksha/psql/ReadFeaturesByRefTilesTest.kt | 4 +- .../naksha/psql/ReadFeaturesByTagsTest.kt | 4 +- .../kotlin/naksha/psql/ReadHistoryTest.kt | 8 +- .../kotlin/naksha/psql/ReadLimitTest.kt | 2 +- .../kotlin/naksha/psql/ReadOrderedTest.kt | 2 +- .../naksha/psql/RecreateAfterDeleteTest.kt | 12 +- .../naksha/psql/TupleNumberPersistenceTest.kt | 4 +- .../kotlin/naksha/psql/UpdateFeatureTest.kt | 6 +- .../kotlin/naksha/psql/UpsertFeatureTest.kt | 2 +- .../kotlin/naksha/psql/PsqlAdminMap.kt | 6 +- .../psql/DeleteFeatureByVersionTest.java | 2 +- .../naksha/psql/PsqlErrorMappingTest.kt | 2 +- .../com/here/naksha/lib/view/ViewLayer.java | 2 +- .../here/naksha/lib/view/ViewReadSession.java | 11 +- .../here/naksha/lib/view/MockReadSession.java | 10 +- .../com/here/naksha/lib/view/PsqlTests.java | 8 +- .../storage/http/HttpStorageReadSession.java | 10 +- 83 files changed, 712 insertions(+), 727 deletions(-) create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCatalog.kt delete mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaMap.kt diff --git a/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/copy/service/CopyService.java b/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/copy/service/CopyService.java index fe01b29a1..7351d9db1 100644 --- a/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/copy/service/CopyService.java +++ b/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/copy/service/CopyService.java @@ -11,7 +11,7 @@ import naksha.model.NakshaException; import naksha.model.SessionOptions; import naksha.model.objects.NakshaCollection; -import naksha.model.objects.NakshaMap; +import naksha.model.objects.NakshaCatalog; import naksha.model.request.*; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; @@ -225,7 +225,7 @@ private Response performWriteRequest( private WriteRequest buildCreateMapRequest(String mapId) { WriteRequest writeRequest = new WriteRequest(); - NakshaMap map = new NakshaMap(mapId); + NakshaCatalog map = new NakshaCatalog(mapId); Write write = new Write().createMap(map); writeRequest.add(write); return writeRequest; @@ -234,7 +234,7 @@ private WriteRequest buildCreateMapRequest(String mapId) { private WriteRequest buildCreateCollectionRequest(CopyElement target) { WriteRequest writeRequest = new WriteRequest(); NakshaCollection collection = new NakshaCollection(target.getCollectionId()) - .withMapId(target.getMapId()); + .withCatalogId(target.getMapId()); Write write = new Write().createCollection(collection); writeRequest.add(write); return writeRequest; diff --git a/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/storages/GeneratingSession.java b/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/storages/GeneratingSession.java index 64c03c112..297b924d2 100644 --- a/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/storages/GeneratingSession.java +++ b/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/storages/GeneratingSession.java @@ -3,7 +3,7 @@ import naksha.model.*; import naksha.model.objects.NakshaCollection; import naksha.model.objects.NakshaFeature; -import naksha.model.objects.NakshaMap; +import naksha.model.objects.NakshaCatalog; import naksha.model.request.*; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -109,25 +109,25 @@ public void setLockTimeout(int i) { @Nullable @Override - public NakshaMap getMapById(@NotNull String mapId) { + public NakshaCatalog getMapById(@NotNull String mapId) { throw new NakshaException(NakshaError.UNSUPPORTED_OPERATION, ""); } @Nullable @Override - public NakshaMap getMapByNumber(int mapNumber) { + public NakshaCatalog getMapByNumber(int mapNumber) { throw new NakshaException(NakshaError.UNSUPPORTED_OPERATION, ""); } @Nullable @Override - public NakshaCollection getCollectionById(@NotNull NakshaMap map, @NotNull String collectionId) { + public NakshaCollection getCollectionById(@NotNull NakshaCatalog map, @NotNull String collectionId) { throw new NakshaException(NakshaError.UNSUPPORTED_OPERATION, ""); } @Nullable @Override - public NakshaCollection getCollectionByNumber(@NotNull NakshaMap map, int collectionNumber) { + public NakshaCollection getCollectionByNumber(@NotNull NakshaCatalog map, int collectionNumber) { throw new NakshaException(NakshaError.UNSUPPORTED_OPERATION, ""); } diff --git a/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/copy/service/psql/PsqlCopyTest.java b/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/copy/service/psql/PsqlCopyTest.java index c510e0833..c7adb088b 100644 --- a/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/copy/service/psql/PsqlCopyTest.java +++ b/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/copy/service/psql/PsqlCopyTest.java @@ -32,7 +32,7 @@ import naksha.model.SessionOptions; import naksha.model.objects.NakshaCollection; import naksha.model.objects.NakshaFeature; -import naksha.model.objects.NakshaMap; +import naksha.model.objects.NakshaCatalog; import naksha.model.objects.NakshaStorage; import naksha.model.request.ReadFeatures; import naksha.model.request.Request; @@ -314,7 +314,7 @@ private String createUniqueMap(IStorage storage, SessionOptions sessionOptions) private void addMapToTheStorage(IStorage storage, String mapId, SessionOptions sessionOptions) { WriteRequest writeRequest = new WriteRequest(); - NakshaMap map = new NakshaMap().withId(mapId); + NakshaCatalog map = new NakshaCatalog().withId(mapId); Write createMap = new Write().createMap(map); writeRequest.add(createMap); diff --git a/here-naksha-lib-geo/src/commonMain/kotlin/naksha/geo/SpType.kt b/here-naksha-lib-geo/src/commonMain/kotlin/naksha/geo/SpType.kt index caa8bd1e1..25fbefdda 100644 --- a/here-naksha-lib-geo/src/commonMain/kotlin/naksha/geo/SpType.kt +++ b/here-naksha-lib-geo/src/commonMain/kotlin/naksha/geo/SpType.kt @@ -29,33 +29,42 @@ class SpType : JsEnum() { @JsStatic fun of(value: String): SpType = get(value, SpType::class) + /** + * Returns the given value as [SpType]. + * @param value the value. + * @return the [SpType] representing this value; `null` if the value does not represent any [SpGeometry]. + */ + @JvmStatic + @JsStatic + fun ofDefined(value: String?): SpType? = if (value != null) getDefined(value, SpType::class) else null + @JvmField @JsStatic - val Point = def(SpType::class, "Point") + val Point = def(SpType::class, "Point") { self -> self._klass = SpPoint::class } @JvmField @JsStatic - val MultiPoint = def(SpType::class, "MultiPoint") + val MultiPoint = def(SpType::class, "MultiPoint") { self -> self._klass = SpMultiPoint::class } @JvmField @JsStatic - val LineString = def(SpType::class, "LineString") + val LineString = def(SpType::class, "LineString") { self -> self._klass = SpLineString::class } @JvmField @JsStatic - val MultiLineString = def(SpType::class, "MultiLineString") + val MultiLineString = def(SpType::class, "MultiLineString") { self -> self._klass = SpMultiLineString::class } @JvmField @JsStatic - val Polygon = def(SpType::class, "Polygon") + val Polygon = def(SpType::class, "Polygon") { self -> self._klass = SpPolygon::class } @JvmField @JsStatic - val MultiPolygon = def(SpType::class, "MultiPolygon") + val MultiPolygon = def(SpType::class, "MultiPolygon") { self -> self._klass = SpMultiPolygon::class } @JvmField @JsStatic - val GeometryCollection = def(SpType::class, "GeometryCollection") + val GeometryCollection = def(SpType::class, "GeometryCollection") { self -> self._klass = SpGeometryCollection::class } } /** @@ -69,4 +78,12 @@ class SpType : JsEnum() { if (any !is AnyObject) return false return any.getRaw("type") == typeName } + + private var _klass: KClass? = null + + /** + * The [KClass] referring to the [SpGeometry] type; access throws an exception, when this not [isDefined]. + */ + val klass: KClass + get() = _klass!! } \ No newline at end of file diff --git a/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/DefaultStorageHandler.java b/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/DefaultStorageHandler.java index 4f8e0efd3..dc64b274b 100644 --- a/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/DefaultStorageHandler.java +++ b/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/DefaultStorageHandler.java @@ -36,7 +36,7 @@ import naksha.model.SessionOptions; import naksha.model.StreamInfo; import naksha.model.objects.NakshaCollection; -import naksha.model.objects.NakshaMap; +import naksha.model.objects.NakshaCatalog; import naksha.model.objects.NakshaStorage; import naksha.model.request.ErrorResponse; import naksha.model.request.ReadFeatures; @@ -455,7 +455,7 @@ private Response retryDueToUninitializedStorage( @NotNull SessionOptions sessionOptions, @NotNull IStorage storageImpl, @NotNull String mapId) { - WriteRequest createMapRequest = new WriteRequest().add(new Write().createMap(new NakshaMap(mapId))); + WriteRequest createMapRequest = new WriteRequest().add(new Write().createMap(new NakshaCatalog(mapId))); return singleWrite(sessionOptions, storageImpl, createMapRequest); } @@ -531,7 +531,7 @@ private void applyMapIdAndCollectionId( WriteRequest wr = (WriteRequest) request; if (isOnlyWriteCollections(wr)) { collectionsFrom(wr).forEach(collectionFromRequest -> { - collectionFromRequest.setMapId(mapId); + collectionFromRequest.setCatalogId(mapId); collectionFromRequest.setId(collectionId); }); } diff --git a/here-naksha-lib-handlers/src/jvmTest/java/com/here/naksha/lib/handlers/DefaultStorageHandlerTest.java b/here-naksha-lib-handlers/src/jvmTest/java/com/here/naksha/lib/handlers/DefaultStorageHandlerTest.java index 3e2f2cf8a..949998cfa 100644 --- a/here-naksha-lib-handlers/src/jvmTest/java/com/here/naksha/lib/handlers/DefaultStorageHandlerTest.java +++ b/here-naksha-lib-handlers/src/jvmTest/java/com/here/naksha/lib/handlers/DefaultStorageHandlerTest.java @@ -640,7 +640,7 @@ NakshaCollection correctCollection() { case SPACE_PROPERTIES: return JvmBoxingUtil.box(space.getProperties(), SpaceProperties.class).getCollection(); case SPACE_ID: - return new NakshaCollection(space.getId()).withMapId(getMapId()); + return new NakshaCollection(space.getId()).withCatalogId(getMapId()); default: throw new IllegalStateException("Unexpected collection source: " + validCollectionSource); } @@ -704,7 +704,7 @@ private static SpaceProperties spacePropertiesWithCollection(String collectionId } final NakshaCollection nakshaCollection = new NakshaCollection(); nakshaCollection.setId(collectionId); - nakshaCollection.setMapId(getMapId()); + nakshaCollection.setCatalogId(getMapId()); SpaceProperties spaceProperties = new SpaceProperties(); spaceProperties.setCollection(nakshaCollection); return spaceProperties; @@ -715,7 +715,7 @@ private static DefaultStorageHandlerProperties handlerPropertiesWithCollection(S NakshaCollection collection = collectionId != null ? new NakshaCollection() : null; if (collection != null) { collection.setId(collectionId); - collection.setMapId(getMapId()); + collection.setCatalogId(getMapId()); } properties.setCollection(collection); return properties; @@ -724,7 +724,7 @@ private static DefaultStorageHandlerProperties handlerPropertiesWithCollection(S private static DefaultStorageHandlerProperties handlerProperties(String storageId) { final NakshaCollection nakshaCollection = new NakshaCollection(); nakshaCollection.setId("handler_collection"); - nakshaCollection.setMapId(getMapId()); + nakshaCollection.setCatalogId(getMapId()); DefaultStorageHandlerProperties properties = new DefaultStorageHandlerProperties(); properties.setStorageId(storageId); properties.setCollection(nakshaCollection); diff --git a/here-naksha-lib-handlers/src/jvmTest/java/com/here/naksha/lib/handlers/internal/IntHandlerForSpacesTest.java b/here-naksha-lib-handlers/src/jvmTest/java/com/here/naksha/lib/handlers/internal/IntHandlerForSpacesTest.java index 335d30fe6..c40f70e53 100644 --- a/here-naksha-lib-handlers/src/jvmTest/java/com/here/naksha/lib/handlers/internal/IntHandlerForSpacesTest.java +++ b/here-naksha-lib-handlers/src/jvmTest/java/com/here/naksha/lib/handlers/internal/IntHandlerForSpacesTest.java @@ -3,7 +3,6 @@ import static com.here.naksha.lib.core.HubInternalIdentifiers.EVENT_HANDLERS; import static com.here.naksha.lib.core.HubInternalIdentifiers.SPACES; import static java.util.Collections.emptyList; -import static naksha.model.NakshaError.ILLEGAL_ARGUMENT; import static naksha.model.NakshaError.NOT_FOUND; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; @@ -138,7 +137,7 @@ private static Stream> persistingWritesWithInvalidSpace() { private static Stream> persistingSpaceWithoutValidHandlers() { Space space = space("space_id", "no_desc", "some_title", List.of("handler_1", "handler_2", "handler_3")); NakshaCollection collection = new NakshaCollection("test_collection"); - collection.setMapId("tes_map_id"); + collection.setCatalogId("tes_map_id"); space.getProperties().setCollection(collection); return Stream.of( named("PUT Space without valid handlers", new WriteRequest().add(new Write().upsertFeature(null, SPACES, space))), diff --git a/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/NakshaHub.java b/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/NakshaHub.java index a91f54222..c343d20d1 100644 --- a/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/NakshaHub.java +++ b/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/NakshaHub.java @@ -66,7 +66,7 @@ import naksha.model.objects.NakshaCollection; import naksha.model.objects.NakshaFeature; import naksha.model.objects.NakshaFeatureList; -import naksha.model.objects.NakshaMap; +import naksha.model.objects.NakshaCatalog; import naksha.model.objects.NakshaStorage; import naksha.model.request.ErrorResponse; import naksha.model.request.ReadFeatures; @@ -199,7 +199,7 @@ public NakshaHub( } private NakshaContext setupMapAndContext(String mapId) { - NakshaMap map = new NakshaMap().withId(mapId); + NakshaCatalog map = new NakshaCatalog().withId(mapId); Write createMap = new Write().upsertMap(map, false); NakshaContext initialContext = NakshaContext.currentContext().withAuthor(NakshaHubConfig.defaultAppName()); psqlStorage.runInWriteSession(SessionOptions.from(initialContext), writer -> { diff --git a/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHAdminStorageReader.java b/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHAdminStorageReader.java index 4d75f0b4d..2dc551bc3 100644 --- a/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHAdminStorageReader.java +++ b/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHAdminStorageReader.java @@ -27,7 +27,7 @@ import naksha.model.NakshaVersion; import naksha.model.SessionOptions; import naksha.model.objects.NakshaCollection; -import naksha.model.objects.NakshaMap; +import naksha.model.objects.NakshaCatalog; import naksha.model.request.FeatureTuple; import naksha.model.request.Request; import naksha.model.request.Response; @@ -113,17 +113,17 @@ public Response executeParallel(@NotNull Request request) { } @Override - public @Nullable NakshaMap getMapById(@NotNull String mapId) { + public @Nullable NakshaCatalog getMapById(@NotNull String mapId) { return session.getMapById(mapId); } @Override - public @Nullable NakshaMap getMapByNumber(int mapNumber) { + public @Nullable NakshaCatalog getMapByNumber(int mapNumber) { return session.getMapByNumber(mapNumber); } @Override - public @Nullable NakshaCollection getCollectionById(@NotNull NakshaMap map, @NotNull String collectionId) { + public @Nullable NakshaCollection getCollectionById(@NotNull NakshaCatalog map, @NotNull String collectionId) { return session.getCollectionById(map, collectionId); } @@ -133,7 +133,7 @@ public void loadTuples(@NotNull List featureTuples, int } @Override - public @Nullable NakshaCollection getCollectionByNumber(@NotNull NakshaMap map, int collectionNumber) { + public @Nullable NakshaCollection getCollectionByNumber(@NotNull NakshaCatalog map, int collectionNumber) { return session.getCollectionByNumber(map, collectionNumber); } diff --git a/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHSpaceStorageReader.java b/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHSpaceStorageReader.java index 332dae7f2..061fe2555 100644 --- a/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHSpaceStorageReader.java +++ b/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHSpaceStorageReader.java @@ -46,7 +46,7 @@ import naksha.model.SessionOptions; import naksha.model.StreamInfo; import naksha.model.objects.NakshaCollection; -import naksha.model.objects.NakshaMap; +import naksha.model.objects.NakshaCatalog; import naksha.model.request.ErrorResponse; import naksha.model.request.FeatureTuple; import naksha.model.request.ReadCollections; @@ -337,17 +337,17 @@ public Response executeParallel(@NotNull Request request) { } @Override - public @Nullable NakshaMap getMapById(@NotNull String mapId) { + public @Nullable NakshaCatalog getMapById(@NotNull String mapId) { throw NOT_SUPPORTED_ERROR; } @Override - public @Nullable NakshaMap getMapByNumber(int mapNumber) { + public @Nullable NakshaCatalog getMapByNumber(int mapNumber) { throw NOT_SUPPORTED_ERROR; } @Override - public @Nullable NakshaCollection getCollectionById(@NotNull NakshaMap map, @NotNull String collectionId) { + public @Nullable NakshaCollection getCollectionById(@NotNull NakshaCatalog map, @NotNull String collectionId) { throw NOT_SUPPORTED_ERROR; } @@ -357,7 +357,7 @@ public void loadTuples(@NotNull List featureTuples, int } @Override - public @Nullable NakshaCollection getCollectionByNumber(@NotNull NakshaMap map, int collectionNumber) { + public @Nullable NakshaCollection getCollectionByNumber(@NotNull NakshaCatalog map, int collectionNumber) { throw NOT_SUPPORTED_ERROR; } diff --git a/here-naksha-lib-hub/src/jvmTest/java/com/here/naksha/lib/hub/mock/NHAdminReaderMock.java b/here-naksha-lib-hub/src/jvmTest/java/com/here/naksha/lib/hub/mock/NHAdminReaderMock.java index 586bb8d44..845ddb20e 100644 --- a/here-naksha-lib-hub/src/jvmTest/java/com/here/naksha/lib/hub/mock/NHAdminReaderMock.java +++ b/here-naksha-lib-hub/src/jvmTest/java/com/here/naksha/lib/hub/mock/NHAdminReaderMock.java @@ -27,7 +27,6 @@ import java.util.stream.Stream; import naksha.geo.ProxyGeoUtil; import naksha.geo.SpGeometry; -import naksha.jbon.JbDictionary; import naksha.model.IReadSession; import naksha.model.IStorage; import naksha.model.NakshaError; @@ -37,7 +36,7 @@ import naksha.model.objects.NakshaCollection; import naksha.model.objects.NakshaFeature; import naksha.model.objects.NakshaFeatureList; -import naksha.model.objects.NakshaMap; +import naksha.model.objects.NakshaCatalog; import naksha.model.request.ErrorResponse; import naksha.model.request.FeatureTuple; import naksha.model.request.ReadFeatures; @@ -280,22 +279,22 @@ public Response executeParallel(@NotNull Request request) { } @Override - public @Nullable NakshaMap getMapById(@NotNull String mapId) { + public @Nullable NakshaCatalog getMapById(@NotNull String mapId) { throw new NakshaException(new NakshaError(NakshaError.UNSUPPORTED_OPERATION, "Not supported by mock yet")); } @Override - public @Nullable NakshaMap getMapByNumber(int mapNumber) { + public @Nullable NakshaCatalog getMapByNumber(int mapNumber) { throw new NakshaException(new NakshaError(NakshaError.UNSUPPORTED_OPERATION, "Not supported by mock yet")); } @Override - public @Nullable NakshaCollection getCollectionById(@NotNull NakshaMap map, @NotNull String collectionId) { + public @Nullable NakshaCollection getCollectionById(@NotNull NakshaCatalog map, @NotNull String collectionId) { throw new NakshaException(new NakshaError(NakshaError.UNSUPPORTED_OPERATION, "Not supported by mock yet")); } @Override - public @Nullable NakshaCollection getCollectionByNumber(@NotNull NakshaMap map, int collectionNumber) { + public @Nullable NakshaCollection getCollectionByNumber(@NotNull NakshaCatalog map, int collectionNumber) { throw new NakshaException(new NakshaError(NakshaError.UNSUPPORTED_OPERATION, "Not supported by mock yet")); } diff --git a/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/HeapBook.kt b/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/HeapBook.kt index 5563ccb98..6d8ecbc5c 100644 --- a/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/HeapBook.kt +++ b/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/HeapBook.kt @@ -44,7 +44,7 @@ class HeapBook( override fun namesLength(): Int = _names.size - override fun getByName(name: String): Any? { + override fun get(name: String): Any? { val i = _nameIndex[name] ?: return null return _values.getOrNull(i) } diff --git a/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/IBook.kt b/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/IBook.kt index 6d737bd48..7cdc414ad 100644 --- a/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/IBook.kt +++ b/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/IBook.kt @@ -4,6 +4,7 @@ package naksha.jbon import naksha.base.Int64 import kotlin.js.JsExport +import kotlin.js.JsName /** * An interface to be implemented by all books. Books can contain a combination of the following types, and only of these: @@ -54,9 +55,22 @@ interface IBook { * Returns the element at the given index. If no such index exists, returns _null_. * @param index the index to query. * @return the value being one of: `null`, `Boolean`, `Int`, `Int64`, `Double`, `String`, `Map`, or `List`. - * @since 3.0.0 + * @since 3.0 + */ + @JsName("getByIndex") + operator fun get(index: Int): Any? + + /** + * Returns the value associated with the given name by looking up the index via [indexOfName] and then reading the value via [get]. Returns _null_ when the name is not found or the index maps to a _null_ slot. + * @param name the member name to look up. + * @return the value, or _null_ if the name is not present. + * @since 3.0 */ - fun get(index: Int): Any? + @JsName("getByName") + operator fun get(name: String): Any? { + val i = indexOfName(name) + return if (i < 0) null else get(i) + } /** * Returns the index of the given string or -1, if the string is not part of the dictionary. @@ -107,19 +121,6 @@ interface IBook { */ fun namesLength(): Int = 0 - /** - * Returns the value associated with the given name by looking up the index via [indexOfName] - * and then reading the value via [get]. Returns _null_ when the name is not found or the - * index maps to a _null_ slot. - * @param name the member name to look up. - * @return the value, or _null_ if the name is not present. - * @since 3.0.0 - */ - fun getByName(name: String): Any? { - val i = indexOfName(name) - return if (i < 0) null else get(i) - } - /** * Find all entries in the dictionary that have the given hash. * @param hash the hash to find. diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/AbstractStorage.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/AbstractStorage.kt index caa62ebf1..d5e53d1ca 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/AbstractStorage.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/AbstractStorage.kt @@ -79,7 +79,7 @@ abstract class AbstractStorage : IStorage { if (configRef.get() == null || create==true || upgrade==true) { val _config = storage.proxy(configKlass) this._id = storage.id - this._number = storage.number + this._number = Naksha.featureNumber(storage.id) this.hardCap = storage.hardCap initStorage(_config, create, upgrade) this.configRef.set(_config) diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/FetchMode.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/FetchMode.kt index f0e82e8c5..d6f4b6890 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/FetchMode.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/FetchMode.kt @@ -38,19 +38,19 @@ inline fun FetchMode.noMeta(): Int = this and META_CLEAR inline fun FetchMode.fetchMeta(): Boolean = (this and META_BIT) == META_BIT /** - * Set the _feature_ bit (covers [feature][Tuple.jbonBytes] and tags). + * Set the _feature_ bit (covers [feature][Tuple.featureBytes] and tags). * @since 3.0.0 */ inline fun FetchMode.withFeature(): Int = this or FEATURE_BIT /** - * Clear the _feature_ bit (covers [feature][Tuple.jbonBytes] and tags). + * Clear the _feature_ bit (covers [feature][Tuple.featureBytes] and tags). * @since 3.0.0 */ inline fun FetchMode.noFeature(): Int = this and FEATURE_CLEAR /** - * Test if the _feature_ bit is set (which covers [feature][Tuple.jbonBytes] and tags). + * Test if the _feature_ bit is set (which covers [feature][Tuple.featureBytes] and tags). * @since 3.0.0 */ inline fun FetchMode.fetchFeature(): Boolean = (this and FEATURE_BIT) == FEATURE_BIT diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/ISession.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/ISession.kt index 50ec35e5f..6ac37f9f6 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/ISession.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/ISession.kt @@ -3,7 +3,7 @@ package naksha.model import naksha.model.objects.NakshaCollection -import naksha.model.objects.NakshaMap +import naksha.model.objects.NakshaCatalog import naksha.model.request.* import kotlin.js.JsExport import kotlin.js.JsName @@ -116,7 +116,7 @@ interface ISession : AutoCloseable { * @return the map; _null_ if no such map exists. * @since 3.0 */ - fun getMapById(mapId: String): NakshaMap? + fun getMapById(mapId: String): NakshaCatalog? /** * Returns the map for the given number. @@ -126,7 +126,7 @@ interface ISession : AutoCloseable { * @return the map; _null_ if no such map exists. * @since 3.0 */ - fun getMapByNumber(mapNumber: Int): NakshaMap? + fun getMapByNumber(mapNumber: Int): NakshaCatalog? /** * Returns the collection for the given identifier. @@ -137,7 +137,7 @@ interface ISession : AutoCloseable { * @return the collection; _null_ if no such collection exists. * @since 3.0 */ - fun getCollectionById(map: NakshaMap, collectionId: String): NakshaCollection? + fun getCollectionById(map: NakshaCatalog, collectionId: String): NakshaCollection? /** * Returns the collection for the given number. @@ -148,7 +148,7 @@ interface ISession : AutoCloseable { * @return the collection; _null_ if no such collection exists. * @since 3.0 */ - fun getCollectionByNumber(map: NakshaMap, collectionNumber: Int): NakshaCollection? + fun getCollectionByNumber(map: NakshaCatalog, collectionNumber: Int): NakshaCollection? /** * Load all tuples into the given [feature-tuples][FeatureTuple]. diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/IStorage.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/IStorage.kt index c2e00e3e9..1f1771271 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/IStorage.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/IStorage.kt @@ -141,7 +141,7 @@ interface IStorage : IDictReader { * * - Throws [NakshaError.UNINITIALIZED], if the storage failed to initialize. * @param feature the feature to encode; _null_ if no specific one is available. - * @param context the context in which the encoding happens (for example the [map][naksha.model.objects.NakshaMap] or [collection][naksha.model.objects.NakshaCollection]); _null_ if none is available. + * @param context the context in which the encoding happens (for example the [map][naksha.model.objects.NakshaCatalog] or [collection][naksha.model.objects.NakshaCollection]); _null_ if none is available. * @return best [DataEncoding] to use. * @since 3.0 */ diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/LibModel.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/LibModel.kt index 970c901ed..e909aeddb 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/LibModel.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/LibModel.kt @@ -101,7 +101,7 @@ const val GEOMETRY_BIT: FetchMode = 2 const val GEOMETRY_CLEAR: FetchMode = GEOMETRY_BIT.inv() /** - * The _feature_ bit, covers [feature][Tuple.jbonBytes] and tags. + * The _feature_ bit, covers [feature][Tuple.featureBytes] and tags. * @since 3.0.0 */ const val FEATURE_BIT: FetchMode = 4 diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt index 12b9fd8b1..26a3c28b1 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt @@ -12,8 +12,6 @@ import naksha.geo.SpGeometry import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_ARGUMENT import naksha.model.NakshaError.NakshaErrorCompanion.STORAGE_NOT_FOUND import naksha.model.NakshaVersion.Companion.CURRENT -import naksha.model.objects.NakshaCollection -import naksha.model.objects.NakshaMap import naksha.model.objects.NakshaStorage import kotlin.js.JsExport import kotlin.js.JsName @@ -78,7 +76,7 @@ class Naksha private constructor() { /** * The identifier of the collection in which catalogs (maps) are stored, located only within the [admin-map][ADMIN_CATALOG_ID] _(`naksha~catalogs`)_. - * @see [naksha.model.objects.NakshaMap] + * @see [naksha.model.objects.NakshaCatalog] * @since 3.0 */ const val CATALOGS_COL_ID = "naksha~catalogs" @@ -588,7 +586,7 @@ class Naksha private constructor() { @JvmStatic @JsStatic fun getStorage(storage: NakshaStorage): IStorage? { - val s = storagesByNumber[storage.number] ?: return null + val s = storagesByNumber[storage.databaseNumber] ?: return null val s2 = storagesById[storage.id] ?: return null return if (s!==s2 || s.config != storage) null else s } @@ -655,16 +653,16 @@ class Naksha private constructor() { fun useStorage(config: NakshaStorage): IStorage = _useStorage(config, null) private fun _useStorage(config: NakshaStorage, forceCreateOrUpgrade: Boolean?): IStorage { - var s = storagesByNumber[config.number] + var s = storagesByNumber[config.databaseNumber] var s2 = storagesById[config.id] if (s !== s2) { lock.acquire().use { - s = storagesByNumber[config.number] + s = storagesByNumber[config.databaseNumber] s2 = storagesById[config.id] if (s !== s2) { throw NakshaException( ILLEGAL_ARGUMENT, - "The storage-id (${config.id}) and -number (${config.number}) belong to different storages") + "The storage-id (${config.id}) and -number (${config.databaseNumber}) belong to different storages") } } } @@ -675,12 +673,12 @@ class Naksha private constructor() { return localS } lock.acquire().use { - var storage = storagesByNumber[config.number] + var storage = storagesByNumber[config.databaseNumber] val storage2 = storagesById[config.id] if (storage !== storage2) { throw NakshaException( ILLEGAL_ARGUMENT, - "The storage-id (${config.id}) and -number (${config.number}) belong to different storages") + "The storage-id (${config.id}) and -number (${config.databaseNumber}) belong to different storages") } if (storage != null) { if (storage.config.configEquals(config)) { @@ -692,7 +690,7 @@ class Naksha private constructor() { storage = Platform.newInstanceOf(klass) storage.invokeInitStorage(config, create = forceCreateOrUpgrade, upgrade = forceCreateOrUpgrade) storagesById[config.id] = storage - storagesByNumber[config.number] = storage + storagesByNumber[config.databaseNumber] = storage return storage } } @@ -732,19 +730,19 @@ class Naksha private constructor() { @JvmStatic @JsStatic fun removeStorage(config: NakshaStorage): IStorage? { - val s = storagesByNumber[config.number] + val s = storagesByNumber[config.databaseNumber] if (s == null || s.config != config) return null lock.acquire().use { - val storage = storagesByNumber[config.number] + val storage = storagesByNumber[config.databaseNumber] if (storage == null || storage.config != config) return null val storage2 = storagesById[config.id] if (storage !== storage2) { throw NakshaException( ILLEGAL_ARGUMENT, - "The storage-id (${config.id}) and -number (${config.number}) belong to different storages") + "The storage-id (${config.id}) and -number (${config.databaseNumber}) belong to different storages") } storagesById.remove(config.id) - storagesByNumber.remove(config.number) + storagesByNumber.remove(config.databaseNumber) storage.invokeShutdownStorage(true) return storage } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/NakshaError.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/NakshaError.kt index a06ecc2ba..0e8f06633 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/NakshaError.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/NakshaError.kt @@ -40,6 +40,14 @@ open class NakshaError() : AnyObject() { */ const val EXCEPTION = "Exception" + /** + * Thrown when a state is found that must not be found, for example some other part of the code should have prevented this state at this point. + * + * This results in a 500 Internal Server Error. + * @since 3.0.0 + */ + const val INTERNAL_ERROR = "InternalError" + /** * Returned when an already initialized storage is initialized, providing a wrong _storage-id_ and/or _storage-number_. * @since 3.0.0 diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Tuple.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Tuple.kt index f05c8d27d..970683b99 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Tuple.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Tuple.kt @@ -2,15 +2,21 @@ package naksha.model +import naksha.base.AnyList +import naksha.base.AnyObject import naksha.base.Int64 import naksha.base.ListProxy import naksha.base.MapProxy import naksha.base.Platform.PlatformCompanion.gzipDeflate import naksha.base.Platform.PlatformCompanion.gzipInflate +import naksha.base.PlatformList +import naksha.base.PlatformMap +import naksha.base.PlatformMapApi.PlatformMapApiCompanion.map_get import naksha.base.WeakRef import naksha.jbon.IBook import naksha.model.objects.Member import naksha.geo.SpGeometry +import naksha.geo.SpType import naksha.jbon.BookType import naksha.jbon.HeapBook import naksha.jbon.JB2_MAGIC @@ -38,7 +44,7 @@ data class Tuple @JvmOverloads constructor( * Feature serialized with the encoding described by the collection's dataEncoding. * @since 3.0 */ - @JvmField val jbonBytes: ByteArray, + @JvmField val featureBytes: ByteArray, /** * The members book provided by storage at read time. Contains dedicated member values, such as `id`, `tn`, etc. @@ -82,13 +88,13 @@ data class Tuple @JvmOverloads constructor( // Update the tuple-number. val tnMember = collection.useMember(StandardMembers.Tn) - val colTn = tnMember.readTupleNumber(collection) ?: + val colTn = tnMember.getTupleNumber(collection) ?: throw NakshaException(ILLEGAL_ARGUMENT, "The given collection does not have a valid tuple-number (uuid)") if (colTn.featureNumber > Int.MAX_VALUE || colTn.featureNumber < Int.MIN_VALUE) { throw NakshaException(ILLEGAL_ARGUMENT, "The given collection does have an invalid feature-number (uuid)") } // Read the current tuple-number of the feature; if any. - val prevTn: TupleNumber? = collection.useMember(StandardMembers.Tn).readTupleNumber(feature) + val prevTn: TupleNumber? = collection.useMember(StandardMembers.Tn).getTupleNumber(feature) val newTn: TupleNumber if (prevTn != null) { if (action != Action.VERSION && action == Action.CREATE) { @@ -106,14 +112,14 @@ data class Tuple @JvmOverloads constructor( colTn.catalogNumber, // The feature-number of the collection is the collection-number of the feature we want to store in the collection. colTn.featureNumber.toInt(), - // The feature-number of the actual feature. Will either be set explicit or calcualted. - feature.featureNumber, + // The feature-number of the feature, either the `id` is a feature-number or it is calculated form hashing the `id`. + Naksha.featureNumber(feature.id), // The version is the one of the transaction. session.useTransaction().version.number ) } // Update the feature with its new tuple-number. - tnMember.write(feature, newTn) + tnMember.set(feature, newTn) val globalBookTn: TupleNumber? if (globalBook != null) { if (newTn.databaseNumber != globalBook.databaseNumber || globalBook.featureNumber == null) { @@ -214,14 +220,14 @@ data class Tuple @JvmOverloads constructor( * @since 3.0 */ @JvmField - val tupleNumber: TupleNumber = membersBook.getByName(StandardMembers.Tn.name) as TupleNumber + val tupleNumber: TupleNumber = membersBook[StandardMembers.Tn.name] as TupleNumber /** * The next-version at which this tuple was superseded. `NULL`-sentinel indicates the tuple is the current _([Version.HEAD])_ state. * @since 3.0 */ var nextVersion: Int64 - get() = membersBook.getByName(StandardMembers.NextVersion.name) as Int64 + get() = membersBook[StandardMembers.NextVersion.name] as Int64 set(version: Int64) { val members = this.membersBook if (members is HeapBook) { @@ -279,7 +285,7 @@ data class Tuple @JvmOverloads constructor( get() { var id: String? = _id if (id != null) return id - id = membersBook.getByName(StandardMembers.Id.name) as String? + id = membersBook[StandardMembers.Id.name] as String? if (id != null) { _id = id return id @@ -314,7 +320,7 @@ data class Tuple @JvmOverloads constructor( * @return the value from the [membersBook] book or `null`, if the member is missing, the value is `null`, or not the requested type. * @since 3.0 */ - fun getString(member: Member): String? = membersBook.getByName(member.name) as? String + fun getString(member: Member): String? = membersBook[member.name] as? String /** * Get a long member. @@ -325,7 +331,7 @@ data class Tuple @JvmOverloads constructor( */ @JvmOverloads fun getLong(member: Member, alt: Int64 = Int64(0L)): Int64 = - membersBook.getByName(member.name)?.let { v -> + membersBook[member.name]?.let { v -> when (v) { is Int64 -> v is Long -> Int64(v) @@ -343,7 +349,7 @@ data class Tuple @JvmOverloads constructor( */ @JvmOverloads fun getInt(member: Member, alt: Int = 0): Int = - membersBook.getByName(member.name)?.let { v -> + membersBook[member.name]?.let { v -> when (v) { is Int -> v is Number -> v.toInt() @@ -360,7 +366,7 @@ data class Tuple @JvmOverloads constructor( */ @JvmOverloads fun getDouble(member: Member, alt: Double = Double.NaN): Double = - membersBook.getByName(member.name)?.let { v -> + membersBook[member.name]?.let { v -> when (v) { is Double -> v is Number -> v.toDouble() @@ -376,7 +382,7 @@ data class Tuple @JvmOverloads constructor( * @since 3.0 */ @JvmOverloads - fun getBoolean(member: Member, alt: Boolean = false): Boolean = membersBook.getByName(member.name) as? Boolean ?: alt + fun getBoolean(member: Member, alt: Boolean = false): Boolean = membersBook[member.name] as? Boolean ?: alt /** * Get the raw value of the member. @@ -384,7 +390,37 @@ data class Tuple @JvmOverloads constructor( * @return the value from the [membersBook] book or `null`, if the member is missing, the value is `null`, or not the requested type. * @since 3.0 */ - fun getMember(member: Member): Any? = membersBook.getByName(member.name) + fun getMember(member: Member): Any? { + return when (val raw = membersBook[member.name]) { + is String -> raw + is Int64 -> raw + is Byte -> Int64(raw.toInt()) + is Short -> Int64(raw.toInt()) + is Int -> Int64(raw) + is Long -> Int64(raw) + is Float -> raw.toDouble() + is Double -> raw + is ByteArray -> raw + is SpGeometry -> raw + is TagMap -> raw + is TagList -> raw + is ListProxy<*> -> raw + is PlatformList -> raw.proxy(AnyList::class) + is MapProxy<*, *> -> { + // Detect geometry. + // If noSpGeometry, we can't differ between a TagMap and a simple Object in raw JSON, so treat is as Object. + val type = SpType.ofDefined(raw["type"] as String?) + if (type != null) raw.proxy(type.klass) else raw + } + is PlatformMap -> { + // Detect geometry. + // If noSpGeometry, we can't differ between a TagMap and a simple Object in raw JSON, so treat is as Object. + val type = SpType.ofDefined(map_get(raw, "type") as String?) + if (type != null) raw.proxy(type.klass) else raw.proxy(AnyObject::class) + } + else -> null + } + } /** * Get the byte-array value of the member. @@ -392,7 +428,7 @@ data class Tuple @JvmOverloads constructor( * @return the value from the [membersBook] book or `null`, if the member is missing, the value is `null`, or not the requested type. * @since 3.0 */ - fun getByteArray(member: Member): ByteArray? = membersBook.getByName(member.name) as ByteArray? + fun getByteArray(member: Member): ByteArray? = membersBook[member.name] as ByteArray? /** * Get the [SpGeometry] value of the member. @@ -401,10 +437,10 @@ data class Tuple @JvmOverloads constructor( * @since 3.0 */ fun getSpatial(member: Member): SpGeometry? { - val raw = membersBook.getByName(member.name) + val raw = membersBook[member.name] if (raw is SpGeometry) return raw if (raw is ByteArray) return try { Naksha.decodeGeometry(raw) } catch (_: Exception) { null } - if (raw is MapProxy<*,*>) return raw.proxy(SpGeometry::class) + if (raw is PlatformMap) return raw.proxy(SpGeometry::class) return null } @@ -415,9 +451,9 @@ data class Tuple @JvmOverloads constructor( * @since 3.0 */ fun getTags(member: Member): TagMap? { - val raw = membersBook.getByName(member.name) + val raw = membersBook[member.name] if (raw is TagMap) return raw - if (raw is MapProxy<*, *>) return raw.proxy(TagMap::class) + if (raw is PlatformMap) return raw.proxy(TagMap::class) return null } @@ -428,9 +464,9 @@ data class Tuple @JvmOverloads constructor( * @since 3.0 */ fun getTagList(member: Member): TagList? { - val raw = membersBook.getByName(member.name) + val raw = membersBook[member.name] if (raw is TagList) return raw - if (raw is ListProxy<*>) return raw.proxy(TagList::class) + if (raw is PlatformList) return raw.proxy(TagList::class) return null } @@ -440,9 +476,10 @@ data class Tuple @JvmOverloads constructor( * @return the value from the [membersBook] book or `null`, if the member is missing, the value is `null`, or not the requested type. * @since 3.0 */ - fun getSet(member: Member): List<*>? { - val raw = membersBook.getByName(member.name) - if (raw is List<*>) return raw + fun getSet(member: Member): ListProxy<*>? { + val raw = membersBook[member.name] + if (raw is ListProxy<*>) return raw + if (raw is PlatformList) return raw.proxy(AnyList::class) return null } @@ -454,7 +491,7 @@ data class Tuple @JvmOverloads constructor( * @throws NakshaException if any error occurs. */ fun decodeFeature(globalBook: IBook?): NakshaFeature { // TODO: Java: After switching back to Java, we can allow arbitrary return types. - val rawBytes = if (isGzipped(jbonBytes)) gzipInflate(jbonBytes) else jbonBytes + val rawBytes = if (isGzipped(featureBytes)) gzipInflate(featureBytes) else featureBytes val decoder = JbDecoder2(globalBook, membersBook) decoder.mapBytes(rawBytes) return decoder.toAnyObject().proxy(NakshaFeature::class) diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt index 9ff2b7c0e..9613a601b 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt @@ -7,6 +7,7 @@ import naksha.base.Int64 import naksha.base.MapProxy import naksha.base.NotNullEnum import naksha.base.NotNullProperty +import naksha.base.NullableProperty import naksha.base.Proxy import naksha.geo.SpGeometry import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_STATE @@ -92,6 +93,14 @@ class Member() : AnyObject(), Comparator { */ var dataType: MemberType by DATA_TYPE + /** + * The index of this member in the storage; the index is set by the storage ones a collection is created. + * + * This value is maintained by the storage; it **MUST NOT** be modified by clients. Storages may manage this value or ignore it, this is implementation dependent. Changes done by the client are ignored by the storage. + * @since 3.0 + */ + val index: Int? by INDEX + /** True iff the underlying map has an entry for [dataType]. */ fun hasDataType(): Boolean = hasRaw("dataType") @@ -193,7 +202,7 @@ class Member() : AnyObject(), Comparator { * @param feature The feature to read from. * @return the read value or `null`, if the feature does not store a valid value at the member path. */ - fun readTupleNumber(feature: MapProxy<*,*>): TupleNumber? { + fun getTupleNumber(feature: MapProxy<*,*>): TupleNumber? { val raw = feature.getPath(path) if (raw is TupleNumber) return raw if (raw is String) return TupleNumber.fromString(raw) @@ -205,7 +214,7 @@ class Member() : AnyObject(), Comparator { * @param feature The feature to read from. * @return the read value or `null`, if the feature does not store a valid value at the member path. */ - fun readBoolean(feature: MapProxy<*,*>): Boolean? { + fun getBoolean(feature: MapProxy<*,*>): Boolean? { val raw = feature.getPath(path) if (raw is Boolean) return raw return null @@ -227,7 +236,7 @@ class Member() : AnyObject(), Comparator { * @param feature The feature to read from. * @return the read value or `null`, if the feature does not store a valid value at the member path. */ - fun readLong(feature: MapProxy<*,*>): Int64? { + fun getInt64(feature: MapProxy<*,*>): Int64? { val raw = feature.getPath(path) if (raw is Int64) return raw if (raw is Long) return Int64(raw) @@ -240,7 +249,7 @@ class Member() : AnyObject(), Comparator { * @param feature The feature to read from. * @return the read value or `null`, if the feature does not store a valid value at the member path. */ - fun readDouble(feature: MapProxy<*,*>): Double? { + fun getDouble(feature: MapProxy<*,*>): Double? { val raw = feature.getPath(path) if (raw is Double) return raw if (raw is Number) return raw.toDouble() @@ -252,7 +261,7 @@ class Member() : AnyObject(), Comparator { * @param feature The feature to read from. * @return the read value or `null`, if the feature does not store a valid value at the member path. */ - fun readGeometry(feature: MapProxy<*,*>): SpGeometry? { + fun getGeometry(feature: MapProxy<*,*>): SpGeometry? { val raw = feature.getPath(path) if (raw is SpGeometry) return raw return null @@ -263,7 +272,7 @@ class Member() : AnyObject(), Comparator { * @param feature The feature to read from. * @return the read value or `null`, if the feature does not store a valid value at the member path. */ - fun readByteArray(feature: MapProxy<*,*>): ByteArray? { + fun getByteArray(feature: MapProxy<*,*>): ByteArray? { val raw = feature.getPath(path) if (raw is ByteArray) return raw return null @@ -275,13 +284,14 @@ class Member() : AnyObject(), Comparator { * @return the previous value. * @throws RuntimeException If the given feature has a broken path, so the path requires an array, but an object exists already. */ - fun write(feature: MapProxy<*,*>, value: Any?): Any? = feature.setPath(value, path) + fun set(feature: MapProxy<*,*>, value: Any?): Any? = feature.setPath(value, path) override fun compare(a: Member, b: Member): Int = a.dataType.sortOrder - b.dataType.sortOrder companion object Member_C { private val NAME = NotNullProperty(String::class) { _, _ -> "" } private val DATA_TYPE = NotNullEnum(MemberType::class) { _, _ -> MemberType.STRING } + private val INDEX = NullableProperty(Int::class) private val PATH = NotNullProperty(JsonPath::class) { self, _ -> JsonPath(listOf("properties", self.name)) } private val INTERNAL = NotNullProperty(Boolean::class) { _, _ -> false } } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberList.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberList.kt index cad247b5d..9191e7b7a 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberList.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberList.kt @@ -3,7 +3,7 @@ package naksha.model.objects import naksha.base.ListProxy -import naksha.model.NakshaError +import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_STATE import naksha.model.NakshaException import kotlin.js.JsExport import kotlin.js.JsName @@ -46,6 +46,61 @@ open class MemberList() : ListProxy(Member::class) { MemberList().apply { addAll(members.toList()) } } + /** + * Sort this list by the sort-order of the [MemberType]. + * @return this. + * @since 3.0 + * @throws NakshaException with error [ILLEGAL_STATE], if any member is `null` or has no `dataType`. + */ + fun sortByDataType(): MemberList { + sortBy { member -> member?.dataType?.sortOrder ?: throw NakshaException(ILLEGAL_STATE, "Member is null or has no dataType") } + return this + } + + /** + * Sort this list by the index of the [MemberType]. + * @return this. + * @since 3.0 + * @throws NakshaException with error [ILLEGAL_STATE], if any member is `null` or has no valid `index`. + */ + fun sortByIndex(): MemberList { + sortBy { member -> member?.index ?: throw NakshaException(ILLEGAL_STATE, "Member is null or has no index") } + return this + } + + /** + * Tests if this list is [sorted by data-type][sortByDataType]. + * @return _true_ if the entries are sorted; _false_ otherwise. + */ + fun isSortedByDataType(): Boolean { + val END = this.size + var max = 0 + for (i in 0 until END) { + val member = this[i] ?: return false + val sortOrder = member.dataType.sortOrder + // The elements towards the end of the list must have the biggest sort-order value, we always sort ascending. + // This results in members in byte-alignment order, so INT8, FLOAT8, INT4, FLOAT4, ... + // This prevents padding in the database. + if (max > sortOrder) return false + max = sortOrder + } + return true + } + + /** + * Tests if this list is [sorted by data-type][sortByDataType]. + * @return _true_ if the entries are sorted; _false_ otherwise. + */ + fun isSortedByIndex(): Boolean { + val END = this.size + for (i in 0 until END) { + val member = this[i] ?: return false + val index = member.index ?: return false + if (index != i) return false + } + return true + } + /** * Get the member with the given name from this list. * @param name The name of the member. @@ -63,12 +118,12 @@ open class MemberList() : ListProxy(Member::class) { */ fun validate() { for (i in 0 until this.size) { - val member = this[i] ?: throw NakshaException(NakshaError.ILLEGAL_STATE, "Member at index $i is null") + val member = this[i] ?: throw NakshaException(ILLEGAL_STATE, "Member at index $i is null") val memberName = member.name for (j in (i + 1) until this.size) { - val later = this[j] ?: throw NakshaException(NakshaError.ILLEGAL_STATE, "Member at index $j is null") + val later = this[j] ?: throw NakshaException(ILLEGAL_STATE, "Member at index $j is null") if (memberName == later.name) { - throw NakshaException(NakshaError.ILLEGAL_STATE, "Member at index $i has same name as member at $j: $memberName") + throw NakshaException(ILLEGAL_STATE, "Member at index $i has same name as member at $j: $memberName") } } } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberType.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberType.kt index b4914c5cb..47a380cd3 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberType.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberType.kt @@ -194,6 +194,10 @@ class MemberType : JsEnum() { } } + /** + * The sort order of the type to ensure no padding is needed, so `INT8`, `FLOAT8`, `INT4`, `FLOAT4`, ... + * @since 3.0 + */ var sortOrder: Int = -1 private set(value) { val it: Iterator = iterate(MemberType::class) diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCatalog.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCatalog.kt new file mode 100644 index 000000000..c3e8a4f32 --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCatalog.kt @@ -0,0 +1,83 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model.objects + +import naksha.base.Int64 +import naksha.base.NullableProperty +import naksha.geo.SpBoundingBox +import naksha.geo.SpGeometry +import naksha.geo.SpPoint +import kotlin.js.JsExport +import kotlin.js.JsName + +/** + * A map within a storage; maps are used to group collections. + * @since 3.0 + */ +@JsExport +open class NakshaCatalog() : NakshaFeature() { + + /** + * Create a new map feature with the given identifier. + * @param id the identifier to set. + * @since 3.0 + */ + @Suppress("LeakingThis") + @JsName("of") + constructor(id: String): this() { + this.id = id + this.type = typeDefaultValue() + this.featureType = featureTypeDefaultValue() + } + + companion object NakshaMap_C { + /** + * The feature-type of this feature itself _(`naksha.Catalog`)_. + * @since 3.0 + */ + const val FEATURE_TYPE = "naksha.Catalog" + + private val DATABASE_NUMBER = NullableProperty(Int64::class) + private val DATABASE_ID = NullableProperty(String::class) + private val CATALOG_NUMBER = NullableProperty(Int::class) + } + + override fun featureTypeDefaultValue(): String = FEATURE_TYPE + override fun withId(value: String): NakshaCatalog = super.withId(value) as NakshaCatalog + override fun withType(value: String): NakshaCatalog = super.withType(value) as NakshaCatalog + override fun withFeatureType(value: String): NakshaCatalog = super.withFeatureType(value) as NakshaCatalog + override fun withBbox(value: SpBoundingBox?): NakshaCatalog = super.withBbox(value) as NakshaCatalog + override fun withGeometry(value: SpGeometry?): NakshaCatalog = super.withGeometry(value) as NakshaCatalog + override fun withReferencePoint(value: SpPoint?): NakshaCatalog = super.withReferencePoint(value) as NakshaCatalog + override fun withProperties(value: NakshaProperties): NakshaCatalog = super.withProperties(value) as NakshaCatalog + override fun withMomType(value: String?): NakshaCatalog = super.withMomType(value) as NakshaCatalog + + /** + * The database-number of the collection; **NOT** the database-number of the collection-feature itself, even while they are guaranteed to be the same. + * @since 3.0 + */ + var databaseNumber: Int64? by DATABASE_NUMBER + // TODO: Fix this, we need to calculate the database-number from the database-id, if an id is given! + + /** + * The database-id of the collection; **NOT** the database-id of the collection-feature itself, even while they are guaranteed to be the same. + * @since 3.0 + */ + var databaseId: String? by DATABASE_ID + + /** + * @see [databaseId] + */ + fun withDatabaseId(value: String?): NakshaCatalog { + databaseId = value + return this + } + + /** + * The catalog-number of the collection; **NOT** the catalog-number of the collection-feature itself, which would always be `0` _(`naksha~admin`)_. + * @since 3.0 + */ + var catalogNumber: Int? by CATALOG_NUMBER + // TODO: Fix this, we need to calculate the catalog-number from the catalog-id (aka `id`). + // Actually the feature-number of the catalog and catalog-number must be the same! +} \ No newline at end of file diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt index 7a0c59533..3aa360517 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt @@ -9,6 +9,8 @@ import naksha.geo.SpPoint import naksha.model.DataEncoding import naksha.model.Naksha import naksha.model.NakshaError +import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_STATE +import naksha.model.NakshaError.NakshaErrorCompanion.NOT_FOUND import naksha.model.NakshaException import kotlin.js.JsExport import kotlin.js.JsName @@ -22,7 +24,7 @@ import kotlin.jvm.JvmStatic */ @JsExport open class NakshaCollection() : NakshaFeature() { -\ + /** * Create a Naksha collection with settings. * @param id the collection-identifier. @@ -45,7 +47,7 @@ open class NakshaCollection() : NakshaFeature() { storeMeta: StoreMode = StoreMode.ON, ) : this() { this.id = id - this.mapId = mapId + this.catalogId = mapId this.storageClass = storageClass this.partitions = partitions this.storeDeleted = storeDeleted @@ -55,7 +57,6 @@ open class NakshaCollection() : NakshaFeature() { override fun featureTypeDefaultValue(): String = FEATURE_TYPE override fun withId(value: String): NakshaCollection = super.withId(value) as NakshaCollection - override fun withFeatureNumber(value: Int64): NakshaCollection = super.withFeatureNumber(value) as NakshaCollection override fun withType(value: String): NakshaCollection = super.withType(value) as NakshaCollection override fun withFeatureType(value: String): NakshaCollection = super.withFeatureType(value) as NakshaCollection override fun withBbox(value: SpBoundingBox?): NakshaCollection = super.withBbox(value) as NakshaCollection @@ -64,38 +65,48 @@ open class NakshaCollection() : NakshaFeature() { override fun withProperties(value: NakshaProperties): NakshaCollection = super.withProperties(value) as NakshaCollection override fun withMomType(value: String?): NakshaCollection = super.withMomType(value) as NakshaCollection - override fun featureNumberOfId(id: String): Int64 = Naksha.collectionNumber(id).toInt64() + /** + * The database-number of the collection; **NOT** the database-number of the collection-feature itself, even while they are guaranteed to be the same. + * @since 3.0 + */ + var databaseNumber: Int64? by DATABASE_NUMBER + // TODO: Fix this, we need to calculate the database-number from the database-id, if an id is given! /** - * The number of the collection, which is basically [featureNumber]. + * The database-id of the collection; **NOT** the database-id of the collection-feature itself, even while they are guaranteed to be the same. * @since 3.0 */ - val number: Int - get() = featureNumber.toInt() + var databaseId: String? by DATABASE_ID /** - * Always return `0`, because all collections are always stored in `naksha~collections` collection. + * The catalog-number of the collection; **NOT** the catalog-number of the collection-feature itself, which would always be `0` _(`naksha~admin`)_. * @since 3.0 - * @see [Naksha.COLLECTIONS_COL_ID] - * @see [Naksha.COLLECTIONS_COL_FN] */ - override val collectionNumber: Int - get() = Naksha.COLLECTIONS_COL_FN + var catalogNumber: Int? by CATALOG_NUMBER + // TODO: Fix this, we need to calculate the catalog-number from the catalog-id, if an id is given! /** - * The map-id of the map in which the collection is located; `null` if not yet known. + * The custom identifier of the catalog in which the collection is located; ; **NOT** the catalog-id of the collection-feature itself, which would always be `naksha~admin`. * @since 3.0 */ - var mapId by MAP_ID + var catalogId: String? by CATALOG_ID /** - * @see [mapId] + * @see [catalogId] */ - fun withMapId(value: String?): NakshaCollection { - mapId = value + fun withCatalogId(value: String?): NakshaCollection { + catalogId = value return this } + /** + * The collection-number of the collection; **NOT** the collection-number of the collection-feature itself, which would always be `0` _(`naksha~collections`)_. This should be the same as the feature-number! + * @since 3.0 + */ + var collectionNumber: Int? by COLLECTION_NUMBER + // TODO: Fix this, we need to calculate the collection-number from the collection-id (aka `id`). + // Actually the feature-number of the collection and collection-number must be the same! + /** * If partitions is given, then the collection is internally partitioned in the storage, optimised for large quantities of features. The default is no partitions; as a rule of thumb, add one more partition for every 10 to 20 million features expected. * @@ -186,55 +197,6 @@ open class NakshaCollection() : NakshaFeature() { return this } - /** - * If the `featureType`as returned by [NakshaFeature.featureType] equals to this value, then the [metadata feature-type][naksha.model.Metadata.ft] will be `null`, otherwise [metadata feature-type][naksha.model.Metadata.ft] is set to the [NakshaFeature.featureType]. - * - * ### Note - * The index on the [feature-type][naksha.model.Metadata.ft] is partial, features are only indexed when `ft` is not `null`, what is always the case, when it matches the [defaultFeatureType]. This is based upon the assumption, that in most cases all features within a collection do have the same feature-type. If this assumption holds true, and index would be a big waste, even when only a few features differ from the [defaultFeatureType], adding all values into the index would be a waste. So, this property is for the query planner to take advantage of this fact, when searching for feature-type. If the feature-type is the [defaultFeatureType], this means most of the time a full collection scan, so usage of the feature-type index is not helpful. - * @since 3.0 - */ - var defaultFeatureType by DEFAULT_FEATURE_TYPE - - /** - * @see [defaultFeatureType] - */ - open fun withDefaultFeatureType(value: String?): NakshaCollection { - removeRaw("defaultFeatureType") - return this - } - - /** - * The feature encoding to use for new rows in this collection. - * - * - If _null_, the [dataEncoding][NakshaMap.dataEncoding] of the containing [map][NakshaMap] is used; if that is also _null_, [Naksha.DEFAULT_DATA_ENCODING] is used. - * @since 3.0 - */ - var dataEncoding by DATA_ENCODING - - /** - * @see [dataEncoding] - */ - open fun withDataEncoding(value: DataEncoding): NakshaCollection { - this.dataEncoding = value - return this - } - - /** - * The identifier of the global dictionary to use, when encoding new rows. - * - * - If _null_, the storage will use whatever is best for the storage. - * @since 3.0 - */ - var encodeDict by STRING_NULL - - /** - * @see [encodeDict] - */ - open fun withEncodeDict(value: String?): NakshaCollection { - this.encodeDict = value - return this - } - /** * If [StoreMode.OFF] the storage will not retain historic states of features in this collection, which boosts performance in certain operations. */ @@ -398,6 +360,15 @@ open class NakshaCollection() : NakshaFeature() { @JvmOverloads open fun useMember(member: Member, comparePath: Boolean = false): Member = member.asSame(useMembers().get(member.name), comparePath) + /** + * Search for the given member in this collection. + * @param memberName the name of the member to use. + * @return the member. + * @throws NakshaException with error [ILLEGAL_STATE], if no such member was found. + */ + @JsName("useMemberByName") + open fun useMember(memberName: String): Member = useMembers().get(memberName) ?: throw NakshaException(ILLEGAL_STATE, "Member $memberName does not exist") + /** * Search for the given member in this collection. * @param member The member to search for, compares name and type. @@ -649,14 +620,15 @@ open class NakshaCollection() : NakshaFeature() { @JsStatic val UNKNOWN = Int64(-1) + private val DATABASE_NUMBER = NullableProperty(Int64::class) + private val DATABASE_ID = NullableProperty(String::class) + private val CATALOG_ID = NullableProperty(String::class) + private val CATALOG_NUMBER = NullableProperty(Int::class) + private val COLLECTION_NUMBER = NullableProperty(Int::class) private val PARTITIONS = NotNullProperty(Int::class) { _, _ -> 1 } private val SHIFT = NotNullProperty(Int::class) { _, _ -> 41 } private val STORAGE_CLASS = NullableProperty(String::class) private val PROTECTION_CLASS = NullableProperty(String::class) - private val DATA_ENCODING = NullableEnum(DataEncoding::class) - private val MAP_ID = NullableProperty(String::class) - private val STRING_NULL = NullableProperty(String::class) - private val DEFAULT_FEATURE_TYPE = NotNullProperty(String::class) { _, _ -> TYPE } private val MEMBERS = NullableProperty(MemberList::class) private val INDICES = NullableProperty(IndexList::class) private val MAX_AGE = NotNullProperty(Int64::class) { _, _ -> Int64(-1) } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaDictionary.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaDictionary.kt index 2c771b986..4bedc09c7 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaDictionary.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaDictionary.kt @@ -37,7 +37,6 @@ open class NakshaDictionary() : NakshaFeature() { override fun featureTypeDefaultValue(): String = FEATURE_TYPE override fun withId(value: String): NakshaDictionary = super.withId(value) as NakshaDictionary - override fun withFeatureNumber(value: Int64): NakshaDictionary = super.withFeatureNumber(value) as NakshaDictionary override fun withType(value: String): NakshaDictionary = super.withType(value) as NakshaDictionary override fun withFeatureType(value: String): NakshaDictionary = super.withFeatureType(value) as NakshaDictionary override fun withBbox(value: SpBoundingBox?): NakshaDictionary = super.withBbox(value) as NakshaDictionary diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaFeature.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaFeature.kt index 216c9244a..8b9e258fc 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaFeature.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaFeature.kt @@ -97,101 +97,6 @@ open class NakshaFeature() : AnyObject() { return this } - /** - * Returns the [tuple-number][TupleNumber] of this feature, may be [TupleNumber.HEAD], if the feature is not yet persisted. - * @since 3.0 - */ - val tupleNumber: TupleNumber - get() = properties.xyz.guid?.tupleNumber ?: TupleNumber.HEAD - - private var cachedId: String? = null - private var cachedFeatureNumber: Int64? = null - - /** - * Calculates the feature number from the given identifier. - * @param id the identifier. - * @return the feature-number. - */ - protected open fun featureNumberOfId(id: String): Int64 = Naksha.featureNumber(id) - - /** - * The feature-number of the feature. - * - * If the feature is in [HEAD][TupleNumber.HEAD] state, so not yet persisted, and no custom feature number was set, then the method will calculate the feature-number from the [id]. - * @since 3.0 - */ - var featureNumber: Int64 - get() { - val id = this.id - val internalNumber = Naksha.internalIdToNumber[id] - if (internalNumber != null) return internalNumber.toInt64() - val cachedId = this.cachedId - var cachedFeatureNumber = this.cachedFeatureNumber - - // If the user changed the id (we by intention compare the reference, not the value!). - @Suppress("StringReferentialEquality") - if (id === cachedId && cachedFeatureNumber != null) return cachedFeatureNumber - - // If the feature exists already, and the `id` was not changed, return existing feature number. - val guid = properties.xyz.guid - if (tupleNumber != TupleNumber.HEAD && guid != null && id == guid.id) return guid.tupleNumber.featureNumber - - // Calculate a feature number for new feature. - cachedFeatureNumber = featureNumberOfId(id) - this.cachedId = id - this.cachedFeatureNumber = cachedFeatureNumber - return cachedFeatureNumber - } - set(value) { - if (value < 0) { - throw illegalArg( - "The given feature-number is invalid, must be positive 0 to 9,223,372,036,854,775,807, but was $value" - ) - } - // When the user sets the feature-number, this means the `id` becomes the feature-number too! - val newId = value.toString() - cachedFeatureNumber = featureNumberOfId(newId) - cachedId = newId - id = newId - } - - /** - * Sets a custom feature-number, which in fact changes the [id] of the feature, and must be a 63-bit unsigned integer (`0 .. 9,223,372,036,854,775,807`). - * @see [featureNumber] - */ - open fun withFeatureNumber(value: Int64): NakshaFeature { - this.featureNumber = value - return this - } - - /** - * The global unique identifier of the feature, exists only if the feature is already persisted in a storage. - * @since 3.0 - */ - val guid: Guid? - get() = properties.xyz.guid - - /** - * Returns the collection-number of the collection in which the feature is currently persisted; `null` if the feature is not yet persisted. - * @since 3.0 - */ - open val collectionNumber: Int? - get() = guid?.tupleNumber?.collectionNumber - - /** - * Returns the map-number of the map in which the feature is currently persisted; `null` if the feature is not yet persisted. - * @since 3.0 - */ - open val mapNumber: Int? - get() = guid?.tupleNumber?.catalogNumber - - /** - * Returns the storage-number of the storage in which the feature is currently persisted; `null` if the feature is not yet persisted. - * @since 3.0 - */ - open val storageNumber: Int64? - get() = guid?.tupleNumber?.databaseNumber - /** * The type of the feature, to be [GeoJSON](https://datatracker.ietf.org/doc/html/rfc7946) compatible, one of the following is expected: * - `FeatureCollection` diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaMap.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaMap.kt deleted file mode 100644 index d3bee639f..000000000 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaMap.kt +++ /dev/null @@ -1,104 +0,0 @@ -@file:Suppress("OPT_IN_USAGE") - -package naksha.model.objects - -import naksha.base.* -import naksha.geo.SpBoundingBox -import naksha.geo.SpGeometry -import naksha.geo.SpPoint -import naksha.model.* -import kotlin.js.JsExport -import kotlin.js.JsName - -/** - * A map within a storage; maps are used to group collections. - * @since 3.0 - */ -@JsExport -open class NakshaMap() : NakshaFeature() { - - /** - * Create a new map feature with the given identifier. - * @param id the identifier to set. - * @since 3.0 - */ - @Suppress("LeakingThis") - @JsName("of") - constructor(id: String): this() { - this.id = id - this.type = typeDefaultValue() - this.featureType = featureTypeDefaultValue() - } - - companion object NakshaMap_C { - /** - * The feature-type of this feature itself _(`naksha.Map`)_. - * @since 3.0 - */ - const val FEATURE_TYPE = "naksha.Map" - - private val STORAGE_ID = NullableProperty(String::class) - private val DATA_ENCODING = NullableEnum(DataEncoding::class) - } - - override fun featureTypeDefaultValue(): String = FEATURE_TYPE - override fun withId(value: String): NakshaMap = super.withId(value) as NakshaMap - override fun withFeatureNumber(value: Int64): NakshaMap = super.withFeatureNumber(value) as NakshaMap - override fun withType(value: String): NakshaMap = super.withType(value) as NakshaMap - override fun withFeatureType(value: String): NakshaMap = super.withFeatureType(value) as NakshaMap - override fun withBbox(value: SpBoundingBox?): NakshaMap = super.withBbox(value) as NakshaMap - override fun withGeometry(value: SpGeometry?): NakshaMap = super.withGeometry(value) as NakshaMap - override fun withReferencePoint(value: SpPoint?): NakshaMap = super.withReferencePoint(value) as NakshaMap - override fun withProperties(value: NakshaProperties): NakshaMap = super.withProperties(value) as NakshaMap - override fun withMomType(value: String?): NakshaMap = super.withMomType(value) as NakshaMap - - /** - * The feature encoding to use for new rows of all collections of this map that do not have an own [dataEncoding][NakshaCollection.dataEncoding]. - * - * - If _null_, the storage will use [Naksha.DEFAULT_DATA_ENCODING]. - * @since 3.0 - */ - var dataEncoding by DATA_ENCODING - - override fun featureNumberOfId(id: String): Int64 = Naksha.mapNumber(id).toInt64() - - /** - * The number of the map, which is basically [featureNumber]. - * @since 3.0 - */ - val number: Int - get() = featureNumber.toInt() - - /** - * Always return `2`, because all catalogs (maps) are always stored in `naksha~catalogs` collection. - * @since 3.0 - * @see [Naksha.CATALOGS_COL_ID] - * @see [Naksha.CATALOGS_COL_FN] - */ - override val collectionNumber: Int - get() = Naksha.CATALOGS_COL_FN - - /** - * Always return `0`, because all maps are always stored in `naksha~admin` map. - * @since 3.0 - * @see [Naksha.ADMIN_CATALOG_ID] - * @see [Naksha.ADMIN_CATALOG_FN] - */ - override val mapNumber: Int - get() = Naksha.ADMIN_CATALOG_FN - - /** - * The storage-id of the storage in which the map is located; `null` if not yet known. - * @since 3.0 - */ - var storageId by STORAGE_ID - - /** - * @see [storageId] - */ - fun withStorageId(value: String?): NakshaMap { - storageId = value - return this - } - -} \ No newline at end of file diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaStorage.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaStorage.kt index ae7cbed1c..aaa7d9450 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaStorage.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaStorage.kt @@ -54,6 +54,7 @@ open class NakshaStorage() : NakshaFeature() { private val HARDCAP = NotNullProperty(Int::class) { _, _ -> 0 } private val CREATE = NotNullProperty(Boolean::class) { _, _ -> false } private val UPGRADE = NotNullProperty(Boolean::class) { _, _ -> false } + //private val DB_NUMBER = NotNullProperty(Int64::class) { self, _ -> Naksha.featureNumber(self.id) } /** * Helper class to parse a JSON configuration into a [NakshaStorage]. @@ -78,7 +79,6 @@ open class NakshaStorage() : NakshaFeature() { override fun featureTypeDefaultValue(): String = FEATURE_TYPE override fun withId(value: String): NakshaStorage = super.withId(value) as NakshaStorage - override fun withFeatureNumber(value: Int64): NakshaStorage = super.withFeatureNumber(value) as NakshaStorage override fun withType(value: String): NakshaStorage = super.withType(value) as NakshaStorage override fun withFeatureType(value: String): NakshaStorage = super.withFeatureType(value) as NakshaStorage override fun withBbox(value: SpBoundingBox?): NakshaStorage = super.withBbox(value) as NakshaStorage @@ -165,6 +165,17 @@ open class NakshaStorage() : NakshaFeature() { return this } + /** + * The database-number to which this storage is hard-wired _(until we support multi-databases per storage)_. + * @since 3.0 + */ + val databaseNumber: Int64 + get() { + // TODO: We need to allow a custom database number, actually we need to decouple the storage from the database. + // However, this is a much larger architectural change, so for now, the storage and the database are hard-wired the same! + return Naksha.featureNumber(id) + } + override fun equals(other: Any?): Boolean { if (other is NakshaStorage) return super.contentDeepEquals(other) return false @@ -178,15 +189,6 @@ open class NakshaStorage() : NakshaFeature() { hardCap.hashCode() } - override fun featureNumberOfId(id: String): Int64 = Naksha.storageNumber(id) - - /** - * The number of the storage, which is basically [featureNumber]. - * @since 3.0 - */ - val number: Int64 - get() = featureNumber - /** * Compares this configuration with another [NakshaStorage] instance. * diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaTx.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaTx.kt index 0db53fea9..44cfebad4 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaTx.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaTx.kt @@ -22,7 +22,6 @@ open class NakshaTx : NakshaFeature() { override fun featureTypeDefaultValue(): String = "naksha.Tx" override fun withId(value: String): NakshaTx = super.withId(value) as NakshaTx - override fun withFeatureNumber(value: Int64): NakshaTx = super.withFeatureNumber(value) as NakshaTx override fun withType(value: String): NakshaTx = super.withType(value) as NakshaTx override fun withFeatureType(value: String): NakshaTx = super.withFeatureType(value) as NakshaTx override fun withBbox(value: SpBoundingBox?): NakshaTx = super.withBbox(value) as NakshaTx diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt index 836cce0c5..1af6b5bac 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt @@ -54,7 +54,7 @@ class StandardMembers private constructor() { * @since 3.0 */ @JvmField @JsStatic - val GlobalBookFeatureNumber = Member("~gbn", MemberType.INT64, JsonPath("gbn")) + val GlobalBookFeatureNumber = Member("~gbfn", MemberType.INT64, JsonPath("gbfn")) /** * `feature` — **Serialised feature** (`BYTE_ARRAY`). The encoded feature blob. The encoding is controlled by [NakshaCollection.dataEncoding]. Mandatory, storage-managed. The feature member is special in that it represents the feature itself, therefore the path is an empty list! diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/FeatureTuple.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/FeatureTuple.kt index 6efd3c122..a83e92250 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/FeatureTuple.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/FeatureTuple.kt @@ -4,6 +4,9 @@ package naksha.model.request import naksha.base.Platform import naksha.model.* +import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_ARGUMENT +import naksha.model.objects.Member +import naksha.model.objects.NakshaCollection import naksha.model.objects.NakshaFeature import naksha.model.objects.StandardMembers import kotlin.js.JsExport @@ -37,9 +40,20 @@ open class FeatureTuple( * @param feature the [NakshaFeature] from which to create this [FeatureTuple]. * @since 3.0 */ - @JsName("fromNakshaFeature") + @JsName("fromFeatureAndCollection") @Suppress("LeakingThis") - constructor(feature: NakshaFeature) : this(feature.tupleNumber) { + constructor(feature: NakshaFeature, collection: NakshaCollection) : this(feature, collection.useMember(StandardMembers.Tn)) + + /** + * Create a feature-tuple from a [NakshaFeature]. + * @param feature the [NakshaFeature] from which to create this [FeatureTuple]. + * @since 3.0 + */ + @JsName("fromFeatureAndMember") + @Suppress("LeakingThis") + constructor(feature: NakshaFeature, tupleNumberMember: Member) : + this(tupleNumberMember.getTupleNumber(feature) ?: throw NakshaException(ILLEGAL_ARGUMENT, "Failed to get tuple-number of feature")) + { this.feature = feature } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadMaps.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadMaps.kt index 3e1a76776..5a5a508cd 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadMaps.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadMaps.kt @@ -10,7 +10,7 @@ import kotlin.js.JsExport import kotlin.js.JsName /** - * A request to read [map features][naksha.model.objects.NakshaMap] from a storage. + * A request to read [map features][naksha.model.objects.NakshaCatalog] from a storage. * @since 3.0 */ @JsExport @@ -64,7 +64,7 @@ open class ReadMaps() : ReadRequest() { /** * Convert this request into a [ReadFeatures] request. * - * Actually, reading maps is not different from reading features, because the storages will have a collection called `naksha~catalogs` in the admin-map in which the [map features][naksha.model.objects.NakshaMap] are stored, or at least the storage will simulate this virtual collection. + * Actually, reading maps is not different from reading features, because the storages will have a collection called `naksha~catalogs` in the admin-map in which the [map features][naksha.model.objects.NakshaCatalog] are stored, or at least the storage will simulate this virtual collection. * * This is necessary, if you want a more fine-grained query, like filter maps or request past states of the map feature. * @return this request as [ReadFeatures] request. diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/SuccessResponse.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/SuccessResponse.kt index 55c987d6f..5e1e3063b 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/SuccessResponse.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/SuccessResponse.kt @@ -6,6 +6,7 @@ import naksha.geo.SpFeatureCollection import naksha.model.* import naksha.model.objects.NakshaFeature import naksha.model.objects.NakshaFeatureList +import naksha.model.objects.XyzMembers import kotlin.js.JsExport import kotlin.js.JsName import kotlin.jvm.JvmOverloads @@ -179,7 +180,12 @@ open class SuccessResponse() : Response() { list.setCapacity(featureList.size) for (feature in featureList) { if (feature == null) continue - list.add(FeatureTuple(feature)) + // TODO: We need to fix this, this is a very dirty hack, but we need to expose the tuple handling expilicty. + // So to say, lets remove the whole FeatureTuple context; a request returns a TupleNumberList. + // Then the user needs to turn this explicitly into a TupleList, and this into a FeatureList. + // We can offer a helper method that does all steps at ones, so turns a SuccessResponse into list of features. + // However, we want to expose the TupleNumber design explicitly. + list.add(FeatureTuple(feature, XyzMembers.XyzTn)) } } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/Write.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/Write.kt index f833211cf..cca8adb51 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/Write.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/Write.kt @@ -15,7 +15,9 @@ import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_STATE import naksha.model.objects.NakshaFeature import naksha.model.objects.NakshaCollection import naksha.model.objects.NakshaDictionary -import naksha.model.objects.NakshaMap +import naksha.model.objects.NakshaCatalog +import naksha.model.objects.StandardMembers +import naksha.model.objects.XyzMembers import kotlin.js.JsExport import kotlin.js.JsName import kotlin.js.JsStatic @@ -149,7 +151,7 @@ open class Write : AnyObject() { /** * The identifier of the map to access; if `null` then the map-id is read from the [NakshaContext]. * - * If a [map][NakshaMap] or [dictionary][NakshaDictionary] should be modified, then use [Naksha.ADMIN_CATALOG_ID]. + * If a [map][NakshaCatalog] or [dictionary][NakshaDictionary] should be modified, then use [Naksha.ADMIN_CATALOG_ID]. * @since 3.0 */ var mapId by MAP_ID @@ -165,7 +167,7 @@ open class Write : AnyObject() { /** * The identifier of the collection to modify; must not be `null` then the map-id is read from the [NakshaContext]. * - * - If a [map][NakshaMap] should be modified, then [Naksha.CATALOGS_COL_ID] should be used, within [Naksha.ADMIN_CATALOG_ID]. + * - If a [map][NakshaCatalog] should be modified, then [Naksha.CATALOGS_COL_ID] should be used, within [Naksha.ADMIN_CATALOG_ID]. * - If a [dictionary][NakshaDictionary] should be modified, the [Naksha.BOOKS_COL_ID] should be used, within [Naksha.ADMIN_CATALOG_ID]. * - If a [collection][NakshaCollection] should be modified, then [Naksha.COLLECTIONS_COL_ID] should be used, must not be used together with [Naksha.ADMIN_CATALOG_ID], because the admin-map does not allow collection modification, it is internally managed. * - If a [feature][NakshaFeature] should be created, then the [NakshaCollection] in which the feature should be stored is required. @@ -348,7 +350,14 @@ open class Write : AnyObject() { val raw = getRaw("featureNumber") if (raw is Int64) return raw - val fn = feature?.featureNumber + // TODO: This is a hack, we need to change this. + // Without the collection, we normally do not know where the tuple-number is located within a feature. + val feature = this.feature + var fn: Int64? = null + if (feature != null) { + fn = XyzMembers.XyzTn.getTupleNumber(feature)?.featureNumber + if (fn == null) fn = StandardMembers.Tn.getTupleNumber(feature)?.featureNumber + } if (fn != null) return fn val id = this.id @@ -549,7 +558,7 @@ open class Write : AnyObject() { * @return this. * @since 3.0 */ - fun createMap(map: NakshaMap): Write { + fun createMap(map: NakshaCatalog): Write { this.mapId = ADMIN_CATALOG_ID this.collectionId = CATALOGS_COL_ID this.op = WriteOp.CREATE @@ -564,7 +573,7 @@ open class Write : AnyObject() { * @return this. * @since 3.0 */ - fun updateMap(map: NakshaMap, atomic: Boolean): Write { + fun updateMap(map: NakshaCatalog, atomic: Boolean): Write { this.mapId = ADMIN_CATALOG_ID this.collectionId = CATALOGS_COL_ID this.op = WriteOp.UPDATE @@ -580,7 +589,7 @@ open class Write : AnyObject() { * @return this. * @since 3.0 */ - fun upsertMap(map: NakshaMap, atomic: Boolean): Write { + fun upsertMap(map: NakshaCatalog, atomic: Boolean): Write { this.mapId = ADMIN_CATALOG_ID this.collectionId = CATALOGS_COL_ID this.op = WriteOp.UPSERT @@ -596,7 +605,7 @@ open class Write : AnyObject() { * @return this. * @since 3.0 */ - fun deleteMap(map: NakshaMap, atomic: Boolean): Write { + fun deleteMap(map: NakshaCatalog, atomic: Boolean): Write { this.mapId = ADMIN_CATALOG_ID this.collectionId = CATALOGS_COL_ID this.op = WriteOp.DELETE @@ -629,7 +638,7 @@ open class Write : AnyObject() { * @since 3.0 */ fun createCollection(collection: NakshaCollection): Write { - this.mapId = collection.mapId + this.mapId = collection.catalogId this.collectionId = COLLECTIONS_COL_ID this.op = WriteOp.CREATE this.feature = collection @@ -643,7 +652,7 @@ open class Write : AnyObject() { * @since 3.0 */ fun updateCollection(collection: NakshaCollection, atomic: Boolean): Write { - this.mapId = collection.mapId + this.mapId = collection.catalogId this.collectionId = COLLECTIONS_COL_ID this.op = WriteOp.UPDATE this.feature = collection @@ -657,7 +666,7 @@ open class Write : AnyObject() { * @since 3.0 */ fun upsertCollection(collection: NakshaCollection): Write { - this.mapId = collection.mapId + this.mapId = collection.catalogId this.collectionId = COLLECTIONS_COL_ID this.op = WriteOp.UPSERT this.feature = collection @@ -671,7 +680,7 @@ open class Write : AnyObject() { * @since 3.0 */ fun deleteCollection(collection: NakshaCollection, atomic: Boolean): Write { - this.mapId = collection.mapId + this.mapId = collection.catalogId this.collectionId = COLLECTIONS_COL_ID this.op = WriteOp.DELETE this.feature = collection @@ -705,7 +714,7 @@ open class Write : AnyObject() { * @since 3.0 */ fun createFeature(collection: NakshaCollection, feature: NakshaFeature): Write { - this.mapId = collection.mapId + this.mapId = collection.catalogId this.collectionId = collection.id this.op = WriteOp.CREATE this.feature = feature @@ -738,7 +747,7 @@ open class Write : AnyObject() { * @since 3.0 */ fun updateFeature(collection: NakshaCollection, feature: NakshaFeature, atomic: Boolean): Write { - this.mapId = collection.mapId + this.mapId = collection.catalogId this.collectionId = collection.id this.op = WriteOp.UPDATE this.feature = feature @@ -772,7 +781,7 @@ open class Write : AnyObject() { * @since 3.0 */ fun upsertFeature(collection: NakshaCollection, feature: NakshaFeature): Write { - this.mapId = collection.mapId + this.mapId = collection.catalogId this.collectionId = collection.id this.op = WriteOp.UPSERT this.feature = feature @@ -804,7 +813,7 @@ open class Write : AnyObject() { * @since 3.0 */ fun deleteFeature(collection: NakshaCollection, feature: NakshaFeature, atomic: Boolean): Write { - this.mapId = collection.mapId + this.mapId = collection.catalogId this.collectionId = collection.id this.op = WriteOp.DELETE this.feature = feature @@ -822,7 +831,7 @@ open class Write : AnyObject() { */ @JvmOverloads fun deleteFeatureById(collection: NakshaCollection, id: String, version: Version? = null): Write { - this.mapId = collection.mapId + this.mapId = collection.catalogId this.collectionId = collection.id this.op = WriteOp.DELETE this.id = id @@ -860,7 +869,7 @@ open class Write : AnyObject() { * @since 3.0 */ fun purgeFeature(collection: NakshaCollection, feature: NakshaFeature, atomic: Boolean): Write { - this.mapId = collection.mapId + this.mapId = collection.catalogId this.collectionId = collection.id this.op = WriteOp.PURGE this.feature = feature @@ -879,7 +888,7 @@ open class Write : AnyObject() { @JsName("purgeFeatureById") @JvmOverloads fun purgeFeatureById(collection: NakshaCollection, id: String, version: Version? = null): Write { - this.mapId = collection.mapId + this.mapId = collection.catalogId this.collectionId = collection.id this.op = WriteOp.PURGE this.id = id diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MetaColumn.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MetaColumn.kt index a3e194eb3..fac777028 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MetaColumn.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MetaColumn.kt @@ -585,7 +585,7 @@ open class MetaColumn() : AnyObject() { fun cs3(): MetaColumn = MetaColumn(CS3) /** - * The name of the virtual columns that stores the [feature][naksha.model.Tuple.jbonBytes]. + * The name of the virtual columns that stores the [feature][naksha.model.Tuple.featureBytes]. * * This can only be queried using a special [property query][IPropertyQuery]. */ diff --git a/here-naksha-lib-model/src/jvmTest/kotlin/naksha/model/PropertyFilterTest.kt b/here-naksha-lib-model/src/jvmTest/kotlin/naksha/model/PropertyFilterTest.kt index 5d06849de..a2e68c8bc 100644 --- a/here-naksha-lib-model/src/jvmTest/kotlin/naksha/model/PropertyFilterTest.kt +++ b/here-naksha-lib-model/src/jvmTest/kotlin/naksha/model/PropertyFilterTest.kt @@ -483,7 +483,7 @@ class PropertyFilterTest { featureNumber = featureNumber(feature.id), version = version, membersBook = members, - jbonBytes = featureBytes + featureBytes = featureBytes ) return FeatureTuple(tupleNumber, tuple) } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgAdminMap.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgAdminMap.kt index 07acb8879..01266e1f1 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgAdminMap.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgAdminMap.kt @@ -15,13 +15,11 @@ import naksha.jbon.JbDictionary import naksha.model.* import naksha.model.Naksha.NakshaCompanion.ADMIN_CATALOG_ID import naksha.model.Naksha.NakshaCompanion.ADMIN_CATALOG_FN -import naksha.model.Naksha.NakshaCompanion.COLLECTIONS_COL_ID import naksha.model.Naksha.NakshaCompanion.CATALOGS_COL_FN import naksha.model.NakshaError.NakshaErrorCompanion.EXCEPTION import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_ARGUMENT import naksha.model.NakshaError.NakshaErrorCompanion.STORAGE_ID_MISMATCH -import naksha.model.objects.NakshaCollection -import naksha.model.objects.NakshaMap +import naksha.model.objects.NakshaCatalog import naksha.psql.PgColumn.PgColumnCompanion.headColumns import kotlin.js.ExperimentalJsExport import kotlin.js.JsExport @@ -57,7 +55,7 @@ abstract class PgAdminMap internal constructor( * @since 3.0.0 */ upgrade: Boolean? -) : PgMap(storage, NakshaMap().withStorageId(storage.id).withId(ADMIN_CATALOG_ID)), IDictReader { +) : PgMap(storage, NakshaCatalog().withDatabaseId(storage.id).withId(ADMIN_CATALOG_ID)), IDictReader { /** * The page-size of the database (`current_setting('block_size')`). * @since 3.0.0 @@ -157,7 +155,7 @@ abstract class PgAdminMap internal constructor( // Called from invokeInitStorage->initStorage, so within a lock! init { val id = config.id // storageId - val number = config.number // storageNumber + val number = config.databaseNumber // storageNumber val doOverride = config.override == true val doCreate = create ?: config.create val doUpgrade = upgrade ?: config.upgrade @@ -330,7 +328,7 @@ SELECT basics.*, procs.* FROM basics, procs; //mapNumberSequenceOid = cursor["map_oid"] //colNumberSequenceOid = cursor["col_oid"] } - logger.info("Storage ${config.id} / ${config.number} initialized, txn-seq-oid=$txnSequenceOid, commit") + logger.info("Storage ${config.id} / ${config.databaseNumber} initialized, txn-seq-oid=$txnSequenceOid, commit") conn.commit() } } @@ -543,8 +541,8 @@ SELECT basics.*, procs.* FROM basics, procs; if (conn == null) return null val outRows = PgColumnRows(catalogs.head) - .withStorageNumber(storage.number) - .withMapNumber(ADMIN_CATALOG_FN) + .withDatabaseNumber(storage.number) + .withCatalogNumber(ADMIN_CATALOG_FN) .withCollectionNumber(CATALOGS_COL_FN) .addColumns(headColumns) val SQL = """SELECT ${outRows.names()} @@ -557,7 +555,7 @@ WHERE id = $1 AND (version & 3) < 2""" if (outRows.size == 0) return null val tuple = outRows[0] ?: return null Naksha.cache.store(tuple) - val nakshaMap = tuple.decodeFeature(null).proxy(NakshaMap::class) + val nakshaMap = tuple.decodeFeature(null).proxy(NakshaCatalog::class) val pgMap = PgMap(storage, nakshaMap) storeMap(pgMap) return pgMap @@ -578,8 +576,8 @@ WHERE id = $1 AND (version & 3) < 2""" // Read from database val outRows = PgColumnRows(catalogs.head) - .withStorageNumber(storage.number) - .withMapNumber(ADMIN_CATALOG_FN) + .withDatabaseNumber(storage.number) + .withCatalogNumber(ADMIN_CATALOG_FN) .withCollectionNumber(CATALOGS_COL_FN) .addColumns(headColumns) val SQL = """ @@ -595,7 +593,7 @@ WHERE id = $1 AND (version & 3) < 2""" if (outRows.size == 0) return null val tuple = outRows[0] ?: return null Naksha.cache.store(tuple) - val nakshaMap = tuple.decodeFeature(null).proxy(NakshaMap::class) + val nakshaMap = tuple.decodeFeature(null).proxy(NakshaCatalog::class) val pgMap = PgMap(storage, nakshaMap) storeMap(pgMap) return pgMap diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumnRows.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumnRows.kt index 01a87595e..691d93748 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumnRows.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumnRows.kt @@ -4,33 +4,29 @@ import naksha.base.Int64 import naksha.jbon.BookType import naksha.jbon.HeapBook import naksha.model.* +import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_ARGUMENT +import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_STATE +import naksha.model.NakshaError.NakshaErrorCompanion.INTERNAL_ERROR +import naksha.model.objects.Member import naksha.model.objects.NakshaCollection +import naksha.model.objects.StandardMembers /** * Helper class to convert rows into arrays of column-data and vice versa. The main purpose is to read and write full tuples, but it supports basically as well virtual columns. * @since 3.0 */ -internal class PgColumnRows(val collection: NakshaCollection) { +internal class PgColumnRows { /** * All columns being added already. * @since 3.0 */ val columns = mutableListOf() - init { - val members = collection.useMembers() - for (i in 0 until members.size) { - val member = members[i] ?: continue - columns.add(PgColumnEntry(i, member.name, PgType.ofMemberType(member.dataType))) - } - } internal val columnByName = mutableMapOf() - private var isComplete: Boolean? = null private var names: String? = null private var namesAggregate: String? = null private var placeholders: String? = null private var arrayTypeNames: Array? = null private fun clearCache(): PgColumnRows { - isComplete = null names = null namesAggregate = null placeholders = null @@ -57,31 +53,76 @@ internal class PgColumnRows(val collection: NakshaCollection) { return this } + /** + * When set, clear the [columns] and add all columns of the given [NakshaCollection]. + * @since 3.0 + */ + var collection: NakshaCollection? = null + set(collection) { + if (collection != null) { + clearCache() + columns.clear() + databaseNumber = collection.databaseNumber + catalogNumber = collection.catalogNumber + collectionNumber = collection.collectionNumber + val members = collection.useMembers() + if (!members.isSortedByIndex()) throw NakshaException(ILLEGAL_ARGUMENT, "The given collection is not sorted by index") + for (i in 0 until members.size) { + val member = members[i] ?: throw NakshaException(INTERNAL_ERROR, "The member at index $i is null; this must not happen") + val index = member.index ?: throw NakshaException(INTERNAL_ERROR, "The member at index $i has no index; this must not happen") + if (index != i) throw NakshaException(INTERNAL_ERROR, "The member at index $i has an member-index $index; this must not happen, expected $i") + columns.add(PgColumnEntry(i, member.name, PgType.ofMemberType(member))) + } + } + field = collection + } + + /** + * Add the members of the given [NakshaCollection] to the row-set. + * + * The members of the given collection must be sorted by index. + * @param collection the [NakshaCollection] of which to add the members. + * @return this + * @since 3.0 + */ + fun withCollection(collection: NakshaCollection): PgColumnRows { + this.collection = collection + return this + } + /** * If all rows are coming from the same storage, the storage-number of it. * @since 3.0 */ - var storageNumber: Int64? = null + var databaseNumber: Int64? = null + set(value) { + collection = null + field = value + } /** - * @see [storageNumber] + * @see [databaseNumber] */ - fun withStorageNumber(value: Int64): PgColumnRows { - storageNumber = value + fun withDatabaseNumber(value: Int64): PgColumnRows { + databaseNumber = value return this } /** - * If all rows are coming from the same map, the map-number of it. + * If all rows are coming from the same catalog, the catalog-number of it. * @since 3.0 */ - var mapNumber: Int? = null + var catalogNumber: Int? = null + set(value) { + collection = null + field = value + } /** - * @see [mapNumber] + * @see [catalogNumber] */ - fun withMapNumber(value: Int): PgColumnRows { - mapNumber = value + fun withCatalogNumber(value: Int): PgColumnRows { + catalogNumber = value return this } @@ -90,6 +131,10 @@ internal class PgColumnRows(val collection: NakshaCollection) { * @since 3.0 */ var collectionNumber: Int? = null + set(value) { + collection = null + field = value + } /** * @see [collectionNumber] @@ -99,25 +144,6 @@ internal class PgColumnRows(val collection: NakshaCollection) { return this } - /** - * Tests if tuple read from these rows will be complete. - * @since 3.0 - */ - val complete: Boolean - get() { - val c = isComplete - if (c != null) return c - var detected = true - for (pgColumn in allColumns) { - if (!hasColumn(pgColumn)) { - detected = false - break - } - } - isComplete = detected - return detected - } - fun addColumn(name: String, type: PgType): PgColumnRows { clearCache() val existing = columnByName[name] @@ -129,19 +155,6 @@ internal class PgColumnRows(val collection: NakshaCollection) { return this } - fun addColumns(cols: List): PgColumnRows { - clearCache() - var i = columns.size - for (col in cols) { - val existing = columnByName[col.name] - if (existing == null) { - val column = PgColumnEntry(col, i++).withSize(size) - columns.add(column) - columnByName[column.name] = column - } - } - return this - } fun getColumn(name: String): PgColumnEntry? = columnByName[name] fun getColumn(index: Int): PgColumnEntry? = if (index in 0 until columns.size) columns[index] else null fun hasColumn(name: String): Boolean = getColumn(name) != null @@ -171,8 +184,8 @@ internal class PgColumnRows(val collection: NakshaCollection) { } fun getB64(row: Int, columnName: String, featureNumber: Int64): TupleNumber? { val raw = getByteArray(row, columnName) ?: return null - val storageNumber = this.storageNumber ?: return null - val mapNumber = this.mapNumber ?: return null + val storageNumber = this.databaseNumber ?: return null + val mapNumber = this.catalogNumber ?: return null val collectionNumber = this.collectionNumber ?: return null return try { TupleNumber.fromB64(raw, storageNumber, mapNumber, collectionNumber, featureNumber) @@ -182,8 +195,8 @@ internal class PgColumnRows(val collection: NakshaCollection) { } fun getB128(row: Int, columnName: String): TupleNumber? { val raw = getByteArray(row, columnName) ?: return null - val storageNumber = this.storageNumber ?: return null - val mapNumber = this.mapNumber ?: return null + val storageNumber = this.databaseNumber ?: return null + val mapNumber = this.catalogNumber ?: return null val collectionNumber = this.collectionNumber ?: return null return try { TupleNumber.fromB128(raw, storageNumber, mapNumber, collectionNumber) @@ -192,31 +205,36 @@ internal class PgColumnRows(val collection: NakshaCollection) { } } - fun getTuple(row: Int, storageNumber: Int64, mapNumber: Int, collectionNumber: Int): Tuple? { + /** + * Read the given row into a tuple, requires that a [collection] is assigned. + * @param row the row number. + * @return the [Tuple] extracted from the row or `null`, if either no [collection] set, the row number is outside the result-set, or something else failed. + * @since 3.0 + */ + fun getTuple(row: Int): Tuple? { if (row < 0 || row >= size) return null - val fn = getInt64(row, PgColumn.fn) ?: return null - val version = getInt64(row, PgColumn.version) ?: return null - val nextVersion = getInt64(row, PgColumn.next_version) - val members = HeapBook(BookType.MEMBER_BOOK) - - return Tuple( - storageNumber = storageNumber, - mapNumber = mapNumber, - collectionNumber = collectionNumber, - featureNumber = fn, - version = naksha.model.Version(version), - nextVersion = nextVersion ?: Int64(-1L), - membersBook = members, - jbonBytes = getByteArray(row, PgColumn.feature) - ) + val collection = this.collection ?: return null + val members = collection.members ?: return null + val membersBook = HeapBook(BookType.MEMBER_BOOK) + var featureBytes: ByteArray? = null + for (i in 0 until members.size) { + val member: Member = members[i] ?: throw NakshaException(ILLEGAL_STATE, "Member #$i of collection ${collection.id} is null") + val name = member.name + val column: PgColumnEntry = getColumn(name) ?: throw NakshaException(ILLEGAL_STATE, "Missing member '$name' at index $i of collection ${collection.id}") + val value = column.values[row] + if (StandardMembers.Feature.name == name) { + // Special case, root feature. + if (value !is ByteArray) throw NakshaException(ILLEGAL_STATE, "The feature root is no byte-array") + featureBytes = value + } else { + membersBook.put(name, value) + } + } + if (featureBytes == null) throw NakshaException(ILLEGAL_STATE, "Missing mandatory member '${StandardMembers.Feature.name}'!") + return Tuple(featureBytes = featureBytes, membersBook = membersBook) } - operator fun get(row: Int): Tuple? { - val storage_num = storageNumber ?: return null - val map_num = mapNumber ?: return null - val col_num = collectionNumber ?: return null - return getTuple(row, storage_num, map_num, col_num) - } + operator fun get(row: Int): Tuple? = getTuple(row) fun set(row: Int, columnName: String, value: Any?): Boolean { val column = getColumn(columnName) @@ -242,55 +260,16 @@ internal class PgColumnRows(val collection: NakshaCollection) { return this } - /** - * Populates the [Member][naksha.model.objects.Member] columns for the given row by walking the [feature] using each member's [path][naksha.model.objects.Member.effectivePath] and coercing the value to the SQL type. - * - * Missing keys and mismatched types both produce a NULL column value. Mismatches additionally emit a warning via [naksha.base.Platform.PlatformCompanion.logger]. - */ - fun setCustomMembers(row: Int, feature: naksha.model.objects.NakshaFeature?, members: naksha.model.objects.MemberList?) { - if (feature == null || members == null) return - for (m in members) { - if (m == null) continue - val raw = PgMemberHelper.walkFeature(feature, m.effectivePath()) - val coerced = PgMemberHelper.coerce(raw, m.dataType, feature.id, m.name) - set(row, PgMemberHelper.pgColumnName(m.name), coerced) - } - } - operator fun set(row: Int, tuple: Tuple) { withMinSize(row) - val members = tuple.membersBook - - set(row, PgColumn.updated_at, members.getByName("updated_at") as? Int64) - set(row, PgColumn.created_at, members.getByName("created_at") as? Int64) - set(row, PgColumn.author_ts, members.getByName("author_ts") as? Int64) - set(row, PgColumn.cv0, members.getByName("cv0") as? Double) - set(row, PgColumn.cv1, members.getByName("cv1") as? Double) - set(row, PgColumn.cv2, members.getByName("cv2") as? Double) - set(row, PgColumn.cv3, members.getByName("cv3") as? Double) - set(row, PgColumn.hash, members.getByName("hash") as? Int) - set(row, PgColumn.here_tile, members.getByName("here_tile") as? Int) - set(row, PgColumn.cc, members.getByName("cc") as? Int) - val fn = tuple.featureNumber - set(row, PgColumn.fn, fn) - set(row, PgColumn.version, tuple.version.txn) - set(row, PgColumn.next_version, tuple.nextVersion) - set(row, PgColumn.base_tn, members.getByName("base_tn") as? ByteArray) - set(row, PgColumn.id, if (fn >= Int64(0)) null else members.getByName("id") as? String) - set(row, PgColumn.app_id, members.getByName("app_id") as? String) - set(row, PgColumn.author, members.getByName("author") as? String) - set(row, PgColumn.origin, members.getByName("origin") as? String) - set(row, PgColumn.target, members.getByName("target") as? String) - set(row, PgColumn.ft, members.getByName("ft") as? String) - set(row, PgColumn.cs0, members.getByName("cs0") as? String) - set(row, PgColumn.cs1, members.getByName("cs1") as? String) - set(row, PgColumn.cs2, members.getByName("cs2") as? String) - set(row, PgColumn.cs3, members.getByName("cs3") as? String) - set(row, PgColumn.tags, members.getByName("tags") as? String) - set(row, PgColumn.ref_point, members.getByName("ref_point") as? ByteArray) - set(row, PgColumn.feature, tuple.jbonBytes) - set(row, PgColumn.geo, members.getByName("geo") as? ByteArray) - set(row, PgColumn.attachment, members.getByName("attachment") as? ByteArray) + val membersBook = tuple.membersBook + val END = membersBook.namesLength() + for (i in 0 until END) { + val memberName = membersBook.getNameAt(i) ?: continue + val column = getColumn(memberName) ?: continue + val value = membersBook[memberName] + set(row, column.name, value) + } } operator fun set(row: Int, cursor: PgCursor) { diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMap.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMap.kt index 62f3e4061..01e516ed6 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMap.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMap.kt @@ -16,7 +16,7 @@ import naksha.model.Naksha.NakshaCompanion.TRANSACTIONS_COL_FN import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_STATE import naksha.model.NakshaException import naksha.model.objects.NakshaCollection -import naksha.model.objects.NakshaMap +import naksha.model.objects.NakshaCatalog import naksha.psql.PgColumn.PgColumnCompanion.headColumns import naksha.psql.PgUtil.PgUtilCompanion.quoteIdent import kotlin.js.JsExport @@ -38,7 +38,7 @@ open class PgMap internal constructor( * The HEAD state of the map. * @since 3.0.0 */ - nakshaMap: NakshaMap, + nakshaMap: NakshaCatalog, /** * The map-id. @@ -73,7 +73,7 @@ open class PgMap internal constructor( * @see [headRef] * @since 3.0 */ - val head: NakshaMap + val head: NakshaCatalog get() = headRef.get() private var _collections: PgCollection? = null @@ -336,8 +336,8 @@ open class PgMap internal constructor( // Read from database val outRows = PgColumnRows(collections.head) - .withStorageNumber(storage.number) - .withMapNumber(this.number) + .withDatabaseNumber(storage.number) + .withCatalogNumber(this.number) .withCollectionNumber(COLLECTIONS_COL_FN) .addColumns(headColumns) setSearchPath(conn) @@ -380,8 +380,8 @@ WHERE id = $1 AND (version & 3) < 2""" // Read from database val outRows = PgColumnRows(collections.head) - .withStorageNumber(storage.number) - .withMapNumber(this.number) + .withDatabaseNumber(storage.number) + .withCatalogNumber(this.number) .withCollectionNumber(COLLECTIONS_COL_FN) .addColumns(headColumns) setSearchPath(conn) diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaBooks.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaBooks.kt index 00e202664..aa5b709d4 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaBooks.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaBooks.kt @@ -13,7 +13,7 @@ import kotlin.js.JsExport */ @JsExport class PgNakshaBooks internal constructor(adminMap: PgAdminMap) : PgCollection(adminMap, NakshaCollection() - .withMapId(Naksha.ADMIN_CATALOG_ID) + .withCatalogId(Naksha.ADMIN_CATALOG_ID) .withId(Naksha.BOOKS_COL_ID) ), PgInternalCollection, IDictManager { diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaCatalogs.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaCatalogs.kt index 537412acc..a54ee7ce4 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaCatalogs.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaCatalogs.kt @@ -12,6 +12,6 @@ import kotlin.js.JsExport */ @JsExport class PgNakshaCatalogs internal constructor(adminMap: PgAdminMap) : PgCollection(adminMap, NakshaCollection() - .withMapId(Naksha.ADMIN_CATALOG_ID) + .withCatalogId(Naksha.ADMIN_CATALOG_ID) .withId(Naksha.CATALOGS_COL_ID) ), PgInternalCollection diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaCollections.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaCollections.kt index e920bfbba..a010446a9 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaCollections.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaCollections.kt @@ -11,6 +11,6 @@ import kotlin.js.JsExport */ @JsExport class PgNakshaCollections internal constructor(map: PgMap) : PgCollection(map, NakshaCollection() - .withMapId(map.id) + .withCatalogId(map.id) .withId(Naksha.COLLECTIONS_COL_ID) ), PgInternalCollection diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaTransactions.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaTransactions.kt index c3c64ab48..7c3073abf 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaTransactions.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaTransactions.kt @@ -28,7 +28,7 @@ import kotlin.js.JsExport */ @JsExport class PgNakshaTransactions internal constructor(adminMap: PgAdminMap) : PgCollection(adminMap, NakshaCollection() - .withMapId(ADMIN_CATALOG_ID) + .withCatalogId(ADMIN_CATALOG_ID) .withId(TRANSACTIONS_COL_ID) .withStoreDeleted(StoreMode.OFF) .withStoreHistory(StoreMode.ON) diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgSession.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgSession.kt index 75d10d3f1..20a1f9de6 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgSession.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgSession.kt @@ -9,7 +9,7 @@ import naksha.base.Platform.PlatformCompanion.newAtomicInt64 import naksha.model.* import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_STATE import naksha.model.objects.NakshaCollection -import naksha.model.objects.NakshaMap +import naksha.model.objects.NakshaCatalog import naksha.model.request.* import naksha.model.request.WriteRequest import naksha.model.objects.NakshaTx @@ -433,8 +433,8 @@ open class PgSession( else collection.effectiveHeadColumns val rows = PgColumnRows() - .withStorageNumber(map.storage.number) - .withMapNumber(map.number) + .withDatabaseNumber(map.storage.number) + .withCatalogNumber(map.number) .withCollectionNumber(collection.number) .withDefaultDataEncoding(collection.head.dataEncoding ?: Naksha.DEFAULT_DATA_ENCODING) .addColumns(effectiveCols) @@ -489,7 +489,7 @@ open class PgSession( } } - override fun getMapById(mapId: String): NakshaMap? { + override fun getMapById(mapId: String): NakshaCatalog? { assertOpen() return (if (mayReadParallel) newReadConnection() else readConnection()).use { storage.adminMap.getPgMapById(it.conn, mapId)?.head @@ -508,7 +508,7 @@ open class PgSession( } } - override fun getMapByNumber(mapNumber: Int): NakshaMap? { + override fun getMapByNumber(mapNumber: Int): NakshaCatalog? { assertOpen() return (if (mayReadParallel) newReadConnection() else readConnection()).use { storage.adminMap.getPgMapByNumber(it.conn, mapNumber)?.head @@ -527,7 +527,7 @@ open class PgSession( } } - override fun getCollectionById(map: NakshaMap, collectionId: String): NakshaCollection? { + override fun getCollectionById(map: NakshaCatalog, collectionId: String): NakshaCollection? { assertOpen() return (if (mayReadParallel) newReadConnection() else readConnection()).use { val pgMap = storage.adminMap.getPgMapById(it.conn, map.id) ?: return null @@ -548,7 +548,7 @@ open class PgSession( } } - override fun getCollectionByNumber(map: NakshaMap, collectionNumber: Int): NakshaCollection? { + override fun getCollectionByNumber(map: NakshaCatalog, collectionNumber: Int): NakshaCollection? { assertOpen() return (if (mayReadParallel) newReadConnection() else readConnection()).use { val pgMap = storage.adminMap.getPgMapById(it.conn, map.id) ?: return null diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWrite.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWrite.kt index 5263fade4..0671532bc 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWrite.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWrite.kt @@ -3,7 +3,7 @@ package naksha.psql import naksha.model.* import naksha.model.objects.NakshaCollection import naksha.model.objects.NakshaFeature -import naksha.model.objects.NakshaMap +import naksha.model.objects.NakshaCatalog import naksha.model.request.Write import naksha.model.request.WriteOp @@ -132,10 +132,10 @@ internal data class PgWrite(val original: Write, val i: Int) { var asPgMap: PgMap? = null /** - * If this modifies a map, the feature cast to [NakshaMap]. + * If this modifies a map, the feature cast to [NakshaCatalog]. * @since 3.0 */ - var asNakshaMap: NakshaMap? = null + var asNakshaMap: NakshaCatalog? = null /** * If the feature is a collection, the [PgCollection] representation. diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt index f9b74f6f5..c822cf9df 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt @@ -10,7 +10,7 @@ import naksha.model.objects.Member import naksha.model.objects.MemberList import naksha.model.objects.MemberType import naksha.model.objects.NakshaCollection -import naksha.model.objects.NakshaMap +import naksha.model.objects.NakshaCatalog import naksha.model.objects.NakshaTx import naksha.model.objects.StandardMembers import naksha.model.request.* @@ -291,10 +291,10 @@ open class PgWriter internal constructor( val op = write.op var pgMap = storage.adminMap.getPgMapById(null, write.id) ?: storage.adminMap.getPgMapById(conn, write.id) - val nakshaMap: NakshaMap? + val nakshaMap: NakshaCatalog? if (op == WriteOp.CREATE || op == WriteOp.UPSERT || op == WriteOp.UPDATE) { val feature = write.feature ?: throw illegalArg("The write #${write.i} is $op, but the feature is null") - nakshaMap = if (feature is NakshaMap) feature else feature.proxy(NakshaMap::class) + nakshaMap = if (feature is NakshaCatalog) feature else feature.proxy(NakshaCatalog::class) nakshaMap.storageId = storage.id if (pgMap == null) { if (op == WriteOp.UPDATE) { @@ -405,7 +405,7 @@ open class PgWriter internal constructor( } /** - * Invoked when a [NakshaMap][naksha.model.objects.NakshaMap] should be physically created. + * Invoked when a [NakshaMap][naksha.model.objects.NakshaCatalog] should be physically created. * @param map the map that should be physically created. * @since 3.0 */ @@ -414,7 +414,7 @@ open class PgWriter internal constructor( } /** - * Invoked when a [NakshaMap][naksha.model.objects.NakshaMap] was created. + * Invoked when a [NakshaMap][naksha.model.objects.NakshaCatalog] was created. * @param map the map that was just created. * @since 3.0 */ diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterBase.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterBase.kt index 6d2d67bdb..76265e279 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterBase.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterBase.kt @@ -73,8 +73,8 @@ internal abstract class PgWriterBase protected constructor( * @since 3.0 */ val inRows = PgColumnRows() - .withStorageNumber(storageNumber) - .withMapNumber(mapNumber) + .withDatabaseNumber(storageNumber) + .withCatalogNumber(mapNumber) .withCollectionNumber(collectionNumber) .withMinSize(writes.size) diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterDelete.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterDelete.kt index 1d384f1e2..4dcfa2c96 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterDelete.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterDelete.kt @@ -145,8 +145,8 @@ ${if (purge) "LEFT JOIN head_deleted ON head_deleted.id = query.id" else ""} override fun doExecute(conn: PgConnection) { if (writes.isEmpty()) return val outRows = PgColumnRows() - .withStorageNumber(storageNumber) - .withMapNumber(mapNumber) + .withDatabaseNumber(storageNumber) + .withCatalogNumber(mapNumber) .withCollectionNumber(collectionNumber) .withDefaultDataEncoding(collection.head.dataEncoding ?: Naksha.DEFAULT_DATA_ENCODING) .addColumns(collection.effectiveHistoryColumns) diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt index bdfb6854d..87c37c0dc 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt @@ -134,8 +134,8 @@ LEFT JOIN inserted ON inserted.id = new_row.id // read back from the DB so the in-memory tuple reflects the final stored value. val keepableByteCols = collection.effectiveHeadColumns.filter { it.type == PgType.BYTE_ARRAY && it !== PgColumn.feature } val rows = PgColumnRows() - .withStorageNumber(storageNumber) - .withMapNumber(mapNumber) + .withDatabaseNumber(storageNumber) + .withCatalogNumber(mapNumber) .withCollectionNumber(collectionNumber) .addColumn("id", PgType.STRING) .addColumn("existing_id", PgType.STRING) diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt index 598c78af9..1d818189f 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt @@ -147,8 +147,8 @@ ${if (head_to_history.isNotEmpty()) "LEFT JOIN head_to_history ON head_to_histor override fun doExecute(conn: PgConnection) { val keepableByteCols = collection.effectiveHeadColumns.filter { it.type == PgType.BYTE_ARRAY && it !== PgColumn.feature } val outRows = PgColumnRows() - .withStorageNumber(storageNumber) - .withMapNumber(mapNumber) + .withDatabaseNumber(storageNumber) + .withCatalogNumber(mapNumber) .withCollectionNumber(collectionNumber) .addColumn(PgColumn.id) .addColumn(PgColumn.fn) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/AttachmentTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/AttachmentTest.kt index 1766eff3a..709c2d997 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/AttachmentTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/AttachmentTest.kt @@ -20,7 +20,7 @@ class AttachmentTest : PgTestBase() { // Write the feature val writeFeaturesReq = WriteRequest().apply { - add(Write().createFeature(collection.mapId, collection.id, featureToCreate).withAttachment(attachmentBytes)) + add(Write().createFeature(collection.catalogId, collection.id, featureToCreate).withAttachment(attachmentBytes)) } executeWrite(writeFeaturesReq).apply { // Verify the result (will come from cache) @@ -41,7 +41,7 @@ class AttachmentTest : PgTestBase() { // Read the feature Naksha.cache.clear() executeRead(ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id featureIds += featureToCreate.id }).apply { @@ -73,7 +73,7 @@ class AttachmentTest : PgTestBase() { // Write the feature val writeFeaturesReq = WriteRequest().apply { - add(Write().upsertFeature(collection.mapId, collection.id, featureToCreate).withAttachment(attachmentBytes)) + add(Write().upsertFeature(collection.catalogId, collection.id, featureToCreate).withAttachment(attachmentBytes)) } executeWrite(writeFeaturesReq).apply { // Verify the result (will come from cache) @@ -96,7 +96,7 @@ class AttachmentTest : PgTestBase() { Naksha.cache.clear() val readFeature: NakshaFeature executeRead(ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id featureIds += featureToCreate.id }).apply { @@ -130,7 +130,7 @@ class AttachmentTest : PgTestBase() { upsertFeature.properties["test"] = "end" val updateFeatureReq = WriteRequest().apply { // We do not modify attachment, therefore it should be UNDEFINED - add(Write().upsertFeature(collection.mapId, collection.id, upsertFeature)) + add(Write().upsertFeature(collection.catalogId, collection.id, upsertFeature)) } executeWrite(updateFeatureReq).apply { assertEquals(1, length) @@ -162,7 +162,7 @@ class AttachmentTest : PgTestBase() { // Write the feature val writeFeaturesReq = WriteRequest().apply { - add(Write().createFeature(collection.mapId, collection.id, featureToCreate).withAttachment(attachmentBytes)) + add(Write().createFeature(collection.catalogId, collection.id, featureToCreate).withAttachment(attachmentBytes)) } executeWrite(writeFeaturesReq).apply { // Verify the result (will come from cache) @@ -185,7 +185,7 @@ class AttachmentTest : PgTestBase() { Naksha.cache.clear() val readFeature: NakshaFeature executeRead(ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id featureIds += featureToCreate.id }).apply { diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ChainCollectionTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ChainCollectionTest.kt index bcd7bd253..57f08c0e0 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ChainCollectionTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ChainCollectionTest.kt @@ -1,7 +1,6 @@ package naksha.psql import naksha.base.Int64 -import naksha.model.Naksha import naksha.model.objects.Index import naksha.model.objects.IndexType import naksha.model.objects.Member @@ -89,14 +88,14 @@ class ChainCollectionTest : PgTestBase( val tail = makeFeature(tailFn, leftFn = midFn, rightFn = null) executeWrite(WriteRequest().apply { - add(Write().createFeature(collection.mapId, collection.id, head)) - add(Write().createFeature(collection.mapId, collection.id, mid)) - add(Write().createFeature(collection.mapId, collection.id, tail)) + add(Write().createFeature(collection.catalogId, collection.id, head)) + add(Write().createFeature(collection.catalogId, collection.id, mid)) + add(Write().createFeature(collection.catalogId, collection.id, tail)) }) // When: reading all three back by their numeric IDs in one request val response = executeRead(ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id featureIds += headFn.toString() featureIds += midFn.toString() @@ -152,7 +151,7 @@ class ChainCollectionTest : PgTestBase( val conn = storage.adminConnection() conn.use { // The head-table name pattern is "" inside the map schema. - val mapId = collection.mapId + val mapId = collection.catalogId val colId = collection.id // Query pg_indexes for our custom indices. val sql = """ @@ -181,14 +180,14 @@ class ChainCollectionTest : PgTestBase( val mid = makeFeature(midFn, leftFn = headFn, rightFn = tailFn) val tail = makeFeature(tailFn, leftFn = midFn, rightFn = null) executeWrite(WriteRequest().apply { - add(Write().upsertFeature(collection.mapId, collection.id, head)) - add(Write().upsertFeature(collection.mapId, collection.id, mid)) - add(Write().upsertFeature(collection.mapId, collection.id, tail)) + add(Write().upsertFeature(collection.catalogId, collection.id, head)) + add(Write().upsertFeature(collection.catalogId, collection.id, mid)) + add(Write().upsertFeature(collection.catalogId, collection.id, tail)) }) // When: reading all features from this collection (no ID filter) val all = executeRead(ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id }) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/CollectionTests.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/CollectionTests.kt index 11cfe095e..b2d76a867 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/CollectionTests.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/CollectionTests.kt @@ -52,7 +52,7 @@ class CollectionTests : PgTestBase(collection = null, mapId = "") { // Then: this collection is queryable and empty val readAllFromCollection = ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id } val collectionContent = executeRead(readAllFromCollection) @@ -60,7 +60,7 @@ class CollectionTests : PgTestBase(collection = null, mapId = "") { // And: Virtual Collections contain the created collection val selectCollectionFromVirt = ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += Naksha.COLLECTIONS_COL_ID featureIds += collection.id } @@ -70,7 +70,7 @@ class CollectionTests : PgTestBase(collection = null, mapId = "") { // When: Collection gets deleted executeWrite( WriteRequest().add( - Write().deleteCollectionById(collection.mapId, collection.id) + Write().deleteCollectionById(collection.catalogId, collection.id) ) ) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/DeleteFeatureBase.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/DeleteFeatureBase.kt index ad56ded2d..5f4fb8c86 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/DeleteFeatureBase.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/DeleteFeatureBase.kt @@ -24,7 +24,7 @@ abstract class DeleteFeatureBase( val featureId = "feature_to_delete" val initialFeature = executeWrite( WriteRequest().add( - Write().createFeature(collection.mapId, collection.id, NakshaFeature(featureId)) + Write().createFeature(collection.catalogId, collection.id, NakshaFeature(featureId)) ) ).let { // this = SuccessResponse val features = assertNotNull(it.features) @@ -36,7 +36,7 @@ abstract class DeleteFeatureBase( val deletedFeatures = executeWrite( WriteRequest().add( - Write().deleteFeatureById(collection.mapId, collection.id, featureId) + Write().deleteFeatureById(collection.catalogId, collection.id, featureId) ) ).let { // this = SuccessResponse val features = assertNotNull(it.features) @@ -49,7 +49,7 @@ abstract class DeleteFeatureBase( // Verify that the feature does not exist Naksha.cache.clear() executeRead(ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id featureIds += initialFeature.id }).let { // this = SuccessResponse @@ -60,7 +60,7 @@ abstract class DeleteFeatureBase( // queryHistory=true (without queryDeleted) returns only past states from the history table. // The tombstone is in HEAD and is NOT included unless queryDeleted=true is also set. executeRead(ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id featureIds += initialFeature.id queryHistory = true @@ -73,7 +73,7 @@ abstract class DeleteFeatureBase( // verify if delete table contains element executeRead(ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id featureIds += initialFeature.id queryDeleted = true @@ -136,7 +136,7 @@ abstract class DeleteFeatureBase( // Confirm the tombstone is visible via queryDeleted and has the right action. Naksha.cache.clear() executeRead(ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id featureIds += featureId queryDeleted = true @@ -164,7 +164,7 @@ abstract class DeleteFeatureBase( assertEquals(1, createCollectionResp.length) assertEquals(1, createCollectionResp.features.size) val collection = assertNotNull(createCollectionResp.features[0]).proxy(NakshaCollection::class) - assertEquals(map.id, collection.mapId) + assertEquals(map.id, collection.catalogId) assertEquals("delete_no_history_but_shadow", collection.id) // Create feature. diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/HistoryUuidTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/HistoryUuidTest.kt index ceabbfa8d..756a4066d 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/HistoryUuidTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/HistoryUuidTest.kt @@ -40,7 +40,7 @@ class HistoryUuidTest: PgTestBase(NakshaCollection( // And: Naksha.cache.clear() val featureVersions = executeRead(ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id featureIds += feature.id queryHistory = true @@ -87,7 +87,7 @@ class HistoryUuidTest: PgTestBase(NakshaCollection( // And: Naksha.cache.clear() val featureVersions = executeRead(ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id featureIds += feature.id queryHistory = true diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/InsertFeatureTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/InsertFeatureTest.kt index 89e43290f..e1ca5a543 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/InsertFeatureTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/InsertFeatureTest.kt @@ -22,7 +22,7 @@ class InsertFeatureTest : PgTestBase() { xyz.tags.clear() xyz.tags.addTag("wicked", false) val writeFeaturesReq = WriteRequest().apply { - add(Write().createFeature(collection.mapId, collection.id, featureToCreate)) + add(Write().createFeature(collection.catalogId, collection.id, featureToCreate)) } // When: executing feature write request @@ -30,7 +30,7 @@ class InsertFeatureTest : PgTestBase() { // And: reading all features from collection val readResponse = executeRead(ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id featureIds += featureToCreate.id }) @@ -96,7 +96,7 @@ class InsertFeatureTest : PgTestBase() { val xyz = featureToCreate.properties.xyz xyz.tags.addTag("wicked", false) val writeFeaturesReq = WriteRequest().apply { - add(Write().createFeature(collection.mapId, collection.id, featureToCreate)) + add(Write().createFeature(collection.catalogId, collection.id, featureToCreate)) } // When: executing feature write request @@ -104,7 +104,7 @@ class InsertFeatureTest : PgTestBase() { // And: reading all features from collection val readResponse = executeRead(ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id featureIds += featureToCreate.id }) @@ -140,7 +140,7 @@ class InsertFeatureTest : PgTestBase() { // Given: features to create val featureToCreate = randomFeature() val writeFeaturesReq = WriteRequest().apply { - add(Write().createFeature(collection.mapId, collection.id, featureToCreate)) + add(Write().createFeature(collection.catalogId, collection.id, featureToCreate)) } // When: executing feature write request @@ -148,7 +148,7 @@ class InsertFeatureTest : PgTestBase() { // And: reading all features from collection val readResponse = executeRead(ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id featureIds += featureToCreate.id }) @@ -169,7 +169,7 @@ class InsertFeatureTest : PgTestBase() { val featuresToCreate = randomFeatures(count = count) val writeFeaturesReq = WriteRequest().apply { featuresToCreate.forEach { featureToCreate -> - add(Write().createFeature(collection.mapId, collection.id, featureToCreate)) + add(Write().createFeature(collection.catalogId, collection.id, featureToCreate)) } } val firstFeatureToCreate = featuresToCreate[0] @@ -193,7 +193,7 @@ class InsertFeatureTest : PgTestBase() { // And: reading all features from collection val readResponse = executeRead(ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id // this.version = version // this.minVersion = version @@ -238,7 +238,7 @@ class InsertFeatureTest : PgTestBase() { Platform.logger.info("Clear cache and reload feature from database") Naksha.cache.clear(storage) val featuresByIdResponse = executeRead(ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id featureIds.add(firstFeatureToCreate.id) }) @@ -258,7 +258,7 @@ class InsertFeatureTest : PgTestBase() { // Read only one feature by bounding box. val featuresByBBox = executeRead(ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id query.spatial = SpIntersects(SpBoundingBox(firstFeatureToCreate.geometry).addMargin(0.0000001).toPolygon()) @@ -277,14 +277,14 @@ class InsertFeatureTest : PgTestBase() { val namedFeature = randomFeature() // has a UUID-style named id (fn < 0) val writeReq = WriteRequest().apply { - add(Write().createFeature(collection.mapId, collection.id, numericFeature)) - add(Write().createFeature(collection.mapId, collection.id, namedFeature)) + add(Write().createFeature(collection.catalogId, collection.id, numericFeature)) + add(Write().createFeature(collection.catalogId, collection.id, namedFeature)) } executeWrite(writeReq) // When: reading both features in a single request with mixed IDs val readResponse = executeRead(ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id featureIds += numericId featureIds += namedFeature.id @@ -308,7 +308,7 @@ class InsertFeatureTest : PgTestBase() { // And val writeReq = WriteRequest().add( - Write().createFeature(collection.mapId, collection.id, featureWithDuplicatedId) + Write().createFeature(collection.catalogId, collection.id, featureWithDuplicatedId) ) val insertDuplicateResponse = newWriteSession().use { session -> session.execute(writeReq) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/PartitioningTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/PartitioningTest.kt index 2ac9f97a7..b3d47d56a 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/PartitioningTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/PartitioningTest.kt @@ -5,7 +5,6 @@ import naksha.model.SessionOptions import naksha.model.objects.NakshaCollection import naksha.model.objects.NakshaFeature import naksha.model.request.* -import naksha.psql.PgTest.PgTest_C.TEST_MAP_ID import kotlin.test.Ignore import kotlin.test.Test import kotlin.test.assertEquals @@ -90,7 +89,7 @@ class PartitioningTest : PgTestBase() { // also - should be able to read val readRequest = ReadFeatures() - readRequest.mapId = partitionedCollection.mapId + readRequest.mapId = partitionedCollection.catalogId readRequest.collectionIds.add(partitionedCollection.id) readRequest.featureIds.add("f1") val readResponse = executeRead(readRequest) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/PgPropertyFilterTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/PgPropertyFilterTest.kt index 94305043e..8cd436850 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/PgPropertyFilterTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/PgPropertyFilterTest.kt @@ -33,7 +33,7 @@ class PgPropertyFilterTest: PgTestBase() { // And: A read request is created with the property query. val readRequest = ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id }.withPropertyQuery(pQuery) // When: read request is executed @@ -65,7 +65,7 @@ class PgPropertyFilterTest: PgTestBase() { // And: A read request is made with the custom filter manually added. val readRequest = ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id resultFilters.add(IdContainsFilter("keep_this")) } @@ -123,7 +123,7 @@ class PgPropertyFilterTest: PgTestBase() { fun shouldNotTriggerFilteringForRequestWithErrorResponse() { // Given: A read request that is designed to fail by targeting a non-existent collection. val readRequest = ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += "non_existent_collection" } @@ -145,7 +145,7 @@ class PgPropertyFilterTest: PgTestBase() { // When: A read request is made to fetch that specific feature by ID, with no result filters. val readRequest = ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id featureIds += feature.id } diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/PgTestBase.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/PgTestBase.kt index 113acbf5b..03431be2e 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/PgTestBase.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/PgTestBase.kt @@ -11,7 +11,7 @@ import naksha.common.test.CommonTestConstants import naksha.model.* import naksha.model.objects.NakshaCollection import naksha.model.objects.NakshaFeature -import naksha.model.objects.NakshaMap +import naksha.model.objects.NakshaCatalog import naksha.model.objects.NakshaStorage import naksha.model.request.* import naksha.psql.PgTest.PgTest_C.TEST_MAP_ID @@ -36,12 +36,12 @@ abstract class PgTestBase( collection: NakshaCollection? = NakshaCollection(""), mapId: String? = null, ) { - private var _map: NakshaMap? = null + private var _map: NakshaCatalog? = null /** * The selected map, throws an exception, when read before initialized _(the constructor by default initialized this)_. */ - val map: NakshaMap + val map: NakshaCatalog get() = assertNotNull(_map, "Illegal state, no map used by the test") private var _collection: NakshaCollection? = null @@ -90,7 +90,7 @@ abstract class PgTestBase( * @param mapId the `id` of the map to use within this test. If `null`, then [PgTest.TEST_MAP_ID] is used, if being an empty string _(`""`)_, then the camel-cased name of the test-class being used. * @return the map object. */ - fun initMap(mapId: String?): NakshaMap { + fun initMap(mapId: String?): NakshaCatalog { val map_id = ensureMapId(mapId) var mapRef = initializedMaps[map_id] if (mapRef == null) { @@ -103,14 +103,14 @@ abstract class PgTestBase( dropMap(map_id) // Create the map. - var map = NakshaMap(map_id) + var map = NakshaCatalog(map_id) val request = WriteRequest() request.writes += Write().createMap(map) val response = executeWrite(request) val features = response.features assertEquals(1, features.size) val feature = features.first() - map = assertNotNull(feature).proxy(NakshaMap::class) + map = assertNotNull(feature).proxy(NakshaCatalog::class) mapRef = MapAndCollections(map) initializedMaps[map_id] = assertNotNull(mapRef) logger.info("Created test map: '$map_id'") @@ -125,27 +125,27 @@ abstract class PgTestBase( * @param mapId the `id` of the map to initialize and use. * @return the map object. */ - fun useMap(mapId: String?): NakshaMap { + fun useMap(mapId: String?): NakshaCatalog { val map = initMap(mapId) this._map = map return map } - fun initCollection(mapId: String, collectionId: String): Pair + fun initCollection(mapId: String, collectionId: String): Pair = initCollection(NakshaCollection(collectionId, mapId)) - fun initCollection(collection: NakshaCollection): Pair { + fun initCollection(collection: NakshaCollection): Pair { if (collection.id == "") { collection.id = defaultName } else { Naksha.verifyId(collection.id) } - if (collection.mapId == null || collection.mapId == "") { - collection.mapId = map.id + if (collection.catalogId == null || collection.catalogId == "") { + collection.catalogId = map.id } else { - Naksha.verifyId(collection.mapId) + Naksha.verifyId(collection.catalogId) } - val mapId = collection.mapId + val mapId = collection.catalogId initMap(mapId) val mapRef = assertNotNull(initializedMaps[mapId], "The map '$mapId' failed to initialized") val colId = collection.id @@ -154,7 +154,7 @@ abstract class PgTestBase( lock.acquire().use { existing = mapRef.collections[colId] if (existing == null) { - collection.mapId = mapId + collection.catalogId = mapId val request = WriteRequest() request.writes += Write().createCollection(collection) storage.newWriteSession(newSessionOptions()).use { session -> @@ -173,10 +173,10 @@ abstract class PgTestBase( return Pair(mapRef.map, assertNotNull(existing)) } - fun useCollection(collectionId: String, mapId: String = map.id): Pair + fun useCollection(collectionId: String, mapId: String = map.id): Pair = useCollection(NakshaCollection(collectionId, mapId)) - fun useCollection(collection: NakshaCollection): Pair { + fun useCollection(collection: NakshaCollection): Pair { val pair = initCollection(collection) this._map = pair.first this._collection = pair.second @@ -297,7 +297,7 @@ abstract class PgTestBase( /** * The map. */ - val map: NakshaMap, + val map: NakshaCatalog, /** * All collections in the map, created for the tests by `id`. diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesAll.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesAll.kt index fcc462c37..274626ede 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesAll.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesAll.kt @@ -19,7 +19,7 @@ class ReadFeaturesAll : PgTestBase() { val featuresToCreate = randomFeatures(COUNT) val writeFeaturesReq = WriteRequest().apply { featuresToCreate.forEach { featureToCreate -> - add(Write().createFeature(collection.mapId, collection.id, featureToCreate)) + add(Write().createFeature(collection.catalogId, collection.id, featureToCreate)) } } val writeFeaturesResp = executeWrite(writeFeaturesReq) @@ -35,7 +35,7 @@ class ReadFeaturesAll : PgTestBase() { @Test fun shouldReturnAllFeatures() { executeRead(ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id }).apply { assertEquals(COUNT, features.size) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByGeometryTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByGeometryTest.kt index 8e1ed50f2..1a8ac73e8 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByGeometryTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByGeometryTest.kt @@ -33,7 +33,7 @@ class ReadFeaturesByGeometryTest : PgTestBase(collection = null, mapId = "") { // And: reading feature val retrievedFeatures = executeRead( ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id featureIds += feature.id } @@ -66,7 +66,7 @@ class ReadFeaturesByGeometryTest : PgTestBase(collection = null, mapId = "") { // And: reading feature val retrievedFeatures = executeRead( ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id featureIds += feature.id } @@ -251,7 +251,7 @@ class ReadFeaturesByGeometryTest : PgTestBase(collection = null, mapId = "") { private fun executeSpatialQuery(spatialQuery: ISpatialQuery): SuccessResponse { return executeRead(ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id query.spatial = spatialQuery }) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByGuuidTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByGuuidTest.kt index 584383d57..082cd9286 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByGuuidTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByGuuidTest.kt @@ -25,7 +25,7 @@ class ReadFeaturesByGuuidTest : // When val readByGuid = ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id guids = GuidList().apply { add(guuidById[inputFeature1.id]) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByMetadataTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByMetadataTest.kt index 4aaece5ea..a781ec057 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByMetadataTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByMetadataTest.kt @@ -233,7 +233,7 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { // And: execute val featuresByAppIdAndAuthor = executeRead(ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id query.metadata = MetaAnd( MetaQuery(MetaColumn.author(), StringOp.EQUALS, author), @@ -391,7 +391,7 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { // And: execute val featuresByAppIdAndAuthor = executeRead(ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id query.metadata = MetaOr( MetaQuery(MetaColumn.author(), StringOp.EQUALS, "this_is_totally_off"), @@ -421,7 +421,7 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { // And: History table is queried for everything besides CREATED val getHistoryWithoutUpdates = ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id queryHistory = true queryDeleted = true @@ -441,7 +441,7 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { private fun insertFeatureAndGetXyz(feature: NakshaFeature): XyzNs { insertFeature(feature = feature) val persistedFeatureResponse = executeRead(ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id featureIds += feature.id }) @@ -454,7 +454,7 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { private fun executeMetaQuery(metaQuery: IMetaQuery): SuccessResponse { return executeRead(ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id query.metadata = metaQuery }) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByOtherTns.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByOtherTns.kt index 7cf2ac783..f2580602c 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByOtherTns.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByOtherTns.kt @@ -49,7 +49,7 @@ class ReadFeaturesByOtherTns : PgTestBase( arrayOf(updatedVersion) ) val byNextTnResp = executeRead(ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id query.metadata = nextVersionQuery queryHistory = true diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByRefTilesTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByRefTilesTest.kt index bb380d344..69cf993e7 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByRefTilesTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByRefTilesTest.kt @@ -46,7 +46,7 @@ class ReadFeaturesByRefTilesTest : PgTestBase(collection = null, mapId = "") { // Given: val getFeaturesFromZagrebAndPrague = ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection!!.id query.refTiles += listOf(zagrebTileLv12.intKey, pragueTileLv12.intKey) } @@ -67,7 +67,7 @@ class ReadFeaturesByRefTilesTest : PgTestBase(collection = null, mapId = "") { // Given: val getFeaturesFromBologna = ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id query.refTiles += bolognaTileLv12.intKey } diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByTagsTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByTagsTest.kt index 044f5396e..ae43a17c4 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByTagsTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByTagsTest.kt @@ -1,13 +1,11 @@ package naksha.psql import naksha.model.TagList -import naksha.model.objects.NakshaCollection import naksha.model.objects.NakshaFeature import naksha.model.request.ReadFeatures import naksha.model.request.SuccessResponse import naksha.model.request.query.* import naksha.model.RandomFeatures -import naksha.psql.PgTest.PgTest_C.TEST_MAP_ID import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -225,7 +223,7 @@ class ReadFeaturesByTagsTest : PgTestBase() { private fun executeTagsQuery(tagQuery: ITagQuery): SuccessResponse { return executeRead(ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection!!.id query.tags = tagQuery }) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadHistoryTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadHistoryTest.kt index 0819fc387..cd0c9535f 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadHistoryTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadHistoryTest.kt @@ -23,7 +23,7 @@ class ReadHistoryTest : PgTestBase() { val featuresToCreate = randomFeatures(COUNT) val writeFeaturesReq = WriteRequest().apply { featuresToCreate.forEach { featureToCreate -> - add(Write().createFeature(collection.mapId, collection.id, featureToCreate)) + add(Write().createFeature(collection.catalogId, collection.id, featureToCreate)) } } val writeFeaturesResp = executeWrite(writeFeaturesReq) @@ -81,7 +81,7 @@ class ReadHistoryTest : PgTestBase() { // Clear cache, and read the history of the feature. Naksha.cache.clear() executeRead(ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds.add(collection.id) featureIds.add(featureId) queryHistory = true @@ -121,7 +121,7 @@ class ReadHistoryTest : PgTestBase() { } executeRead(ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds.add(collection.id) featureIds.add(featureId) queryHistory = true @@ -143,7 +143,7 @@ class ReadHistoryTest : PgTestBase() { } executeRead(ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds.add(collection.id) featureIds.add(featureId) queryHistory = true diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadLimitTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadLimitTest.kt index 4f7f29027..f834b156d 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadLimitTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadLimitTest.kt @@ -21,7 +21,7 @@ class ReadLimitTest : PgTestBase() { // When val readWithLimit = executeRead(ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id limit = 2 }) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadOrderedTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadOrderedTest.kt index 9c8198f71..4e4846173 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadOrderedTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadOrderedTest.kt @@ -28,7 +28,7 @@ class ReadOrderedTest : PgTestBase() { val featuresToCreate = randomFeatures(COUNT) val writeFeaturesReq = WriteRequest().apply { featuresToCreate.forEach { featureToCreate -> - add(Write().createFeature(collection.mapId, collection.id, featureToCreate)) + add(Write().createFeature(collection.catalogId, collection.id, featureToCreate)) } } val writeFeaturesResp = executeWrite(writeFeaturesReq) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/RecreateAfterDeleteTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/RecreateAfterDeleteTest.kt index c9458273e..7541b59c1 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/RecreateAfterDeleteTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/RecreateAfterDeleteTest.kt @@ -40,7 +40,7 @@ class RecreateAfterDeleteTest : PgTestBase() { ) Naksha.cache.clear() val updated = executeRead(ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id featureIds += featureId }).features.first()!! @@ -54,7 +54,7 @@ class RecreateAfterDeleteTest : PgTestBase() { // Confirm tombstone is visible via queryDeleted=true Naksha.cache.clear() val deleted = executeRead(ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id featureIds += featureId queryDeleted = true @@ -65,7 +65,7 @@ class RecreateAfterDeleteTest : PgTestBase() { // Confirm feature is invisible in a normal read Naksha.cache.clear() val notFound = executeRead(ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id featureIds += featureId }) @@ -85,7 +85,7 @@ class RecreateAfterDeleteTest : PgTestBase() { // Confirm feature is visible again in a normal read Naksha.cache.clear() val found = executeRead(ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id featureIds += featureId }) @@ -96,7 +96,7 @@ class RecreateAfterDeleteTest : PgTestBase() { // History after auto-purge: DELETED (archived tombstone), UPDATED, CREATED (old lifecycle). // Total = 1 (HEAD) + 3 (history) = 4, in descending version order. val historyOnly = executeRead(ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id featureIds += featureId queryHistory = true @@ -111,7 +111,7 @@ class RecreateAfterDeleteTest : PgTestBase() { // queryHistory + queryDeleted: same result — no tombstone in HEAD (was auto-purged), // so queryDeleted=true adds nothing here. val full = executeRead(ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id featureIds += featureId queryHistory = true diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/TupleNumberPersistenceTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/TupleNumberPersistenceTest.kt index 756ef6270..fd7af33bb 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/TupleNumberPersistenceTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/TupleNumberPersistenceTest.kt @@ -67,8 +67,8 @@ class TupleNumberPersistenceTest : PgTestBase(collection = null, mapId = "") { // And: `storeNumber` checks out storage.adminConnection().use { conn -> - val pgMap = storage.adminMap.getPgMapById(conn, collection.mapId!!) - require(pgMap != null) { "Missing map ${collection.mapId}" } + val pgMap = storage.adminMap.getPgMapById(conn, collection.catalogId!!) + require(pgMap != null) { "Missing map ${collection.catalogId}" } val pgCollection = pgMap.getPgCollectionById(conn, collection.id) require(pgCollection != null) { "Missing collection ${collection.id}" } assertEquals(storage.number, persistedTuple.tupleNumber.databaseNumber) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/UpdateFeatureTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/UpdateFeatureTest.kt index b6491eaa4..ee113eedd 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/UpdateFeatureTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/UpdateFeatureTest.kt @@ -93,7 +93,7 @@ class UpdateFeatureTest : PgTestBase(collection = null, mapId = "") { // READ FEATURE HISTORY Naksha.cache.clear() val readResp = executeRead(ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id featureIds += initialFeature.id queryHistory = true @@ -116,7 +116,7 @@ class UpdateFeatureTest : PgTestBase(collection = null, mapId = "") { assertEquals(2, updatedTuple.getInt(naksha.model.objects.StandardMembers.ChangeCountXyz)) assertEquals(createdTuple.getByteArray(naksha.model.objects.StandardMembers.Geometry), updatedTuple.getByteArray(naksha.model.objects.StandardMembers.Geometry)) assertEquals(createdTuple.getString(naksha.model.objects.StandardMembers.XyzTags), updatedTuple.getString(naksha.model.objects.StandardMembers.XyzTags)) - assertNotEquals(createdTuple.jbonBytes, updatedTuple.jbonBytes) + assertNotEquals(createdTuple.featureBytes, updatedTuple.featureBytes) assertEquals(createdTuple.getByteArray(naksha.model.objects.StandardMembers.ReferencePoint), updatedTuple.getByteArray(naksha.model.objects.StandardMembers.ReferencePoint)) assertNull(createdTuple.decodeFeature()?.properties["new_attr"]) assertEquals("some_value", updatedTuple.decodeFeature()?.properties["new_attr"]) @@ -260,7 +260,7 @@ class UpdateFeatureTest : PgTestBase(collection = null, mapId = "") { private fun fetchSingleFeature(id: String): NakshaFeature { Naksha.cache.clear() val readFeatureResp = executeRead(ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id featureIds += id }) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/UpsertFeatureTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/UpsertFeatureTest.kt index b3d9af008..c2b222fa3 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/UpsertFeatureTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/UpsertFeatureTest.kt @@ -35,7 +35,7 @@ class UpsertFeatureTest : PgTestBase() { // And: Retrieving feature by id val retrievedFeatures = executeRead(ReadFeatures().apply { - mapId = collection.mapId + mapId = collection.catalogId collectionIds += collection.id featureIds += initialFeature.id queryHistory = true diff --git a/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlAdminMap.kt b/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlAdminMap.kt index fbd6e1547..5771ad466 100644 --- a/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlAdminMap.kt +++ b/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlAdminMap.kt @@ -5,7 +5,7 @@ import naksha.base.Platform.PlatformCompanion.logger import naksha.jbon.JbDictionary import naksha.model.* import naksha.model.objects.NakshaCollection -import naksha.model.objects.NakshaMap +import naksha.model.objects.NakshaCatalog import naksha.psql.PgUtil.PgUtilCompanion.quoteLiteral /** @@ -35,11 +35,11 @@ class PsqlAdminMap internal constructor( if (context is NakshaCollection) { val collectionEncoding = context.dataEncoding if (collectionEncoding != null) return collectionEncoding - val mapId = context.mapId ?: return Naksha.DEFAULT_DATA_ENCODING + val mapId = context.catalogId ?: return Naksha.DEFAULT_DATA_ENCODING val pgMap = getPgMapById(null, mapId) return pgMap?.head?.dataEncoding ?: Naksha.DEFAULT_DATA_ENCODING } - if (context is NakshaMap) { + if (context is NakshaCatalog) { return context.dataEncoding ?: Naksha.DEFAULT_DATA_ENCODING } return Naksha.DEFAULT_DATA_ENCODING diff --git a/here-naksha-lib-psql/src/jvmTest/java/naksha/psql/DeleteFeatureByVersionTest.java b/here-naksha-lib-psql/src/jvmTest/java/naksha/psql/DeleteFeatureByVersionTest.java index c989c78d3..10fc66f32 100644 --- a/here-naksha-lib-psql/src/jvmTest/java/naksha/psql/DeleteFeatureByVersionTest.java +++ b/here-naksha-lib-psql/src/jvmTest/java/naksha/psql/DeleteFeatureByVersionTest.java @@ -151,7 +151,7 @@ void shouldFailEntireRequestIfPartialDeletionFails() { private NakshaFeatureList getFeatureByIds(String... ids) { ReadFeatures readAll = new ReadFeatures() - .withMapId(getCollection().getMapId()) + .withMapId(getCollection().getCatalogId()) .addCollectionId(getCollection().getId()); readAll.setFeatureIds(StringList.of(ids)); return executeRead(readAll, newSessionOptions()).getFeatures(); diff --git a/here-naksha-lib-psql/src/jvmTest/kotlin/naksha/psql/PsqlErrorMappingTest.kt b/here-naksha-lib-psql/src/jvmTest/kotlin/naksha/psql/PsqlErrorMappingTest.kt index 566e6bb5f..036ee0b3c 100644 --- a/here-naksha-lib-psql/src/jvmTest/kotlin/naksha/psql/PsqlErrorMappingTest.kt +++ b/here-naksha-lib-psql/src/jvmTest/kotlin/naksha/psql/PsqlErrorMappingTest.kt @@ -34,7 +34,7 @@ class PsqlErrorMappingTest : PgTestBase() { fun shouldReturnConflictingCollectionError() { // Given val createAlreadyExistingCollection = WriteRequest().add( - Write().createCollection(NakshaCollection(collection.id, collection.mapId)) + Write().createCollection(NakshaCollection(collection.id, collection.catalogId)) ) // When diff --git a/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/ViewLayer.java b/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/ViewLayer.java index 94a9db412..7247f5006 100644 --- a/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/ViewLayer.java +++ b/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/ViewLayer.java @@ -53,7 +53,7 @@ public ViewLayer(@NotNull IStorage storage, @Nullable String mapId, @NotNull Str * @since 2.0 */ public ViewLayer(@NotNull IStorage storage, @NotNull NakshaCollection collection) { - this(storage, collection.getMapId(), collection.getId()); + this(storage, collection.getCatalogId(), collection.getId()); } /** diff --git a/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/ViewReadSession.java b/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/ViewReadSession.java index d45c504fc..52a07367c 100644 --- a/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/ViewReadSession.java +++ b/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/ViewReadSession.java @@ -30,8 +30,7 @@ import naksha.model.*; import naksha.model.objects.NakshaCollection; -import naksha.model.objects.NakshaMap; -import naksha.model.objects.NakshaStorage; +import naksha.model.objects.NakshaCatalog; import naksha.model.request.*; import naksha.model.request.query.AnyOp; import naksha.model.request.query.IPropertyQuery; @@ -257,22 +256,22 @@ public void loadTuples(@NotNull List featureTuples, int } @Override - public @Nullable NakshaMap getMapById(@NotNull String mapId) { + public @Nullable NakshaCatalog getMapById(@NotNull String mapId) { throw new UnsupportedOperationException(); } @Override - public @Nullable NakshaMap getMapByNumber(int mapNumber) { + public @Nullable NakshaCatalog getMapByNumber(int mapNumber) { throw new UnsupportedOperationException(); } @Override - public @Nullable NakshaCollection getCollectionById(@NotNull NakshaMap map, @NotNull String collectionId) { + public @Nullable NakshaCollection getCollectionById(@NotNull NakshaCatalog map, @NotNull String collectionId) { throw new UnsupportedOperationException(); } @Override - public @Nullable NakshaCollection getCollectionByNumber(@NotNull NakshaMap map, int collectionNumber) { + public @Nullable NakshaCollection getCollectionByNumber(@NotNull NakshaCatalog map, int collectionNumber) { throw new UnsupportedOperationException(); } diff --git a/here-naksha-lib-view/src/jvmTest/java/com/here/naksha/lib/view/MockReadSession.java b/here-naksha-lib-view/src/jvmTest/java/com/here/naksha/lib/view/MockReadSession.java index 9cd4fb278..733bfb792 100644 --- a/here-naksha-lib-view/src/jvmTest/java/com/here/naksha/lib/view/MockReadSession.java +++ b/here-naksha-lib-view/src/jvmTest/java/com/here/naksha/lib/view/MockReadSession.java @@ -24,7 +24,7 @@ import naksha.model.IStorage; import naksha.model.SessionOptions; import naksha.model.objects.NakshaCollection; -import naksha.model.objects.NakshaMap; +import naksha.model.objects.NakshaCatalog; import naksha.model.request.*; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -103,22 +103,22 @@ public boolean isClosed() { } @Override - public @Nullable NakshaMap getMapById(@NotNull String mapId) { + public @Nullable NakshaCatalog getMapById(@NotNull String mapId) { return null; } @Override - public @Nullable NakshaMap getMapByNumber(int mapNumber) { + public @Nullable NakshaCatalog getMapByNumber(int mapNumber) { return null; } @Override - public @Nullable NakshaCollection getCollectionById(@NotNull NakshaMap map, @NotNull String collectionId) { + public @Nullable NakshaCollection getCollectionById(@NotNull NakshaCatalog map, @NotNull String collectionId) { return null; } @Override - public @Nullable NakshaCollection getCollectionByNumber(@NotNull NakshaMap map, int collectionNumber) { + public @Nullable NakshaCollection getCollectionByNumber(@NotNull NakshaCatalog map, int collectionNumber) { return null; } diff --git a/here-naksha-lib-view/src/jvmTest/java/com/here/naksha/lib/view/PsqlTests.java b/here-naksha-lib-view/src/jvmTest/java/com/here/naksha/lib/view/PsqlTests.java index 83e84c2c5..f1ea2acb4 100644 --- a/here-naksha-lib-view/src/jvmTest/java/com/here/naksha/lib/view/PsqlTests.java +++ b/here-naksha-lib-view/src/jvmTest/java/com/here/naksha/lib/view/PsqlTests.java @@ -25,7 +25,7 @@ import naksha.model.NakshaContext; import naksha.model.SessionOptions; import naksha.model.objects.NakshaFeature; -import naksha.model.objects.NakshaMap; +import naksha.model.objects.NakshaCatalog; import naksha.model.objects.NakshaStorage; import naksha.model.request.*; import org.jetbrains.annotations.NotNull; @@ -67,7 +67,7 @@ final boolean runTest() { public static final String TEST_APP_ID = "test_app"; public static final String TEST_AUTHOR = "test_author"; static IStorage storage; - static NakshaMap map; + static NakshaCatalog map; static @Nullable NakshaContext nakshaContext; protected static @NotNull SuccessResponse assertSuccess(@NotNull Response response) { @@ -107,10 +107,10 @@ static void beforeTest() { executeWrite(new WriteRequest().add(new Write().deleteMapById(TEST_MAP_ID))); // Create the map. - SuccessResponse response = executeWrite(new WriteRequest().add(new Write().createMap(new NakshaMap(TEST_MAP_ID)))); + SuccessResponse response = executeWrite(new WriteRequest().add(new Write().createMap(new NakshaCatalog(TEST_MAP_ID)))); assertEquals(1, response.getFeatures().size()); NakshaFeature raw = response.getFeatures().get(0); assertNotNull(raw); - map = javaProxy(raw, NakshaMap.class); + map = javaProxy(raw, NakshaCatalog.class); } } diff --git a/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/HttpStorageReadSession.java b/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/HttpStorageReadSession.java index 8429edc67..f7ed05d50 100644 --- a/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/HttpStorageReadSession.java +++ b/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/HttpStorageReadSession.java @@ -27,7 +27,7 @@ import naksha.model.NakshaError; import naksha.model.SessionOptions; import naksha.model.objects.NakshaCollection; -import naksha.model.objects.NakshaMap; +import naksha.model.objects.NakshaCatalog; import naksha.model.request.ErrorResponse; import naksha.model.request.FeatureTuple; import naksha.model.request.Request; @@ -136,12 +136,12 @@ public Response executeParallel(@NotNull Request request) { } @Override - public @Nullable NakshaMap getMapById(@NotNull String mapId) { + public @Nullable NakshaCatalog getMapById(@NotNull String mapId) { throw new NotImplementedException("Not supported by HTTP storage"); } @Override - public @Nullable NakshaMap getMapByNumber(int mapNumber) { + public @Nullable NakshaCatalog getMapByNumber(int mapNumber) { return null; } @@ -151,7 +151,7 @@ public void loadTuples(@NotNull List featureTuples, int } @Override - public @Nullable NakshaCollection getCollectionByNumber(@NotNull NakshaMap map, int collectionNumber) { + public @Nullable NakshaCollection getCollectionByNumber(@NotNull NakshaCatalog map, int collectionNumber) { throw new NotImplementedException("Not supported by HTTP storage"); } @@ -161,7 +161,7 @@ public void loadTuples(@NotNull List featureTuples, int } @Override - public @Nullable NakshaCollection getCollectionById(@NotNull NakshaMap map, @NotNull String collectionId) { + public @Nullable NakshaCollection getCollectionById(@NotNull NakshaCatalog map, @NotNull String collectionId) { // TODO: Technically, this translates into creating an ReadCollections query! throw new NotImplementedException("Not supported by HTTP storage"); } From cdfc3074a014a629db240413008d2a1ddd8a1065 Mon Sep 17 00:00:00 2001 From: kkin-here <284318677+kkin-here@users.noreply.github.com> Date: Mon, 15 Jun 2026 13:31:29 +0200 Subject: [PATCH 16/57] Handle Xyz members (#606) Signed-off-by: kkin-here <284318677+kkin-here@users.noreply.github.com> --- .../naksha/model/objects/StandardIndices.kt | 186 +++--------------- .../kotlin/naksha/model/objects/XyzIndices.kt | 140 ++++++++++++- .../kotlin/naksha/model/MemberTest.kt | 2 +- .../commonMain/kotlin/naksha/psql/PgIndex.kt | 2 +- 4 files changed, 165 insertions(+), 165 deletions(-) diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardIndices.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardIndices.kt index 99758c13f..f8e00d144 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardIndices.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardIndices.kt @@ -7,7 +7,12 @@ import kotlin.js.JsStatic import kotlin.jvm.JvmField /** - * The canonical set of standard indices that every Naksha storage understands. + * The canonical, storage-managed indices that every Naksha storage understands. + * + * These are flavour-independent: the [MANDATORY] indices are always present, the standard optional + * [GistGeometry] indexes the standard [StandardMembers.Geometry] member, and the [SPECIAL] indices + * are declared explicitly per collection (e.g. `naksha~transactions`). The default index set for a + * Data-Hub (XYZ) compatible collection lives in [XyzIndices], the index counterpart of [XyzMembers]. * @since 3.0 */ @JsExport @@ -59,8 +64,7 @@ class StandardIndices private constructor() { val GlobalBookNumber = Index("gbn", IndexType.BTREE, "gbn").withInternal(true) /** - * All mandatory indices, in declaration order. These are always created by the storage and must - * not appear in [NakshaCollection.indices]. + * All mandatory indices, in declaration order. These are always created by the storage. * @since 3.0 */ @JvmField @JsStatic @@ -74,7 +78,26 @@ class StandardIndices private constructor() { val MANDATORY_NAMES: Set = MANDATORY.map { it.name }.toHashSet() // ------------------------------------------------------------------------- - // Special indices — not in MANDATORY or DEFAULT; declared explicitly per collection + // Standard optional indices — index a standard optional member (see StandardMembers). + // ------------------------------------------------------------------------- + + /** + * `gist_geo` — spatial ([IndexType.SPATIAL]) GIST index over the geometry member + * (WHERE `geo IS NOT NULL`). + * + * Geometry is a **standard** member (part of the GeoJSON standard, see + * [StandardMembers.Geometry]), so its index is defined here. An index refers to a member by + * its identity (name + type), not by JSON path, so this same index applies to the XYZ + * geometry member ([XyzMembers.XyzGeometry], also named `geo`); [XyzIndices.ALL] references + * it rather than redeclaring it. + * @since 3.0 + */ + @JvmField @JsStatic + val GistGeometry = Index("gist_geo", IndexType.SPATIAL, "geo") + + // ------------------------------------------------------------------------- + // Special indices — not added automatically; declared explicitly per collection + // (e.g. naksha~transactions). The default XYZ index set lives in XyzIndices. // ------------------------------------------------------------------------- /** @@ -101,147 +124,6 @@ class StandardIndices private constructor() { @JvmField @JsStatic val GlobalVersion = Index("gv", IndexType.BTREE, "gv") - // ------------------------------------------------------------------------- - // Default indices — created when NakshaCollection.indices is null - // ------------------------------------------------------------------------- - - /** - * `gist_geo` — spatial ([IndexType.SPATIAL]) GIST index over the geometry member - * (WHERE `geo IS NOT NULL`). Default index. See [StandardMembers.Geometry] - * @since 3.0 - */ - @JvmField @JsStatic - val GistGeometry = Index("gist_geo", IndexType.SPATIAL, "geo") - - /** - * `here_tile` — index on `here_tile`, `fn`, `version` (WHERE `here_tile IS NOT NULL`). - * Default index. - * @since 3.0 - */ - @JvmField @JsStatic - val HereTile = Index("here_tile", IndexType.BTREE, "here_tile", "fn", "version") - - /** - * `app_id` — index on `app_id`, `updated_at`, `fn`, `version` (WHERE `app_id IS NOT NULL`). - * Default index. - * @since 3.0 - */ - @JvmField @JsStatic - val AppId = Index("app_id", IndexType.BTREE, "app_id", "updated_at", "fn", "version") - - /** - * `author` — index on the effective author and author timestamp, `fn`, `version` - * (WHERE effective author IS NOT NULL). Default index. - * @since 3.0 - */ - @JvmField @JsStatic - val Author = Index("author", IndexType.BTREE, "author", "author_ts", "fn", "version") - - /** - * `tags` — inverted ([IndexType.SET]) index over the `tags` member, supporting element - * containment queries. Default index. - * @since 3.0 - */ - @JvmField @JsStatic - val Tags = Index("tags", IndexType.SET, "tags") - - /** - * `feature_type` — index on `ft`, `fn`, `version` (WHERE `ft IS NOT NULL`). Default index. - * @since 3.0 - */ - @JvmField @JsStatic - val FeatureType = Index("feature_type", IndexType.BTREE, "ft", "fn", "version") - - /** - * `cv0` — index on custom numeric value 0, `fn`, `version` (WHERE `cv0 IS NOT NULL`). Default index. - * @since 3.0 - */ - @JvmField @JsStatic - val CustomValue0 = Index("cv0", IndexType.BTREE, "cv0", "fn", "version") - - /** - * `cv1` — index on custom numeric value 1, `fn`, `version` (WHERE `cv1 IS NOT NULL`). Default index. - * @since 3.0 - */ - @JvmField @JsStatic - val CustomValue1 = Index("cv1", IndexType.BTREE, "cv1", "fn", "version") - - /** - * `cv2` — index on custom numeric value 2, `fn`, `version` (WHERE `cv2 IS NOT NULL`). Default index. - * @since 3.0 - */ - @JvmField @JsStatic - val CustomValue2 = Index("cv2", IndexType.BTREE, "cv2", "fn", "version") - - /** - * `cv3` — index on custom numeric value 3, `fn`, `version` (WHERE `cv3 IS NOT NULL`). Default index. - * @since 3.0 - */ - @JvmField @JsStatic - val CustomValue3 = Index("cv3", IndexType.BTREE, "cv3", "fn", "version") - - /** - * `cs0` — index on custom string value 0, `fn`, `version` (WHERE `cs0 IS NOT NULL`). Default index. - * @since 3.0 - */ - @JvmField @JsStatic - val CustomString0 = Index("cs0", IndexType.BTREE, "cs0", "fn", "version") - - /** - * `cs1` — index on custom string value 1, `fn`, `version` (WHERE `cs1 IS NOT NULL`). Default index. - * @since 3.0 - */ - @JvmField @JsStatic - val CustomString1 = Index("cs1", IndexType.BTREE, "cs1", "fn", "version") - - /** - * `cs2` — index on custom string value 2, `fn`, `version` (WHERE `cs2 IS NOT NULL`). Default index. - * @since 3.0 - */ - @JvmField @JsStatic - val CustomString2 = Index("cs2", IndexType.BTREE, "cs2", "fn", "version") - - /** - * `cs3` — index on custom string value 3, `fn`, `version` (WHERE `cs3 IS NOT NULL`). Default index. - * @since 3.0 - */ - @JvmField @JsStatic - val CustomString3 = Index("cs3", IndexType.BTREE, "cs3", "fn", "version") - - /** - * `ref_point` — spatial ([IndexType.SPATIAL]) index over the reference-point geometry member - * (WHERE `ref_point IS NOT NULL`). Default index. - * @since 3.0 - */ - @JvmField @JsStatic - val ReferencePoint = Index("ref_point", IndexType.SPATIAL, "ref_point") - - /** - * All default indices (created when [NakshaCollection.indices] is `null`), in declaration order. - * - * Does **not** include the [MANDATORY] indices — those are always present regardless. - * @since 3.0 - */ - @JvmField @JsStatic - val XYZ_INDICES: List = listOf( - HereTile, - AppId, - Author, - Tags, - FeatureType, - CustomValue0, CustomValue1, CustomValue2, CustomValue3, - CustomString0, CustomString1, CustomString2, CustomString3, - ReferencePoint, - GistGeometry, - ) - - /** - * The names of all [XYZ_INDICES] indices, for fast lookup. - * @since 3.0 - */ - @JvmField @JsStatic - val DEFAULT_NAMES: Set = XYZ_INDICES.map { it.name }.toHashSet() - /** * All special indices — not added automatically but recognised by all storage implementations. * @since 3.0 @@ -255,19 +137,5 @@ class StandardIndices private constructor() { */ @JvmField @JsStatic val SPECIAL_NAMES: Set = SPECIAL.map { it.name }.toHashSet() - - /** - * All standard indices: [MANDATORY] followed by [XYZ_INDICES] followed by [SPECIAL]. - * @since 3.0 - */ - @JvmField @JsStatic - val ALL: List = MANDATORY + XYZ_INDICES + SPECIAL - - /** - * The names of all standard indices, for fast lookup. - * @since 3.0 - */ - @JvmField @JsStatic - val ALL_NAMES: Set = ALL.map { it.name }.toHashSet() } } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzIndices.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzIndices.kt index e990231bc..2d7e69156 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzIndices.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzIndices.kt @@ -7,7 +7,16 @@ import kotlin.js.JsStatic import kotlin.jvm.JvmField /** - * The canonical set of standard indices that every Naksha storage understands. + * The default indices created for a Data-Hub (XYZ) compatible collection. + * + * This is the index counterpart of [XyzMembers]: it indexes the members declared there (e.g. + * [XyzMembers.XyzTags], [XyzMembers.XyzAppId], [XyzMembers.XyzHereTile]) and is applied via + * [NakshaCollection.withXyzIndices]. + * + * An index refers to a member by its identity (name + type), not by JSON path, so indices that + * target a member which is also standard (e.g. the geometry member, see [StandardIndices.GistGeometry]) + * are **referenced** from [StandardIndices] rather than redeclared here. The storage-managed indices + * that every collection always has live in [StandardIndices.MANDATORY]. * @since 3.0 */ @JsExport @@ -15,7 +24,130 @@ class XyzIndices private constructor() { companion object XyzIndices_C { - // TODO: Please fix me, we need an own listOf(...)! - @JvmField val ALL: List = StandardIndices.ALL + /** + * `here_tile` — index on `here_tile`, `fn`, `version` (WHERE `here_tile IS NOT NULL`). + * See [XyzMembers.XyzHereTile]. + * @since 3.0 + */ + @JvmField @JsStatic + val XyzHereTile = Index("here_tile", IndexType.BTREE, "here_tile", "fn", "version") + + /** + * `app_id` — index on `app_id`, `updated_at`, `fn`, `version` (WHERE `app_id IS NOT NULL`). + * See [XyzMembers.XyzAppId]. + * @since 3.0 + */ + @JvmField @JsStatic + val XyzAppId = Index("app_id", IndexType.BTREE, "app_id", "updated_at", "fn", "version") + + /** + * `author` — index on the effective author and author timestamp, `fn`, `version` + * (WHERE effective author IS NOT NULL). See [XyzMembers.XyzAuthor]. + * @since 3.0 + */ + @JvmField @JsStatic + val XyzAuthor = Index("author", IndexType.BTREE, "author", "author_ts", "fn", "version") + + /** + * `tags` — inverted ([IndexType.SET]) index over the `tags` member, supporting element + * containment queries. See [XyzMembers.XyzTags]. + * @since 3.0 + */ + @JvmField @JsStatic + val XyzTags = Index("tags", IndexType.SET, "tags") + + /** + * `feature_type` — index on `ft`, `fn`, `version` (WHERE `ft IS NOT NULL`). + * See [XyzMembers.XyzFeatureType]. + * @since 3.0 + */ + @JvmField @JsStatic + val XyzFeatureType = Index("feature_type", IndexType.BTREE, "ft", "fn", "version") + + /** + * `cv0` — index on custom numeric value 0, `fn`, `version` (WHERE `cv0 IS NOT NULL`). + * @since 3.0 + */ + @JvmField @JsStatic + val XyzCustomValue0 = Index("cv0", IndexType.BTREE, "cv0", "fn", "version") + + /** + * `cv1` — index on custom numeric value 1, `fn`, `version` (WHERE `cv1 IS NOT NULL`). + * @since 3.0 + */ + @JvmField @JsStatic + val XyzCustomValue1 = Index("cv1", IndexType.BTREE, "cv1", "fn", "version") + + /** + * `cv2` — index on custom numeric value 2, `fn`, `version` (WHERE `cv2 IS NOT NULL`). + * @since 3.0 + */ + @JvmField @JsStatic + val XyzCustomValue2 = Index("cv2", IndexType.BTREE, "cv2", "fn", "version") + + /** + * `cv3` — index on custom numeric value 3, `fn`, `version` (WHERE `cv3 IS NOT NULL`). + * @since 3.0 + */ + @JvmField @JsStatic + val XyzCustomValue3 = Index("cv3", IndexType.BTREE, "cv3", "fn", "version") + + /** + * `cs0` — index on custom string value 0, `fn`, `version` (WHERE `cs0 IS NOT NULL`). + * @since 3.0 + */ + @JvmField @JsStatic + val XyzCustomString0 = Index("cs0", IndexType.BTREE, "cs0", "fn", "version") + + /** + * `cs1` — index on custom string value 1, `fn`, `version` (WHERE `cs1 IS NOT NULL`). + * @since 3.0 + */ + @JvmField @JsStatic + val XyzCustomString1 = Index("cs1", IndexType.BTREE, "cs1", "fn", "version") + + /** + * `cs2` — index on custom string value 2, `fn`, `version` (WHERE `cs2 IS NOT NULL`). + * @since 3.0 + */ + @JvmField @JsStatic + val XyzCustomString2 = Index("cs2", IndexType.BTREE, "cs2", "fn", "version") + + /** + * `cs3` — index on custom string value 3, `fn`, `version` (WHERE `cs3 IS NOT NULL`). + * @since 3.0 + */ + @JvmField @JsStatic + val XyzCustomString3 = Index("cs3", IndexType.BTREE, "cs3", "fn", "version") + + /** + * `ref_point` — spatial ([IndexType.SPATIAL]) index over the reference-point geometry member + * (WHERE `ref_point IS NOT NULL`). See [XyzMembers.XyzReferencePoint]. + * @since 3.0 + */ + @JvmField @JsStatic + val XyzReferencePoint = Index("ref_point", IndexType.SPATIAL, "ref_point") + + /** + * All indices for a default XYZ collection, in declaration order: the [StandardIndices.MANDATORY] + * indices (always present), followed by the XYZ default indices, followed by the geometry index + * (referenced from [StandardIndices.GistGeometry], since geometry is a standard member). + * + * Does **not** include the [StandardIndices.SPECIAL] indices (`pn`/`pt`/`gv`), which are declared + * explicitly only where needed (e.g. `naksha~transactions`). + * @since 3.0 + */ + @JvmField @JsStatic + val ALL: List = StandardIndices.MANDATORY + listOf( + XyzHereTile, + XyzAppId, + XyzAuthor, + XyzTags, + XyzFeatureType, + XyzCustomValue0, XyzCustomValue1, XyzCustomValue2, XyzCustomValue3, + XyzCustomString0, XyzCustomString1, XyzCustomString2, XyzCustomString3, + XyzReferencePoint, + StandardIndices.GistGeometry, + ) } -} \ No newline at end of file +} diff --git a/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/MemberTest.kt b/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/MemberTest.kt index 9d9b1d9e4..77f1768f7 100644 --- a/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/MemberTest.kt +++ b/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/MemberTest.kt @@ -107,7 +107,7 @@ class MemberTest { @Test fun standardTagsMemberDefaultsToSet() { assertEquals(MemberType.SET, naksha.model.objects.StandardMembers.XyzTags.dataType) - assertEquals(IndexType.SET, naksha.model.objects.StandardIndices.Tags.type) + assertEquals(IndexType.SET, naksha.model.objects.XyzIndices.XyzTags.type) } @Test diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgIndex.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgIndex.kt index 035eb204b..004146dd3 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgIndex.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgIndex.kt @@ -696,7 +696,7 @@ ${if (where==null) "" else "WHERE $where"};""" idx.type = when { pgIdx === gist_geo || pgIdx === spgist_geo || pgIdx === ref_point -> IndexType.SPATIAL // The built-in tags index is a GIN index over the `tags` member, which is a SET - // (JSON array of unique strings) by default — matches StandardIndices.Tags. + // (JSON array of unique strings) by default — matches XyzIndices.XyzTags. pgIdx === tags -> IndexType.SET else -> IndexType.BTREE } From e5fdf0670881369a0af68ad3ffc2b08eb1a9c4b6 Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Thu, 18 Jun 2026 08:51:31 +0200 Subject: [PATCH 17/57] Next round of AI code cleanup, now members are as they should be, still much code is not up-to-date. Signed-off-by: Alexander Lowey-Weber --- .../activitylog/ActivityLogHandler.java | 4 +- .../activitylog/ActivityLogHandlerTest.java | 8 +- .../commonMain/kotlin/naksha/model/Action.kt | 8 + .../kotlin/naksha/model/IMemberProcessor.kt | 9 +- .../kotlin/naksha/model/ISession.kt | 3 +- .../commonMain/kotlin/naksha/model/Naksha.kt | 81 +- .../commonMain/kotlin/naksha/model/Version.kt | 4 +- .../kotlin/naksha/model/objects/IndexType.kt | 2 +- .../kotlin/naksha/model/objects/Member.kt | 43 +- .../kotlin/naksha/model/objects/MemberList.kt | 14 +- .../naksha/model/objects/NakshaCatalog.kt | 42 +- .../naksha/model/objects/NakshaCollection.kt | 66 +- .../naksha/model/objects/NakshaStorage.kt | 1 + .../naksha/model/objects/StandardMembers.kt | 31 +- .../kotlin/naksha/model/objects/XyzMembers.kt | 17 +- .../naksha/model/request/RequestQuery.kt | 6 +- .../model/request/query/IMemberQuery.kt | 15 + .../naksha/model/request/query/IMetaQuery.kt | 15 - .../naksha/model/request/query/IQuery.kt | 2 +- .../query/{MetaAnd.kt => MemberAnd.kt} | 4 +- .../query/{MetaNot.kt => MemberNot.kt} | 6 +- .../request/query/{MetaOr.kt => MemberOr.kt} | 4 +- .../query/{MetaQuery.kt => MemberQuery.kt} | 10 +- .../naksha/model/request/query/MetaColumn.kt | 669 ------------- .../naksha/model/request/query/Property.kt | 4 +- .../naksha/model/TupleNumberQueryTest.kt | 4 +- .../commonMain/kotlin/naksha/psql/LibPsql.kt | 45 +- .../psql/{PgAdminMap.kt => PgAdminCatalog.kt} | 145 ++- .../naksha/psql/{PgMap.kt => PgCatalog.kt} | 97 +- .../kotlin/naksha/psql/PgCollection.kt | 458 ++++----- .../commonMain/kotlin/naksha/psql/PgColumn.kt | 887 ++---------------- ...PgColumnEntry.kt => PgColumnWithValues.kt} | 27 +- .../naksha/psql/PgDistributionPartition.kt | 72 ++ .../commonMain/kotlin/naksha/psql/PgHead.kt | 105 --- .../kotlin/naksha/psql/PgHeadPartition.kt | 18 - .../kotlin/naksha/psql/PgHeadTable.kt | 116 +++ .../kotlin/naksha/psql/PgHistory.kt | 73 -- .../kotlin/naksha/psql/PgHistoryPartition.kt | 149 ++- .../kotlin/naksha/psql/PgHistoryTable.kt | 120 +++ .../kotlin/naksha/psql/PgHistoryYear.kt | 96 -- .../commonMain/kotlin/naksha/psql/PgIndex.kt | 862 ++--------------- .../kotlin/naksha/psql/PgMapList.kt | 6 +- .../kotlin/naksha/psql/PgMemberHelper.kt | 11 +- .../commonMain/kotlin/naksha/psql/PgMeta.kt | 13 - .../kotlin/naksha/psql/PgNakshaBooks.kt | 2 +- .../kotlin/naksha/psql/PgNakshaCatalogs.kt | 2 +- .../kotlin/naksha/psql/PgNakshaCollections.kt | 2 +- .../naksha/psql/PgNakshaTransactions.kt | 2 +- .../kotlin/naksha/psql/PgQueryBuilder.kt | 10 +- .../kotlin/naksha/psql/PgQueryWhereBuilder.kt | 12 +- .../commonMain/kotlin/naksha/psql/PgRead.kt | 23 +- .../commonMain/kotlin/naksha/psql/PgReader.kt | 4 +- .../kotlin/naksha/psql/PgRelation.kt | 12 +- .../psql/{PgColumnRows.kt => PgRows.kt} | 123 +-- .../kotlin/naksha/psql/PgSession.kt | 66 +- .../kotlin/naksha/psql/PgStorage.kt | 6 +- .../commonMain/kotlin/naksha/psql/PgTable.kt | 720 ++------------ .../commonMain/kotlin/naksha/psql/PgType.kt | 14 +- .../commonMain/kotlin/naksha/psql/PgWrite.kt | 10 +- .../commonMain/kotlin/naksha/psql/PgWriter.kt | 32 +- .../kotlin/naksha/psql/PgWriterBase.kt | 8 +- .../kotlin/naksha/psql/PgWriterDelete.kt | 2 +- .../kotlin/naksha/psql/PgWriterUpdate.kt | 2 +- .../kotlin/naksha/psql/PgWriterUpsert.kt | 2 +- .../naksha/psql/ReadFeaturesByMetadataTest.kt | 50 +- .../naksha/psql/ReadFeaturesByOtherTns.kt | 4 +- .../naksha/psql/TupleNumberPersistenceTest.kt | 2 +- .../jsMain/kotlin/naksha/psql/Plv8Trigger.kt | 2 +- .../{PsqlAdminMap.kt => PsqlAdminCatalog.kt} | 14 +- .../src/jvmMain/kotlin/naksha/psql/PsqlMap.kt | 12 +- .../jvmMain/kotlin/naksha/psql/PsqlStorage.kt | 16 +- .../kotlin/naksha/psql/PsqlStorageListener.kt | 2 +- .../kotlin/naksha/psql/PsqlTestStorage.kt | 4 +- 73 files changed, 1488 insertions(+), 4044 deletions(-) create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/IMemberQuery.kt delete mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/IMetaQuery.kt rename here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/{MetaAnd.kt => MemberAnd.kt} (72%) rename here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/{MetaNot.kt => MemberNot.kt} (72%) rename here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/{MetaOr.kt => MemberOr.kt} (72%) rename here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/{MetaQuery.kt => MemberQuery.kt} (72%) delete mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MetaColumn.kt rename here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/{PgAdminMap.kt => PgAdminCatalog.kt} (84%) rename here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/{PgMap.kt => PgCatalog.kt} (82%) rename here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/{PgColumnEntry.kt => PgColumnWithValues.kt} (68%) create mode 100644 here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgDistributionPartition.kt delete mode 100644 here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgHead.kt delete mode 100644 here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgHeadPartition.kt create mode 100644 here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgHeadTable.kt delete mode 100644 here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgHistory.kt create mode 100644 here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgHistoryTable.kt delete mode 100644 here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgHistoryYear.kt delete mode 100644 here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMeta.kt rename here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/{PgColumnRows.kt => PgRows.kt} (82%) rename here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/{PsqlAdminMap.kt => PsqlAdminCatalog.kt} (94%) diff --git a/here-naksha-handler-activitylog/src/jvmMain/java/com/here/naksha/handler/activitylog/ActivityLogHandler.java b/here-naksha-handler-activitylog/src/jvmMain/java/com/here/naksha/handler/activitylog/ActivityLogHandler.java index ffe16f250..65c7548d7 100644 --- a/here-naksha-handler-activitylog/src/jvmMain/java/com/here/naksha/handler/activitylog/ActivityLogHandler.java +++ b/here-naksha-handler-activitylog/src/jvmMain/java/com/here/naksha/handler/activitylog/ActivityLogHandler.java @@ -57,7 +57,7 @@ import naksha.model.request.WriteRequest; import naksha.model.request.query.AnyOp; import naksha.model.request.query.MetaColumn; -import naksha.model.request.query.MetaQuery; +import naksha.model.request.query.MemberQuery; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -173,7 +173,7 @@ private ReadFeatures featuresWhereNextVersionIsOneOf(List tupleNumb for (int i = 0; i < tupleNumbers.size(); i++) { versions[i] = tupleNumbers.get(i).version.value; } - MetaQuery nextVersionQuery = new MetaQuery(MetaColumn.nextVersion(), AnyOp.IS_ANY_OF, versions); + MemberQuery nextVersionQuery = new MemberQuery(MetaColumn.nextVersion(), AnyOp.IS_ANY_OF, versions); ReadFeatures requestPredecessors = new ReadFeatures(); requestPredecessors.setCollectionIds(StringList.of(properties.getSpaceId())); requestPredecessors.setQueryHistory(true); diff --git a/here-naksha-handler-activitylog/src/jvmTest/java/com/here/naksha/handler/activitylog/ActivityLogHandlerTest.java b/here-naksha-handler-activitylog/src/jvmTest/java/com/here/naksha/handler/activitylog/ActivityLogHandlerTest.java index ca3dae438..71acace3c 100644 --- a/here-naksha-handler-activitylog/src/jvmTest/java/com/here/naksha/handler/activitylog/ActivityLogHandlerTest.java +++ b/here-naksha-handler-activitylog/src/jvmTest/java/com/here/naksha/handler/activitylog/ActivityLogHandlerTest.java @@ -54,9 +54,9 @@ import naksha.model.request.Write; import naksha.model.request.WriteRequest; import naksha.model.request.query.AnyOp; -import naksha.model.request.query.IMetaQuery; +import naksha.model.request.query.IMemberQuery; import naksha.model.request.query.MetaColumn; -import naksha.model.request.query.MetaQuery; +import naksha.model.request.query.MemberQuery; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -469,8 +469,8 @@ private boolean isHistoryAwareReadFeatures(ReadRequest readRequest) { } private boolean containsNextVersionMetaQuery(ReadFeatures readFeatures, TupleNumber... expectedTns) { - IMetaQuery metaQuery = readFeatures.getQuery().getMetadata(); - if (!(metaQuery instanceof MetaQuery mq)) return false; + IMemberQuery metaQuery = readFeatures.getQuery().getMetadata(); + if (!(metaQuery instanceof MemberQuery mq)) return false; boolean basicCheck = mq.getColumn().equals(MetaColumn.nextVersion()) && mq.getOp().equals(AnyOp.IS_ANY_OF); if (!basicCheck) return false; diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Action.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Action.kt index b1a7cb856..44f1ead68 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Action.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Action.kt @@ -1,5 +1,6 @@ package naksha.model +import naksha.base.Int64 import naksha.base.JsEnum import kotlin.js.JsExport import kotlin.js.JsStatic @@ -119,6 +120,13 @@ class Action : JsEnum() { @JsStatic @JvmStatic fun fromValue(value: Int): Action = FROM_VALUE[value] ?: VERSION + + /** + * Helper to obtain an [Action] from its 64-bit version value. Returns [VERSION] for unrecognised values. + */ + @JsStatic + @JvmStatic + fun fromVersion(version: Int64): Action = FROM_VALUE[version.toInt() and 3] ?: VERSION } /** diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/IMemberProcessor.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/IMemberProcessor.kt index d468cbf45..d534f6b9e 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/IMemberProcessor.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/IMemberProcessor.kt @@ -5,14 +5,17 @@ import naksha.model.objects.NakshaCollection import naksha.model.objects.NakshaFeature /** - * Optional callback, invoked by the storage after the value of a member has been extracted. This allowing some business logic to mutate the value before it is actually persisted. + * Optional callback, invoked by the storage after the value of a member has been extracted. This allows some business logic to mutate the value before it is actually persisted. */ fun interface IMemberProcessor { /** + * Invoked by the storage before it persists a feature. + * + * Beware that the `session`, `feature` and `collection` **must** be treated as read-only and **must not** be modified, even while it is technically possible. Modification of the objects is strictly forbidden for this method, doing so anyway will cause undefined behavior and may lead to data loss. The method **must** only read the given arguments and generate the result or just return the given value. * @param session The current session as context. * @param collection The collection in which the feature is located. - * @param feature The feature being processed. - * @param member The name of the member that is processed. + * @param feature The feature being processed _(the member is still in the feature in the original value)_. + * @param member The member that is processed. * @param value The value that is about to be extracted from the feature, and to be persisted in a dedicated storage slot. * @return the value that should be stored, can be just the same as the given one or any other acceptable primitive. */ diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/ISession.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/ISession.kt index 6ac37f9f6..a7478ceda 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/ISession.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/ISession.kt @@ -52,8 +52,7 @@ interface ISession : AutoCloseable { /** * Returns the [MemberProcessorMap] for this session. * - * Use the map to register, remove, or inspect [IMemberProcessor] instances for individual members. - * Processors are invoked in the order in which they were added. + * Use the map to register, remove, or inspect [IMemberProcessor] instances for individual member processing. Processors are invoked in the order in which they were added. * @return the member processor map. * @since 3.0 */ diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt index 26a3c28b1..8c678ceaf 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt @@ -179,19 +179,55 @@ class Naksha private constructor() { @JsStatic @JvmStatic fun verifyId(id: String?): String { + verifyId(id, internal = false, throwOnError = true) + return id!! + } + + /** + * Tests if the given **id** is a valid identifier, so matches: + * + * `[a-z_][a-z0-9_:-~$]{31}` + * + * If the given identifier is invalid, the methods throws [NakshaError.ILLEGAL_ID]. + * @param id the identifier to test. + * @return the given identifier, tested. + * @since 3.0 + * @see [isValidId] + */ + @JsStatic + @JvmStatic + fun verifyInternalId(id: String?): String { + verifyId(id, internal = true, throwOnError = true) + return id!! + } + + @JsStatic + @JvmStatic + private fun verifyId(id: String?, internal: Boolean, throwOnError: Boolean): Boolean { if (id.isNullOrEmpty()) { - throw illegalId("The given identifier is null or empty") + if (throwOnError) throw illegalId("The given identifier is null or empty") + else return false } if (id == "naksha") { - throw illegalId("The identifier 'naksha' is forbidden") + if (throwOnError) throw illegalId("The identifier 'naksha' is forbidden") + else return false } if (id.length > MAX_ID_LENGTH) { - throw illegalId("The identifier '$id' is too long: ${id.length}, must be maximal $MAX_ID_LENGTH") + if (throwOnError) throw illegalId("The identifier '$id' is too long: ${id.length}, must be maximal $MAX_ID_LENGTH") + else return false } var i = 0 var c = id[i++] if (c.code < 'a'.code || c.code > 'z'.code) { - throw illegalId("The first character must be a-z, but was $c") + if (!internal) { + if (throwOnError) throw illegalId("The first character must be a-z, but was $c") + else return false + } + // Internal identifiers may start with `_` + if (c.code != '_'.code) { + if (throwOnError) throw illegalId("The first character must be a-z or '_' (underscore), but was $c") + else return false + } } while (i < id.length) { c = id[i++] @@ -199,10 +235,20 @@ class Naksha private constructor() { in 'a'.code..'z'.code -> continue in '0'.code..'9'.code -> continue '_'.code, ':'.code, '-'.code -> continue - else -> throw illegalId("Invalid character at index $i: '$c', expected [a-z0-9_:-]") + '~'.code, '$'.code -> if (!internal) { + if (throwOnError) throw illegalId("Invalid character at index $i: '$c', expected [a-z0-9_:-]") + else return false + } else continue + else -> if (!internal) { + if (throwOnError) throw illegalId("Invalid character at index $i: '$c', expected [a-z0-9_:-]") + else return false + } else { + if (throwOnError) throw illegalId("Invalid character at index $i: '$c', expected [a-z0-9_:-~$]") + else return false + } } } - return id + return true } /** @@ -213,7 +259,12 @@ class Naksha private constructor() { */ @JsStatic @JvmStatic - fun isInternalId(id: String?): Boolean = id != null && id.startsWith(INTERNAL_PREFIX) + fun isInternalId(id: String?): Boolean = + // Every identifier that is a valid internal identifier + verifyId(id, internal = true, throwOnError = false) + // but not a valid normal identifier + && !verifyId(id, internal = false, throwOnError = false) + // is actually an internal identifier /** * Generates an [MD5](https://en.wikipedia.org/wiki/MD5) hash above the given identifier, which is used to extract many values from it. @@ -239,16 +290,16 @@ class Naksha private constructor() { private val is31BitUnsigned = Regex("^[1-9][0-9]{0,9}\$") /** - * A method to calculate a valid storage-number from the storage-id. + * A method to calculate a valid database-number from the database-id. * - * @param id the id, from which to extract the storage-number. - * @return the storage-number. + * @param id the id, from which to extract the database-number. + * @return the database-number. * @since 3.0 * @see [hashId] */ @JsStatic @JvmStatic - fun storageNumber(id: String): Int64 { + fun databaseNumber(id: String): Int64 { if (id == "0" || is63BitUnsigned.matches(id)) { try { return id.toLong(10).toInt64() @@ -259,16 +310,16 @@ class Naksha private constructor() { } /** - * A method to calculate a valid map-number from the map-id. + * A method to calculate a valid catalog-number from the catalog-id. * - * @param id the map-id, from which to extract the map-number. - * @return the map-number. + * @param id the catalog-id, from which to extract the catalog-number. + * @return the catalog-number. * @since 3.0 * @see [hashId] */ @JsStatic @JvmStatic - fun mapNumber(id: String): Int { + fun catalogNumber(id: String): Int { if (id == ADMIN_CATALOG_ID) return ADMIN_CATALOG_FN if (id == "0" || is31BitUnsigned.matches(id)) { try { diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Version.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Version.kt index 1086a77c5..8e9d36640 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Version.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Version.kt @@ -192,9 +192,9 @@ open class Version(@JvmField val number: Int64) : Comparable { } /** - * The _HEAD_ sentinel version _(9_007_199_254_740_991L aka `2^53-1`)_. Can be used as well to mask version to ensure valid range. + * The _HEAD_ sentinel version _(`9_007_199_254_740_991` aka `2^53-1`)_. Can be used as well to mask version to ensure valid version number, like `version & Version.HEAD`. * - * When a [Tuple] is the current HEAD state its `nextVersion` is synthesised as this value. + * When a [Tuple] is the _HEAD_ state its next-version is synthesized as this value. * @since 3.0 */ @JvmField diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/IndexType.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/IndexType.kt index 46de4becb..01cf6f1a5 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/IndexType.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/IndexType.kt @@ -11,7 +11,7 @@ import kotlin.reflect.KClass * The kind of index to create for an [Index]. * * - [BTREE] — ordered index for equality and range queries on primitive columns (numbers, booleans, strings, byte-arrays). - * - [SPATIAL] — spatial index over a geometry column (e.g. the built-in `geo`). + * - [SPATIAL] — spatial index over a geometry column ([MemberType.SPATIAL]) (e.g. the built-in `geo`). * - [TAGS] — inverted index over a tags column ([MemberType.TAGS] or [MemberType.TAGS_FROM_ARRAY]); * supports key/value containment lookups. * - [SET] — inverted index over a set column ([MemberType.SET]); supports element containment lookups. diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt index 9613a601b..0a007d81b 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt @@ -10,6 +10,7 @@ import naksha.base.NotNullProperty import naksha.base.NullableProperty import naksha.base.Proxy import naksha.geo.SpGeometry +import naksha.model.Naksha import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_STATE import naksha.model.NakshaException import naksha.model.TupleNumber @@ -17,18 +18,18 @@ import kotlin.js.JsExport import kotlin.js.JsName /** - * A column materialized on a [NakshaCollection] — either a mandatory/default built-in column or a - * user-defined one. + * A materialized part of a feature. * - * At write time, the storage walks the feature using [path], extracts the value, coerces it to the - * [dataType], and stores it in a storage-specific column derived from [name]. The value also remains - * in the encoded feature blob. + * At write time, the storage walks the feature using [path], extracts the value, coerces it to the [dataType], and stores it in a storage-specific location derived from the [name]. The value is removed from the feature, and instead of the actual value a reference into the dedicated storage place is added, so that when decoding the feature, it can be copied back. * - * The [name] must be a valid Naksha identifier (see [naksha.model.Naksha.verifyId]). - * Mandatory columns (e.g. `fn`, `version`, `id`, `feature`) are injected by the storage and must - * not be redeclared by the client with a different type. + * The [name] must be a valid Naksha identifier (see [naksha.model.Naksha.verifyId]). Mandatory members are injected by the storage and **must not** be redeclared by the client with a different type. Mandatory members are: + * - [Tuple-Number][naksha.model.objects.StandardMembers.StandardMembers_C.Tn] + * - [NextVersion][naksha.model.objects.StandardMembers.StandardMembers_C.NextVersion] + * - [GlobalBookFeatureNumber][naksha.model.objects.StandardMembers.StandardMembers_C.GlobalBookFeatureNumber] + * - [Id][naksha.model.objects.StandardMembers.StandardMembers_C.Id] + * - [Feature][naksha.model.objects.StandardMembers.StandardMembers_C.Feature] * - * If [path] is not set, the storage defaults to `["properties", ]` at write time. + * If [path] is not explicitly set, the implicit path defaults to `["properties", ]`. * @since 3.0 */ @JsExport @@ -138,23 +139,23 @@ class Member() : AnyObject(), Comparator { fun read(proxy: Proxy): Any? = proxy.getPath(path) /** - * Whether this member is storage-managed (internal). When `true`, the storage controls the DDL for this member. Defaults to `false`. + * Whether this member is a storage-managed mandatory member. When `true`, the storage controls the DDL for this member. Defaults to `false`. * @since 3.0 */ - private var internal: Boolean by INTERNAL + private var mandatory: Boolean by MANDATORY - /** True iff the underlying map has an entry for [internal]. */ - fun isInternal(): Boolean = internal + /** True if this member is mandatory. */ + fun isMandatory(): Boolean = mandatory - /** Remove [internal] from the underlying map; returns this for chaining. */ - internal fun removeInternal(): Member { - removeRaw("internal") + /** Remove [mandatory] from the underlying map; returns this for chaining. */ + internal fun removeMandatory(): Member { + removeRaw("mandatory") return this } - /** Fluent setter for [internal]; returns this for chaining. */ - internal fun withInternal(value: Boolean): Member { - internal = value + /** Fluent setter for [mandatory]; returns this for chaining. */ + internal fun withMandatory(value: Boolean = true): Member { + mandatory = value return this } @@ -225,7 +226,7 @@ class Member() : AnyObject(), Comparator { * @param feature The feature to read from. * @return the read value or `null`, if the feature does not store a valid value at the member path. */ - fun readString(feature: MapProxy<*,*>): String? { + fun getString(feature: MapProxy<*,*>): String? { val raw = feature.getPath(path) if (raw is String) return raw return null @@ -293,6 +294,6 @@ class Member() : AnyObject(), Comparator { private val DATA_TYPE = NotNullEnum(MemberType::class) { _, _ -> MemberType.STRING } private val INDEX = NullableProperty(Int::class) private val PATH = NotNullProperty(JsonPath::class) { self, _ -> JsonPath(listOf("properties", self.name)) } - private val INTERNAL = NotNullProperty(Boolean::class) { _, _ -> false } + private val MANDATORY = NotNullProperty(Boolean::class) { _, _ -> false } } } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberList.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberList.kt index 9191e7b7a..42a2f6f16 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberList.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberList.kt @@ -4,6 +4,7 @@ package naksha.model.objects import naksha.base.ListProxy import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_STATE +import naksha.model.NakshaError.NakshaErrorCompanion.INTERNAL_ERROR import naksha.model.NakshaException import kotlin.js.JsExport import kotlin.js.JsName @@ -47,13 +48,18 @@ open class MemberList() : ListProxy(Member::class) { } /** - * Sort this list by the sort-order of the [MemberType]. + * Sort this list by the sort-order of the [MemberType] and updates the `index` of all members. * @return this. * @since 3.0 * @throws NakshaException with error [ILLEGAL_STATE], if any member is `null` or has no `dataType`. */ - fun sortByDataType(): MemberList { + fun sortByDataTypeAndAssignIndex(): MemberList { sortBy { member -> member?.dataType?.sortOrder ?: throw NakshaException(ILLEGAL_STATE, "Member is null or has no dataType") } + // Save the order. + for (i in 0 until size) { + val member = this[i] ?: throw NakshaException(INTERNAL_ERROR, "Member is null, that must not happen, should have caught before") + member["index"] = i + } return this } @@ -69,7 +75,7 @@ open class MemberList() : ListProxy(Member::class) { } /** - * Tests if this list is [sorted by data-type][sortByDataType]. + * Tests if this list is sorted by [data-type][sortByDataTypeAndAssignIndex]. * @return _true_ if the entries are sorted; _false_ otherwise. */ fun isSortedByDataType(): Boolean { @@ -88,7 +94,7 @@ open class MemberList() : ListProxy(Member::class) { } /** - * Tests if this list is [sorted by data-type][sortByDataType]. + * Tests if this list is sorted by [index][sortByIndex]. * @return _true_ if the entries are sorted; _false_ otherwise. */ fun isSortedByIndex(): Boolean { diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCatalog.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCatalog.kt index c3e8a4f32..c1c3856a3 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCatalog.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCatalog.kt @@ -7,6 +7,11 @@ import naksha.base.NullableProperty import naksha.geo.SpBoundingBox import naksha.geo.SpGeometry import naksha.geo.SpPoint +import naksha.model.Naksha +import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_ARGUMENT +import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_STATE +import naksha.model.NakshaException +import naksha.model.TupleNumber import kotlin.js.JsExport import kotlin.js.JsName @@ -37,9 +42,7 @@ open class NakshaCatalog() : NakshaFeature() { */ const val FEATURE_TYPE = "naksha.Catalog" - private val DATABASE_NUMBER = NullableProperty(Int64::class) private val DATABASE_ID = NullableProperty(String::class) - private val CATALOG_NUMBER = NullableProperty(Int::class) } override fun featureTypeDefaultValue(): String = FEATURE_TYPE @@ -53,11 +56,22 @@ open class NakshaCatalog() : NakshaFeature() { override fun withMomType(value: String?): NakshaCatalog = super.withMomType(value) as NakshaCatalog /** - * The database-number of the collection; **NOT** the database-number of the collection-feature itself, even while they are guaranteed to be the same. + * Helper to get/set the [TupleNumber] of the catalog-feature. All catalogs features follow the old XYZ-Hub style, therefore the location of the [TupleNumber] is clear. * @since 3.0 */ - var databaseNumber: Int64? by DATABASE_NUMBER - // TODO: Fix this, we need to calculate the database-number from the database-id, if an id is given! + var tupleNumber: TupleNumber? + get() = XyzMembers.XyzTn.getTupleNumber(this) + set(value) { + XyzMembers.XyzTn.set(this, value) + } + + /** + * The database-number of the catalog; the catalog-feature itself is stored in the same database as the catalog it describes. + * @since 3.0 + * @throws NakshaException with error [ILLEGAL_STATE], when the collection does not have a valid [tupleNumber]. + */ + val databaseNumber: Int64 + get() = tupleNumber?.databaseNumber ?: throw NakshaException(ILLEGAL_STATE, "The collection has no tuple-number") /** * The database-id of the collection; **NOT** the database-id of the collection-feature itself, even while they are guaranteed to be the same. @@ -68,16 +82,24 @@ open class NakshaCatalog() : NakshaFeature() { /** * @see [databaseId] */ - fun withDatabaseId(value: String?): NakshaCatalog { + fun withDatabaseId(value: String): NakshaCatalog { + val tn = tupleNumber + if (tn != null) { + if (Naksha.databaseNumber(value) != tn.databaseNumber) { + throw NakshaException(ILLEGAL_ARGUMENT, "The given database-id does not match the database-number of the collection.") + } + } databaseId = value return this } /** - * The catalog-number of the collection; **NOT** the catalog-number of the collection-feature itself, which would always be `0` _(`naksha~admin`)_. + * The catalog-number of the catalog, this is actually the same as the feature-number. + * + * It is **NOT** the catalog-number of this catalog-feature, so where the catalog-feature itself is stored, which has always the catalog-number `0`, because all catalog features are always stored in the catalog `naksha~admin`. * @since 3.0 + * @see [Naksha.catalogNumber] */ - var catalogNumber: Int? by CATALOG_NUMBER - // TODO: Fix this, we need to calculate the catalog-number from the catalog-id (aka `id`). - // Actually the feature-number of the catalog and catalog-number must be the same! + val catalogNumber: Int + get() = Naksha.catalogNumber(id) } \ No newline at end of file diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt index 3aa360517..e09be2d93 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt @@ -9,9 +9,11 @@ import naksha.geo.SpPoint import naksha.model.DataEncoding import naksha.model.Naksha import naksha.model.NakshaError +import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_ARGUMENT import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_STATE import naksha.model.NakshaError.NakshaErrorCompanion.NOT_FOUND import naksha.model.NakshaException +import naksha.model.TupleNumber import kotlin.js.JsExport import kotlin.js.JsName import kotlin.js.JsStatic @@ -66,27 +68,53 @@ open class NakshaCollection() : NakshaFeature() { override fun withMomType(value: String?): NakshaCollection = super.withMomType(value) as NakshaCollection /** - * The database-number of the collection; **NOT** the database-number of the collection-feature itself, even while they are guaranteed to be the same. + * Helper to get/set the [TupleNumber] of the collection-feature. All collection features follow the old XYZ-Hub style, therefore the location of the [TupleNumber] is clear at `properties->@ns:com:here:xyz->uuid`. * @since 3.0 */ - var databaseNumber: Int64? by DATABASE_NUMBER - // TODO: Fix this, we need to calculate the database-number from the database-id, if an id is given! + var tupleNumber: TupleNumber? + get() = XyzMembers.XyzTn.getTupleNumber(this) + set(value) { + XyzMembers.XyzTn.set(this, value) + } + + /** + * The database-number of the collection; the collection-feature itself is stored in the same database as the collection it describes. + * @since 3.0 + * @throws NakshaException with error [ILLEGAL_STATE], when the collection does not have a valid [tupleNumber]. + */ + val databaseNumber: Int64 + get() = tupleNumber?.databaseNumber ?: throw NakshaException(ILLEGAL_STATE, "The collection has no tuple-number") /** - * The database-id of the collection; **NOT** the database-id of the collection-feature itself, even while they are guaranteed to be the same. + * The database-id of the collection; the collection-feature itself is stored in the same database as the collection it describes. * @since 3.0 */ var databaseId: String? by DATABASE_ID /** - * The catalog-number of the collection; **NOT** the catalog-number of the collection-feature itself, which would always be `0` _(`naksha~admin`)_. + * @see [databaseId] + */ + fun withDatabaseId(value: String): NakshaCollection { + val tn = tupleNumber + if (tn != null) { + if (Naksha.databaseNumber(value) != tn.databaseNumber) { + throw NakshaException(ILLEGAL_ARGUMENT, "The given database-id does not match the database-number of the collection.") + } + } + databaseId = value + return this + } + + /** + * The catalog-number of the collection; the collection-feature itself is stored in the same catalog as the collection it describes. * @since 3.0 + * @throws NakshaException with error [ILLEGAL_STATE], when the collection does not have a valid [tupleNumber]. */ - var catalogNumber: Int? by CATALOG_NUMBER - // TODO: Fix this, we need to calculate the catalog-number from the catalog-id, if an id is given! + val catalogNumber: Int + get() = tupleNumber?.catalogNumber ?: throw NakshaException(ILLEGAL_STATE, "The collection has no tuple-number") /** - * The custom identifier of the catalog in which the collection is located; ; **NOT** the catalog-id of the collection-feature itself, which would always be `naksha~admin`. + * The custom identifier of the catalog in which the collection is located; the collection-feature itself is stored in the same catalog as the collection it describes. * @since 3.0 */ var catalogId: String? by CATALOG_ID @@ -94,18 +122,27 @@ open class NakshaCollection() : NakshaFeature() { /** * @see [catalogId] */ - fun withCatalogId(value: String?): NakshaCollection { + fun withCatalogId(value: String): NakshaCollection { catalogId = value + val tn = tupleNumber + if (tn != null) { + if (Naksha.catalogNumber(value) != tn.catalogNumber) { + throw NakshaException(ILLEGAL_ARGUMENT, "The given catalog-id does not match the catalog-number of the collection.") + } + } + databaseId = value return this } /** - * The collection-number of the collection; **NOT** the collection-number of the collection-feature itself, which would always be `0` _(`naksha~collections`)_. This should be the same as the feature-number! + * The collection-number of the collection, this is actually the same as the feature-number. + * + * It is **NOT** the collection-number of this collection-feature, so where the collection-feature itself is stored, which has always the collection-number `0`, because all collection features are always stored in the collection `naksha~collections`. * @since 3.0 + * @see [Naksha.collectionNumber] */ - var collectionNumber: Int? by COLLECTION_NUMBER - // TODO: Fix this, we need to calculate the collection-number from the collection-id (aka `id`). - // Actually the feature-number of the collection and collection-number must be the same! + val collectionNumber: Int + get() = Naksha.collectionNumber(id) /** * If partitions is given, then the collection is internally partitioned in the storage, optimised for large quantities of features. The default is no partitions; as a rule of thumb, add one more partition for every 10 to 20 million features expected. @@ -620,11 +657,8 @@ open class NakshaCollection() : NakshaFeature() { @JsStatic val UNKNOWN = Int64(-1) - private val DATABASE_NUMBER = NullableProperty(Int64::class) private val DATABASE_ID = NullableProperty(String::class) private val CATALOG_ID = NullableProperty(String::class) - private val CATALOG_NUMBER = NullableProperty(Int::class) - private val COLLECTION_NUMBER = NullableProperty(Int::class) private val PARTITIONS = NotNullProperty(Int::class) { _, _ -> 1 } private val SHIFT = NotNullProperty(Int::class) { _, _ -> 41 } private val STORAGE_CLASS = NullableProperty(String::class) diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaStorage.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaStorage.kt index aaa7d9450..a7156531d 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaStorage.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaStorage.kt @@ -11,6 +11,7 @@ import naksha.model.Naksha import naksha.model.NakshaError import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_ARGUMENT import naksha.model.NakshaException +import naksha.model.TupleNumber import kotlin.js.* import kotlin.jvm.JvmOverloads import kotlin.jvm.JvmStatic diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt index 1af6b5bac..e487dad85 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt @@ -20,55 +20,46 @@ class StandardMembers private constructor() { // ------------------------------------------------------------------------- /** - * `tn` — **Tuple-Number**. The [naksha.model.TupleNumber] of this feature. This persists out of: + * `_tn` — **Tuple-Number**. The [naksha.model.TupleNumber] of this feature. This persists out of: * - `database-number: long` - The database in which the feature is stored. * - `catalog-number: int` - The catalog in which the feature is stored. * - `collection-number: int` - The collection in which the feature is stored. * - `feature-number: long` - The unique identifier of the feature within the collection. * - `version: long` - The version of this feature state. * - * Note all storages will persist the tuple-number like this, i.e. `lib-psql` will only store the `feature-number` as `fn` and the `version`. The other parts can be deducted from the storage location. + * **Beware**: Not all storages will persist the full tuple-number, i.e. `lib-psql` will only store the `feature-number` as `_fn` and the `version` as `_version`, because the other parts can be deducted from the table location. Searching for parts of the tuple-number is supported by the [ReadFeatures][naksha.model.request.ReadFeatures] request using the [version][naksha.model.request.ReadFeatures.version] property. * @since 3.0 */ @JvmField @JsStatic - val Tn = Member("~tn", MemberType.TUPLE_NUMBER, JsonPath("tn")) + val Tn = Member("_tn", MemberType.TUPLE_NUMBER, JsonPath("tn")).withMandatory() /** - * `nv` — **next-version** (`INT64`). The version at which this tuple was superseded - * by the next state. Present only in history; in head the value is intrinsically the current - * HEAD sentinel and is not stored as a physical member. Mandatory, storage-managed. + * `_nv` — **next-version** (`INT64`). The version at which this tuple was superseded by the next state. Present only in _HISTORY_; in _HEAD_ the value is intrinsically the current [HEAD version][naksha.model.Version.HEAD]. * @since 3.0 */ @JvmField @JsStatic - val NextVersion = Member("~nv", MemberType.INT64, JsonPath("nv")) + val NextVersion = Member("_nv", MemberType.INT64, JsonPath("nv")).withMandatory() /** - * `gbn` — **Global-book-number** (`INT64`). References the `global_book_fn` in - * `naksha~books`, which carries the JBON2 global dictionary needed to decode this tuple's - * [Feature] blob when global-book encoding is used. Assigned by the sequencer; `null` when - * no global book is in use. Mandatory, storage-managed (but physically `null` until the - * sequencer populates it). - * - * A conditional non-unique index on this member is always created so that the sequencer can - * efficiently locate all tuples that reference a particular book. + * `_global_book_fn` — **Global-book-feature-number** (`INT64`). References the feature-number in `naksha~books`, which carries the JBON2 global dictionary needed to decode this tuple's [Feature] blob when global-book encoding is used. * @since 3.0 */ @JvmField @JsStatic - val GlobalBookFeatureNumber = Member("~gbfn", MemberType.INT64, JsonPath("gbfn")) + val GlobalBookFeatureNumber = Member("_global_book_fn", MemberType.INT64, JsonPath("gbfn")).withMandatory() /** - * `feature` — **Serialised feature** (`BYTE_ARRAY`). The encoded feature blob. The encoding is controlled by [NakshaCollection.dataEncoding]. Mandatory, storage-managed. The feature member is special in that it represents the feature itself, therefore the path is an empty list! + * `_id` — custom feature string identifier. Either stringified feature-number _(as positive decimal number, i.e. `12345678`)_ or the unique identifier of the feature, resulting in a negative feature-number, generated by hashing the `id`. Technically, the storage can store `null`, when the feature-number is positive. * @since 3.0 */ @JvmField @JsStatic - val Feature = Member("~feature", MemberType.BYTE_ARRAY, JsonPath()) + val Id = Member("_id", MemberType.STRING, JsonPath("id")).withMandatory() /** - * `id` — feature string identifier. Either stringified `feature-number` _(as positive decimal number, i.e. `12345678`)_ or the unique identifier of the feature, resulting in a negative `feature-number`, generated by hashing the `id`. Technically, the storage can store `null`, when the `feature-number` is positive. + * `_feature` — **Serialised feature** (`BYTE_ARRAY`). * @since 3.0 */ @JvmField @JsStatic - val Id = Member("~id", MemberType.STRING, JsonPath("id")) + val Feature = Member("_feature", MemberType.BYTE_ARRAY, JsonPath()).withMandatory() /** * All mandatory members, in declaration order. diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzMembers.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzMembers.kt index f704f011f..9b7673e6f 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzMembers.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzMembers.kt @@ -8,6 +8,10 @@ import kotlin.js.JsExport import kotlin.js.JsStatic import kotlin.jvm.JvmField +/** + * All members being part of the classic XYZ-Hub architecture, plus further extensions added later in Data-Hub and Naksha v1, v2. All internal adminstrative object are stored in this format. + * @since 3.0 + */ @JsExport class XyzMembers private constructor() { companion object XyzMembers_C { @@ -20,35 +24,35 @@ class XyzMembers private constructor() { * @since 3.0 */ @JvmField @JsStatic - val XyzTn = Member("~tn", MemberType.TUPLE_NUMBER, JsonPath("properties", "@ns:com:here:xyz", "uuid")) + val XyzTn = Member(StandardMembers.Tn, JsonPath("properties", "@ns:com:here:xyz", "uuid")) /** * The same as [StandardMembers.NextVersion], but with a Data-Hub compatible path. * @since 3.0 */ @JvmField @JsStatic - val XyzNextVersion = Member("~nv", MemberType.INT64, JsonPath("properties", "@ns:com:here:xyz", "nextVersion")) + val XyzNextVersion = Member(StandardMembers.NextVersion, JsonPath("properties", "@ns:com:here:xyz", "nextVersion")) /** * The same as [StandardMembers.GlobalBookFeatureNumber], but with a Data-Hub compatible path. * @since 3.0 */ @JvmField @JsStatic - val XyzGlobalBookFeatureNumber = Member("~gbfn", MemberType.INT64, JsonPath("properties", "@ns:com:here:xyz", "globalBookFn")) + val XyzGlobalBookFeatureNumber = Member(StandardMembers.GlobalBookFeatureNumber, JsonPath("properties", "@ns:com:here:xyz", "globalBookFn")) /** * The same as [StandardMembers.Feature]. * @since 3.0 */ @JvmField @JsStatic - val XyzFeature = Member("~feature", MemberType.BYTE_ARRAY, JsonPath()) + val XyzFeature = Member(StandardMembers.Feature, JsonPath()) /** * The same as [StandardMembers.Feature]. * @since 3.0 */ @JvmField @JsStatic - val XyzId = Member("~id", MemberType.STRING, JsonPath("id")) + val XyzId = Member(StandardMembers.Id, JsonPath("id")) // ------------------------------------------------------------------------- // Optional members. @@ -223,8 +227,7 @@ class XyzMembers private constructor() { val XyzTags = Member("tags", MemberType.SET, JsonPath("properties", "@ns:com:here:xyz", "tags")) /** - * `ref_point` — geometry reference point (always a single point), stored as TWKB. Used to - * compute the [XyzHereTile] value. `null` if the feature has no explicit reference point. + * `ref_point` — geometry reference point (always a single point), stored as TWKB. Used to compute the [XyzHereTile] value. `null` if the feature has no explicit reference point. * Default member. * @since 3.0 */ diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/RequestQuery.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/RequestQuery.kt index 802dafeae..4dad4db4e 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/RequestQuery.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/RequestQuery.kt @@ -20,7 +20,7 @@ import kotlin.jvm.JvmField * - [naksha.model.request.query.SpOr] - logical OR for spatial conditions * - [naksha.model.request.query.TagOr] - logical OR for tag conditions * - [naksha.model.request.query.POr] - logical OR for property conditions - * - [naksha.model.request.query.MetaOr] - logical OR for metadata conditions + * - [naksha.model.request.query.MemberOr] - logical OR for metadata conditions * * @since 3.0 */ @@ -35,7 +35,7 @@ open class RequestQuery : AnyObject() { private val SPATIAL_QUERY_OR_NULL = NullableProperty(ISpatialQuery::class) private val TAG_QUERY_OR_NULL = NullableProperty(ITagQuery::class) private val PROPERTIES_QUERY_OR_NULL = NullableProperty(IPropertyQuery::class) - private val METADATA_QUERY_OR_NULL = NullableProperty(IMetaQuery::class) + private val METADATA_QUERY_OR_NULL = NullableProperty(IMemberQuery::class) } /** @@ -62,7 +62,7 @@ open class RequestQuery : AnyObject() { /** * Search for features matching the given metadata query. * @since 3.0.0 - * @see IMetaQuery + * @see IMemberQuery */ var metadata by METADATA_QUERY_OR_NULL diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/IMemberQuery.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/IMemberQuery.kt new file mode 100644 index 000000000..f1c0ffe6f --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/IMemberQuery.kt @@ -0,0 +1,15 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model.request.query + +import kotlin.js.JsExport + +/** + * Marker interface for member queries. + * @see MemberAnd + * @see MemberOr + * @see MemberNot + * @see MemberQuery + */ +@JsExport +interface IMemberQuery : IQuery \ No newline at end of file diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/IMetaQuery.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/IMetaQuery.kt deleted file mode 100644 index 38f8e4df2..000000000 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/IMetaQuery.kt +++ /dev/null @@ -1,15 +0,0 @@ -@file:Suppress("OPT_IN_USAGE") - -package naksha.model.request.query - -import kotlin.js.JsExport - -/** - * Marker interface for metadata queries. - * @see MetaAnd - * @see MetaOr - * @see MetaNot - * @see MetaQuery - */ -@JsExport -interface IMetaQuery : IQuery \ No newline at end of file diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/IQuery.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/IQuery.kt index bc41e5e64..1115860f4 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/IQuery.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/IQuery.kt @@ -6,7 +6,7 @@ import kotlin.js.JsExport /** * Marker interface for queries. - * @see IMetaQuery + * @see IMemberQuery * @see IPropertyQuery * @see ISpatialQuery * @see ITagQuery diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MetaAnd.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MemberAnd.kt similarity index 72% rename from here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MetaAnd.kt rename to here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MemberAnd.kt index c8b5d2492..bbd76bb46 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MetaAnd.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MemberAnd.kt @@ -10,14 +10,14 @@ import kotlin.js.JsName * Logically AND combine. */ @JsExport -class MetaAnd() : ListProxy(IMetaQuery::class), IMetaQuery { +class MemberAnd() : ListProxy(IMemberQuery::class), IMemberQuery { /** * Create a logical AND combination of the given queries. * @param queries the queries to combine. */ @JsName("of") - constructor(vararg queries: IMetaQuery) : this() { + constructor(vararg queries: IMemberQuery) : this() { addAll(queries) } } \ No newline at end of file diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MetaNot.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MemberNot.kt similarity index 72% rename from here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MetaNot.kt rename to here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MemberNot.kt index d4dd25fa5..58928b140 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MetaNot.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MemberNot.kt @@ -11,19 +11,19 @@ import kotlin.js.JsName * Negates the query. */ @JsExport -class MetaNot() : AnyObject(), IMetaQuery { +class MemberNot() : AnyObject(), IMemberQuery { /** * Create a negation of the given query. * @param query the query to negate. */ @JsName("of") - constructor(query: IMetaQuery) : this() { + constructor(query: IMemberQuery) : this() { this.query = query } companion object SpNot_C { - private val QUERY = NotNullProperty(IMetaQuery::class) + private val QUERY = NotNullProperty(IMemberQuery::class) } /** diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MetaOr.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MemberOr.kt similarity index 72% rename from here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MetaOr.kt rename to here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MemberOr.kt index cb9a175b3..0e0f0c220 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MetaOr.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MemberOr.kt @@ -10,14 +10,14 @@ import kotlin.js.JsName * Logically OR combine. */ @JsExport -class MetaOr() : ListProxy(IMetaQuery::class), IMetaQuery { +class MemberOr() : ListProxy(IMemberQuery::class), IMemberQuery { /** * Create a logical AND combination of the given queries. * @param queries the queries to combine. */ @JsName("of") - constructor(vararg queries: IMetaQuery) : this() { + constructor(vararg queries: IMemberQuery) : this() { addAll(queries) } } \ No newline at end of file diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MetaQuery.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MemberQuery.kt similarity index 72% rename from here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MetaQuery.kt rename to here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MemberQuery.kt index 48e4e75bc..bc6faadfb 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MetaQuery.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MemberQuery.kt @@ -9,11 +9,11 @@ import kotlin.js.JsExport import kotlin.js.JsName /** - * A meta-data query within the Naksha feature. + * A query about a specific [member][naksha.model.objects.Member]. * @since 3.0 */ @JsExport -open class MetaQuery() : AnyObject(), IMetaQuery { +open class MemberQuery() : AnyObject(), IMemberQuery { /** * Create an initialized property query. * @param column the column of the metadata to query. @@ -29,9 +29,9 @@ open class MetaQuery() : AnyObject(), IMetaQuery { } companion object PropertyQueryCompanion { - private val COLUMNS = NotNullProperty(MetaColumn::class) - private val QUERY_OP = NotNullProperty(AnyOp::class) - private val ANY = NullableProperty(Any::class) + private val COLUMNS = NotNullProperty(MetaColumn::class) + private val QUERY_OP = NotNullProperty(AnyOp::class) + private val ANY = NullableProperty(Any::class) } /** diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MetaColumn.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MetaColumn.kt deleted file mode 100644 index fac777028..000000000 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MetaColumn.kt +++ /dev/null @@ -1,669 +0,0 @@ -@file:Suppress("OPT_IN_USAGE", "MemberVisibilityCanBePrivate") - -package naksha.model.request.query - -import naksha.base.AnyObject -import naksha.base.NotNullProperty -import kotlin.js.JsExport -import kotlin.js.JsName -import kotlin.js.JsStatic -import kotlin.jvm.JvmStatic - -/** - * The meta-columns are virtual columns that can be searched by. - * - * Beware, that not all storages may support all columns as search target, therefore it is not recommended to search directly against these columns. They represent the properties of a [Tuple][naksha.model.Tuple] and its [Metadata][naksha.model.Metadata], and can be used when searching for features via [ReadFeatures][naksha.model.request.ReadFeatures]. Within a [ReadFeatures][naksha.model.request.ReadFeatures] search, the property [request.query][naksha.model.request.ReadFeatures.query] refers to a [RequestQuery][naksha.model.request.RequestQuery], which allows searching for meta-columns via [request.query.metadata][naksha.model.request.RequestQuery.metadata], which should be set to a [MetaQuery][naksha.model.request.query.MetaQuery]. - * - * For example: - * ```kotlin - * val request = ReadFeatures() - * request.query.metadata = - * MetaQuery(MetaColumn.author(), StringOp.EQUALS, "foo") - * ``` - * To query for `author` and `appId`, an [MetaAnd] can be used: - * ```kotlin - * val request = ReadFeatures() - * request.query.metadata = MetaAnd( - * MetaQuery(MetaColumn.author(), StringOp.EQUALS, "foo"), - * MetaQuery(MetaColumn.appId(), StringOp.EQUALS, "bar") - * ) - * ``` - * - * @since 3.0 - * @see [MetaQuery] - * @see [MetaAnd] - * @see [MetaOr] - * @see [MetaNot] - */ -@JsExport -open class MetaColumn() : AnyObject() { - - /** - * Create a column reference. - * @param name the field name. - * @since 3.0.0 - */ - @JsName("of") - constructor(name: String) : this() { - this.name = name - } - - override fun toString(): String = getOr("name", "") - override fun hashCode(): Int = toString().hashCode() - override fun equals(other: Any?): Boolean = toString() == other.toString() - - companion object TupleColumn_C { - /** - * The name of the virtual columns that stores the [feature-id][naksha.model.Tuple.id]. - * - * Supported [query operations][AnyOp] are: - * - [StringOp] - * - [AnyOp.IS_ANY_OF] - */ - const val ID = "id" - - /** - * Returns a new meta-column for [ID]. - * @return a new meta-column. - */ - @JvmStatic - @JsStatic - fun id(): MetaColumn = MetaColumn(ID) - - /** - * The name of the virtual columns that stores the [creation timestamp][naksha.model.Metadata.createdAt]. - * - * This value is exposed through [naksha.model.XyzNs.createdAt]. - * - * Supported [query operations][AnyOp] are: - * - [DoubleOp] - * - [AnyOp.IS_ANY_OF] - */ - const val CREATED_AT = "createdAt" - - /** - * Returns a new meta-column for [CREATED_AT]. - * @return a new meta-column. - */ - @JvmStatic - @JsStatic - fun createdAt(): MetaColumn = MetaColumn(CREATED_AT) - - /** - * The name of the virtual columns that stores the feature-number. - * - * This value is exposed through [naksha.model.XyzNs.uuid]. - * - * Supported [query operations][AnyOp] are: - * - [DoubleOp] - * - [AnyOp.IS_ANY_OF] - */ - const val FN = "fn" - - /** - * Returns a new meta-column for [FN]. - * @return a new meta-column. - */ - @JvmStatic - @JsStatic - fun fn(): MetaColumn = MetaColumn(FN) - - /** - * The name of the virtual columns that stores the [update timestamp][naksha.model.Metadata.updatedAt]. - * - * This value is exposed through [naksha.model.XyzNs.updatedAt]. - * - * Supported [query operations][AnyOp] are: - * - [DoubleOp] - * - [AnyOp.IS_ANY_OF] - */ - const val UPDATED_AT = "updatedAt" - - /** - * Returns a new meta-column for [UPDATED_AT]. - * @return a new meta-column. - */ - @JvmStatic - @JsStatic - fun updatedAt(): MetaColumn = MetaColumn(UPDATED_AT) - - /** - * The amount of changes that have been applied to a feature, a value between `1` and `2,147,483,647`. - * - * This value is exposed through [naksha.model.XyzNs.changeCount]. - * - * Supported [query operations][AnyOp] are: - * - [DoubleOp] - * - [AnyOp.IS_ANY_OF] - */ - const val CHANGE_COUNT = "changeCount" - - /** - * Returns a new meta-column for [CHANGE_COUNT]. - * @return a new meta-column. - */ - @JvmStatic - @JsStatic - fun changeCount(): MetaColumn = MetaColumn(CHANGE_COUNT) - - /** - * The name of the virtual column that exposes the binary [action][naksha.model.Action], stored in the lower two bits of [naksha.model.Metadata.tupleNumber]'s version. - * - * This value is exposed through [naksha.model.XyzNs.action]. - * - * Supported [query operations][AnyOp] are: - * - [DoubleOp] - * - [AnyOp.IS_ANY_OF] - */ - const val ACTION = "action" - - /** - * Returns a new meta-column for [ACTION]. - * @return a new meta-column. - */ - @JvmStatic - @JsStatic - fun action(): MetaColumn = MetaColumn(ACTION) - - /** - * The name of the virtual columns that stores the [hash][naksha.model.Metadata.calculateHash]. - * - * This value is exposed through [naksha.model.XyzNs.hash]. - * - * Supported [query operations][AnyOp] are: - * - [DoubleOp] - * - [AnyOp.IS_ANY_OF] - */ - const val HASH = "hash" - - /** - * Returns a new meta-column for [HASH]. - * @return a new meta-column. - */ - @JvmStatic - @JsStatic - fun hash(): MetaColumn = MetaColumn(HASH) - - /** - * The name of the virtual column that stores the [next version][naksha.model.Metadata.nextVersion]. - * - * Supported [query operations][AnyOp] are: - * - [DoubleOp] - * - [AnyOp.IS_ANY_OF] - */ - const val NEXT_VERSION = "next_version" - - /** - * Returns a new meta-column for [NEXT_VERSION]. - * @return a new meta-column. - */ - @JvmStatic - @JsStatic - fun nextVersion(): MetaColumn = MetaColumn(NEXT_VERSION) - - /** - * The name of the virtual columns that stores the [version][naksha.model.Metadata.version] (_transaction number_). - * - * This value is exposed through [naksha.model.XyzNs.version]. - * - * Supported [query operations][AnyOp] are: - * - [DoubleOp] - * - [AnyOp.IS_ANY_OF] - */ - const val VERSION = "txn" - - /** - * Returns a new meta-column for [VERSION]. - * @return a new meta-column. - */ - @JvmStatic - @JsStatic - fun version(): MetaColumn = MetaColumn(VERSION) - - /** - * The name of the virtual columns that stores the [uid][naksha.model.Metadata.uid]. - * - * This value is exposed through [naksha.model.XyzNs.uid]. - * - * Supported [query operations][AnyOp] are: - * - [DoubleOp] - * - [AnyOp.IS_ANY_OF] - */ - const val UID = "uid" - - /** - * Returns a new meta-column for [UID]. - * @return a new meta-column. - */ - @JvmStatic - @JsStatic - fun uid(): MetaColumn = MetaColumn(UID) - - /** - * The name of the virtual column that stores the [tuple-number][naksha.model.Metadata.tupleNumber], encoded as [160-bit variant][naksha.model.TupleNumberVariant.B160], as generated by [TupleNumber.toByteArray][naksha.model.TupleNumber.toByteArray]. - * - * This value is part of the [naksha.model.XyzNs.uuid]. - * - * Supported [query operations][AnyOp] are: - * - [AnyOp.IS_ANY_OF] - * @see [naksha.model.TupleNumberVariant.B160] - */ - const val TUPLE_NUMBER = "tuple_number" - - /** - * Returns a new meta-column for [TUPLE_NUMBER]. - * @return a new meta-column. - */ - @JvmStatic - @JsStatic - fun tupleNumber(): MetaColumn = MetaColumn(TUPLE_NUMBER) - - /** - * The name of the virtual column that stores the [base tuple-number][naksha.model.Metadata.baseTupleNumber], encoded as [128-bit variant][naksha.model.TupleNumberVariant.B128], as generated by [TupleNumber.toByteArray][naksha.model.TupleNumber.toByteArray]. - * - * This value is part of the [naksha.model.XyzNs.muuid]. - * - * Supported [query operations][AnyOp] are: - * - [AnyOp.IS_NULL] - * - [AnyOp.IS_NOT_NULL] - * - [AnyOp.IS_ANY_OF] - * @see [naksha.model.TupleNumberVariant.B128] - */ - const val BASE_TUPLE_NUMBER = "base_tn" - - /** - * Returns a new meta-column for [BASE_TUPLE_NUMBER]. - * @return a new meta-column. - */ - @JvmStatic - @JsStatic - fun baseTupleNumber(): MetaColumn = MetaColumn(BASE_TUPLE_NUMBER) - - /** - * The name of the virtual column that stores the [binary HERE tile number][naksha.geo.HereTile], indexing the [metadata HERE tile number][naksha.model.Metadata.hereTile]. - * - * The [binary HERE tile number][naksha.geo.HereTile] where the [reference-point][naksha.model.objects.NakshaFeature.referencePoint] of the [feature][naksha.model.objects.NakshaFeature] is located. It is possible to search directly the grid, but another options is to use the specialise [SpRefInHereTile] query. While this is more flexible, the specialised query will have a much better cache rate, and may run much faster. - * - * This value is part of the [naksha.model.XyzNs.hereTile]. - * - * Supported [query operations][AnyOp] are: - * - [DoubleOp] - * - [AnyOp.IS_ANY_OF] - */ - const val HERE_TILE = "hereTile" - - /** - * Returns a new meta-column for [HERE_TILE]. - * @return a new meta-column. - */ - @JvmStatic - @JsStatic - fun hereTile(): MetaColumn = MetaColumn(HERE_TILE) - - /** - * The name of the virtual column that stores the [author][naksha.model.Metadata.author]. - * - * This value is exposed as [naksha.model.XyzNs.author]. - * - * Supported [query operations][AnyOp] are: - * - [StringOp] - * - [AnyOp.IS_NULL] - * - [AnyOp.IS_NOT_NULL] - * - [AnyOp.IS_ANY_OF] - */ - const val AUTHOR = "author" - - /** - * Returns a new meta-column for [AUTHOR]. - * @return a new meta-column. - */ - @JvmStatic - @JsStatic - fun author(): MetaColumn = MetaColumn(AUTHOR) - - /** - * The name of the virtual column that stores the [origin][naksha.model.Metadata.origin] as string. - * - * This value is exposed as [naksha.model.XyzNs.origin]. - * - * Supported [query operations][AnyOp] are: - * - [StringOp] - * - [AnyOp.IS_NULL] - * - [AnyOp.IS_NOT_NULL] - * - [AnyOp.IS_ANY_OF] - */ - const val ORIGIN = "origin" - - /** - * Returns a new meta-column for [ORIGIN]. - * @return a new meta-column. - */ - @JvmStatic - @JsStatic - fun origin(): MetaColumn = MetaColumn(ORIGIN) - - /** - * The name of the virtual column that stores the [target][naksha.model.Metadata.target] as string. - * - * This value is exposed as [naksha.model.XyzNs.target]. - * - * Supported [query operations][AnyOp] are: - * - [StringOp] - * - [AnyOp.IS_NULL] - * - [AnyOp.IS_NOT_NULL] - * - [AnyOp.IS_ANY_OF] - */ - const val TARGET = "target" - - /** - * Returns a new meta-column for [TARGET]. - * @return a new meta-column. - */ - @JvmStatic - @JsStatic - fun target(): MetaColumn = MetaColumn(TARGET) - - /** - * The name of the virtual columns that stores the [author change timestamp][naksha.model.Metadata.authorTs]. - * - * This value is exposed as [naksha.model.XyzNs.authorTs]. - * - * Supported [query operations][AnyOp] are: - * - [DoubleOp] - * - [AnyOp.IS_NULL] - * - [AnyOp.IS_NOT_NULL] - * - [AnyOp.IS_ANY_OF] - */ - const val AUTHOR_TS = "author_ts" - - /** - * Returns a new meta-column for [AUTHOR_TS]. - * @return a new meta-column. - */ - @JvmStatic - @JsStatic - fun authorTs(): MetaColumn = MetaColumn(AUTHOR_TS) - - /** - * The name of the virtual columns that stores the [author change timestamp][naksha.model.Metadata.appId]. - * - * This value is exposed as [naksha.model.XyzNs.appId]. - * - * Supported [query operations][AnyOp] are: - * - [StringOp] - * - [AnyOp.IS_ANY_OF] - */ - const val APP_ID = "app_id" - - /** - * Returns a new meta-column for [APP_ID]. - * @return a new meta-column. - */ - @JvmStatic - @JsStatic - fun appId(): MetaColumn = MetaColumn(APP_ID) - - /** - * The name of the virtual columns that stores the [type][naksha.model.Metadata.ft]. - * - * This value is exposed as [naksha.model.XyzNs.featureType]. - * - * Supported [query operations][AnyOp] are: - * - [StringOp] - * - [AnyOp.IS_NULL] - * - [AnyOp.IS_NOT_NULL] - * - [AnyOp.IS_ANY_OF] - */ - const val FEATURE_TYPE = "type" - - /** - * Returns a new meta-column for [FEATURE_TYPE]. - * @return a new meta-column. - */ - @JvmStatic - @JsStatic - fun featureType(): MetaColumn = MetaColumn(FEATURE_TYPE) - - /** - * The name of the virtual columns that stores the [cv][naksha.model.Metadata.cv0] (_custom value_). - * - * This value is exposed through [naksha.model.XyzNs.cv0]. - * - * Supported [query operations][AnyOp] are: - * - [DoubleOp] - * - [AnyOp.IS_ANY_OF] - */ - const val CV0 = "cv0" - - /** - * Returns a new meta-column for [CV0]. - * @return a new meta-column. - */ - @JvmStatic - @JsStatic - fun cv0(): MetaColumn = MetaColumn(CV0) - - /** - * The name of the virtual columns that stores the [cv][naksha.model.Metadata.cv1] (_custom value_). - * - * This value is exposed through [naksha.model.XyzNs.cv1]. - * - * Supported [query operations][AnyOp] are: - * - [DoubleOp] - * - [AnyOp.IS_ANY_OF] - */ - const val CV1 = "cv1" - - /** - * Returns a new meta-column for [CV1]. - * @return a new meta-column. - */ - @JvmStatic - @JsStatic - fun cv1(): MetaColumn = MetaColumn(CV1) - - /** - * The name of the virtual columns that stores the [cv][naksha.model.Metadata.cv2] (_custom value_). - * - * This value is exposed through [naksha.model.XyzNs.cv2]. - * - * Supported [query operations][AnyOp] are: - * - [DoubleOp] - * - [AnyOp.IS_ANY_OF] - */ - const val CV2 = "cv2" - - /** - * Returns a new meta-column for [CV2]. - * @return a new meta-column. - */ - @JvmStatic - @JsStatic - fun cv2(): MetaColumn = MetaColumn(CV2) - - /** - * The name of the virtual columns that stores the [cv][naksha.model.Metadata.cv3] (_custom value_). - * - * This value is exposed through [naksha.model.XyzNs.cv3]. - * - * Supported [query operations][AnyOp] are: - * - [DoubleOp] - * - [AnyOp.IS_ANY_OF] - */ - const val CV3 = "cv3" - - /** - * Returns a new meta-column for [CV3]. - * @return a new meta-column. - */ - @JvmStatic - @JsStatic - fun cv3(): MetaColumn = MetaColumn(CV3) - - /** - * The name of the virtual columns that stores the [first custom value][naksha.model.Metadata.cs0]. - * - * This value is exposed as [naksha.model.XyzNs.cs0]. - * - * Supported [query operations][AnyOp] are: - * - [StringOp] - * - [AnyOp.IS_NULL] - * - [AnyOp.IS_NOT_NULL] - * - [AnyOp.IS_ANY_OF] - */ - const val CS0 = "cs0" - - /** - * Returns a new meta-column for [CS0]. - * @return a new meta-column. - */ - @JvmStatic - @JsStatic - fun cs0(): MetaColumn = MetaColumn(CS0) - - /** - * The name of the virtual columns that stores the [first custom value][naksha.model.Metadata.cs1]. - * - * This value is exposed as [naksha.model.XyzNs.cs1]. - * - * Supported [query operations][AnyOp] are: - * - [StringOp] - * - [AnyOp.IS_NULL] - * - [AnyOp.IS_NOT_NULL] - * - [AnyOp.IS_ANY_OF] - */ - const val CS1 = "cs1" - - /** - * Returns a new meta-column for [CS1]. - * @return a new meta-column. - */ - @JvmStatic - @JsStatic - fun cs1(): MetaColumn = MetaColumn(CS1) - - /** - * The name of the virtual columns that stores the [first custom value][naksha.model.Metadata.cs2]. - * - * This value is exposed as [naksha.model.XyzNs.cs2]. - * - * Supported [query operations][AnyOp] are: - * - [StringOp] - * - [AnyOp.IS_NULL] - * - [AnyOp.IS_NOT_NULL] - * - [AnyOp.IS_ANY_OF] - */ - const val CS2 = "cs2" - - /** - * Returns a new meta-column for [CS2]. - * @return a new meta-column. - */ - @JvmStatic - @JsStatic - fun cs2(): MetaColumn = MetaColumn(CS2) - - /** - * The name of the virtual columns that stores the [first custom value][naksha.model.Metadata.cs3]. - * - * This value is exposed as [naksha.model.XyzNs.cs3]. - * - * Supported [query operations][AnyOp] are: - * - [StringOp] - * - [AnyOp.IS_NULL] - * - [AnyOp.IS_NOT_NULL] - * - [AnyOp.IS_ANY_OF] - */ - const val CS3 = "cs3" - - /** - * Returns a new meta-column for [CS3]. - * @return a new meta-column. - */ - @JvmStatic - @JsStatic - fun cs3(): MetaColumn = MetaColumn(CS3) - - /** - * The name of the virtual columns that stores the [feature][naksha.model.Tuple.featureBytes]. - * - * This can only be queried using a special [property query][IPropertyQuery]. - */ - const val FEATURE = "feature" - - /** - * Returns a new meta-column for [FEATURE]. - * @return a new meta-column. - */ - @JvmStatic - @JsStatic - fun feature(): MetaColumn = MetaColumn(FEATURE) - - /** - * The name of the virtual columns that stores the geometry. - * - * This can only be queried using a special [spatial query][ISpatialQuery]. - */ - const val GEOMETRY = "geo" - - /** - * Returns a new meta-column for [GEOMETRY]. - * @return a new meta-column. - */ - @JvmStatic - @JsStatic - fun geometry(): MetaColumn = MetaColumn(GEOMETRY) - - /** - * The name of the virtual columns that stores the reference point. - * - * This can only be queried using a special [spatial query][ISpatialQuery]. - */ - const val REF_POINT = "referencePoint" - - /** - * Returns a new meta-column for [REF_POINT]. - * @return a new meta-column. - */ - @JvmStatic - @JsStatic - fun referencePoint(): MetaColumn = MetaColumn(REF_POINT) - - /** - * The name of the virtual columns that stores the tags. - * - * This can only be queried using a special [tag query][ITagQuery]. - */ - const val TAGS = "tags" - - /** - * Returns a new meta-column for [TAGS]. - * @return a new meta-column. - */ - @JvmStatic - @JsStatic - fun tags(): MetaColumn = MetaColumn(TAGS) - - /** - * The name of the virtual columns that stores the attachment. - * - * Attachments can't be queried! - */ - const val ATTACHMENT = "attachment" - - /** - * Returns a new meta-column for [ATTACHMENT]. - * @return a new meta-column. - */ - @JvmStatic - @JsStatic - fun attachment(): MetaColumn = MetaColumn(ATTACHMENT) - - private val NAME = NotNullProperty(String::class) { _, _ -> "" } - } - - /** - * The name of the field. - */ - var name by NAME -} \ No newline at end of file diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/Property.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/Property.kt index 806be4a1d..564e46efa 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/Property.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/Property.kt @@ -9,8 +9,6 @@ import naksha.base.StringList import naksha.base.fn.Fn1 import kotlin.js.JsExport import kotlin.js.JsName -import kotlin.js.JsStatic -import kotlin.jvm.JvmStatic /** * The reference to a property within a feature. @@ -18,7 +16,7 @@ import kotlin.jvm.JvmStatic * **Warning:** You should not search for the `id`, `geometry`, or anything from [`properties->@ns:com:here:xyz`][naksha.model.XyzNs] using this query, because there are specialized, and optimized, dedicated queries available. So avoid things like `PQuery(Property("id"), StringOp.EQUALS, "foo"`. * @see naksha.model.request.ReadFeatures.featureIds * @see ISpatialQuery - * @see IMetaQuery + * @see IMemberQuery * @see ITagQuery */ @JsExport diff --git a/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/TupleNumberQueryTest.kt b/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/TupleNumberQueryTest.kt index 6ab6f833a..75330f482 100644 --- a/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/TupleNumberQueryTest.kt +++ b/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/TupleNumberQueryTest.kt @@ -3,7 +3,7 @@ package naksha.model import naksha.base.Int64 import naksha.model.request.query.AnyOp import naksha.model.request.query.MetaColumn -import naksha.model.request.query.MetaQuery +import naksha.model.request.query.MemberQuery import kotlin.random.Random import kotlin.test.Ignore import kotlin.test.Test @@ -21,7 +21,7 @@ class TupleNumberQueryTest { randomTupleNumber().toByteArray(TupleNumberVariant.B64), randomTupleNumber().toByteArray(TupleNumberVariant.B64) ) - val metaQuery = MetaQuery(MetaColumn.nextVersion(), AnyOp.IS_ANY_OF, serializedTupleNumbers) + val metaQuery = MemberQuery(MetaColumn.nextVersion(), AnyOp.IS_ANY_OF, serializedTupleNumbers) // Then assertIs>(metaQuery.value) diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/LibPsql.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/LibPsql.kt index 82ef287bc..c6bdc428b 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/LibPsql.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/LibPsql.kt @@ -35,64 +35,49 @@ val adminVersion = NakshaVersion.of("3.0.0-beta.24") internal const val PG_S = "\$" /** - * ``: The identifier for the HEAD-table, no prefix. + * ``: The postfix for the HEAD-table, no prefix. */ internal const val PG_HEAD = "" /** - * `$del`: The identifier for the DELETION-table. - */ -internal const val PG_DEL = "${PG_S}del" - -/** - * `$hst`: The identifier for the HISTORY-table. + * `$hst`: The postfix for the HISTORY-table. */ internal const val PG_HST = "${PG_S}hst" /** - * `$meta`: The identifier for the META-table. + * `$meta`: The postfix for the META-table. */ internal const val PG_META = "${PG_S}meta" /** - * `$i_???`: The prefix used for indices, followed by the index identifier, e.g. `$i_id` - */ -internal const val PG_IDX = "${PG_S}i_" - -/** - * `$ci_???`: The prefix used for user-defined (custom) indices, followed by the index identifier, e.g. `$ci_my_idx`. - */ -internal const val PG_CUSTOM_IDX = "${PG_S}ci_" - -/** - * `$c_??`: The prefix used for constraints, followed by the identifier of the constraint. + * `$i`: The prefix used for index-names. The pattern is `{tablename}$i{ */ -internal const val PG_CONSTRAINT = "${PG_S}c_" +internal const val PG_IDX = "${PG_S}i" /** - * The name of the constraint above [next_version][PgColumn.next_version] (yearly partition). + * `$c`: The prefix used for constraints, followed by the identifier of the constraint. */ -internal const val PG_TN_NEXT_CONSTRAINT = "${PG_CONSTRAINT}nt" +internal const val PG_CONSTRAINT = "${PG_S}c" /** - * The name of the partition-constraint above [id][PgColumn.id]. + * `$c_nv`: The postfix of the history-constraint above [next_version][PgColumn.NEXT_VERSION] _(shifted partition)_. */ -internal const val PG_ID_CONSTRAINT = "${PG_CONSTRAINT}id" +internal const val PG_HISTORY_CONSTRAINT = "${PG_CONSTRAINT}nv" /** - * `$p_`: The prefix used for numerated partitions, the final value is `$p???` with `?` being `[0-9]`. + * `$c_fn`: The postfix of the distribution-constraint above [feature-number][PgColumn.FN]. */ -internal const val PG_PART = "${PG_S}p" +internal const val PG_DIST_CONSTRAINT = "${PG_CONSTRAINT}fn" /** - * `$head`: The separator used for HEAD-table index names: `{tableName}$head${indexName}`. + * `$h`: The prefix used for history-partitions. */ -internal const val PG_HEAD_IDX = "${PG_S}head${PG_S}" +internal const val PG_HISTORY_PARTITION = "${PG_S}h" /** - * `$y_`: The prefix used for yearly partitions of the TRANSACTIONS table, the final value is `$y????` with `?` being `[0-9]`. + * `$p`: The prefix used for distribution-partitions. */ -internal const val PG_YEAR = "${PG_S}y" +internal const val PG_DIST_PARTITION = "${PG_S}p" /** * The prefix used for all internal tables. diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgAdminMap.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgAdminCatalog.kt similarity index 84% rename from here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgAdminMap.kt rename to here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgAdminCatalog.kt index 01266e1f1..191c4fb66 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgAdminMap.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgAdminCatalog.kt @@ -15,12 +15,11 @@ import naksha.jbon.JbDictionary import naksha.model.* import naksha.model.Naksha.NakshaCompanion.ADMIN_CATALOG_ID import naksha.model.Naksha.NakshaCompanion.ADMIN_CATALOG_FN -import naksha.model.Naksha.NakshaCompanion.CATALOGS_COL_FN import naksha.model.NakshaError.NakshaErrorCompanion.EXCEPTION import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_ARGUMENT +import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_STATE import naksha.model.NakshaError.NakshaErrorCompanion.STORAGE_ID_MISMATCH import naksha.model.objects.NakshaCatalog -import naksha.psql.PgColumn.PgColumnCompanion.headColumns import kotlin.js.ExperimentalJsExport import kotlin.js.JsExport @@ -31,7 +30,7 @@ import kotlin.js.JsExport * @since 3.0.0 */ @JsExport -abstract class PgAdminMap internal constructor( +abstract class PgAdminCatalog internal constructor( /** * The storage to which this admin-map belongs. * @since 3.0.0 @@ -55,7 +54,7 @@ abstract class PgAdminMap internal constructor( * @since 3.0.0 */ upgrade: Boolean? -) : PgMap(storage, NakshaCatalog().withDatabaseId(storage.id).withId(ADMIN_CATALOG_ID)), IDictReader { +) : PgCatalog(storage, NakshaCatalog().withDatabaseId(storage.id).withId(ADMIN_CATALOG_ID)), IDictReader { /** * The page-size of the database (`current_setting('block_size')`). * @since 3.0.0 @@ -249,7 +248,7 @@ SELECT basics.*, procs.* FROM basics, procs; if (admin_schema_oid == null) { if (!doCreate) throw forbidden("Creation of admin-map needed, but forbidden by config") logger.info("Install Naksha admin-map in version $psql_version for storage $id / $number") - schemaOid = createAdminMap(conn, config, id, number, psql_version) + schemaOid = createAdminCatalog(conn, config, id, number, psql_version) } else { schemaOid = admin_schema_oid if (has_naksha_version != true) { @@ -290,7 +289,7 @@ SELECT basics.*, procs.* FROM basics, procs; logger.info("The admin-map of '$id' is in version $installed_version, this library uses version $psql_version") if (doOverride) { logger.warn("Forcefully upgrade storage '$id' admin-map (current=$installed_version, new=$psql_version)") - upgradeAdminMap(conn, config, id, number, psql_version, admin_schema_oid, installed_version) + upgradeAdminCatalog(conn, config, id, number, psql_version, admin_schema_oid, installed_version) } else { if (installed_version > psql_version) { throw illegalState("The storage '$id' is in a newer version ($installed_version) than this library ($psql_version), access denied (otherwise we risk damaging the storage)") @@ -300,10 +299,10 @@ SELECT basics.*, procs.* FROM basics, procs; throw illegalState("The storage '$id' is in a newer version ($installed_version) that this library ($psql_version), access denied (there is a risk damaging the storage)") } logger.info("Upgrade Naksha admin-map from $installed_version to $psql_version for storage $id") - upgradeAdminMap(conn, config, id, number, psql_version, admin_schema_oid, installed_version) + upgradeAdminCatalog(conn, config, id, number, psql_version, admin_schema_oid, installed_version) } else if (doUpgrade){ logger.info("Upgrade Naksha admin-map from $installed_version to $psql_version for storage $id") - upgradeAdminMap(conn, config, id, number, psql_version, admin_schema_oid, installed_version) + upgradeAdminCatalog(conn, config, id, number, psql_version, admin_schema_oid, installed_version) } else { logger.info("In storage '$id' admin-map is in version $installed_version, this library is version $psql_version, but we should not upgrade the storage, and are okay working with the older version") } @@ -312,15 +311,7 @@ SELECT basics.*, procs.* FROM basics, procs; logger.info("The admin-map of '$id' is up-to-date: $psql_version") } } - // Always ensure internal collections have all currently-required columns on every startup. - // This is a lightweight migration: ALTER TABLE … ADD COLUMN IF NOT EXISTS is a no-op when - // the column already exists, and propagates automatically to all partitions in PostgreSQL. setSearchPath(conn) - for (c in listOf(transactions, books, catalogs)) { - c.headTable.addMissingCustomColumns(conn) - c.historyTable?.addMissingCustomColumns(conn) - c.metaTable?.addMissingCustomColumns(conn) - } logger.info("Load OID of '$NAKSHA_TXN_SEQ' from admin schema (schema-oid=$schemaOid)") val SQL = "SELECT oid FROM pg_class WHERE relnamespace = $schemaOid AND relname = '$NAKSHA_TXN_SEQ'" conn.execute(SQL).fetch().use { cursor -> @@ -345,7 +336,7 @@ SELECT basics.*, procs.* FROM basics, procs; * @return the admin schema `OID`. * @since 3.0.0 */ - protected abstract fun createAdminMap( + protected abstract fun createAdminCatalog( conn: PgConnection, config: PgConfig, storageId: String, @@ -366,7 +357,7 @@ SELECT basics.*, procs.* FROM basics, procs; * @param installedVersion the PSQL version that is currently installed that should be upgraded, if any (maybe only the schema exists). * @since 3.0.0 */ - protected abstract fun upgradeAdminMap( + protected abstract fun upgradeAdminCatalog( conn: PgConnection, config: PgConfig, storageId: String, @@ -445,55 +436,44 @@ SELECT basics.*, procs.* FROM basics, procs; } } - /** - * Returns the current map-number, so the last used one. - * @param conn the connection to use to access the database. - * @return the current _(last used)_ map-number. - * @since 3.0.0 - */ -// fun getMapNumber(conn: PgConnection): Int { -// val QUERY = "SELECT currval($1) as mapnum" -// val cursor = conn.execute(QUERY, arrayOf(mapNumberSequenceOid)).fetch() -// cursor.use { -// val number: Int = cursor["mapnum"] -// return number -// } -// } + protected val catalogCache = AtomicMap() /** - * Allocate a new map-number. - * @param conn the connection to use to access the database. - * @return the allocated map-number. - * @since 3.0.0 + * Store the given [PgCatalog] into the cache. + * @param catalog the catalog to store, must have a valid _HEAD_ state **and** must have a valid [TupleNumber]. + * @since 3.0 */ -// fun newMapNumber(conn: PgConnection): Int { -// val QUERY = "SELECT nextval($1) as mapnum" -// val cursor = conn.execute(QUERY, arrayOf(mapNumberSequenceOid)).fetch() -// cursor.use { -// val number: Int = cursor["mapnum"] -// return number -// } -// } - - // TODO: Implement the methods, then make them open, so we can override them for the JVM implementation - // We only want to cache in JVM, not within the database! - - protected val mapCache = AtomicMap() - protected val mapNumberById = AtomicMap() - - protected fun storeMap(map: PgMap) { - mapNumberById[map.id] = map.number - // TODO: Improve this, we should keep the PgMap that has the higher version! - mapCache[map.number] = map + protected fun storeCatalog(catalog: PgCatalog) { + do { + val id = catalog.id + val newCatalog = catalog.head + val catalogNumber = newCatalog.catalogNumber + val newVersion = newCatalog.tupleNumber?.version ?: throw NakshaException( + ILLEGAL_STATE, + "Cannot store catalog '$id' in cache, missing `tupleNumber`" + ) + + val existing = catalogCache[catalogNumber] + val existingTn = existing?.head?.tupleNumber + val existingVersion: Int64? = if (existingTn != null && Action.fromVersion(existingTn.version) != Action.DELETE) existingTn.version else null + if (existingVersion != null && existingVersion > newVersion) { + logger.debug("Do not update catalog '$id', the existing version ($existingVersion) is newer than the new ($newVersion)") + break + } + if (existing != null) { + if (catalogCache.replace(catalogNumber, existing, catalog)) break + } else { + if (catalogCache.putIfAbsent(catalogNumber, catalog) == null) break + } + } while (true) } - protected fun invalidateMap(map: PgMap) { - mapCache.remove(map.number, map) - //mapNumberById.remove(map.id, map.number) + protected fun invalidateCatalog(catalog: PgCatalog) { + catalogCache.remove(catalog.head.catalogNumber, catalog) } /** - * Create a new [map][PgMap] using the given connection, and return it. + * Create a new [map][PgCatalog] using the given connection, and return it. * * ### Note * Does not commit the given connection, therefore the map is not yet persisted, but can be used through the given connection. The method neither creates the corresponding entry in the collection's collection of the admin-map, it only creates the schema and collection-number sequence counter! @@ -504,11 +484,11 @@ SELECT basics.*, procs.* FROM basics, procs; * @return the created map. * @since 3.0.0 */ - fun createPgMap(conn: PgConnection, map: PgMap) { + fun createPgCatalog(conn: PgConnection, map: PgCatalog) { if (Naksha.isInternalId(map.id)) throw NakshaException(ILLEGAL_ARGUMENT, "Can't create internal maps: ${map.id}") conn.execute("CREATE SCHEMA IF NOT EXISTS ${map.quotedId}").close() map.createPgCollection(conn, map.collections) // 0 - invalidateMap(map) + invalidateCatalog(map) } /** @@ -520,10 +500,10 @@ SELECT basics.*, procs.* FROM basics, procs; * @param map the map to delete. * @since 3.0.0 */ - fun deletePgMap(conn: PgConnection, map: PgMap) { + fun deletePgCatalog(conn: PgConnection, map: PgCatalog) { if (Naksha.isInternalId(map.id)) throw NakshaException(ILLEGAL_ARGUMENT, "Can't delete internal maps: ${map.id}") conn.execute("DROP SCHEMA IF EXISTS ${map.quotedId} CASCADE").close() - invalidateMap(map) + invalidateCatalog(map) } /** @@ -533,18 +513,13 @@ SELECT basics.*, procs.* FROM basics, procs; * @return the map, if it exists; _null_ otherwise. * @since 3.0.0 */ - fun getPgMapById(conn: PgConnection?, id: String): PgMap? { + fun getPgCatalogById(conn: PgConnection?, id: String): PgCatalog? { if (ADMIN_CATALOG_ID == id) return this - val number = mapNumberById[id] - val existing = if (number != null) mapCache[number] else null - if (existing != null) return existing - if (conn == null) return null + val catalogNumber = Naksha.catalogNumber(id) + val existing = catalogCache[catalogNumber] + if (existing != null || conn==null) return existing - val outRows = PgColumnRows(catalogs.head) - .withDatabaseNumber(storage.number) - .withCatalogNumber(ADMIN_CATALOG_FN) - .withCollectionNumber(CATALOGS_COL_FN) - .addColumns(headColumns) + val outRows = PgRows().useHeadTable().withCollection(catalogs.head) val SQL = """SELECT ${outRows.names()} FROM "naksha~admin".${catalogs.headTable.quotedName} WHERE id = $1 AND (version & 3) < 2""" @@ -556,9 +531,9 @@ WHERE id = $1 AND (version & 3) < 2""" val tuple = outRows[0] ?: return null Naksha.cache.store(tuple) val nakshaMap = tuple.decodeFeature(null).proxy(NakshaCatalog::class) - val pgMap = PgMap(storage, nakshaMap) - storeMap(pgMap) - return pgMap + val pgCatalog = PgCatalog(storage, nakshaMap) + storeCatalog(pgCatalog) + return pgCatalog } /** @@ -568,18 +543,14 @@ WHERE id = $1 AND (version & 3) < 2""" * @return the map, if it exists; _null_ otherwise. * @since 3.0.0 */ - fun getPgMapByNumber(conn: PgConnection?, number: Int): PgMap? { + fun getPgCatalogByNumber(conn: PgConnection?, number: Int): PgCatalog? { if (ADMIN_CATALOG_FN == number) return this - val existing = mapCache[number] + val existing = catalogCache[number] if (existing != null) return existing if (conn == null) return null // Read from database - val outRows = PgColumnRows(catalogs.head) - .withDatabaseNumber(storage.number) - .withCatalogNumber(ADMIN_CATALOG_FN) - .withCollectionNumber(CATALOGS_COL_FN) - .addColumns(headColumns) + val outRows = PgRows().useHeadTable().withCollection(catalogs.head) val SQL = """ SELECT ${outRows.names()} FROM "naksha~admin".${catalogs.headTable.quotedName} @@ -594,9 +565,9 @@ WHERE id = $1 AND (version & 3) < 2""" val tuple = outRows[0] ?: return null Naksha.cache.store(tuple) val nakshaMap = tuple.decodeFeature(null).proxy(NakshaCatalog::class) - val pgMap = PgMap(storage, nakshaMap) - storeMap(pgMap) - return pgMap + val pgCatalog = PgCatalog(storage, nakshaMap) + storeCatalog(pgCatalog) + return pgCatalog } /** @@ -606,7 +577,7 @@ WHERE id = $1 AND (version & 3) < 2""" * @since 3.0.0 */ fun listPgMaps(conn: PgConnection): PgMapList - = PgMapList().withAll(mapCache.mapNotNull { it.value }) + = PgMapList().withAll(catalogCache.mapNotNull { it.value }) // TODO: This only reads the cache, but we need to load from database! abstract fun getDataEncoding(feature: Any?, context: Any?): DataEncoding diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMap.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCatalog.kt similarity index 82% rename from here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMap.kt rename to here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCatalog.kt index 01e516ed6..6e5ee83aa 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMap.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCatalog.kt @@ -4,6 +4,7 @@ package naksha.psql import naksha.base.* import naksha.base.Platform.PlatformCompanion.logger +import naksha.model.Action import naksha.model.Naksha import naksha.model.Naksha.NakshaCompanion.COLLECTIONS_COL_ID import naksha.model.Naksha.NakshaCompanion.COLLECTIONS_COL_FN @@ -15,9 +16,9 @@ import naksha.model.Naksha.NakshaCompanion.TRANSACTIONS_COL_ID import naksha.model.Naksha.NakshaCompanion.TRANSACTIONS_COL_FN import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_STATE import naksha.model.NakshaException +import naksha.model.TupleNumber import naksha.model.objects.NakshaCollection import naksha.model.objects.NakshaCatalog -import naksha.psql.PgColumn.PgColumnCompanion.headColumns import naksha.psql.PgUtil.PgUtilCompanion.quoteIdent import kotlin.js.JsExport import kotlin.jvm.JvmField @@ -27,30 +28,30 @@ import kotlin.jvm.JvmField * @since 3.0 */ @JsExport -open class PgMap internal constructor( +open class PgCatalog internal constructor( /** - * The reference to the storage. + * The reference to the storage (effectively the same as the database, for now). * @since 3.0.0 */ open val storage: PgStorage, /** - * The HEAD state of the map. + * The HEAD state of the catalog. * @since 3.0.0 */ - nakshaMap: NakshaCatalog, + nakshaCatalog: NakshaCatalog, /** - * The map-id. + * The custom catalog-identifier. * @since 3.0 */ - val id: String = nakshaMap.id, + val id: String = nakshaCatalog.id, /** - * The map-number. + * The catalog-number of the catalog, actually the same as the feature-number of the [NakshaCatalog] feature. * @since 3.0 */ - val number: Int = nakshaMap.number + val catalogNumber: Int = Naksha.catalogNumber(id), ) { /** * The map-identifier quoted optionally in double quotes. @@ -63,10 +64,10 @@ open class PgMap internal constructor( * The _HEAD_ state of the map. * * ### Note - * If the map is deleted, this value stays unmodified, because the [PgMap] will be removed from caching. However, if only the _HEAD_ state of the map is modified, so basically an `UPDATE` is done, the _HEAD_ reference is replaced on-the-fly. + * If the map is deleted, this value stays unmodified, because the [PgCatalog] will be removed from caching. However, if only the _HEAD_ state of the map is modified, so basically an `UPDATE` is done, the _HEAD_ reference is replaced on-the-fly. * @since 3.0 */ - val headRef = AtomicNonNullRef(nakshaMap) + val headRef = AtomicNonNullRef(nakshaCatalog) /** * Reads [headRef]. @@ -100,24 +101,46 @@ open class PgMap internal constructor( } protected val collectionCache = AtomicMap() - protected val collectionNumberById = AtomicMap() + /** + * Store the given [PgCollection] into the cache. + * @param collection the collection to store, must have a valid _HEAD_ state **and** must have a valid [TupleNumber]. + * @since 3.0 + */ protected fun storeCollection(collection: PgCollection) { - collectionNumberById[collection.id] = collection.number - // TODO: Improve this, we should keep the PgCollection that has the higher version! - collectionCache[collection.number] = collection + do { + val id = collection.id + val newCollection = collection.head + val collectionNumber = newCollection.collectionNumber + val newVersion = newCollection.tupleNumber?.version ?: throw NakshaException( + ILLEGAL_STATE, + "Cannot store collection '$id' in cache, missing `tupleNumber`" + ) + + val existing = collectionCache[collectionNumber] + val existingTn = existing?.head?.tupleNumber + val existingVersion: Int64? = if (existingTn != null && Action.fromVersion(existingTn.version) != Action.DELETE) existingTn.version else null + if (existingVersion != null && existingVersion > newVersion) { + logger.debug("Do not update collection '$id', the existing version ($existingVersion) is newer than the new ($newVersion)") + break + } + if (existing != null) { + if (collectionCache.replace(collectionNumber, existing, collection)) break + } else { + if (collectionCache.putIfAbsent(collectionNumber, collection) == null) break + } + } while (true) } internal fun invalidateCollection(collection: PgCollection) { - collectionCache.remove(collection.number, collection) - //collectionNumberById.remove(collection.id, collection.number) + collectionCache.remove(collection.head.collectionNumber, collection) } /** * Returns the `search_path` so that this map is on the top, followed by `naksha~admin`, `topology`, `hint_plan`, `public`. * @return the `search_path` so that this map is on the top, followed by `naksha~admin`, `topology`, `hint_plan`, `public`. */ - fun getSearchPath(): String = if (this is PgAdminMap) { + fun getSearchPath(): String = if (this is PgAdminCatalog) { "SET search_path = \"naksha~admin\", topology, hint_plan, public" } else { "SET search_path = ${quotedId}, \"naksha~admin\", topology, hint_plan, public" @@ -191,8 +214,8 @@ open class PgMap internal constructor( val history = collection.historyTable if (history != null) { history.create(conn) - history.createYear(conn, NOW.year) - history.createYear(conn, NOW.year + 1) + history.createPartition(conn, NOW.year) + history.createPartition(conn, NOW.year + 1) history.createIndex(conn, PgIndex.id) history.createIndex(conn, PgIndex.version) history.createIndex(conn, PgIndex.gbn_idx) @@ -210,7 +233,7 @@ open class PgMap internal constructor( */ private fun refreshPgCollection(conn: PgConnection, collection: PgCollection): PgCollection { // TODO: Fix me! - val cursor = PgRelation.select(conn, collection.map.id, id) + val cursor = PgRelation.select(conn, collection.catalog.id, id) cursor.use { // // NOTE: We ignore all unknown relations, that allows users to add some own indices and relations! @@ -271,19 +294,19 @@ open class PgMap internal constructor( "Invalid amount of HEAD partitions found, must be 2..256, but is ${headPartitions.size}" ) } - collection.headTable = PgHead(collection, headRelation.storageClass, parts) + collection.headTable = PgHeadTable(collection, headRelation.storageClass, parts) } else { - collection.headTable = PgHead(collection, headRelation.storageClass, 0) + collection.headTable = PgHeadTable(collection, headRelation.storageClass, 0) } for (index in headIndices) collection.headTable.addIndex(index) } if (historyRelation != null) { - val history = PgHistory(collection.headTable) + val history = PgHistoryTable(collection.headTable) collection.historyTable = history for (entry in historyYears) history.years[entry.key] = PgHistoryYear(history, entry.key) } if (metaRelation != null) { - val meta = PgMeta(collection.headTable) + val meta = PgMetaTable(collection.headTable) collection.metaTable = meta for (index in metaIndices) meta.addIndex(index) } @@ -307,7 +330,7 @@ open class PgMap internal constructor( val history = collection.historyTable if (history != null) builder.append("DROP TABLE IF EXISTS ${history.quotedName} CASCADE;\n") val SQL = builder.toString() - logger.info("Dropped collection {}@{}", collection.id, collection.number) + logger.info("Dropped collection '{}' with collection-number {}", collection.id, collection.head.collectionNumber) conn.execute(SQL).close() invalidateCollection(collection) } @@ -320,7 +343,7 @@ open class PgMap internal constructor( * @since 3.0.0 */ fun getPgCollectionById(conn: PgConnection?, id: String): PgCollection? { - if (this is PgAdminMap) { + if (this is PgAdminCatalog) { return when (id) { COLLECTIONS_COL_ID -> collections TRANSACTIONS_COL_ID -> transactions @@ -330,16 +353,12 @@ open class PgMap internal constructor( } } if (id == COLLECTIONS_COL_ID) return collections - val number = collectionNumberById[id] - val existing = if (number != null) collectionCache[number] else null + val collectionNumber = Naksha.collectionNumber(id) + val existing = collectionCache[collectionNumber] if (existing != null || conn == null) return existing // Read from database - val outRows = PgColumnRows(collections.head) - .withDatabaseNumber(storage.number) - .withCatalogNumber(this.number) - .withCollectionNumber(COLLECTIONS_COL_FN) - .addColumns(headColumns) + val outRows = PgRows().withCollection(collections.head) setSearchPath(conn) val SQL = """SELECT ${outRows.names()} FROM ${collections.headTable.quotedName} @@ -365,7 +384,7 @@ WHERE id = $1 AND (version & 3) < 2""" * @since 3.0.0 */ fun getPgCollectionByNumber(conn: PgConnection?, number: Int): PgCollection? { - if (this is PgAdminMap) { + if (this is PgAdminCatalog) { return when (number) { COLLECTIONS_COL_FN -> collections TRANSACTIONS_COL_FN -> transactions @@ -379,11 +398,7 @@ WHERE id = $1 AND (version & 3) < 2""" if (existing != null || conn == null) return existing // Read from database - val outRows = PgColumnRows(collections.head) - .withDatabaseNumber(storage.number) - .withCatalogNumber(this.number) - .withCollectionNumber(COLLECTIONS_COL_FN) - .addColumns(headColumns) + val outRows = PgRows().useHeadTable().withCollection(collections.head) setSearchPath(conn) val SQL = """SELECT ${outRows.names()} FROM ${collections.headTable.quotedName} @@ -408,7 +423,7 @@ WHERE fn = $1 AND (version & 3) < 2""" * @return the list of existing collections, _(empty, when no collections exist)_. * @since 3.0.0 */ - fun listPgCollections(conn: PgConnection, map: PgMap): PgCollectionList { + fun listPgCollections(conn: PgConnection, map: PgCatalog): PgCollectionList { val list = PgCollectionList() // TODO: Implement me! return list diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt index e795e0393..d48c0c6ae 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt @@ -2,13 +2,33 @@ package naksha.psql import naksha.base.* import naksha.model.* +import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_ARGUMENT +import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_STATE +import naksha.model.NakshaError.NakshaErrorCompanion.INTERNAL_ERROR +import naksha.model.objects.IndexList +import naksha.model.objects.Member +import naksha.model.objects.MemberType.MemberType_C.BYTE_ARRAY +import naksha.model.objects.MemberType.MemberType_C.INT64 +import naksha.model.objects.MemberType.MemberType_C.STRING +import naksha.model.objects.MemberType.MemberType_C.TUPLE_NUMBER import naksha.model.objects.NakshaCollection +import naksha.model.objects.StandardMembers.StandardMembers_C.Feature +import naksha.model.objects.StandardMembers.StandardMembers_C.GlobalBookFeatureNumber +import naksha.model.objects.StandardMembers.StandardMembers_C.Id +import naksha.model.objects.StandardMembers.StandardMembers_C.NextVersion +import naksha.model.objects.StandardMembers.StandardMembers_C.Tn import naksha.model.objects.StoreMode -import naksha.psql.PgUtil.PgUtilCompanion.quoteIdent +import naksha.psql.PgColumn.PgColumn_C.EXTENDED +import naksha.psql.PgColumn.PgColumn_C.EXTERNAL +import naksha.psql.PgColumn.PgColumn_C.MAIN +import naksha.psql.PgColumn.PgColumn_C.PLAIN import kotlin.js.JsExport +import kotlin.jvm.JvmField /** - * A collection is a set of database tables, that together form a logical feature store. This lower level implementation supports methods to create the collection physically (so the whole set of tables), to refresh the information about the collection, drop the tables, to add, or remove indices at runtime, aso. + * A collection is a set of database tables that together form a logical feature store. This lower level implementation supports methods to create the collection physically (so the whole set of tables), to refresh the information about the collection, drop the tables, to add, or remove indices at runtime, aso. + * + * This is a wrapper around the [NakshaCollection]. * * @since 3.0 */ @@ -16,129 +36,209 @@ import kotlin.js.JsExport @JsExport open class PgCollection internal constructor( /** - * The map in which the collection is located. + * The catalog in which the collection is located. * @since 3.0 */ - val map: PgMap, + val catalog: PgCatalog, /** * The HEAD state of the collection. * @since 3.0 */ nakshaCollection: NakshaCollection, +) { /** - * The map-id. + * The custom-identifier of the collection. * @since 3.0 */ - val id: String = nakshaCollection.id, + @JvmField + val id: String = nakshaCollection.id /** - * The map-number. + * The collection-number of the collection, actually the same as the feature-number of the [NakshaCollection] feature. * @since 3.0 */ - val number: Int = nakshaCollection.number -) { + @JvmField + val collectionNumber: Int = Naksha.collectionNumber(id) + /** - * The weak-reference to this [PgCollection], should be used when the collection should be cached. + * The amount of bit the [next-version][PgColumn.NEXT_VERSION] should be shifted right to calculate the history-partition. * @since 3.0 + * @see [PgHistoryTable] */ - @Suppress("LeakingThis") - val weakRef = WeakRef(this) - + @JvmField + val shift: Int = nakshaCollection.shift + /** - * The storage in which the collection is located. + * Convert the given member into a [PgColumn], only fails for the standard member [Tn]. + * @param member the member to convert, must not me [Tuple-Number][StandardMembers.Tn]. + * @param index the real index in the physical table at which to place the member. + * @return the [PgColumn] for the member. + * @throws NakshaException with error [ILLEGAL_ARGUMENT], if the given member is [Tn]. * @since 3.0 */ - val storage: PgStorage - get() = map.storage + private fun fromMember(member: Member, index: Int): PgColumn { + if (Tn.name == member.name) { + throw NakshaException(ILLEGAL_ARGUMENT, "The tuple-number can't be converted using PgColumn.fromMember") + } + val memberName = member.name + // These mandatory members get special storage handling. + when (memberName) { + GlobalBookFeatureNumber.name -> return PgColumn(index, memberName, INT64, "STORAGE $PLAIN") + Id.name -> return PgColumn(index, memberName, STRING, "STORAGE $PLAIN") + Feature.name -> return PgColumn(index, memberName, BYTE_ARRAY, "STORAGE $EXTERNAL") + } + // The storage is by default defined by data-type + val memberType = member.dataType + return when (memberType) { + BYTE_ARRAY, TUPLE_NUMBER -> PgColumn(index, memberName, STRING, "STORAGE $EXTENDED") + STRING -> PgColumn(index, memberName, STRING, "COLLATE \"C\" STORAGE $MAIN") + else -> PgColumn(index, memberName, STRING, "STORAGE $MAIN") + } + } + + private fun generateColumns(nakshaCollection: NakshaCollection): Array { + val members = nakshaCollection.useMembers() + if (!members.isSortedByIndex()) { + members.sortByDataTypeAndAssignIndex() + } + var i = 0 + return Array(members.size + 1) { // we split tuple-number into `fn` and `version`, therefore +1 + when (it) { + // The first three members are fixed to: + 0 -> PgColumn.FN + 1 -> PgColumn.VERSION + 2 -> PgColumn.NEXT_VERSION + else -> { + val col: PgColumn + var member = members[i++] ?: throw NakshaException(ILLEGAL_STATE, "Member #${i-1} is null") + var name = member.name + // Tn is already added as `fn` and `version`, and `next_version` is as well already added. + while (name==Tn.name || name==NextVersion.name) { + member = members[i++] ?: throw NakshaException(ILLEGAL_STATE, "Member #${i-1} is null") + name = member.name + } + col = fromMember(member, it) + col + } + } + } + } /** - * The _HEAD_ state of the collection. + * The columns to expect in the table. * - * ### Note - * If the collection is deleted, this value stays unmodified, because the [PgCollection] will be removed from caching. However, if only the _HEAD_ state of the collection is modified, so basically an `UPDATE` is done, the _HEAD_ reference is replaced on-the-fly. + * The columns are filled from the members-book and vice versa. Most of the time the columns match the members, but there are details in which they can differ, i.e. for the tuple-number. * @since 3.0 */ - val headRef = AtomicNonNullRef(nakshaCollection) + @JvmField + val columns: Array = generateColumns(nakshaCollection) + + private fun indicesFor(nakshaCollection: NakshaCollection, onHead: Boolean): Array { + return Array(nakshaCollection.indices?.size ?: 0) { i -> + // We know that when this method is called, indices must not be null! + val indices: IndexList = nakshaCollection.indices!! + val index = indices[i] ?: throw NakshaException(ILLEGAL_STATE, "Index #$i must not be null") + val indexName = index.name + val indexType = index.type + + val nakshaOn = index.on + val on: ArrayList = ArrayList(nakshaOn.size) + on@ for (i in 0 ..< nakshaOn.size) { + val name = nakshaOn[i] ?: throw NakshaException(ILLEGAL_STATE, "Index '$indexName->on[$i]' must not be null") + if (onHead && NextVersion.name == name) continue // no NEXT_VERSION in HEAD + for (column in columns) { + if (column.name == name) { + on.add(column) + continue@on + } + } + throw NakshaException(ILLEGAL_ARGUMENT, "Index '$indexName->on[$i]' refers to member '$name', but no such member exists") + } + val nakshaInclude = index.include + val include: ArrayList? + if (!nakshaInclude.isNullOrEmpty()) { + include = ArrayList(nakshaInclude.size) + include@ for (i in 0 ..< nakshaInclude.size) { + val name = nakshaInclude[i] ?: throw NakshaException(ILLEGAL_STATE, "Index '$indexName->include[$i]' must not be null") + if (onHead && NextVersion.name == name) continue // no NEXT_VERSION in HEAD + for (column in columns) { + if (column.name == name) { + include.add(column) + continue@include + } + } + throw NakshaException(ILLEGAL_ARGUMENT,"Index '$indexName->include[$i]' refers to member '$name', but no such member exists") + } + } else { + include = null + } + PgIndex(indexName, indexType, on.toTypedArray(), include?.toTypedArray() ?: emptyArray()) + } + } /** - * Reads [headRef]. - * @see [headRef] + * The indices of the HEAD table. * @since 3.0 */ - val head: NakshaCollection - get() = headRef.get() + @JvmField + val headIndices: Array = indicesFor(nakshaCollection, onHead = true) /** - * The storage class of the collection. + * The indices of the HISTORY table. + * @since 3.0 */ - var storageClass: PgStorageClass = PgStorageClass.Unknown - internal set + @JvmField + val historyIndices: Array = indicesFor(nakshaCollection, onHead = false) /** - * The columns present in the HEAD (and META/DELETED) tables of this collection. - * - * Always includes the full set of default head columns. If the collection declares additional - * members that map to known [PgColumn] entries (e.g. `pn`, `pt`, `gv`) those columns are - * appended so that every physically-present column is covered in a single list. + * Tests if the history should be stored. * @since 3.0 */ - val effectiveHeadColumns: List - get() { - val members = head.members ?: return PgColumn.headColumns - val byName = PgColumn.allColumnsByName - val base = PgColumn.headColumns - val extras = mutableListOf() - for (m in members) { - if (m == null) continue - val col = byName[m.name] - if (col != null && col !in base && col !in extras) extras.add(col) - } - return if (extras.isEmpty()) base else base + extras - } + val storeHistory: Boolean + get() = head.storeHistory === StoreMode.ON /** - * The columns present in the HISTORY tables of this collection. - * - * Mirrors [effectiveHeadColumns] but includes [PgColumn.next_version] for HISTORY. + * Tests if deleted [Tuple] should be kept in _HEAD_; if not they are automatically purged when being deleted. * @since 3.0 */ - val effectiveHistoryColumns: List - get() { - val members = head.members ?: return PgColumn.allColumns - val byName = PgColumn.allColumnsByName - val base = PgColumn.allColumns - val extras = mutableListOf() - for (m in members) { - if (m == null) continue - val col = byName[m.name] - if (col != null && col !in base && col !in extras) extras.add(col) - } - return if (extras.isEmpty()) base else base + extras - } + val storeDeleted: Boolean + get() = head.storeDeleted === StoreMode.ON /** - * The subset of [PgColumn.copyIntoHistoryColumns] that actually exist in this collection's tables. - * Used when copying HEAD rows into HISTORY to avoid referencing absent optional columns. + * The weak-reference to this [PgCollection], should be used when the collection should be cached. + * @since 3.0 + */ + @Suppress("LeakingThis") + val weakRef = WeakRef(this) + + /** + * The storage in which the collection is located. * @since 3.0 */ - val effectiveCopyIntoHistoryColumns: List - get() { - val effective = effectiveHeadColumns.toSet() - return PgColumn.copyIntoHistoryColumns.filter { it in effective } - } + val storage: PgStorage + get() = catalog.storage /** - * The subset of [PgColumn.updateColumns] that actually exist in this collection's tables. - * Used in UPDATE CTEs to avoid referencing absent optional columns. + * The _HEAD_ state of the collection. * @since 3.0 */ - val effectiveUpdateColumns: List - get() { - val effective = effectiveHeadColumns.toSet() - return PgColumn.updateColumns.filter { it in effective } - } + val headRef = AtomicNonNullRef(nakshaCollection) + + /** + * Reads [headRef]. + * @see [headRef] + * @since 3.0 + */ + val head: NakshaCollection + get() = headRef.get() + + /** + * The storage class of the collection. + */ + var storageClass: PgStorageClass = JsEnum.getDefined(nakshaCollection.storageClass, PgStorageClass::class) ?: PgStorageClass.Unknown + internal set /** * The amount of performance partitions. @@ -147,9 +247,9 @@ open class PgCollection internal constructor( get() = head.partitions /** - * The `HEAD` table, so where to store features or transactions into. + * The `HEAD` table, so where to store features into. * - * If this is an ordinary table, that can be partitioned using [PgPlatform.partitionNumber] above the [PgColumn.id], except when this is a `TRANSACTION` collection, then the partitioning is done by [Version.year], extracted from [PgColumn.txn]. + * If this is an ordinary table, that can be partitioned using [PgPlatform.partitionNumber] above the [feature-number][PgColumn.FN]. * * Writing directly into partitions, or reading from them, is discouraged, but in some cases necessary to improve performance drastically. In AWS the speed of every [single-flow](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-network-bandwidth.html) connection is limited to 5 Gbps (10 Gbps when being in the same [cluster placement group](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/placement-strategies.html#placement-groups-cluster)), but still always limited. When the PostgresQL database and the client both have higher bandwidth, then multiple parallel connection need to be used, for example to saturate the HERE temporary or consistent store bandwidth of 200 Gbps, between 20 and 40 connections are needed. * @@ -157,199 +257,45 @@ open class PgCollection internal constructor( * - The `TRANSACTION` table is not designed for ultra-high throughput, but rather as a storage that allows easy and fast garbage collection and to query ordered by transaction numbers or _sequence numbers_. * - Internal tables do not allow to add or remove indices, while ordinary consumer tables do allow this. */ - var headTable: PgHead - internal set + @JvmField + val headTable: PgHeadTable = PgHeadTable(this) /** - * The history can be disabled fully or temporary. When disabled fully, no history tables are created, in that case this property will be _null_. + * The history table of the collection. * - * If the history tables were created, they are always partitioned by the year of `txn_next`. Each yearly table is partitioned again the same way that [HEAD][headTable] is partitioned, not doing this would create a bottleneck when modifying features in parallel, because then the parallel connections would have a congestion in the history. The history therefore managed the same way as [HEAD][headTable], so using the [PgPlatform.partitionNumber] above the `feature.id`. - */ - var historyTable: PgHistory? - internal set - - /** - * An optional metadata table, never partitioned. This table is used to as internal storage for metadata, like statistics, calculated by background jobs and other information like this. It can be used as well by applications, and is accessible from outside, but does not have any history or track changes. + * The history table is always partitioned by the [next-version][PgColumn.NEXT_VERSION], which is shifted right by a [shift]. Each [history partition][PgHistoryPartition] is partitioned again the same way that [HEAD][headTable] is partitioned. Not doing this would create a bottleneck when modifying features in parallel, because then the parallel connections would have a congestion in the history, when moving old data into history. The history is therefore managed the same way as [HEAD][headTable], so using the [PgPlatform.partitionNumber] above the [feature-number][PgColumn.FN]. */ - var metaTable: PgTable? - internal set + @JvmField + val historyTable: PgHistoryTable = PgHistoryTable(this) /** - * Tests if this is an internal collection. Internal collections have some limitation, for example it is not possible to add or drop indices, nor can they be created through the normal [create] method. They are basically immutable by design, but the content can be read and modified to some degree. - * - * **Warning**: Internal tables may have further limitations, even about the content, for example the transaction log only allows to mutate `tags` for normal external clients. Internal clients may perform other mutations, e.g. the internal _sequencer_ is allowed to set the `seqNumber`, `seqTs`, `geo` and `geo_ref` columns, additionally it will update the feature counts, when necessary. However, for normal clients the transaction log is immutable, and the _sequencer_ will only alter the transactions ones in their lifetime. + * Returns the [PgColumn] that corresponds to the given member. + * @param member the [Member] for which to return the column. + * @return the [PgColumn] that corresponds to the given member. + * @throws NakshaException if the given `member` does not have a dedicated [PgColumn]. + * @since 3.0 */ - val internal: Boolean - get() = id.startsWith("naksha~") - - init { - val partitions = if (nakshaCollection.partitions == 1) 0 else nakshaCollection.partitions - storageClass = PgStorageClass.of(nakshaCollection.storageClass) - @Suppress("LeakingThis") - headTable = PgHead(this, storageClass, partitions) - historyTable = if (nakshaCollection.storeHistory == StoreMode.OFF) null else PgHistory(headTable) - metaTable = if (nakshaCollection.storeMeta == StoreMode.OFF) null else PgMeta(headTable) - } + fun column(member: Member): PgColumn = column(member.name) /** - * Add the before and after triggers. - * @param sql The SQL API. - * @param id The collection identifier. - * @param schema The schema name. - * @param schemaOid The object-id of the schema to look into. + * Returns the [PgColumn] that has the given name. + * @param name the name of the column to return. + * @return the [PgColumn] that with the given name. + * @throws NakshaException if the given `name` does not have a dedicated [PgColumn]. + * @since 3.0 */ - @Deprecated(message = "Needs to be fixed before used", level = DeprecationLevel.ERROR) - private fun collectionAttachTriggers(sql: PgConnection, id: String, schema: String, schemaOid: Int) { - var triggerName = id + "_before" - var rows = sql.execute("SELECT tgname FROM pg_trigger WHERE tgname = $1 AND tgrelid = $2", arrayOf(triggerName, schemaOid)) - if (rows.isRow()) { - val schemaQuoted = quoteIdent(schema) - val tableNameQuoted = quoteIdent(id) - val triggerNameQuoted = quoteIdent(triggerName) - sql.execute( - """CREATE TRIGGER $triggerNameQuoted BEFORE INSERT OR UPDATE ON ${schemaQuoted}.${tableNameQuoted} -FOR EACH ROW EXECUTE FUNCTION naksha_trigger_before();""" - ) - } - - triggerName = id + "_after" - rows = sql.execute("SELECT tgname FROM pg_trigger WHERE tgname = $1 AND tgrelid = $2", arrayOf(triggerName, schemaOid)) - if (rows.isRow()) { - val schemaQuoted = quoteIdent(schema) - val tableNameQuoted = quoteIdent(id) - val triggerNameQuoted = quoteIdent(triggerName) - sql.execute( - """CREATE TRIGGER $triggerNameQuoted AFTER INSERT OR UPDATE OR DELETE ON ${schemaQuoted}.${tableNameQuoted} -FOR EACH ROW EXECUTE FUNCTION naksha_trigger_after();""" - ) + fun column(name: String): PgColumn { + for (column in columns) { + if (column.name == name) return column } + throw NakshaException(INTERNAL_ERROR, "headColumn($name) called, but no such columns exists") } /** - * Diff the [prev] and [next] collection definitions and apply the resulting schema changes (ADD/DROP COLUMN, CREATE/DROP custom index) to all variant tables (HEAD, HISTORY, DELETED, META). - * - * Members: - * - Same name + same `dataType` → no-op. - * - Same name + different `dataType` → throws [NakshaError.ILLEGAL_ARGUMENT] (type changes are not supported). - * - Added → `ALTER TABLE ADD COLUMN` on each root variant. - * - Removed → requires [force] = `true`; otherwise throws [NakshaError.ILLEGAL_ARGUMENT]. With `force`, emits `ALTER TABLE DROP COLUMN` on each root variant. + * If this is an internal collection. * - * Custom indexes: - * - Identity is `(name, type, on, include, unique)`. Any difference → drop + create. - * - * Only root tables are touched; partition tables inherit `ADD/DROP COLUMN` via Postgres declarative partitioning. - * - * @since 3.0 + * Internal collections have some limitation, for example it is not possible to add or drop indices, nor can they be created through normal methods. They are basically immutable by design, but the content can be read and modified to some degree. */ - internal fun applyMembersAndIndexes( - conn: PgConnection, - prev: NakshaCollection, - next: NakshaCollection, - force: Boolean - ) { - diffMembers(conn, prev, next, force) - diffIndexes(conn, prev, next) - } - - private fun diffMembers(conn: PgConnection, prev: NakshaCollection, next: NakshaCollection, force: Boolean) { - val prevMembers = prev.members - val nextMembers = next.members - val prevByName = mutableMapOf() - val nextByName = mutableMapOf() - // Mandatory columns are always present in the DB and never appear in members lists, - // so we skip them here entirely. - val mandatoryNames = PgColumn.mandatoryMembers.map { it.name }.toSet() - if (prevMembers != null) for (m in prevMembers) if (m != null && m.name !in mandatoryNames) prevByName[m.name] = m - if (nextMembers != null) for (m in nextMembers) if (m != null && m.name !in mandatoryNames) nextByName[m.name] = m - - // Type-change check. - for ((name, nm) in nextByName) { - val pm = prevByName[name] ?: continue - if (pm.dataType != nm.dataType) { - throw naksha.model.illegalArg( - "Change of dataType for member '$name' is not supported (was ${pm.dataType}, requested ${nm.dataType})" - ) - } - } - - // Added. - for ((name, nm) in nextByName) { - if (prevByName.containsKey(name)) continue - val pgIdent = "\"${PgMemberHelper.pgColumnName(name)}\"" - val pgType = PgMemberHelper.pgSqlTypeFor(nm.dataType) - for (root in mutableRootTables()) { - val sql = "ALTER TABLE ${root.quotedName} ADD COLUMN IF NOT EXISTS $pgIdent $pgType" - conn.execute(sql).close() - } - } - - // Removed. - for ((name, _) in prevByName) { - if (nextByName.containsKey(name)) continue - if (!force) { - throw naksha.model.illegalArg( - "Member '$name' is being removed from collection '$id', but Write.force is false. " + - "Set Write.force = true to allow ALTER TABLE DROP COLUMN." - ) - } - val pgIdent = "\"${PgMemberHelper.pgColumnName(name)}\"" - for (root in mutableRootTables()) { - val sql = "ALTER TABLE ${root.quotedName} DROP COLUMN IF EXISTS $pgIdent" - conn.execute(sql).close() - } - } - } - - private fun diffIndexes(conn: PgConnection, prev: NakshaCollection, next: NakshaCollection) { - val prevIdx = prev.indices - val nextIdx = next.indices - val prevByName = mutableMapOf() - val nextByName = mutableMapOf() - // Internal (mandatory) indices are always managed by the storage; skip them in the diff. - if (prevIdx != null) for (ci in prevIdx) if (ci != null && !ci.isInternal()) prevByName[ci.name] = ci - if (nextIdx != null) for (ci in nextIdx) if (ci != null && !ci.isInternal()) nextByName[ci.name] = ci - - // Removed (or changed): drop on every root. - for ((name, pi) in prevByName) { - val ni = nextByName[name] - if (ni == null || !customIndexEquals(pi, ni)) { - for (root in mutableRootTables()) root.dropCustomIndex(conn, name) - } - } - - // Added (or changed): create on every root. - for ((name, ni) in nextByName) { - val pi = prevByName[name] - if (pi == null || !customIndexEquals(pi, ni)) { - for (root in mutableRootTables()) root.createCustomIndex(conn, ni) - } - } - } - - private fun customIndexEquals(a: naksha.model.objects.Index, b: naksha.model.objects.Index): Boolean { - if (a.name != b.name) return false - if (a.type != b.type) return false - if (a.isUnique() != b.isUnique()) return false - if (!listsEqual(a.on, b.on)) return false - if (!listsEqual(a.include, b.include)) return false - return true - } - - private fun listsEqual(a: naksha.base.StringList?, b: naksha.base.StringList?): Boolean { - if (a == null && b == null) return true - if (a == null || b == null) return a?.isEmpty() ?: b?.isEmpty() ?: true - if (a.size != b.size) return false - for (i in 0 until a.size) { - if (a[i] != b[i]) return false - } - return true - } - - private fun mutableRootTables(): List { - val result = mutableListOf() - result.add(headTable) - historyTable?.let { result.add(it) } - metaTable?.let { result.add(it) } - return result - } + @JvmField + var internal: Boolean = id.startsWith("naksha~") } \ No newline at end of file diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumn.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumn.kt index b0dd74279..d5509b8e1 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumn.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumn.kt @@ -2,118 +2,99 @@ package naksha.psql -import naksha.base.Int64 -import naksha.base.JsEnum -import naksha.model.DataEncoding +import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_ARGUMENT +import naksha.model.NakshaException import naksha.model.objects.Member import naksha.model.objects.MemberType -import naksha.model.request.query.MetaColumn +import naksha.model.objects.MemberType.MemberType_C.BYTE_ARRAY +import naksha.model.objects.MemberType.MemberType_C.INT64 +import naksha.model.objects.MemberType.MemberType_C.STRING +import naksha.model.objects.MemberType.MemberType_C.TUPLE_NUMBER +import naksha.model.objects.StandardMembers.StandardMembers_C.Feature +import naksha.model.objects.StandardMembers.StandardMembers_C.GlobalBookFeatureNumber +import naksha.model.objects.StandardMembers.StandardMembers_C.Id +import naksha.model.objects.StandardMembers.StandardMembers_C.NextVersion +import naksha.model.objects.StandardMembers.StandardMembers_C.Tn import kotlin.js.JsExport +import kotlin.js.JsName import kotlin.js.JsStatic import kotlin.jvm.JvmField import kotlin.jvm.JvmStatic -import kotlin.reflect.KClass -import naksha.model.TupleNumberVariant.TupleNumberVariant_C.B64 -import naksha.model.TupleNumberVariant.TupleNumberVariant_C.B128 /** - * A column descriptor for database columns, especially helpful when reading columns using `SELECT * FROM table`. + * A column descriptor for database columns. * - * The extras are used to add constraints, and the storage-method: - * - **PLAIN** prevents either compression or out-of-line storage. - * - **EXTENDED** allows both compression and out-of-line storage. - * - **EXTERNAL** allows out-of-line storage but not compression. - * - **MAIN** allows compression but not out-of-line storage. Actually, out-of-line storage will still be performed for such columns, but only as a last resort when there is no other way to make the row small enough to fit on a page. + * The [extra] is used to add constraints, and the storage-method: + * - [PLAIN] prevents either compression or out-of-line storage. + * - [EXTENDED] allows both compression and out-of-line storage. + * - [EXTERNAL] allows out-of-line storage but not compression. + * - [MAIN] allows compression but not out-of-line storage. Actually, out-of-line storage will still be performed for such columns, but only as a last resort when there is no other way to make the row small enough to fit on a page. * * The TOAST code will compress and/or move field values out-of-line until the row value is shorter than TOAST_TUPLE_TARGET bytes. * - * **Note**: We order the columns by intention, to minimize the storage size. The _bytea_ columns will be GZIP compressed on demand by the client storing the data (see [DataEncoding]). Note that in some cases this is not useful, for example, when a `TWKB` geometry or reference-point is given, it is often so small, that compression would increase the size. The general rule is, that anything being smaller than 100 byte, should not be compressed, which only leaves [feature] and [attachment] as compression candidates. We know that [feature] is stored GZIP compressed, therefore the only candidate left is [attachment]. So, we store [feature] _EXTERNAL_, [attachment] _EXTENDED_, and everything else as _PLAIN_ to keep it available for fast access. [tags] is stored as raw `jsonb` so the column itself is directly indexable (GIN); Postgres will TOAST/compress it automatically. - * * See [storage-toast.html](https://www.postgresql.org/docs/current/storage-toast.html). */ @JsExport -class PgColumn : JsEnum() { - private fun checkThatNotDefined(column: PgColumn) { - check(!isDefined) { "Changing ${column.name} is not allowed!" } - } - - private var _i: Int = -1 - +data class PgColumn( /** - * The index in the columns list of a Naksha standard table. A value from 0 to n-1 if this column is part of Naksha standard tables, with n being the amount of columns. A value of `-1`, if this column is no standard column. + * The index of the column in the table. + * @since 3.0 */ - var i: Int - get() = _i - set(value) { - checkThatNotDefined(this) - _i = value - } + @JvmField + val index: Int, /** * The name of the column. + * @since 3.0 */ - val name: String - get() = super.text - - private var _type: PgType? = null + @JvmField + val name: String, /** - * The type of the column. + * The type of the column provided as [MemberType]. + * @since 3.0 */ - var type: PgType - get() { - return _type ?: PgType.NULL - } - set(value) { - checkThatNotDefined(this) - _type = value - } - - private var _extra: String? = null + @JvmField + val memberType: MemberType, /** * Optional extras for the definition, for example "NOT NULL". + * @since 3.0 */ - var extra: String? - get() = _extra - set(value) { - checkThatNotDefined(this) - _extra = value - } + @JvmField + val extra: String?, /** - * The stringified version, the same as returned by [toString]. + * The type of the column. + * @since 3.0 */ - var sqlDefinition: String = "" - get() { - // TODO: KotlinCompilerBug - How can field ever be null? - @Suppress("UselessCallOnNotNull") - if (field.isNullOrEmpty()) { - field = "$name $type" + if (extra != null) " $extra" else "" - } - return field - } - private set - - private var _ident: String? = null + @JvmField + val pgType: PgType = PgType.ofMemberType(memberType), /** - * The SQL quoted identifier. - * - * The same result (just slower) can be archived using `PgUtil.quoteIdent(col.name)`. + * The SQL quoted identifier, the same result (just slower) can be archived using `PgUtil.quoteIdent(col.name)`. * @return the SQL quoted identifier (with optional double quotes). + * @see PgUtil.quoteIdent + */ + @JvmField + val ident: String = PgUtil.quoteIdent(name), + + /** + * The SQL code to added into a `CREATE TABLE` statements. + */ + @JvmField + val sql: String = "$ident $pgType" + if (extra != null) " $extra" else "" +) { + /** + * Create a new column with the same states, just a different index. + * @param index the new index. + * @param source the column to copy with all other attributes. + * @since 3.0 */ - val ident: String - get() { - var ident = this._ident - if (ident == null) { - ident = PgUtil.quoteIdent(this.name) - this._ident = ident - } - return ident - } + @JsName("reindex") + constructor(index: Int, source: PgColumn) : this(index, source.name, source.memberType, source.extra, source.pgType, source.ident) - companion object PgColumnCompanion { + companion object PgColumn_C { /** * Prevents either compression or out-of-line storage. This is the only possible strategy for columns of non-TOAST-able data types. * @since 3.0 @@ -139,771 +120,33 @@ class PgColumn : JsEnum() { const val MAIN = "MAIN" /** - * Returns the columns instance for the given name. - * @param columnName the column name. - * @return the column enumeration value. - * @since 3.0 - */ - @JvmStatic - @JsStatic - fun of(columnName: String): PgColumn = get(columnName, PgColumn::class) - - /** - * The epoch timestamp in millisecond when the [tuple][naksha.model.Tuple] was produced, which is the last time the feature was modified. - * - * **Optional** — `NULL` is allowed; NULL means the timestamp was not recorded. - * @since 3.0 - */ - @JvmField - @JsStatic - val updated_at = def(PgColumn::class, "updated_at") { self -> - self._i = 3 - self._type = PgType.INT64 - } - - /** - * The epoch timestamp in millisecond when the [feature][naksha.model.objects.NakshaFeature] was originally created. If the value is _null_, this means this [tuple][naksha.model.Tuple] is the initial state of the [feature][naksha.model.objects.NakshaFeature], so the value is the same as [updated_at]. - * @since 3.0 - */ - @JvmField - @JsStatic - val created_at = def(PgColumn::class, "created_at") { self -> - self._i = 4 - self._type = PgType.INT64 - } - - /** - * The epoch timestamp in millisecond when the [feature][naksha.model.objects.NakshaFeature] was modified last by the author. If the value is _null_, this means this [tuple][naksha.model.Tuple] was changed by the author, so the value is the same as [updated_at]. - * @since 3.0 - */ - @JvmField - @JsStatic - val author_ts = def(PgColumn::class, "author_ts") { self -> - self._i = 5 - self._type = PgType.INT64 - } - - /** - * If this has a custom value, otherwise _null_. - * @since 3.0 - */ - @JvmField - @JsStatic - val cv0 = def(PgColumn::class, "cv0") { self -> - self._i = 7 - self._type = PgType.DOUBLE - } - - /** - * If this has a custom value, otherwise _null_. - * @since 3.0 - */ - @JvmField - @JsStatic - val cv1 = def(PgColumn::class, "cv1") { self -> - self._i = 8 - self._type = PgType.DOUBLE - } - - /** - * If this has a custom value, otherwise _null_. - * @since 3.0 - */ - @JvmField - @JsStatic - val cv2 = def(PgColumn::class, "cv2") { self -> - self._i = 9 - self._type = PgType.DOUBLE - } - - /** - * If this has a custom value, otherwise _null_. - * @since 3.0 - */ - @JvmField - @JsStatic - val cv3 = def(PgColumn::class, "cv3") { self -> - self._i = 10 - self._type = PgType.DOUBLE - } - - /** - * The unique hash of this [tuple][naksha.model.Tuple] (state), calculated by the storage using the static [Metadata.calculateHash] method. + * The feature-number. * - * **Optional** — `NULL` is allowed. + * Together with [VERSION], forms the primary identification of a tuple within a collection. The lower 16 bits of this value are used as the partition key for distribution partitioning (see [naksha.model.Naksha.featureNumber]). * @since 3.0 */ @JvmField @JsStatic - val hash = def(PgColumn::class, "hash") { self -> - self._i = 11 - self._type = PgType.INT - } - - /** - * The binary [HERE tile-key][naksha.geo.HereTile.intKey] of the reference-point of the [tuple][naksha.model.Tuple] (state). This is calculated using the static [Metadata.calculateHereTile] method. - * - * **Optional** — `NULL` is allowed; NULL means no reference point / tile is known. - * @since 3.0 - */ - @JvmField - @JsStatic - val here_tile = def(PgColumn::class, "here_tile") { self -> - self._i = 12 - self._type = PgType.INT - } - - /** - * The `change-count`. - * - * **Optional** — `NULL` is allowed. - * @since 3.0 - */ - @JvmField - @JsStatic - val cc = def(PgColumn::class, "cc") { self -> - self._i = 13 - self._type = PgType.INT - } - - /** - * The feature-number — the per-collection identifier of the feature this tuple belongs to. - * - * Together with [version], forms the primary identification of a tuple within a collection. The lower 16 bits of this value are used as the partition key for distribution partitioning (see [naksha.model.Naksha.featureNumber]). - * @since 3.0 - */ - @JvmField - @JsStatic - val fn = def(PgColumn::class, "fn") { self -> - self._i = 0 - self._type = PgType.INT64 - self._extra = "STORAGE $PLAIN NOT NULL" - } + val FN = PgColumn(0, "_fn", INT64, "STORAGE $PLAIN NOT NULL") /** * The version (with action in the lower 2 bits) of this tuple. * - * Together with [fn], forms the primary identification of a tuple within a collection. See [naksha.model.Version] for the layout. - * @since 3.0 - */ - @JvmField - @JsStatic - val version = def(PgColumn::class, "version") { self -> - self._i = 1 - self._type = PgType.INT64 - self._extra = "STORAGE $PLAIN NOT NULL" - } - - /** - * The next version of the tuple (with action in the lower 2 bits). - * - * Stored only in _HISTORY_ tables. In _HEAD_ this is intrinsically [HEAD][naksha.model.Version.HEAD] (the row is the head state); the column is omitted from HEAD DDL. - * - * For a tombstone state (final row of a feature's lifetime), this equals the row's own `version`. - * @since 3.0 - */ - @JvmField - @JsStatic - val next_version = def(PgColumn::class, "next_version") { self -> - self._i = 2 - self._type = PgType.INT64 - self._extra = "STORAGE $PLAIN" // prevents out-of-line storage - } - - /** - * The [tuple-number][naksha.model.TupleNumber] of the _BASE_ state upon which a [three-way-merge](https://en.wikipedia.org/wiki/Merge_(version_control)#Three-way_merge) was done, using the [128-bit encoding][B128]. - * - * This value is set, when a client reads the _HEAD_ state of a feature, then modifies it into some _NEW_ state, and tries to save its changes, but meanwhile other clients did the same, and a conflict arises. The changes the client did are based upon an older _HEAD_, which we will name _BASE_, and do not reflect the changes the other clients did meanwhile. - * - * In this situation an automatic [three-way-merge](https://en.wikipedia.org/wiki/Merge_(version_control)#Three-way_merge) can be done. The changes of the other clients are calculated as difference between _HEAD_ and _BASE_, and the changes the client did is calculated as difference between _NEW_ and _BASE_. Then the difference are added to a patch. This can fail, if both clients changes the same properties, what will cause a conflict. If successful, the patch is applied to _BASE_ and will produce a new _NEW2_ state, that actually contains the changes of the client plus the changes of the other clients. This _NEW2_ state can be written into the storage; it references the old _HEAD_ as its previous tuple. This can fail again, if others were faster in doing the same, but is an idempotent operation, and can simply be repeated until either conflicting or solved. - * - * To document that this happened, the shared _BASE_ state is referred vai `base_tn`. - * - * This technically allows to calculate back, what the client actually modified. For this, the difference between _HEAD_ and _BASE_ is calculated, and then the difference between the previous tuple and _BASE_ is subtracted, resulting in a patch that can be applied to _BASE_ to receive the original _NEW_ state the client had in memory and wanted to persist. This difference will as well document which properties were changed by the client. - * - * The encoding stores, in order, Big-Endian encoded: - * - feature-number: 64 - * - version (with action in lower 2 bits): 64 - * @since 3.0 - * @see [B128] - */ - @JvmField - @JsStatic - val base_tn = def(PgColumn::class, "base_tn") { self -> - self._i = 24 - self._type = PgType.BYTE_ARRAY - self._extra = "STORAGE $PLAIN" // prevents either compression or out-of-line storage - } - - /** - * The feature-id. - * - * **Conditionally mandatory**: - * - When `fn < 0` (named feature, MSB set): `id` MUST NOT be `NULL` — it carries the user-supplied string identifier. - * - When `fn >= 0` (anonymous / numeric feature): `id` MUST be `NULL` — the logical id is `fn` decimally encoded. - * @since 3.0 - */ - @JvmField - @JsStatic - val id = def(PgColumn::class, "id") { self -> - self._i = 14 - self._type = PgType.STRING - self._extra = "STORAGE $PLAIN COLLATE \"C\"" // prevents either compression or out-of-line storage; NOT NULL enforced via CHECK constraint - } - - /** - * The application-id of the application that produced a [tuple][naksha.model.Tuple]. - * @since 3.0 - */ - @JvmField - @JsStatic - val app_id = def(PgColumn::class, "app_id") { self -> - self._i = 15 - self._type = PgType.STRING - self._extra = "STORAGE $PLAIN COLLATE \"C\"" - } - - /** - * The author that takes ownership for the [tuple][naksha.model.Tuple]. - * @since 3.0 - */ - @JvmField - @JsStatic - val author = def(PgColumn::class, "author") { self -> - self._i = 16 - self._type = PgType.STRING - self._extra = "STORAGE $PLAIN COLLATE \"C\"" - } - - /** - * The stringified [Guid][naksha.model.Guid] from where the feature originates. - * - * Whenever a feature is copied from a foreign storage, map, or collection, or the feature-id is changed in an update, the `origin` refers to the originating feature. - * - * When a client decides to read the _HEAD_ state of a feature, and then to merge a foreign feature logically into it, the client should set `origin` to the foreign feature it merged into this one. This allows to track this, and to rebase the new feature, should the original one being updated. - * - * Eventually `origin` is used to perform rebasing. A rebase is a complex [three-way-merge](https://en.wikipedia.org/wiki/Merge_(version_control)#Three-way_merge). Assume a feature is modified, then it is possible to search for all features in other storages, maps, and collections that have `origin` set to this feature. Actually, we only search for features having the same originating feature, so who have an `origin` starting with the ID of the originating feature. When found, a rebase can be started. - * - * To perform an actual rebase, the client that will do the rebase, will first find the latest state of the originating feature, _ORIGIN-HEAD_. Then it will read the state that `origin` refers to as _ORIGIN-BASE_, calculating a difference between the _ORIGIN-HEAD_ and the _ORIGIN-BASE_, being _ORIGIN-DIFF_. Now the history of the rebase feature need to read, until the moment is found, where the origin was set. This is the _REBASE-BASE_ state. Then the latest version of the feature is looked up, and read as _REBASE-HEAD_. This allows to calculate the difference, being _REBASE-DIFF_. - * - * Now, we have two differences, the _ORIGIN-DIFF_, describing what was changed in the origin since the feature was forked, and _REBASE-DIFF_, which describes what was change in the to rebase collection, since the feature was forked. This allows to add both differences, resulting in a _REBASE-PATCH_ or a conflict, if both collections modified the same properties. Some of these conflicts can be automatically solved by one wins over the other, but this is out of scope of this description. Eventually, when successfully created the _REBASE_PATCH_, this patch can be applied to _REBASE-BASE_ to produce a _REBASE-NEW_ state, that has _REBASE-HEAD_ as precedence state. When this is applied, the `origin` need to be updated to the one to which the rebase was performed. - * - * This was a simplified description, but should allow to understand the basic concepts and meaning of this property. - * - * The `origin` is sticky, the value is kept until updated, the feature is forked again, or the ID is changed. - * @since 3.0 - */ - @JvmField - @JsStatic - val origin = def(PgColumn::class, "origin") { self -> - self._i = 17 - self._type = PgType.STRING - self._extra = "STORAGE $PLAIN COLLATE \"C\"" // prevents either compression or out-of-line storage - } - - /** - * The stringified [Guid][naksha.model.Guid] of the feature into which this feature was integrated. - * - * This value is set in the following situations: - * - **Join**: If multiple features are deleted, and then replaced with a single feature, then the features that are deleted will have `action` set to `DELETED` and the `target` will refer to the feature into which they are joined. The target feature will have the `action` set to `JOINED` to indicate, that there are deleted features, which target this one. - * @since 3.0 - */ - @JvmField - @JsStatic - val target = def(PgColumn::class, "target") { self -> - self._i = 18 - self._type = PgType.STRING - self._extra = "STORAGE $PLAIN COLLATE \"C\"" // prevents either compression or out-of-line storage - } - - /** - * A customer feature-type, [type][naksha.model.objects.NakshaFeature.type] of the [feature][naksha.model.objects.NakshaFeature], _null_ if it matches the [default-type of the collection][naksha.model.objects.NakshaCollection.typeDefaultValue]. - * @since 3.0 - */ - @JvmField - @JsStatic - val ft = def(PgColumn::class, "ft") { self -> - self._i = 19 - self._type = PgType.STRING - self._extra = "STORAGE $PLAIN COLLATE \"C\"" // prevents either compression or out-of-line storage - } - - /** - * A custom string, _null_ if not used. - * @since 3.0 - */ - @JvmField - @JsStatic - val cs0 = def(PgColumn::class, "cs0") { self -> - self._i = 20 - self._type = PgType.STRING - self._extra = "STORAGE $PLAIN COLLATE \"C\"" // prevents either compression or out-of-line storage - } - - /** - * A custom string, _null_ if not used. - * @since 3.0 - */ - @JvmField - @JsStatic - val cs1 = def(PgColumn::class, "cs1") { self -> - self._i = 21 - self._type = PgType.STRING - self._extra = "STORAGE $PLAIN COLLATE \"C\"" // prevents either compression or out-of-line storage - } - - /** - * A custom string, _null_ if not used. - * @since 3.0 - */ - @JvmField - @JsStatic - val cs2 = def(PgColumn::class, "cs2") { self -> - self._i = 22 - self._type = PgType.STRING - self._extra = "STORAGE $PLAIN COLLATE \"C\"" // prevents either compression or out-of-line storage - } - - /** - * A custom string, _null_ if not used. - * @since 3.0 - */ - @JvmField - @JsStatic - val cs3 = def(PgColumn::class, "cs3") { self -> - self._i = 23 - self._type = PgType.STRING - self._extra = "STORAGE $PLAIN COLLATE \"C\"" // prevents either compression or out-of-line storage - } - - /** - * The tags of the [tuple][naksha.model.Tuple], stored as raw `jsonb`. - * - * By default ([naksha.model.objects.MemberType.SET]) this is a JSON array of unique strings - * ([naksha.model.TagList]), persisted unmodified so the element order is preserved. When the - * tags member is declared as [naksha.model.objects.MemberType.TAGS] or - * [naksha.model.objects.MemberType.TAGS_FROM_ARRAY], it is a JSON object - * ([naksha.model.TagMap]) instead. - * @since 3.0 - */ - @JvmField - @JsStatic - val tags = def(PgColumn::class, "tags") { self -> - self._i = 25 - self._type = PgType.JSONB - } - - /** - * The reference-point of the [feature][naksha.model.objects.NakshaFeature]. - * @since 3.0 - */ - @JvmField - @JsStatic - val ref_point = def(PgColumn::class, "ref_point") { self -> - self._i = 26 - self._type = PgType.BYTE_ARRAY - self._extra = "STORAGE $EXTERNAL" - } - - /** - * The geometry of the [feature][naksha.model.objects.NakshaFeature]. - * @since 3.0 - */ - @JvmField - @JsStatic - val geo = def(PgColumn::class, "geo") { self -> - self._i = 27 - self._type = PgType.BYTE_ARRAY - self._extra = "STORAGE $EXTERNAL" - } - - /** - * The serialized [feature][naksha.model.objects.NakshaFeature]. - * - * **Mandatory** — MUST NOT be `NULL`. Every tuple must carry a serialized feature blob. - * Not indexable; encoding is configurable (default: JBON2_GZIP). - * @since 3.0 - */ - @JvmField - @JsStatic - val feature = def(PgColumn::class, "feature") { self -> - self._i = 28 - self._type = PgType.BYTE_ARRAY - self._extra = "STORAGE $EXTERNAL NOT NULL" - } - - /** - * An arbitrary binary attachment. - * @since 3.0 - */ - @JvmField - @JsStatic - val attachment = def(PgColumn::class, "attachment") { self -> - self._i = 29 - self._type = PgType.BYTE_ARRAY - self._extra = "STORAGE $EXTERNAL" - } - - /** - * The global-book-number that references the JBON2 global dictionary entry needed to decode - * the [feature] blob when global-book encoding is used. Assigned by the sequencer. - * - * **Optional** — `NULL` when no global book is in use for this tuple. - * @since 3.0 - */ - @JvmField - @JsStatic - val gbn = def(PgColumn::class, "gbn") { self -> - self._i = 6 - self._type = PgType.INT64 - self._extra = "STORAGE $PLAIN" - } - - /** - * Publisher-assigned gap-free sequential number ordered by **visibility** (not execution order). - * After publish-number `N` comes `N+1` without holes, regardless of transaction commit order. - * Assigned by an external publisher component. `NULL` until published. - * @since 3.0 - */ - @JvmField - @JsStatic - val pn = def(PgColumn::class, "pn") { self -> - self._i = 30 - self._type = PgType.INT64 - self._extra = "STORAGE $PLAIN" - } - - /** - * Millisecond epoch timestamp at which the publisher recognised and sequenced this transaction, - * assigning it its [pn]. `NULL` until published. - * @since 3.0 - */ - @JvmField - @JsStatic - val pt = def(PgColumn::class, "pt") { self -> - self._i = 31 - self._type = PgType.INT64 - self._extra = "STORAGE $PLAIN" - } - - /** - * HERE-internal global version number assigned by HERE's global sequencing infrastructure. - * `NULL` until the global sequencer has processed the transaction. - * @since 3.0 - */ - @JvmField - @JsStatic - val gv = def(PgColumn::class, "gv") { self -> - self._i = 32 - self._type = PgType.INT64 - self._extra = "STORAGE $PLAIN" - } - - /** - * All columns being used with Naksha, ordered by type alignment group for minimal PostgreSQL - * tuple padding: - * 1. INT64 (8-byte): fn, version, next_version, updated_at, created_at, author_ts, gbn - * 2. FLOAT64 (8-byte): cv0, cv1, cv2, cv3 - * 3. INT32 (4-byte): hash, here_tile, cc - * 4. STRING (var): id, app_id, author, origin, target, ft, cs0, cs1, cs2, cs3 - * 5. BYTE_ARRAY (var): base_tn, tags, ref_point, geo, feature, attachment - * - * The SPECIAL columns ([pn], [pt], [gv]) follow the same INT64 slot rule but are - * only physically present when explicitly declared; they live in [allColumnsByName] only. - * @since 3.0 - */ - @JvmField - @JsStatic - val allColumns = listOf( - // INT64 group - fn, version, next_version, - updated_at, created_at, author_ts, - gbn, - // FLOAT64 group - cv0, cv1, cv2, cv3, - // INT32 group - hash, here_tile, cc, - // STRING group - id, app_id, author, origin, target, ft, - cs0, cs1, cs2, cs3, - // BYTE_ARRAY group - base_tn, tags, ref_point, geo, feature, attachment, - ) - - /** - * Columns present in a _HEAD_ table — same as [allColumns] except for [next_version], which is intrinsically [HEAD][naksha.model.Version.HEAD] in HEAD rows and therefore not stored. - * @since 3.0 - */ - @JvmField - @JsStatic - val headColumns = allColumns.filter { it !== next_version } - - /** - * All columns indexed by name, for fast lookup. - * Includes [allColumns] plus the optional SPECIAL columns ([pn], [pt], [gv]) that are - * only physically present when a collection explicitly declares them as members. - * @since 3.0 - */ - @JvmField - @JsStatic - val allColumnsByName: Map = (allColumns + listOf(pn, pt, gv)).associateBy { it.name } - - /** - * The minimal mandatory columns for a HEAD / META / DELETED table: - * `fn`, `version`, `id`, `feature`, and `gbn`. - * - * Used when [naksha.model.objects.NakshaCollection.members] is an **empty list** (explicitly set to `[]`) - * to create a lean table with no optional built-in columns. - * @since 3.0 - */ - @JvmField - @JsStatic - val mandatoryColumns: List = listOf(fn, version, id, feature, gbn) - - /** - * The minimal mandatory columns for a HISTORY table: - * [mandatoryColumns] plus [next_version] (which is the HISTORY partition key). - * @since 3.0 - */ - @JvmField - @JsStatic - val mandatoryHistoryColumns: List = listOf(fn, version, next_version, id, feature, gbn) - - /** - * The names of all HEAD database columns, as comma-separated list. - * @since 3.0 - */ - @JsStatic - @JvmField - val headColumnNames = headColumns.joinToString(",") { it.name } - - /** - * The names of all database columns, as comma separated list. - * @since 3.0 - */ - @JsStatic - @JvmField - val allColumnNames = allColumns.joinToString(",") { it.name } - - /** - * The placeholders when inserting all columns, like $1, $2, ... - * @since 3.0 - */ - @JsStatic - @JvmField - val allColumnPlaceholders = allColumns.joinToString(",") { "\$${(it.i + 1)}" } - - /** - * The type names for the values of all columns, for example `int8` for a column being [Int64]. - * @since 3.0 - */ - @JsStatic - @JvmField - val allColumnTypeNames = Array(allColumns.size) { allColumns[it].type.text } - - /** - * The array type names for the values of all columns, for example `int8[]` for a column being [Int64]. - * - * To be used when multiple rows need to be transferred into the database, for example, creating a plan for all columns: - * ```kotlin - * val SQL = """WITH input AS ( - * SELECT * FROM - * UNNEST(${allColumnPlaceholders}) AS t($allColumnNames) - * ), ...""" - * val plan = conn.prepare(SQL, allColumnArrayTypeNames) - * val values = Array(n) { ... } // an array of arrays - * val cursor = plan.execute(values) - * ``` - * @since 3.0 - */ - @JsStatic - @JvmField - val allColumnArrayTypeNames = Array(allColumns.size) { allColumns[it].type.text+"[]" } - - /** - * All columns that are needed when we copy a feature from _HEAD_ into _HISTORY_, so all except for: - * - [next_version] - * @since 3.0 - */ - @JvmField - @JsStatic - val copyIntoHistoryColumns = listOf( - updated_at, created_at, author_ts, - cv0, cv1, cv2, cv3, - hash, here_tile, cc, - fn, version, base_tn, // removed: next_version, prev_tn - id, app_id, author, origin, target, ft, - cs0, cs1, cs2, cs3, - tags, ref_point, geo, feature, attachment, - gbn, - pn, pt, gv, - ) - - /** - * The names of all database columns, as comma separated list. - * @since 3.0 - */ - @JsStatic - @JvmField - val copyIntoHistoryColumnNames = copyIntoHistoryColumns.joinToString(",") { it.name } - - /** - * All columns that we copy from the user data, when we update a row. - * - * This excludes the columns that need updates: - * - [cc] - we need to increment change-count + * Together with [FN], forms the primary identification of a tuple within a collection. See [naksha.model.Version] for the layout. * @since 3.0 */ @JvmField @JsStatic - val updateColumns = listOf( - updated_at, created_at, author_ts, - cv0, cv1, cv2, cv3, - hash, here_tile, // removed: cc - base_tn, // removed: fn, version (PK columns; not set via updates), next_version (HEAD has none) - id, app_id, author, origin, target, ft, - cs0, cs1, cs2, cs3, - tags, ref_point, geo, feature, // removed: attachment (needs special handling) - gbn, - ) - - /** - * The names of all database columns, as comma separated list. - * @since 3.0 - */ - @JsStatic - @JvmField - val updateColumnsNames = updateColumns.joinToString(",") { it.name } + val VERSION = PgColumn(1, "_version", INT64, "STORAGE $PLAIN NOT NULL") /** - * All columns that we copy, when we create a tombstone state (deleted). - * - * In that case we copy from _HEAD_ into a temporary CTE table, then further to _HISTORY_ and/or _SHADOW_, but we need to update some columns, therefore this excludes the columns that need updates: - * - [next_tn] - will become the current [tn] to signal tombstone state _(dead-end)_ - * - [tn] - must be updated to match current `version`, with action bits set to DELETED (this is the `final_tn`) - * - [fn] - copied from the HEAD row directly - * - [base_tn] - needs to be set to `null` + * The next-version (with action in the lower 2 bits) of this tuple, only available in the history. * @since 3.0 */ @JvmField @JsStatic - val tombstoneColumns = listOf( - updated_at, created_at, author_ts, - cv0, cv1, cv2, cv3, - hash, here_tile, // removed: cc - // removed: fn, version, next_version, and base_tn, - id, app_id, author, origin, target, ft, - cs0, cs1, cs2, cs3, - tags, ref_point, geo, feature, attachment, - gbn, - ) - - init { - // This is only self-check code. - for ((i, col) in allColumns.withIndex()) { - check(i == col.i) { "Invalid columns, column '${col.name}' should be at index ${col.i}, but found at $i" } - } - } - - /** - * Maps a [PgColumn] to its equivalent [MemberType], used when generating [Member] objects - * for mandatory and default member lists. - */ - internal fun pgTypeToMemberType(col: PgColumn): MemberType = when { - col === geo || col === ref_point -> MemberType.SPATIAL - else -> when (col.type) { - PgType.INT64 -> MemberType.INT64 - PgType.DOUBLE -> MemberType.FLOAT64 - PgType.INT -> MemberType.INT32 - PgType.SHORT -> MemberType.INT16 - PgType.FLOAT -> MemberType.FLOAT32 - PgType.BOOLEAN -> MemberType.BOOLEAN - PgType.STRING -> MemberType.STRING - PgType.BYTE_ARRAY -> MemberType.BYTE_ARRAY - else -> MemberType.STRING - } - } - - /** - * The mandatory [Member]s that the storage always injects into every collection, regardless of - * what the client provides: `fn` (INT64), `version` (INT64), `id` (STRING), `feature` (BYTE_ARRAY). - * - * These are derived from [mandatoryColumns]. Clients must not declare them with a different type. - * @since 3.0 - */ - @JvmField - @JsStatic - val mandatoryMembers: List = mandatoryColumns.map { col -> - Member(col.name, pgTypeToMemberType(col)) - } - - /** - * The mandatory [Member]s for a HISTORY table: [mandatoryMembers] plus `next_version` (INT64). - * @since 3.0 - */ - @JvmField - @JsStatic - val mandatoryHistoryMembers: List = mandatoryHistoryColumns.map { col -> - Member(col.name, pgTypeToMemberType(col)) - } - - /** - * The default [Member]s injected when the client does **not** provide a [members][naksha.model.objects.NakshaCollection.members] - * list (backward-compatible full schema). These correspond to all optional built-in columns - * (i.e. [headColumns] minus [mandatoryColumns]). - * @since 3.0 - */ - @JsStatic - val defaultMembers: List by lazy { - val mandatory = mandatoryColumns.map { it.name }.toSet() - headColumns.filter { it.name !in mandatory }.map { col -> - Member(col.name, pgTypeToMemberType(col)) - } - } - - /** - * Returns the [PgColumn] that matches to the official [MetaColumn]. - * @param metaColumn the [MetaColumn] to resolve. - * @return the [PgColumn] that matches this [MetaColumn]; if any. - * @since 3.0 - */ - @JvmStatic - @JsStatic - fun ofRowColumn(metaColumn: MetaColumn): PgColumn? = when (metaColumn.name) { - MetaColumn.NEXT_VERSION -> next_version - MetaColumn.UPDATED_AT -> updated_at - MetaColumn.CREATED_AT -> created_at - MetaColumn.AUTHOR_TS -> author_ts - MetaColumn.CV0 -> cv0 - MetaColumn.CV1 -> cv1 - MetaColumn.CV2 -> cv2 - MetaColumn.CV3 -> cv3 - MetaColumn.HASH -> hash - MetaColumn.HERE_TILE -> here_tile - MetaColumn.CHANGE_COUNT -> cc - MetaColumn.TUPLE_NUMBER -> fn // tn is split: callers comparing the binary form must decode to (fn, version) - MetaColumn.VERSION -> version - MetaColumn.BASE_TUPLE_NUMBER -> base_tn - MetaColumn.ID -> id - MetaColumn.APP_ID -> app_id - MetaColumn.AUTHOR -> author - MetaColumn.ORIGIN -> origin - MetaColumn.TARGET -> target - MetaColumn.FEATURE_TYPE -> ft - MetaColumn.CS0 -> cs0 - MetaColumn.CS1 -> cs1 - MetaColumn.CS2 -> cs2 - MetaColumn.CS3 -> cs3 - MetaColumn.TAGS -> tags - MetaColumn.REF_POINT -> ref_point - MetaColumn.GEOMETRY -> geo - MetaColumn.FEATURE -> feature - MetaColumn.ATTACHMENT -> attachment - else -> null - } + val NEXT_VERSION = PgColumn(2, NextVersion.name, INT64, "STORAGE $PLAIN NOT NULL") } - @Suppress("NON_EXPORTABLE_TYPE") - override fun namespace(): KClass = PgColumn::class - override fun initClass() {} + override fun toString(): String = ident } \ No newline at end of file diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumnEntry.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumnWithValues.kt similarity index 68% rename from here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumnEntry.kt rename to here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumnWithValues.kt index 9e6092387..06dd8f5ad 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumnEntry.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumnWithValues.kt @@ -3,14 +3,31 @@ package naksha.psql import naksha.base.AnyList import naksha.base.Int64 +/** + * Rows as selected in [PgRows]. + * @since 3.0 + */ @Suppress("UNCHECKED_CAST") -internal data class PgColumnEntry( - val index: Int, - val name: String, - val type: PgType, +internal data class PgColumnWithValues( + /** + * The database column. + * @since 3.0 + */ + val column: PgColumn, + + /** + * The index of the [PgRows], can differ from the physical indexing. + * @since 3.0 + */ + val index: Int = column.index, + + /** + * When used in [PgRows], the values of the column for each row. + * @since 3.0 + */ val values: AnyList = AnyList() ) { - fun withSize(size: Int): PgColumnEntry { + fun withSize(size: Int): PgColumnWithValues { values.size = size return this } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgDistributionPartition.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgDistributionPartition.kt new file mode 100644 index 000000000..dd85d2de5 --- /dev/null +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgDistributionPartition.kt @@ -0,0 +1,72 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.psql + +import naksha.model.NakshaError.NakshaErrorCompanion.INTERNAL_ERROR +import naksha.model.NakshaException +import naksha.model.objects.StandardMembers.StandardMembers_C.Id +import naksha.model.objects.StandardMembers.StandardMembers_C.NextVersion +import naksha.psql.PgColumn.PgColumn_C.FN +import naksha.psql.PgColumn.PgColumn_C.VERSION +import naksha.psql.PgUtil.PgUtilCompanion.quoteIdent +import kotlin.js.JsExport +import kotlin.jvm.JvmField + +/** + * A distribution partition of either [PgHeadTable] or [PgHistoryPartition], used to store a huge amount of features in a collection. + * + * The name of the partitions will be `{name}${featureNumber % partitions.size}`, e.g. `foo$0` or `foo$hst$2026$0`. + * @since 3.0 + * @see [PgHeadTable] + * @see [PgHistoryPartition] + */ +@JsExport +class PgDistributionPartition private constructor( + /** + * The parent table, either [PgHeadTable] or [PgHistoryPartition]. + * @since 3.0 + */ + parent: PgTable, + + /** + * The partition-number of this partition in the parent partitions, a value between `0` and `n`, with `n` being `parent.partitions.size - 1`. + * @since 3.0 + */ + @JvmField + val partitionIndex: Int +) : PgTable(parent.collection, parent.name + '$' + partitionIndex, parent) { + /** + * Create a distribution partition in the [PgHeadTable]. + */ + constructor(parent: PgHeadTable, partitionNumber: Int) : this(parent as PgTable, partitionNumber) + + /** + * Create a distribution partition in the history aka [PgHistoryPartition]. + */ + constructor(parent: PgHistoryPartition, partitionNumber: Int) : this(parent as PgTable, partitionNumber) + + override fun CREATE_SQL(): String { + val (CREATE_TABLE, TABLESPACE) = CREATE_TABLE_and_TABLESPACE() + val ID = collection.column(Id) + val NEXT_VERSION = collection.column(NextVersion) + + // partition of HEAD. + if (parent is PgHeadTable) return """$CREATE_TABLE $quotedName +PARTITION OF ${parent.quotedName} (${parent.CONSTRAINT(partitionIndex)}) +FOR VALUES FROM ($partitionIndex) TO (${partitionIndex+1}) +WITH (fillfactor=50,toast_tuple_target=$toast_tuple_target)$TABLESPACE; +CREATE INDEX ${quoteIdent(name, "\$i_version")} ON $quotedName USING btree ($VERSION) INCLUDE ($FN, $ID);""" + + // partition of HISTORY-PARTITION. + if (parent is PgHistoryPartition) { + val root = parent.parent as PgHistoryTable + return """$CREATE_TABLE $quotedName +PARTITION OF ${parent.quotedName} (${root.CONSTRAINT(parent.partitionIndex, partitionIndex)}) +FOR VALUES FROM ($partitionIndex) TO (${partitionIndex+1}) +WITH (fillfactor=100,toast_tuple_target=$toast_tuple_target)$TABLESPACE; +CREATE INDEX ${quoteIdent(name, "\$i_version")} ON $quotedName USING btree ($VERSION, $NEXT_VERSION) INCLUDE ($FN, $ID);""" + } + + throw NakshaException(INTERNAL_ERROR, "The distribution partition must have PgHeadTable or PgHistoryPartition as parent") + } +} \ No newline at end of file diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgHead.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgHead.kt deleted file mode 100644 index d00d8a112..000000000 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgHead.kt +++ /dev/null @@ -1,105 +0,0 @@ -package naksha.psql - -import naksha.model.NakshaError.NakshaErrorCompanion.PARTITION_NOT_FOUND -import naksha.model.NakshaException -import naksha.psql.PgUtil.PgUtilCompanion.partitionNumber -import kotlin.js.JsExport -import kotlin.js.JsName -import kotlin.jvm.JvmField - -/** - * The HEAD table of a collection. - * @since 3.0 - * @see [PgTable] - * @see [PgHeadPartition] - */ -@Suppress("OPT_IN_USAGE") -@JsExport -open class PgHead protected constructor( - collection: PgCollection, - name: String, - storageClass: PgStorageClass, - isVolatile: Boolean, - partitionOf: PgTable? = null, - partitionOfValue: Int = -1, - partitionBy: PgColumn? = null, - partitionCount: Int = 0 -) : PgTable( - collection, name, storageClass, isVolatile, - partitionOf, partitionOfValue, partitionBy, partitionCount -) { - /** - * Create a new ordinary HEAD table. - * @param c the collection for which to create the HEAD table. - * @param storageClass the storage-class for this table and all related. - * @param partCount the amount of partitions to create (0 or 2 to 256). - */ - @JsName("of") - constructor(c: PgCollection, storageClass: PgStorageClass, partCount: Int) : this( - c, "${c.id}${PG_HEAD}", storageClass, true, - partitionCount = if (partCount <= 1) 0 else partCount, - partitionBy = if (partCount >= 2) PgColumn.fn else null - ) - - /** - * All performance partitions to stored into; empty if no performance partitioning. - */ - @JvmField - val partitions: Array = if (partitionCount <= 1) emptyArray() else Array(partitionCount) { - PgHeadPartition(this, it) - } - - /** - * Calculate the performance partition into which to write the feature with the given ID. - * @param featureId the ID of the feature to locate the performance partition for. - * @return either the performance partition to put the feature into; _null_ if the table is not partitioned, features need to be written into the table itself. - */ - operator fun get(featureId: String): PgHeadPartition? { - val partitions = this.partitions - if (partitions.size == 0) return null - val i = partitionNumber(featureId) % partitions.size - check(i >= partitions.size) { throw NakshaException(PARTITION_NOT_FOUND, "Partition $i not found in table $name") } - return partitions[i] - } - - override fun create(conn: PgConnection) { - super.create(conn) - for (partition in partitions) partition.create(conn) - } - - override fun createIndex(conn: PgConnection, index: PgIndex) { - if (this.partitionByColumn != null) { - for (partition in partitions) partition.createIndex(conn, index) - } else { - super.createIndex(conn, index) - } - if (index !in indices) indices += index - } - - override fun addIndex(index: PgIndex) { - if (this.partitionByColumn != null) { - for (partition in partitions) partition.addIndex(index) - } else { - super.addIndex(index) - } - if (index !in indices) indices += index - } - - override fun removeIndex(index: PgIndex) { - if (this.partitionByColumn != null) { - for (partition in partitions) partition.removeIndex(index) - } else { - super.removeIndex(index) - } - if (index in indices) indices -= index - } - - override fun dropIndex(conn: PgConnection, index: PgIndex) { - if (this.partitionByColumn != null) { - for (partition in partitions) partition.dropIndex(conn, index) - } else { - super.dropIndex(conn, index) - } - if (index in indices) indices -= index - } -} \ No newline at end of file diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgHeadPartition.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgHeadPartition.kt deleted file mode 100644 index d7eae6929..000000000 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgHeadPartition.kt +++ /dev/null @@ -1,18 +0,0 @@ -@file:Suppress("OPT_IN_USAGE") - -package naksha.psql - -import kotlin.js.JsExport - -/** - * A feature partition for performance optimisation. - * @property head the head table. - * @param index the index in [PgHead.partitions]. - * @since 3.0 - * @see [PgHead] - */ -@JsExport -class PgHeadPartition internal constructor(val head: PgHead, index: Int) : PgTable( - head.collection, "${head.name}${PG_PART}${PgUtil.partitionSuffix(index)}", head.storageClass, true, - partitionOfTable = head, partitionOfValue = index -) \ No newline at end of file diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgHeadTable.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgHeadTable.kt new file mode 100644 index 000000000..d68aef6fd --- /dev/null +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgHeadTable.kt @@ -0,0 +1,116 @@ +package naksha.psql + +import naksha.base.Int64 +import naksha.model.objects.StandardMembers.StandardMembers_C.Id +import naksha.psql.PgColumn.PgColumn_C.FN +import naksha.psql.PgColumn.PgColumn_C.NEXT_VERSION +import naksha.psql.PgColumn.PgColumn_C.VERSION +import naksha.psql.PgUtil.PgUtilCompanion.partitionNumber +import naksha.psql.PgUtil.PgUtilCompanion.quoteIdent +import kotlin.js.JsExport +import kotlin.jvm.JvmField + +/** + * The _HEAD_ table of a [collection][naksha.model.objects.NakshaCollection], storing the latest [tuple][naksha.model.Tuple] of the [features][naksha.model.objects.NakshaFeature] being part of the [collection][naksha.model.objects.NakshaCollection]. + * @since 3.0 + */ +@Suppress("OPT_IN_USAGE") +@JsExport +class PgHeadTable( + /** The collection to which this HEAD table belongs. */ + collection: PgCollection, +) : PgTable(collection, collection.id, null) { + + /** + * All distribution partitions; if not partitioned, then an empty array. + * @since 3.0 + */ + @JvmField + val partitions: Array = if (collection.partitions <= 1) emptyArray() else Array(collection.partitions) { + PgDistributionPartition(this, it) + } + + @Suppress("FunctionName") + internal fun CONSTRAINT(): String { + val ID = collection.column(Id) + return """ + CONSTRAINT ${quoteIdent(name, "\$c_pkey")} PRIMARY KEY ($FN) INCLUDE ($VERSION, $ID), + CONSTRAINT ${quoteIdent(name, "\$c_fn")} CHECK (($FN < 0 AND $ID IS NOT NULL) OR ($FN >= 0 AND $ID IS NULL)), + CONSTRAINT ${quoteIdent(name, "\$c_nv")} CHECK ($NEXT_VERSION IS NULL), + CONSTRAINT ${quoteIdent(name, "\$c_id")} UNIQUE ($ID) INCLUDE ($VERSION, $FN})""" + } + + @Suppress("FunctionName") + internal fun CONSTRAINT(distributionPartition: Int): String { + return """${CONSTRAINT()}, + CONSTRAINT ${quoteIdent(name, "\$c_fnr")} CHECK ((($FN & 65535)::int4 % ${collection.partitions})=$distributionPartition) + """ + } + + override fun CREATE_SQL(): String { + val (CREATE_TABLE, TABLESPACE) = CREATE_TABLE_and_TABLESPACE() + val ID = collection.column(Id) + + // HEAD is NOT distribution partitioned. + if (partitions.isEmpty()) return """$CREATE_TABLE $quotedName (${columnDefinitions()}, ${CONSTRAINT()}) +WITH (fillfactor=50,toast_tuple_target=$toast_tuple_target)$TABLESPACE; +CREATE INDEX ${quoteIdent(name, "\$i_version")} ON $quotedName USING btree ($VERSION) INCLUDE ($FN, $ID);""" + + // HEAD is distribution partitioned. + return """$CREATE_TABLE $quotedName (${columnDefinitions()}) +WITH (fillfactor=50,toast_tuple_target=$toast_tuple_target) +PARTITION BY RANGE ((($FN & 65535)::int4 % ${collection.partitions})$TABLESPACE;""" + } + + /** + * Calculate the distribution-partition into which to write the feature with the given feature-id. + * @param featureId the ID of the feature to locate the performance partition for. + * @return either the performance partition to put the feature into; _null_ if the table is not partitioned, features need to be written into the table itself. + */ + operator fun get(featureId: String): PgDistributionPartition? + = if (partitions.isEmpty()) null else partitions[partitionNumber(featureId) % partitions.size] + + operator fun get(featureNumber: Int64): PgDistributionPartition? + = if (partitions.isEmpty()) null else partitions[featureNumber.toInt() % partitions.size] + + override fun create(conn: PgConnection) { + super.create(conn) + for (partition in partitions) partition.create(conn) + } + + override fun createIndex(conn: PgConnection, index: PgIndex) { + if (!partitions.isEmpty()) { + for (partition in partitions) partition.createIndex(conn, index) + } else { + super.createIndex(conn, index) + } + if (index !in indices) indices += index + } + + override fun addIndex(index: PgIndex) { + if (!partitions.isEmpty()) { + for (partition in partitions) partition.addIndex(index) + } else { + super.addIndex(index) + } + if (index !in indices) indices += index + } + + override fun removeIndex(index: PgIndex) { + if (!partitions.isEmpty()) { + for (partition in partitions) partition.removeIndex(index) + } else { + super.removeIndex(index) + } + if (index in indices) indices -= index + } + + override fun dropIndex(conn: PgConnection, index: PgIndex) { + if (!partitions.isEmpty()) { + for (partition in partitions) partition.dropIndex(conn, index) + } else { + super.dropIndex(conn, index) + } + if (index in indices) indices -= index + } +} \ No newline at end of file diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgHistory.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgHistory.kt deleted file mode 100644 index 418505748..000000000 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgHistory.kt +++ /dev/null @@ -1,73 +0,0 @@ -@file:Suppress("OPT_IN_USAGE") - -package naksha.psql - -import naksha.model.Version -import kotlin.js.JsExport -import kotlin.js.JsName -import kotlin.jvm.JvmField - -/** - * The HISTORY table, partitioned by [next_version][PgColumn.next_version]. - * @since 3.0 - * @see [PgTable] - * @see [PgHistoryYear] - */ -@JsExport -class PgHistory(val head: PgHead) : PgTable( - head.collection, "${head.collection.id}${PG_HST}", head.storageClass, false, - partitionByColumn = PgColumn.next_version -) { - /** - * All partitions, with key being the partition key (`next_version >> shift`). - */ - @JvmField - val years: MutableMap = mutableMapOf() - @JsName("getYear") - operator fun get(txn_next: Version): PgHistoryYear? = years[txn_next.year] - @JsName("setYear") - operator fun set(txn_next: Version, partition: PgHistoryYear) { - years[txn_next.year] = partition - } - - override fun create(conn: PgConnection) { - super.create(conn) - for (entry in years) entry.value.create(conn) - } - - fun createYear(conn: PgConnection, partitionKey: Int) { - var yearTable = years[partitionKey] - if (yearTable == null) { - yearTable = PgHistoryYear(this, partitionKey) - years[partitionKey] = yearTable - } - yearTable.create(conn) - for (index in indices) { - yearTable.createIndex(conn, index) - } - } - - fun addYear(partitionKey: Int) { - if (partitionKey !in years) { - val yearTable = PgHistoryYear(this, partitionKey) - years[partitionKey] = yearTable - for (index in indices) yearTable.addIndex(index) - } - } - - override fun addIndex(index: PgIndex) { - for (entry in years) entry.value.addIndex(index) - } - - override fun removeIndex(index: PgIndex) { - for (entry in years) entry.value.removeIndex(index) - } - - override fun createIndex(conn: PgConnection, index: PgIndex) { - for (entry in years) entry.value.createIndex(conn, index) - } - - override fun dropIndex(conn: PgConnection, index: PgIndex) { - for (entry in years) entry.value.dropIndex(conn, index) - } -} \ No newline at end of file diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgHistoryPartition.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgHistoryPartition.kt index a5a7104e3..f505678a7 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgHistoryPartition.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgHistoryPartition.kt @@ -2,17 +2,150 @@ package naksha.psql +import naksha.base.Int64 +import naksha.model.Naksha +import naksha.model.NakshaError.NakshaErrorCompanion.PARTITION_NOT_FOUND +import naksha.model.NakshaException +import naksha.model.objects.StandardMembers.StandardMembers_C.Id +import naksha.model.objects.StandardMembers.StandardMembers_C.NextVersion +import naksha.psql.PgColumn.PgColumn_C.FN +import naksha.psql.PgColumn.PgColumn_C.VERSION +import naksha.psql.PgUtil.PgUtilCompanion.quoteIdent import kotlin.js.JsExport +import kotlin.jvm.JvmField /** - * A feature partition for performance optimisation. - * @property year the history year. - * @param index the index in the history year partitions array. + * A history partition for all features modified in a specific next-version range. * @since 3.0 - * @see [PgHistoryYear] + * @see [PgHistoryTable] + * @see [PgDistributionPartition] */ @JsExport -class PgHistoryPartition(val year: PgHistoryYear, index: Int) : PgTable( - year.collection, "${year.name}${PG_PART}${PgUtil.partitionSuffix(index)}", year.storageClass, false, - partitionOfTable = year, partitionOfValue = index -) \ No newline at end of file +class PgHistoryPartition( + /** + * The parent table, must be [PgHistoryTable]. + * @since 3.0 + */ + parent: PgHistoryTable, + + /** + * The partition-index of this partition, actually `{next-version} shr {shift}`. + * @since 3.0 + */ + @JvmField + val partitionIndex: Int +) : PgTable(parent.collection, parent.name + '$' + partitionIndex, parent) { + /** + * All distribution partitions; if not partitioned, then an empty array. + * @since 3.0 + */ + @JvmField + val partitions: Array = if (collection.partitions <= 1) emptyArray() else Array(collection.partitions) { + PgDistributionPartition(this, it) + } + + override fun CREATE_SQL(): String { + val (CREATE_TABLE, TABLESPACE) = CREATE_TABLE_and_TABLESPACE() + val toast_tuple_target:Int = collection.catalog.storage.adminCatalog.maxTupleSize + val parent = this.parent as PgHistoryTable + val ID = collection.column(Id) + val NEXT_VERSION = collection.column(NextVersion) + + // HISTORY-PARTITION is NOT distribution partitioned. + if (partitions.isEmpty()) return """$CREATE_TABLE $quotedName +PARTITION OF ${parent.quotedName} (${parent.CONSTRAINT(partitionIndex)}) +FOR VALUES FROM ($partitionIndex) TO (${partitionIndex+1}) +WITH (fillfactor=50,toast_tuple_target=$toast_tuple_target)$TABLESPACE; +CREATE INDEX ${quoteIdent(name, "\$i_version")} ON $quotedName USING btree ($VERSION, $NEXT_VERSION) INCLUDE ($FN, $ID);""" + + // HISTORY-PARTITION is distribution partitioned. + return """$CREATE_TABLE $quotedName +PARTITION OF ${parent.quotedName} +PARTITION BY RANGE ((($FN & 65535)::int4 % ${collection.partitions}) +FOR VALUES FROM ($partitionIndex) TO (${partitionIndex+1}) +WITH (fillfactor=50,toast_tuple_target=$toast_tuple_target)$TABLESPACE""" + } + + /** + * Calculates the partition-number from the given [feature-number][FN]. + * @param featureNumber the [feature-number][FN] from which to calculate the partition-number. + * @return the calculated partition-number. + * @since 3.0 + */ + fun partitionNumber(featureNumber: Int64): Int = Naksha.partitionNumber(featureNumber) % collection.partitions + + /** + * Calculates the partition-number from the given [feature-id][Id]. + * @param featureId the [feature-id][Id] from which to calculate the partition-number. + * @return the calculated partition-number. + * @since 3.0 + */ + fun partitionNumber(featureId: String): Int = Naksha.partitionNumber(featureId) % collection.partitions + + /** + * Calculate the distribution-partition into which to write the feature with the given feature-number. + * @param featureNumber the feature-number of the feature to return the distribution-partition for. + * @return either the distribution-partition to put the feature into or `null` if the table is not partitioned, features need to be written into the table itself. + */ + operator fun get(featureNumber: Int64): PgDistributionPartition? { + val partitions = this.partitions + if (partitions.isEmpty()) return null + val i = Naksha.partitionNumber(featureNumber) % collection.partitions + check(i >= partitions.size) { throw NakshaException(PARTITION_NOT_FOUND, "Partition $i not found in table $name") } + return partitions[i] + } + + /** + * Calculate the distribution-partition into which to write the feature with the given feature-id. + * @param featureId the feature-id of the feature to return the distribution-partition for. + * @return either the distribution-partition to put the feature into or `null` if the table is not partitioned, features need to be written into the table itself. + */ + operator fun get(featureId: String): PgDistributionPartition? { + val partitions = this.partitions + if (partitions.isEmpty()) return null + val i = Naksha.partitionNumber(featureId) % collection.partitions + check(i >= partitions.size) { throw NakshaException(PARTITION_NOT_FOUND, "Partition $i not found in table $name") } + return partitions[i] + } + + override fun create(conn: PgConnection) { + super.create(conn) + for (partition in partitions) partition.create(conn) + } + + override fun createIndex(conn: PgConnection, index: PgIndex) { + if (!partitions.isEmpty()) { + for (partition in partitions) partition.createIndex(conn, index) + } else { + super.createIndex(conn, index) + } + if (index !in indices) indices += index + } + + override fun addIndex(index: PgIndex) { + if (!partitions.isEmpty()) { + for (partition in partitions) partition.addIndex(index) + } else { + super.addIndex(index) + } + if (index !in indices) indices += index + } + + override fun removeIndex(index: PgIndex) { + if (!partitions.isEmpty()) { + for (partition in partitions) partition.removeIndex(index) + } else { + super.removeIndex(index) + } + if (index in indices) indices -= index + } + + override fun dropIndex(conn: PgConnection, index: PgIndex) { + if (!partitions.isEmpty()) { + for (partition in partitions) partition.dropIndex(conn, index) + } else { + super.dropIndex(conn, index) + } + if (index in indices) indices -= index + } +} \ No newline at end of file diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgHistoryTable.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgHistoryTable.kt new file mode 100644 index 000000000..b915ed6c5 --- /dev/null +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgHistoryTable.kt @@ -0,0 +1,120 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.psql + +import naksha.base.Int64 +import naksha.model.objects.StandardMembers.StandardMembers_C.Id +import naksha.model.objects.StandardMembers.StandardMembers_C.NextVersion +import naksha.psql.PgColumn.PgColumn_C.FN +import naksha.psql.PgColumn.PgColumn_C.VERSION +import naksha.psql.PgUtil.PgUtilCompanion.quoteIdent +import kotlin.js.JsExport +import kotlin.jvm.JvmField + +/** + * The _HISTORY_ table of a collection , partitioned by [next_version][PgColumn.NEXT_VERSION]. + * + * Features are moved into the history table when new states _([tuple][naksha.model.Tuple])_ are created in _HEAD_. In that case the previous [tuple][naksha.model.Tuple] is moved from _HEAD_ into _HISTORY_. When moving the [tuple][naksha.model.Tuple], its next-version property is set to the version of the new _head_ [tuple][naksha.model.Tuple], that replaces it. Within the history table the features are partitioned by this [next-version][naksha.model.objects.StandardMembers.StandardMembers_C.NextVersion]. With the default [shift][naksha.model.objects.NakshaCollection.shift] of 41 the partitioning effectively is done by calendar year of when the [tuple][naksha.model.Tuple] has become obsolete and was moved into _HISTORY_. + * @since 3.0 + * @see [PgHistoryPartition] + * @see [PgDistributionPartition] + * @see [PgTable] + */ +@JsExport +class PgHistoryTable( + /** The collection to which this HEAD table belongs. */ + collection: PgCollection, +) : PgTable(collection, collection.id + "\$hst", null) { + + /** + * All history partitions with the key being the partition-number and the value being the partition. + * + * Beware that history-partitions are named like `{name}$hst${nextVersion >> shift}` for example `foo$hst$1`. + * @since 3.0 + */ + @JvmField + val partitions: MutableMap = mutableMapOf() + + @Suppress("FunctionName") + internal fun CONSTRAINT(historyPartition:Int): String { + val ID = collection.column(Id) + val NEXT_VERSION = collection.column(NextVersion) + return """ + CONSTRAINT ${quoteIdent(name, "\$c_pkey")} PRIMARY KEY ($FN, $VERSION) INCLUDE ($NEXT_VERSION, $ID}), + CONSTRAINT ${quoteIdent(name, "\$c_nv")} CHECK ($NEXT_VERSION IS NOT NULL AND $NEXT_VERSION >= $VERSION), + CONSTRAINT ${quoteIdent(name, "\$c_nvr")} CHECK ((($NEXT_VERSION >> ${collection.shift})::int4) = $historyPartition), + CONSTRAINT ${quoteIdent(name, "\$c_id")} UNIQUE ($ID, $VERSION) INCLUDE ($NEXT_VERSION, $FN}), + CONSTRAINT ${quoteIdent(name, "\$c_fn")} CHECK (($FN < 0 AND $ID IS NOT NULL) OR ($FN >= 0 AND $ID IS NULL))""" + } + + @Suppress("FunctionName") + internal fun CONSTRAINT(historyPartition:Int, distributionPartition: Int): String { + return """${CONSTRAINT(historyPartition)}, + CONSTRAINT ${quoteIdent(name, "\$c_fnr")} CHECK ((($FN & 65535)::int4 % ${collection.partitions})=$distributionPartition) +""" + } + + override fun CREATE_SQL(): String { + val (CREATE_TABLE, TABLESPACE) = CREATE_TABLE_and_TABLESPACE() + val NEXT_VERSION = collection.column(NextVersion) + return """$CREATE_TABLE $quotedName (${columnDefinitions()}) +PARTITION BY RANGE ((($NEXT_VERSION >> ${collection.shift})::int4)) +WITH (fillfactor=100,toast_tuple_target=$toast_tuple_target) +$TABLESPACE""" + } + + /** + * Calculates the partition-number from the given [next-version][NextVersion]. + * @param nextVersion the [next-version][NextVersion] from which to calculate the partition-number. + * @return the calculated partition-number. + * @since 3.0 + */ + fun partitionNumber(nextVersion: Int64): Int = (nextVersion shr collection.shift).toInt() + + operator fun get(nextVersion: Int64): PgHistoryPartition? = partitions[partitionNumber(nextVersion)] + + operator fun set(nextVersion: Int64, partition: PgHistoryPartition) { + partitions[partitionNumber(nextVersion)] = partition + } + + override fun create(conn: PgConnection) { + super.create(conn) + for (entry in partitions) entry.value.create(conn) + } + + fun createPartition(conn: PgConnection, partitionNumber: Int) { + var partition = partitions[partitionNumber] + if (partition == null) { + partition = PgHistoryPartition(this, partitionNumber) + partitions[partitionNumber] = partition + } + partition.create(conn) + for (index in indices) { + partition.createIndex(conn, index) + } + } + + fun addPartition(partitionNumber: Int) { + if (partitionNumber !in partitions) { + val partition = PgHistoryPartition(this, partitionNumber) + partitions[partitionNumber] = partition + for (index in indices) partition.addIndex(index) + } + } + + override fun addIndex(index: PgIndex) { + for (entry in partitions) entry.value.addIndex(index) + } + + override fun removeIndex(index: PgIndex) { + for (entry in partitions) entry.value.removeIndex(index) + } + + override fun createIndex(conn: PgConnection, index: PgIndex) { + for (entry in partitions) entry.value.createIndex(conn, index) + } + + override fun dropIndex(conn: PgConnection, index: PgIndex) { + for (entry in partitions) entry.value.dropIndex(conn, index) + } +} \ No newline at end of file diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgHistoryYear.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgHistoryYear.kt deleted file mode 100644 index f0237ece7..000000000 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgHistoryYear.kt +++ /dev/null @@ -1,96 +0,0 @@ -@file:Suppress("OPT_IN_USAGE") - -package naksha.psql - -import naksha.model.NakshaError.NakshaErrorCompanion.PARTITION_NOT_FOUND -import naksha.model.NakshaException -import kotlin.js.JsExport -import kotlin.jvm.JvmField - -/** - * A history partition for a specific partition-key of the history. There should always be a partition for the current - * key value and the next one. With the default [shift][naksha.model.objects.NakshaCollection.shift] of 41 the key - * value equals the calendar year, so typically the current year and the next year. - * - * Beware that the history is not partitioned by the year when the transaction happened, but by the moment the - * transaction was updated, and moved into the history. This is done, because we have the yearly partitions for - * garbage collection, and if the state of a row is moved into history, from this moment on we want to keep it - * for some time (e.g. one year), otherwise a feature that was updated 3 years ago the last time, and then moved - * into history, would be removed from history instantly. - * @property history the history table. - * @param partitionKey the value of `(next_version >> shift)` that identifies this partition. - * @since 3.0 - * @see [PgHistory] - * @see [PgHistoryPartition] - */ -@JsExport -class PgHistoryYear(val history: PgHistory, val partitionKey: Int) : PgTable( - history.collection, - "${history.name}${PG_S}${partitionKey}", - history.storageClass, - false, - partitionOfTable = history, - partitionOfValue = partitionKey, - partitionByColumn = history.head.partitionByColumn, - partitionCount = history.head.partitionCount -) { - /** - * The performance partitions. - */ - @JvmField - val partitions: Array = Array(history.head.partitions.size) { PgHistoryPartition(this, it) } - - /** - * Calculate the performance partition into which to write the feature with the given ID. - * @param featureId the ID of the feature to locate the performance partition for. - * @return either the performance partition to put the feature into; _null_ if the table is not partitioned, features need to be written into the table itself. - */ - operator fun get(featureId: String): PgHistoryPartition? { - val partitions = this.partitions - if (partitions.isEmpty()) return null - val i = PgUtil.partitionNumber(featureId) % partitions.size - check(i >= partitions.size) { throw NakshaException(PARTITION_NOT_FOUND, "Partition $i not found in table $name") } - return partitions[i] - } - - override fun create(conn: PgConnection) { - super.create(conn) - for (partition in partitions) partition.create(conn) - } - - override fun createIndex(conn: PgConnection, index: PgIndex) { - if (this.partitionByColumn != null) { - for (partition in partitions) partition.createIndex(conn, index) - } else { - super.createIndex(conn, index) - } - if (index !in indices) indices += index - } - - override fun addIndex(index: PgIndex) { - if (this.partitionByColumn != null) { - for (partition in partitions) partition.addIndex(index) - } else { - super.addIndex(index) - } - if (index !in indices) indices += index - } - - override fun removeIndex(index: PgIndex) { - if (this.partitionByColumn != null) { - for (partition in partitions) partition.removeIndex(index) - } else { - super.removeIndex(index) - } - if (index in indices) indices -= index - } - - override fun dropIndex(conn: PgConnection, index: PgIndex) { - if (this.partitionByColumn != null) { - for (partition in partitions) partition.dropIndex(conn, index) - } else { - super.dropIndex(conn, index) - } - if (index in indices) indices -= index - } -} \ No newline at end of file diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgIndex.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgIndex.kt index 004146dd3..32bbe9364 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgIndex.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgIndex.kt @@ -1,834 +1,94 @@ package naksha.psql -import naksha.base.AtomicMap -import naksha.base.JsEnum -import naksha.base.fn.Fx2 +import naksha.model.NakshaError.NakshaErrorCompanion.INTERNAL_ERROR +import naksha.model.NakshaException import naksha.model.objects.Index import naksha.model.objects.IndexType -import naksha.model.request.query.SortOrder -import naksha.model.request.query.SortOrder.SortOrderCompanion.DESCENDING +import naksha.model.objects.IndexType.IndexType_C.BTREE +import naksha.model.objects.IndexType.IndexType_C.SET +import naksha.model.objects.IndexType.IndexType_C.SPATIAL +import naksha.model.objects.IndexType.IndexType_C.TAGS import naksha.psql.PgUtil.PgUtilCompanion.quoteIdent -import naksha.psql.PgColumn.PgColumnCompanion.id as c_id -import naksha.psql.PgColumn.PgColumnCompanion.fn as c_fn -import naksha.psql.PgColumn.PgColumnCompanion.version as c_version -import naksha.psql.PgColumn.PgColumnCompanion.next_version as c_next_version -import naksha.psql.PgColumn.PgColumnCompanion.app_id as c_app_id -import naksha.psql.PgColumn.PgColumnCompanion.author as c_author -import naksha.psql.PgColumn.PgColumnCompanion.author_ts as c_author_ts -import naksha.psql.PgColumn.PgColumnCompanion.updated_at as c_updated_at -import naksha.psql.PgColumn.PgColumnCompanion.geo as c_geo -import naksha.psql.PgColumn.PgColumnCompanion.here_tile as c_here_tile -import naksha.psql.PgColumn.PgColumnCompanion.tags as c_tags -import naksha.psql.PgColumn.PgColumnCompanion.ref_point as c_ref_point -import naksha.psql.PgColumn.PgColumnCompanion.ft as c_ft -import naksha.psql.PgColumn.PgColumnCompanion.cv0 as c_cv0 -import naksha.psql.PgColumn.PgColumnCompanion.cv1 as c_cv1 -import naksha.psql.PgColumn.PgColumnCompanion.cv2 as c_cv2 -import naksha.psql.PgColumn.PgColumnCompanion.cv3 as c_cv3 -import naksha.psql.PgColumn.PgColumnCompanion.cs0 as c_cs0 -import naksha.psql.PgColumn.PgColumnCompanion.cs1 as c_cs1 -import naksha.psql.PgColumn.PgColumnCompanion.cs2 as c_cs2 -import naksha.psql.PgColumn.PgColumnCompanion.cs3 as c_cs3 -import naksha.psql.PgColumn.PgColumnCompanion.gbn as c_gbn import kotlin.js.JsExport -import kotlin.js.JsName -import kotlin.js.JsStatic import kotlin.jvm.JvmField -import kotlin.jvm.JvmStatic -import kotlin.reflect.KClass /** - * The base class for indices. We have different kind of indices, but there is a general rule for all fuzzy indices. - * - * ## Warning - * Identifiers in Postgres are limited to 63 byte (and otherwise will be truncated)! - * - * For HEAD tables the index-name is `{collection-name}{$head$}{index-name}`, e.g. - * `mycol$head$tnu`. For history leaf partitions it is `{leaf-table-name}${index-name}`, e.g. - * `mycol$hst$2026$tnu`. The postfix `$hst$????` is at most 10 byte (year up to 4 digits), so the - * collection name can be up to `63 - 10 - 1 - 3 = 49` byte to fit a 3-character index id. - * - * **This is why we use only three digit identifiers for the indices!** + * The special PostgreSQL internal class that represents an [indices][Index], generated from [custom indices][Index]. * + * The mandatory indices are intrinsic and not exposed as `PgIndex`. Therefore, indices are only those indices that users needs, not what is necessary. */ @Suppress("OPT_IN_USAGE", "MemberVisibilityCanBePrivate") @JsExport -open class PgIndex : JsEnum() { - // TODO: We need to allow `CREATE INDEX CONCURRENTLY`, when the index is not unique. - // However, this can only be done, when being outside of a transaction! - // We need to add special support for index modification outside of transactions. - // So, when a client modifies a collection, we can use a second connection in auto-commit, and - // create missing indices with a special query. - // Storage-API then needs to create a table brittle, without indices, except for minimal ones, - // then after importing, it should switch tables to logged, then add indices, which should be - // done concurrently or the client needs to wait until the index is build, but then we need - // to add an very large timeout. - protected fun sql(using: String, table: PgTable, unique: Boolean, addFillFactor: Boolean, where: String?, includes: List = emptyList()): String { - val filteredIncludes = if (PgTable.isAnyHead(table.name) || PgTable.isMeta(table.name)) - includes.filter { it !== c_next_version } - else includes - val includeClause = if (filteredIncludes.isEmpty()) "" else " INCLUDE (${filteredIncludes.joinToString(", ")})" - return """ -CREATE ${if (unique) "UNIQUE INDEX" else "INDEX "} IF NOT EXISTS ${quoteIdent(id(table))} ON ${table.quotedName} -USING $using$includeClause -${if (addFillFactor) "WITH (fillfactor="+if (table.isVolatile) "80)" else "100)" else ""} ${table.TABLESPACE} -${if (where==null) "" else "WHERE $where"};""" - } - - companion object PgIndex_C { - /** - * The primary key about the [fn][PgColumn.fn] (and, for HISTORY-side tables, [version][PgColumn.version]). - * - * This entry is not used directly to create the index, its only formally here because the column itself has the attribute `PRIMARY KEY`, which is important for joins, replication, and such things. This constraint will always be named automatically by postgres as `table_pkey`. The actual PK DDL is emitted from [PgTable]; the [createFn] below is informational only. - * - * - Always added to all tables as PRIMARY KEY! - * @see [PgAdminMap.createPgCollection] - */ - @JvmField - @JsStatic - val fn_pkey = def(PgIndex::class, "tnu") { self -> - self.name = "fn_unique" - self.internal = true - self.columns = listOf(c_fn) - self.naturalOrder = listOf(DESCENDING) - self.includes = emptyList() - self.createFn = Fx2 { conn, table -> - conn.execute( - self.sql( - """btree ($c_fn DESC)""", - table, unique = true, addFillFactor = true, where = null - ) - ).close() - } - } - - /** - * A unique index above the [id][PgColumn.id], including [fn][PgColumn.fn], [version][PgColumn.version] and [next_version][PgColumn.next_version] column. - * - * Conditional: `WHERE id IS NOT NULL` — rows where `fn >= 0` have `id = NULL` (numeric features) and are excluded. - * - * - Automatically added to [HEAD][PgHead], [DELETED][PgDeleted], and [META][PgMeta]. - * - Must not be added to [HISTORY][PgHistory]. - * @see [PgAdminMap.createPgCollection] - */ - @JvmField - @JsStatic - val id_unique = def(PgIndex::class, "idu") { self -> - self.name = "id_unique" - self.internal = true - self.columns = listOf(c_id) - self.naturalOrder = listOf(DESCENDING) - self.includes = listOf(c_fn, c_version, c_next_version) - self.createFn = Fx2 { conn, table -> - conn.execute( - self.sql( - """btree ($c_id text_pattern_ops DESC)""", - table, unique = true, addFillFactor = true, where = "$c_id IS NOT NULL", - includes = listOf(c_fn, c_version, c_next_version) - ) - ).close() - } - } - - /** - * A non-unique index above the [id][PgColumn.id], [fn][PgColumn.fn] and [version][PgColumn.version], including [next_version][PgColumn.next_version] column. - * - * Conditional: `WHERE id IS NOT NULL` — rows where `fn >= 0` have `id = NULL` (numeric features) and are excluded. - * - * - Automatically added to [HISTORY][PgHistory]. - * - Must not be added to [HEAD][PgHead], [DELETED][PgDeleted], and [META][PgMeta]. - * @see [PgAdminMap.createPgCollection] - */ - @JvmField - @JsStatic - val id = def(PgIndex::class, "idi") { self -> - self.name = "id" - self.internal = true - self.columns = listOf(c_id, c_fn, c_version) - self.naturalOrder = listOf(DESCENDING, DESCENDING, DESCENDING) - self.includes = listOf(c_next_version) - self.createFn = Fx2 { conn, table -> - conn.execute( - self.sql( - """btree ($c_id text_pattern_ops DESC, $c_fn DESC, $c_version DESC)""", - table, unique = false, addFillFactor = true, where = "$c_id IS NOT NULL", - includes = listOf(c_next_version) - ) - ).close() - } - } - - /** - * A non-unique index above the [version][PgColumn.version]. - * - * - Always added to all tables. - * @see [PgAdminMap.createPgCollection] - */ - @JvmField - @JsStatic - val version = def(PgIndex::class, "ver") { self -> - self.name = "version" - self.internal = true - self.columns = listOf(c_version) - self.naturalOrder = listOf(DESCENDING) - self.includes = listOf(c_fn, c_id, c_next_version) - self.createFn = Fx2 { conn, table -> - conn.execute( - self.sql( - """btree ($c_version DESC)""", - table, unique = false, addFillFactor = true, where = null, - includes = listOf(c_fn, c_id, c_next_version) - ) - ).close() - } - } - - /** - * A unique index above the transaction-number _(aka [version][PgColumn.version])_, including [id][PgColumn.id], [fn][PgColumn.fn] and [next_version][PgColumn.next_version] column. - * - * - Automatically added to all [TRANSACTIONS][PgTransactions] tables. - * - Must not be added to any other table. - * @see [PgAdminMap.createPgCollection] - */ - @JvmField - @JsStatic - val txn_unique = def(PgIndex::class, "txn") { self -> - self.name = "txn_unique" - self.internal = true - self.columns = listOf() - self.naturalOrder = listOf(DESCENDING) - self.includes = listOf(c_id, c_fn, c_version, c_next_version) - self.createFn = Fx2 { conn, table -> - conn.execute( - self.sql( - """btree (($c_version & -4) DESC)""", - table, unique = true, addFillFactor = true, where = null, - includes = listOf(c_fn, c_version, c_id, c_next_version) - ) - ).close() - } - } - - /** - * Index above [here_tile][PgColumn.here_tile], [fn][PgColumn.fn] and [version][PgColumn.version], including [id][PgColumn.id] and [next_version][PgColumn.next_version]. - * - * Ordered by: - * - `here_tile` DESC - * - `fn` DESC - * - `version` DESC - * @see [PgAdminMap.createPgCollection] - */ - @JvmField - @JsStatic - val here_tile = def(PgIndex::class, "hti") { self -> - self.name = "here_tile" - self.columns = listOf(c_here_tile, c_fn, c_version) - self.naturalOrder = listOf(DESCENDING, DESCENDING, DESCENDING) - self.includes = listOf(c_id, c_next_version) - self.createFn = Fx2 { conn, table -> - conn.execute( - self.sql( - "btree ($c_here_tile DESC, $c_fn DESC, $c_version DESC)", - table, unique = false, addFillFactor = true, where = "$c_here_tile IS NOT NULL", - includes = listOf(c_id, c_next_version) - ) - ).close() - } - } - - /** - * Index above [app_id][PgColumn.app_id], [updated_at][PgColumn.updated_at], [fn][PgColumn.fn] and [version][PgColumn.version], including [id][PgColumn.id] and [next_version][PgColumn.next_version]. - * - * Ordered by - * - `app_id` DESC - * - `updated_at` DESC - * - `fn` DESC - * - `version` DESC - * @see [PgAdminMap.createPgCollection] - */ - @JvmField - @JsStatic - val app_id = def(PgIndex::class, "aid") { self -> - self.name = "app_id" - self.columns = listOf(c_app_id, c_updated_at, c_fn, c_version) - self.naturalOrder = listOf(DESCENDING, DESCENDING, DESCENDING, DESCENDING) - self.includes = listOf(c_id, c_next_version) - self.createFn = Fx2 { conn, table -> - conn.execute( - self.sql( - """btree ($c_app_id text_pattern_ops DESC, $c_updated_at DESC, $c_fn DESC, $c_version DESC)""", - table, unique = false, addFillFactor = true, where = "$c_app_id IS NOT NULL", - includes = listOf(c_id, c_next_version) - ) - ).close() - } - } - - /** - * Index above the `naksha_author(`[author][PgColumn.author], [app_id][PgColumn.app_id]`)`, `naksha_author_ts(`[author_ts][PgColumn.author_ts], [updated_at][PgColumn.updated_at]`)`, [fn][PgColumn.fn] and [version][PgColumn.version], including [id][PgColumn.id] and [next_version][PgColumn.next_version]. - * - * Ordered by: - * - `naksha_author(author, app_id)` DESC - * - `naksha_author_ts(author_ts, updated_at)` DESC - * - `fn` DESC - * - `version` DESC - * @see [PgAdminMap.createPgCollection] - */ - @JvmField - @JsStatic - val author = def(PgIndex::class, "ath") { self -> - self.name = "author" - self.columns = listOf(c_author, c_author_ts, c_fn, c_version) - self.naturalOrder = listOf(DESCENDING, DESCENDING, DESCENDING, DESCENDING) - self.includes = listOf(c_id, c_next_version) - self.createFn = Fx2 { conn, table -> - conn.execute( - self.sql( - """btree (naksha_author($c_author, $c_app_id) text_pattern_ops DESC, naksha_author_ts($c_author_ts, $c_updated_at) DESC, $c_fn DESC, $c_version DESC)""", - table, unique = false, addFillFactor = true, where = "naksha_author($c_author, $c_app_id) IS NOT NULL", - includes = listOf(c_id, c_next_version) - ) - ).close() - } - } - - /** - * A [GIN](https://www.postgresql.org/docs/current/gin.html) index above the raw `jsonb` [tags][PgColumn.tags] column, plus [fn][PgColumn.fn], [version][PgColumn.version], and [next_version][PgColumn.next_version]. - * - * @see [PgAdminMap.createPgCollection] - */ - @JvmField - @JsStatic - val tags = def(PgIndex::class, "tag") { self -> - self.name = "tags" - self.columns = listOf(c_tags, c_fn, c_version, c_next_version) - self.createFn = Fx2 { conn, table -> - val nv = if (PgTable.isAnyHead(table.name) || PgTable.isMeta(table.name)) "" else ", $c_next_version" - conn.execute( - self.sql( - """gin ($c_tags, $c_fn, $c_version$nv)""", - table, unique = false, addFillFactor = false, where = "$c_tags IS NOT NULL" - ) - ).close() - } - } - - /** - * A two-dimensional [SP-GIST](https://www.postgresql.org/docs/current/spgist.html) index above `naksha_ref_point(`[PgColumn.ref_point]`)`. - * @see [PgAdminMap.createPgCollection] - */ - @JvmField - @JsStatic - val ref_point = def(PgIndex::class, "ref") { self -> - self.name = "ref_point" - self.columns = listOf(c_ref_point) - self.createFn = Fx2 { conn, table -> - conn.execute( - self.sql( - """spgist (naksha_ref_point($c_ref_point))""", - table, unique = false, addFillFactor = true, where = "naksha_ref_point($c_ref_point) IS NOT NULL" - ) - ).close() - } - } - - /** - * A two-dimensional [GIST](https://www.postgresql.org/docs/current/gist.html) index above `naksha_2d(`[PgColumn.geo]`)`. - * @see [PgAdminMap.createPgCollection] - */ - @JvmField - @JsStatic - val gist_geo = def(PgIndex::class, "g2d") { self -> - self.name = "gist_geo" - self.columns = listOf(c_geo, c_fn, c_version, c_next_version) - self.createFn = Fx2 { conn, table -> - val nv = if (PgTable.isAnyHead(table.name) || PgTable.isMeta(table.name)) "" else ", $c_next_version" - conn.execute( - self.sql( - """gist (naksha_2d($c_geo), $c_fn, $c_version$nv)""", - table, unique = false, addFillFactor = true, where = "naksha_2d($c_geo) IS NOT NULL" - ) - ).close() - } - } - - /** - * A two-dimensional [GIST](https://www.postgresql.org/docs/current/gist.html) index above `naksha_2d(`[PgColumn.geo]`)`. - * @see [PgAdminMap.createPgCollection] - */ - @JvmField - @JsStatic - val spgist_geo = def(PgIndex::class, "s2d") { self -> - self.name = "spgist_geo" - self.columns = listOf(c_geo) - self.createFn = Fx2 { conn, table -> - conn.execute( - self.sql( - """spgist (naksha_2d($c_geo))""", - table, unique = false, addFillFactor = true, where = "naksha_2d($c_geo) IS NOT NULL" - ) - ).close() - } - } - - /** - * Index above [feature-type][PgColumn.ft], [fn][PgColumn.fn] and [version][PgColumn.version], including [id][PgColumn.id] and [next_version][PgColumn.next_version]. - * @see [PgAdminMap.createPgCollection] - */ - @JvmField - @JsStatic - val ft = def(PgIndex::class, "ft") { self -> - self.name = "feature_type" - self.columns = listOf(c_ft, c_fn, c_version) - self.naturalOrder = listOf(DESCENDING, DESCENDING, DESCENDING) - self.includes = listOf(c_id, c_next_version) - self.createFn = Fx2 { conn, table -> - conn.execute( - self.sql( - "btree ($c_ft text_pattern_ops DESC, $c_fn DESC, $c_version DESC)", - table, unique = false, addFillFactor = true, where = "$c_ft IS NOT NULL", - includes = listOf(c_id, c_next_version) - ) - ).close() - } - }.alias("feature_type").alias("featureType") - - /** - * Index above [cv0][PgColumn.cv0], [fn][PgColumn.fn] and [version][PgColumn.version], including [id][PgColumn.id] and [next_version][PgColumn.next_version]. - * @see [PgAdminMap.createPgCollection] - */ - @JvmField - @JsStatic - val cv0 = def(PgIndex::class, "cv0") { self -> - self.name = "cv0" - self.columns = listOf(c_cv0, c_fn, c_version) - self.naturalOrder = listOf(DESCENDING, DESCENDING, DESCENDING) - self.includes = listOf(c_id, c_next_version) - self.createFn = Fx2 { conn, table -> - conn.execute( - self.sql( - "btree ($c_cv0 DESC, $c_fn DESC, $c_version DESC)", - table, unique = false, addFillFactor = true, where = "$c_cv0 IS NOT NULL", - includes = listOf(c_id, c_next_version) - ) - ).close() - } - } - - /** - * Index above [cv1][PgColumn.cv1], [fn][PgColumn.fn] and [version][PgColumn.version], including [id][PgColumn.id] and [next_version][PgColumn.next_version]. - * @see [PgAdminMap.createPgCollection] - */ - @JvmField - @JsStatic - val cv1 = def(PgIndex::class, "cv1") { self -> - self.name = "cv1" - self.columns = listOf(c_cv1, c_fn, c_version) - self.naturalOrder = listOf(DESCENDING, DESCENDING, DESCENDING) - self.includes = listOf(c_id, c_next_version) - self.createFn = Fx2 { conn, table -> - conn.execute( - self.sql( - "btree ($c_cv1 DESC, $c_fn DESC, $c_version DESC)", - table, unique = false, addFillFactor = true, where = "$c_cv1 IS NOT NULL", - includes = listOf(c_id, c_next_version) - ) - ).close() - } - } - - /** - * Index above [cv2][PgColumn.cv2], [fn][PgColumn.fn] and [version][PgColumn.version], including [id][PgColumn.id] and [next_version][PgColumn.next_version]. - * @see [PgAdminMap.createPgCollection] - */ - @JvmField - @JsStatic - val cv2 = def(PgIndex::class, "cv2") { self -> - self.name = "cv2" - self.columns = listOf(c_cv2, c_fn, c_version) - self.naturalOrder = listOf(DESCENDING, DESCENDING, DESCENDING) - self.includes = listOf(c_id, c_next_version) - self.createFn = Fx2 { conn, table -> - conn.execute( - self.sql( - "btree ($c_cv2 DESC, $c_fn DESC, $c_version DESC)", - table, unique = false, addFillFactor = true, where = "$c_cv2 IS NOT NULL", - includes = listOf(c_id, c_next_version) - ) - ).close() - } - } - - /** - * Index above [cv3][PgColumn.cv3], [fn][PgColumn.fn] and [version][PgColumn.version], including [id][PgColumn.id] and [next_version][PgColumn.next_version]. - * @see [PgAdminMap.createPgCollection] - */ - @JvmField - @JsStatic - val cv3 = def(PgIndex::class, "cv3") { self -> - self.name = "cv3" - self.columns = listOf(c_cv3, c_fn, c_version) - self.naturalOrder = listOf(DESCENDING, DESCENDING, DESCENDING) - self.includes = listOf(c_id, c_next_version) - self.createFn = Fx2 { conn, table -> - conn.execute( - self.sql( - "btree ($c_cv3 DESC, $c_fn DESC, $c_version DESC)", - table, unique = false, addFillFactor = true, where = "$c_cv3 IS NOT NULL", - includes = listOf(c_id, c_next_version) - ) - ).close() - } - } - - /** - * Index above [cs0][PgColumn.cs0], [fn][PgColumn.fn] and [version][PgColumn.version], including [id][PgColumn.id] and [next_version][PgColumn.next_version]. - * @see [PgAdminMap.createPgCollection] - */ - @JvmField - @JsStatic - val cs0 = def(PgIndex::class, "cs0") { self -> - self.name = "cs0" - self.columns = listOf(c_cs0, c_fn, c_version) - self.naturalOrder = listOf(DESCENDING, DESCENDING, DESCENDING) - self.includes = listOf(c_id, c_next_version) - self.createFn = Fx2 { conn, table -> - conn.execute( - self.sql( - "btree ($c_cs0 DESC, $c_fn DESC, $c_version DESC)", - table, unique = false, addFillFactor = true, where = "$c_cs0 IS NOT NULL", - includes = listOf(c_id, c_next_version) - ) - ).close() - } - } - - /** - * Index above [cs1][PgColumn.cs1], [fn][PgColumn.fn] and [version][PgColumn.version], including [id][PgColumn.id] and [next_version][PgColumn.next_version]. - * @see [PgAdminMap.createPgCollection] - */ - @JvmField - @JsStatic - val cs1 = def(PgIndex::class, "cs1") { self -> - self.name = "cs1" - self.columns = listOf(c_cs1, c_fn, c_version) - self.naturalOrder = listOf(DESCENDING, DESCENDING, DESCENDING) - self.includes = listOf(c_id, c_next_version) - self.createFn = Fx2 { conn, table -> - conn.execute( - self.sql( - "btree ($c_cs1 DESC, $c_fn DESC, $c_version DESC)", - table, unique = false, addFillFactor = true, where = "$c_cs1 IS NOT NULL", - includes = listOf(c_id, c_next_version) - ) - ).close() - } - } - - /** - * Index above [cs2][PgColumn.cs2], [fn][PgColumn.fn] and [version][PgColumn.version], including [id][PgColumn.id] and [next_version][PgColumn.next_version]. - * @see [PgAdminMap.createPgCollection] - */ - @JvmField - @JsStatic - val cs2 = def(PgIndex::class, "cs2") { self -> - self.name = "cs2" - self.columns = listOf(c_cs2, c_fn, c_version) - self.naturalOrder = listOf(DESCENDING, DESCENDING, DESCENDING) - self.includes = listOf(c_id, c_next_version) - self.createFn = Fx2 { conn, table -> - conn.execute( - self.sql( - "btree ($c_cs2 DESC, $c_fn DESC, $c_version DESC)", - table, unique = false, addFillFactor = true, where = "$c_cs2 IS NOT NULL", - includes = listOf(c_id, c_next_version) - ) - ).close() - } - } - - /** - * Index above [cs3][PgColumn.cs3], [fn][PgColumn.fn] and [version][PgColumn.version], including [id][PgColumn.id] and [next_version][PgColumn.next_version]. - * @see [PgAdminMap.createPgCollection] - */ - @JvmField - @JsStatic - val cs3 = def(PgIndex::class, "cs3") { self -> - self.name = "cs3" - self.columns = listOf(c_cs3, c_fn, c_version) - self.naturalOrder = listOf(DESCENDING, DESCENDING, DESCENDING) - self.includes = listOf(c_id, c_next_version) - self.createFn = Fx2 { conn, table -> - conn.execute( - self.sql( - "btree ($c_cs3 DESC, $c_fn DESC, $c_version DESC)", - table, unique = false, addFillFactor = true, where = "$c_cs3 IS NOT NULL", - includes = listOf(c_id, c_next_version) - ) - ).close() - } - } - - /** - * Conditional non-unique index above the [gbn][PgColumn.gbn], including [fn][PgColumn.fn] and [version][PgColumn.version]. - * - * Conditional: `WHERE gbn IS NOT NULL` — rows that do not reference a global book are excluded. - * - * - Automatically added to all tables as a mandatory index. - * @see [PgAdminMap.createPgCollection] - */ - @JvmField - @JsStatic - val gbn_idx = def(PgIndex::class, "gbn") { self -> - self.name = "gbn" - self.internal = true - self.columns = listOf(c_gbn) - self.naturalOrder = listOf(DESCENDING) - self.includes = listOf(c_fn, c_version) - self.createFn = Fx2 { conn, table -> - conn.execute( - self.sql( - "btree ($c_gbn DESC)", - table, unique = false, addFillFactor = true, where = "$c_gbn IS NOT NULL", - includes = listOf(c_fn, c_version) - ) - ).close() - } - } - - /** - * Truncates the identifier to the minimal size that is guaranteed. - * @param id the index identifier. - * @return the identifier truncated to the minimal guaranteed length. - */ - @JvmStatic - @JsStatic - fun truncate(id: String): String = if (id.length > 3) id.substring(0, 3) else id - - /** - * Find the index by name or _relname_. - * @param name the official [name] or the relation name (`relname`), as returned by the database from the `pg_class` table (with `relkind` being `i`). - * @return the index, if it exists. - */ - @JvmStatic - @JsStatic - fun of(name: String): PgIndex? { - val existing = getDefined(name, PgIndex::class) ?: indexByName[name] - if (existing != null) return existing - // Try new HEAD-index naming: {tableName}$head${indexText} - val headSepStart = name.lastIndexOf(PG_HEAD_IDX) - val start: Int - val sepLen: Int - if (headSepStart >= 0) { - start = headSepStart - sepLen = PG_HEAD_IDX.length - } else { - // Fall back to legacy $i_ prefix (custom indices and TXN-era names) - val legacyStart = name.lastIndexOf(PG_IDX) - if (legacyStart >= 0) { - start = legacyStart - sepLen = PG_IDX.length - } else { - // History/partition leaf naming: last $ segment - val lastDollar = name.lastIndexOf(PG_S) - if (lastDollar < 0) return null - start = lastDollar - sepLen = PG_S.length - } - } - // This is a hack for PostgresQL, which will truncate identifiers to 63 byte. - // Therefore, we know that name is limited to 63 characters, which may have truncated the index identifier. - // So we extract what is left from the index identifier and the compare it against all enumeration values. - // Note: It could only have truncated the last byte or many more, dependent on how long the collection id is! - val pg_truncated_id = name.substring(start + sepLen) - for (e in iterate(PgIndex::class)) if (e.text.startsWith(pg_truncated_id)) return e - return null - } - - /** - * Indices by official name. - * @since 3.0.0 - */ - private val indexByName = AtomicMap() - init { - // Sanity check. - val byName = indexByName - val map = HashMap() - for (index in iterate(PgIndex::class)) { - val pg_truncated_id = truncate(index.text) - if (pg_truncated_id in map) { - val c = map[pg_truncated_id]!! - throw Error("Conflict, the index ${index.text} has the same short name as ${c.text}: $pg_truncated_id") - } - map[pg_truncated_id] = index - - val e = byName.putIfAbsent(index.name, index) - if (e != null) throw Error("Conflict, the index ${index.text} has the same official name as ${e.text}: ${e.name}") - } - } - - /** - * The list of default index names to be are added, when _null_ is provided as index list to create. - */ - @JvmField - @JsStatic - var DEFAULT_INDICES = listOf( - id, - here_tile, - app_id, - author, - tags, - ft, - cv0, cv1, cv2, cv3, - cs0, cs1, cs2, cs3, - ref_point, - gist_geo, - ) - - /** - * Converts a [PgIndex] to an [Index] model object, preserving [PgIndex.internal]. - */ - internal fun pgIndexToIndex(pgIdx: PgIndex): Index { - val idx = Index() - idx.name = pgIdx.name - idx.internal = pgIdx.internal - // Determine index type from the PgIndex columns (heuristic: geo columns → SPATIAL, no columns or btree → BTREE) - idx.type = when { - pgIdx === gist_geo || pgIdx === spgist_geo || pgIdx === ref_point -> IndexType.SPATIAL - // The built-in tags index is a GIN index over the `tags` member, which is a SET - // (JSON array of unique strings) by default — matches XyzIndices.XyzTags. - pgIdx === tags -> IndexType.SET - else -> IndexType.BTREE - } - val cols = naksha.base.StringList() - for (c in pgIdx.columns) cols.add(c.name) - idx.on = cols - return idx - } - - /** - * The mandatory [Index]es that the storage always injects into every collection. - * These are storage-managed (`internal = true`) and must not be declared by clients. - * - * - `fn_pkey`: PRIMARY KEY on `fn` - * - `id_unique`: UNIQUE on `id` (HEAD/DELETED/META tables only) - * - `id`: non-unique index on `id` (HISTORY tables only) - * - `version`: index on `version` - * - `gbn`: conditional index on `gbn` WHERE `gbn IS NOT NULL` - * @since 3.0 - */ - @JsStatic - val mandatoryIndices: List by lazy { - listOf(fn_pkey, id_unique, id, version, gbn_idx).map { pgIndexToIndex(it) } - } - - /** - * The default [Index]es injected when the client does **not** provide an [indices][naksha.model.objects.NakshaCollection.indices] - * list (backward-compatible full schema). These correspond to all non-internal entries in [DEFAULT_INDICES]. - * @since 3.0 - */ - @JsStatic - val defaultIndices: List by lazy { - DEFAULT_INDICES.filter { !it.internal }.map { pgIndexToIndex(it) } - } - } - +data class PgIndex( /** - * If the index is internal, that means it is not intentionally manageable from clients. + * The collection unique name of the index. * @since 3.0 */ - var internal: Boolean = false - private set - - private var _name: String? = null + @JvmField + val name: String, /** - * The official index name to be used in [naksha.model.objects.NakshaCollection.indices]. - * @since 3.0.0 + * The index type. + * @since 3.0 */ - var name: String - get() = _name ?: text - protected set(value) { - _name = value - } + @JvmField + var type: IndexType, /** - * Returns the unique identifier of this index in the given table. - * - * - HEAD / META / DELETED root tables → `{tableName}$head${indexName}` - * - History / performance-partition leaf tables → `{tableName}${PG_S}${indexName}` - * (the leaf table name already carries the `$hst$` context) - * - * @param table the table for which to generate the unique index name. - * @return the unique identifier of this index in the given table. + * The columns to index. + * @since 3.0 */ - fun id(table: PgTable): String { - val sep = if (PgTable.isAnyHistory(table.name) || PgTable.isAnyDeleted(table.name)) - PG_S - else - PG_HEAD_IDX - val id = "${table.name}${sep}${text}" - return if (id.length > 63) id.substring(0, 63) else id - } + @JvmField + var on: Array, /** - * Returns the unique identifier of this index in the table with the given name. - * - * - HEAD / META / DELETED root tables → `{tableName}$head${indexName}` - * - History / performance-partition leaf tables → `{tableName}${PG_S}${indexName}` - * - * @param tableName the name of the table for which to generate the unique index name. - * @return the unique identifier of this index in the given table. + * The columns to include into the index to improve queries. + * @since 3.0 */ - @JsName("idByTableName") - fun id(tableName: String): String { - val sep = if (PgTable.isAnyHistory(tableName) || PgTable.isAnyDeleted(tableName)) - PG_S - else - PG_HEAD_IDX - val id = "${tableName}${sep}${text}" - return if (id.length > 63) id.substring(0, 63) else id + @JvmField + var includes: Array = emptyArray() +) { + + internal fun create(conn: PgConnection, tableName: String) { + val includeClause = if (includes.isEmpty()) "" else " INCLUDE (${includes.joinToString(", ")})" + val using = when (type) { + BTREE -> "btree" + SPATIAL -> "gist" + TAGS, SET -> "gin" + else -> throw NakshaException(INTERNAL_ERROR, "Invalid index type for index $name on table $tableName") + } + val indexName = quoteIdent(tableName, "\$i_", tableName) + val fillFactor = if (PgTable.isAnyHead(tableName)) "(fillfactor=50)" else "(fillfactor=100)" + val sql = """CREATE INDEX IF NOT EXISTS $indexName +ON ${quoteIdent(tableName)} +USING $using$includeClause +WITH $fillFactor""" + conn.execute(sql).close() } - /** - * The columns (in order) which are part of the index. - * - * This is only informational purpose, because the index can be much more complicated, for example it could be a partial index, and it does not contain columns only included in the index, see [includes]. - */ - var columns: List = emptyList() - protected set - - /** - * The natural sort order of the index, should hold one entry for each one in [columns]. - * - * **Note**: If the sort-order is empty, but [columns] is not, this means that the index does not support sorting, e.g. `GIN` or `GIST` indices. - */ - var naturalOrder: List = emptyList() - protected set - - /** - * The columns being included only in the index. - */ - var includes: List = emptyList() - protected set - - protected var createFn: Fx2? = null - internal fun create(conn: PgConnection, table: PgTable) { - val createFn = this.createFn - check(createFn != null) { "This index does not support `create` operation" } - return createFn.call(conn, table) + internal fun drop(conn: PgConnection, tableName: String) { + val indexName = quoteIdent(tableName, "\$i_", tableName) + conn.execute("DROP INDEX IF EXISTS $indexName CASCADE").close() } - protected var dropFn: Fx2? = null - internal fun drop(conn: PgConnection, table: PgTable) { - dropFn?.call(conn, table) ?: conn.execute("DROP INDEX IF EXISTS ${quoteIdent(id(table))} CASCADE").close() - } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false - @Suppress("NON_EXPORTABLE_TYPE") - override fun namespace(): KClass = PgIndex::class + other as PgIndex + if (name != other.name) return false + if (type != other.type) return false + if (!on.contentEquals(other.on)) return false + if (!includes.contentEquals(other.on)) return false + return true + } - override fun initClass() { - register(PgIndex::class) + override fun hashCode(): Int { + var result = name.hashCode() + result = 31 * result + type.hashCode() + result = 31 * result + on.contentHashCode() + result = 31 * result + includes.contentHashCode() + return result } -} \ No newline at end of file +} diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMapList.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMapList.kt index 363f172c5..f1048dc21 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMapList.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMapList.kt @@ -7,15 +7,15 @@ import kotlin.js.ExperimentalJsExport import kotlin.js.JsExport /** - * A list of [maps][PgMap]. + * A list of [maps][PgCatalog]. * @since 3.0.0 */ @JsExport -class PgMapList : ListProxy(PgMap::class) { +class PgMapList : ListProxy(PgCatalog::class) { /** * Add all given maps */ - fun withAll(maps: List): PgMapList { + fun withAll(maps: List): PgMapList { addAll(maps) return this } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMemberHelper.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMemberHelper.kt index 285b8ffd1..dd6f19908 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMemberHelper.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMemberHelper.kt @@ -18,7 +18,7 @@ import naksha.model.objects.MemberType import naksha.model.objects.NakshaFeature /** - * Helpers to map [CustomMember] values from a [NakshaFeature] into a [PgColumnRows] row. + * Helpers to map [CustomMember] values from a [NakshaFeature] into a [PgRows] row. * * - [walkFeature]: descend a [NakshaFeature] using the member's path; returns _null_ if the path is missing. * - [coerce]: coerce a raw value to the type of the member; returns _null_ and logs a warning on mismatch. @@ -29,15 +29,6 @@ import naksha.model.objects.NakshaFeature class PgMemberHelper private constructor() { companion object PgMemberHelper_C { - - /** - * The set of all reserved column names — any name that belongs to a built-in [PgColumn]. - * Custom members must not use any of these names; [validateMemberNames] enforces this. - */ - private val reservedColumnNames: Set by lazy { - PgColumn.allColumns.map { it.name }.toSet() - } - /** * Returns the physical Postgres column name for the given member name. * The name is used as-is; collision with built-in columns is prevented by [validateMemberNames]. diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMeta.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMeta.kt deleted file mode 100644 index 513656e64..000000000 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMeta.kt +++ /dev/null @@ -1,13 +0,0 @@ -@file:Suppress("OPT_IN_USAGE") - -package naksha.psql - -import kotlin.js.JsExport - -/** - * A META table. - * @since 3.0 - * @see [PgTable] - */ -@JsExport -class PgMeta(val head: PgHead) : PgTable(head.collection, "${head.collection.id}${PG_META}", head.storageClass, true) diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaBooks.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaBooks.kt index aa5b709d4..1087886aa 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaBooks.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaBooks.kt @@ -12,7 +12,7 @@ import kotlin.js.JsExport * The internal collection in the admin-map, that keeps track of the books (global JBON2 dictionaries) of the storage. */ @JsExport -class PgNakshaBooks internal constructor(adminMap: PgAdminMap) : PgCollection(adminMap, NakshaCollection() +class PgNakshaBooks internal constructor(adminMap: PgAdminCatalog) : PgCollection(adminMap, NakshaCollection() .withCatalogId(Naksha.ADMIN_CATALOG_ID) .withId(Naksha.BOOKS_COL_ID) ), PgInternalCollection, IDictManager { diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaCatalogs.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaCatalogs.kt index a54ee7ce4..c000290e9 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaCatalogs.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaCatalogs.kt @@ -11,7 +11,7 @@ import kotlin.js.JsExport * The internal collection in the admin-map, that keeps track of the catalogs (maps) of the storage. */ @JsExport -class PgNakshaCatalogs internal constructor(adminMap: PgAdminMap) : PgCollection(adminMap, NakshaCollection() +class PgNakshaCatalogs internal constructor(adminMap: PgAdminCatalog) : PgCollection(adminMap, NakshaCollection() .withCatalogId(Naksha.ADMIN_CATALOG_ID) .withId(Naksha.CATALOGS_COL_ID) ), PgInternalCollection diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaCollections.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaCollections.kt index a010446a9..747343aa0 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaCollections.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaCollections.kt @@ -10,7 +10,7 @@ import kotlin.js.JsExport * The internal collection in each map that keeps track of the collections being in the map. */ @JsExport -class PgNakshaCollections internal constructor(map: PgMap) : PgCollection(map, NakshaCollection() +class PgNakshaCollections internal constructor(map: PgCatalog) : PgCollection(map, NakshaCollection() .withCatalogId(map.id) .withId(Naksha.COLLECTIONS_COL_ID) ), PgInternalCollection diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaTransactions.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaTransactions.kt index 7c3073abf..a21f996ec 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaTransactions.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaTransactions.kt @@ -27,7 +27,7 @@ import kotlin.js.JsExport * HERE global sequencer populates them. */ @JsExport -class PgNakshaTransactions internal constructor(adminMap: PgAdminMap) : PgCollection(adminMap, NakshaCollection() +class PgNakshaTransactions internal constructor(adminMap: PgAdminCatalog) : PgCollection(adminMap, NakshaCollection() .withCatalogId(ADMIN_CATALOG_ID) .withId(TRANSACTIONS_COL_ID) .withStoreDeleted(StoreMode.OFF) diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryBuilder.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryBuilder.kt index 875e719d2..31c6449a4 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryBuilder.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryBuilder.kt @@ -2,9 +2,7 @@ package naksha.psql import naksha.model.* import naksha.model.request.* -import naksha.model.request.query.MetaColumn import naksha.model.request.query.SortOrder.SortOrderCompanion.ASCENDING -import naksha.psql.PgColumn.PgColumnCompanion.next_version import kotlin.math.max import kotlin.math.min @@ -129,13 +127,13 @@ class PgQueryBuilder(val session: PgSession, val readRequest: ReadRequest) { val selects = StringBuilder() for (entry in pgCollections.withIndex()) { val pgCollection = entry.value - val map = pgCollection.map + val map = pgCollection.catalog val read = PgRead(pgMap, pgCollection) // Note: To simplify queries, we actually always embed the collection-number internally, // eventually, before returning the result, we decide if we put it into the header // of the tuple-number-binary or individually into each row-identifier. - select_cols[0] = "${pgCollection.number} AS col_num" + select_cols[0] = "${pgCollection.collectionNumber} AS col_num" val select_cols_string = select_cols.joinToString(", ") val where = if (whereQuery.isEmpty()) "" else "WHERE $whereQuery" @@ -218,8 +216,8 @@ SELECT ${if (thePgCollection == null) "col_num, fn, version" else "fn, version"} argValues = whereClause?.argValues?.toTypedArray() ?: emptyArray(), argTypes = whereClause?.argTypeNames ?: emptyArray(), pgStorage.number, - pgMap.number, - thePgCollection?.number + pgMap.catalogNumber, + thePgCollection?.collectionNumber ) } } \ No newline at end of file diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt index 384ce8eea..75f92e1e6 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt @@ -76,7 +76,7 @@ internal class PgQueryWhereBuilder(private val request: ReadFeatures) { val versions = arrayOfNulls(tupleNumbers.size) for (i in tupleNumbers.indices) { fns[i] = tupleNumbers[i].featureNumber - versions[i] = tupleNumbers[i].version.value + versions[i] = tupleNumbers[i].version } val fnPlaceholder = placeholderForArg(fns, PgType.INT64_ARRAY) val versionPlaceholder = placeholderForArg(versions, PgType.INT64_ARRAY) @@ -260,24 +260,24 @@ internal class PgQueryWhereBuilder(private val request: ReadFeatures) { } } - private fun whereNestedMetadata(metaQuery: IMetaQuery) { + private fun whereNestedMetadata(metaQuery: IMemberQuery) { when (metaQuery) { - is MetaNot -> not( + is MemberNot -> not( subClause = metaQuery.query, subClauseResolver = this::whereNestedMetadata ) - is MetaAnd -> and( + is MemberAnd -> and( subClauses = metaQuery.filterNotNull(), subClauseResolver = this::whereNestedMetadata ) - is MetaOr -> or( + is MemberOr -> or( subClauses = metaQuery.filterNotNull(), subClauseResolver = this::whereNestedMetadata ) - is MetaQuery -> { + is MemberQuery -> { val isActionQuery = metaQuery.column == MetaColumn.action() val pgColumn = if (isActionQuery) { diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRead.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRead.kt index b4721e63a..6efe96ee8 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRead.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRead.kt @@ -12,7 +12,7 @@ internal data class PgRead( /** * The map to read from. */ - val map: PgMap, + val catalog: PgCatalog, /** * The collection to read from. @@ -52,7 +52,7 @@ internal data class PgRead( * * Actually, allows grouping so that the same map/collection is merged, and that all reads that are from all partitions of a collection are grouped together, and all that read from the same partition. Therefore, eventually the first read from a group with the same group-id, will always read from the same map, collection, partition(s). */ - val groupId: String = if (!readPartition) "${map.id}:${collection.id}" else "${map.id}:${collection.id}:${partition}" + val groupId: String = if (!readPartition) "${catalog.id}:${collection.id}" else "${catalog.id}:${collection.id}:${partition}" ) { companion object PgRead_C { private val UNDEFINED = ArrayList(0) @@ -68,7 +68,7 @@ internal data class PgRead( * @param map the map to read. * @param collection the collection to read. */ - constructor(map: PgMap, collection: PgCollection): this(map, collection, null, null) + constructor(map: PgCatalog, collection: PgCollection): this(map, collection, null, null) /** * Read a tuple by tuple-number. @@ -76,10 +76,10 @@ internal data class PgRead( * @param adminMap the admin-map. * @param tupleNumber the tuple-number of the tuple to read. */ - constructor(conn: PgConnection, adminMap: PgAdminMap, tupleNumber: TupleNumber) : this( - adminMap.getPgMapByNumber(conn, tupleNumber.catalogNumber) + constructor(conn: PgConnection, adminMap: PgAdminCatalog, tupleNumber: TupleNumber) : this( + adminMap.getPgCatalogByNumber(conn, tupleNumber.catalogNumber) ?: throw mapNotFound("The map for map-number ${tupleNumber.catalogNumber} not found"), - adminMap.getPgMapByNumber(conn, tupleNumber.catalogNumber)?.getPgCollectionByNumber(conn, tupleNumber.collectionNumber) + adminMap.getPgCatalogByNumber(conn, tupleNumber.catalogNumber)?.getPgCollectionByNumber(conn, tupleNumber.collectionNumber) ?: throw collectionNotFound("The collection for collection-number ${tupleNumber.collectionNumber} not found"), tupleNumber, null @@ -91,7 +91,7 @@ internal data class PgRead( * @param collection the collection to read. * @param id the feature-id of the tuple to read. */ - constructor(map: PgMap, collection: PgCollection, id: String) : this(map, collection, null, id) + constructor(map: PgCatalog, collection: PgCollection, id: String) : this(map, collection, null, id) private fun initHeadTables(): List { val headTable = collection.headTable @@ -122,11 +122,6 @@ internal data class PgRead( return tables } - /** - * The meta-table to read. - */ - val metaTable: PgTable? = collection.metaTable - fun initHistoryTables(): List? { val history = collection.historyTable ?: return null // TODO: hack to be be fixed as part of CASL-1095 @@ -137,8 +132,8 @@ internal data class PgRead( if(history.years.isEmpty()){ // see: PgMap.createPgCollection val year = Epoch().year - history.addYear(year) - history.addYear(year + 1) + history.addPartition(year) + history.addPartition(year + 1) } val tables = ArrayList(history.years.size) for (entry in history.years) { diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgReader.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgReader.kt index 86173d5ad..e28e185c0 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgReader.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgReader.kt @@ -38,7 +38,7 @@ class PgReader( val session = this.session val query = PgQueryBuilder(session, request).build() val conn = session.useConnection() - session.storage.adminMap.setSearchPath(conn) + session.storage.adminCatalog.setSearchPath(conn) if (PlatformUtil.ENABLE_INFO) { if (session.logQueries) { session.logAtInfo(query.sql) @@ -63,7 +63,7 @@ class PgReader( val col_num: Int = collectionNumber ?: cursor["col_num"] val fn: naksha.base.Int64 = cursor["fn"] val version: naksha.base.Int64 = cursor["version"] - featureTuples.add(FeatureTuple(TupleNumber(storageNumber, mapNumber, col_num, fn, Version(version)))) + featureTuples.add(FeatureTuple(TupleNumber(storageNumber, mapNumber, col_num, fn, version))) } } return SuccessResponse().withFeatureTupleList(featureTuples) diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRelation.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRelation.kt index 476845bee..37c9de61c 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRelation.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRelation.kt @@ -72,9 +72,9 @@ data class PgRelation( var n = _partNumber if (n != null) return n n = -1 - var i = name.indexOf(PG_PART) + var i = name.indexOf(PG_DIST_PARTITION) if (i > 0) { - i += PG_PART.length + i += PG_DIST_PARTITION.length if (i + 3 <= name.length) try { n = name.substring(i, i + 3).toInt(10) } catch (_: Exception) {} @@ -127,13 +127,13 @@ data class PgRelation( } fun isAnyHeadRelation() = name.indexOf(PG_DEL) < 0 && name.indexOf(PG_HST) < 0 && name.indexOf(PG_META) < 0 - fun isHeadRootRelation() = isAnyHeadRelation() && (isTable() || isPartition()) && name.indexOf(PG_PART) < 0 + fun isHeadRootRelation() = isAnyHeadRelation() && (isTable() || isPartition()) && name.indexOf(PG_DIST_PARTITION) < 0 fun isTxnYearRelation() = isAnyHeadRelation() && isTable() && name.indexOf(PG_YEAR) > 0 // --- fun isAnyDeleteRelation() = name.indexOf(PG_DEL) > 0 - fun isDeleteRootRelation() = isAnyDeleteRelation() && (isTable() || isPartition()) && name.indexOf(PG_PART) < 0 + fun isDeleteRootRelation() = isAnyDeleteRelation() && (isTable() || isPartition()) && name.indexOf(PG_DIST_PARTITION) < 0 // --- @@ -147,12 +147,12 @@ data class PgRelation( /** History year-partition: `$hst$` at end, no `$p` suffix. */ fun isHistoryYearRelation(): Boolean { if (!isAnyHistoryRelation() || (!isTable() && !isPartition())) return false - return year() > 0 && name.indexOf(PG_PART, name.indexOf(PG_HST)) < 0 + return year() > 0 && name.indexOf(PG_DIST_PARTITION, name.indexOf(PG_HST)) < 0 } /** History perf-partition: `$hst$$p`. */ fun isHistoryPartition(): Boolean { if (!isAnyHistoryRelation() || !isTable()) return false - return year() > 0 && name.indexOf(PG_PART, name.indexOf(PG_HST)) >= 0 + return year() > 0 && name.indexOf(PG_DIST_PARTITION, name.indexOf(PG_HST)) >= 0 } // --- diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumnRows.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRows.kt similarity index 82% rename from here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumnRows.kt rename to here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRows.kt index 691d93748..5927618db 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumnRows.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRows.kt @@ -15,18 +15,18 @@ import naksha.model.objects.StandardMembers * Helper class to convert rows into arrays of column-data and vice versa. The main purpose is to read and write full tuples, but it supports basically as well virtual columns. * @since 3.0 */ -internal class PgColumnRows { +internal class PgRows { /** * All columns being added already. * @since 3.0 */ - val columns = mutableListOf() - internal val columnByName = mutableMapOf() + val columns = mutableListOf() + internal val columnByName = mutableMapOf() private var names: String? = null private var namesAggregate: String? = null private var placeholders: String? = null private var arrayTypeNames: Array? = null - private fun clearCache(): PgColumnRows { + private fun clearCache(): PgRows { names = null namesAggregate = null placeholders = null @@ -48,30 +48,55 @@ internal class PgColumnRows { } } - fun withMinSize(size: Int): PgColumnRows { + fun withMinSize(size: Int): PgRows { if (this.size < size) this.size = size return this } /** - * When set, clear the [columns] and add all columns of the given [NakshaCollection]. + * If set to true, then the [StandardMembers.NextVersion] is not added when setting the [collection] or calling [withCollection]. * @since 3.0 */ - var collection: NakshaCollection? = null + var isHead: Boolean = false + + /** + * Disables the [StandardMembers.NextVersion], which does not exist in the _HEAD_ table. + * @param useHead if the _HEAD_ table is used; defaults to _true_. + * @since 3.0 + */ + fun useHeadTable(useHead: Boolean = true): PgRows { + isHead = useHead + return this + } + + /** + * When set, clear the [columns] and add all columns of the given [PgCollection]. + * @since 3.0 + */ + var collection: PgCollection? = null set(collection) { if (collection != null) { clearCache() - columns.clear() - databaseNumber = collection.databaseNumber - catalogNumber = collection.catalogNumber collectionNumber = collection.collectionNumber + catalogNumber = collection.catalog.catalogNumber + databaseNumber = collection.catalog.storage.number + val members = collection.useMembers() - if (!members.isSortedByIndex()) throw NakshaException(ILLEGAL_ARGUMENT, "The given collection is not sorted by index") - for (i in 0 until members.size) { + if (!members.isSortedByIndex()) throw NakshaException(ILLEGAL_ARGUMENT, "The members of the given collection are not sorted by index") + // We add the internal `~fn` (feature-number) and `~version` first. + columns.clear() + var index = 0 + columns.add(PgColumnWithValues(index++, "~fn", PgType.INT64)) + columns.add(PgColumnWithValues(index++, "~version", PgType.INT64)) + for (i in 0 ..< members.size) { val member = members[i] ?: throw NakshaException(INTERNAL_ERROR, "The member at index $i is null; this must not happen") - val index = member.index ?: throw NakshaException(INTERNAL_ERROR, "The member at index $i has no index; this must not happen") - if (index != i) throw NakshaException(INTERNAL_ERROR, "The member at index $i has an member-index $index; this must not happen, expected $i") - columns.add(PgColumnEntry(i, member.name, PgType.ofMemberType(member))) + if (i != member.index) throw NakshaException(INTERNAL_ERROR, "The member at index $i has an member-index $index; this must not happen, expected $i") + // We store the tuple-number in the "~fn" and "~version" columns! + if (StandardMembers.Tn.name == member.name) continue + // In the HEAD table there is no next-version! + if (isHead && StandardMembers.NextVersion.name == member.name) continue + // Everything else as declared. + columns.add(PgColumnWithValues(index++, member.name, PgType.ofMemberType(member))) } } field = collection @@ -85,7 +110,7 @@ internal class PgColumnRows { * @return this * @since 3.0 */ - fun withCollection(collection: NakshaCollection): PgColumnRows { + fun withCollection(collection: NakshaCollection): PgRows { this.collection = collection return this } @@ -103,7 +128,7 @@ internal class PgColumnRows { /** * @see [databaseNumber] */ - fun withDatabaseNumber(value: Int64): PgColumnRows { + fun withDatabaseNumber(value: Int64): PgRows { databaseNumber = value return this } @@ -121,7 +146,7 @@ internal class PgColumnRows { /** * @see [catalogNumber] */ - fun withCatalogNumber(value: Int): PgColumnRows { + fun withCatalogNumber(value: Int): PgRows { catalogNumber = value return this } @@ -139,24 +164,24 @@ internal class PgColumnRows { /** * @see [collectionNumber] */ - fun withCollectionNumber(value: Int): PgColumnRows { + fun withCollectionNumber(value: Int): PgRows { collectionNumber = value return this } - fun addColumn(name: String, type: PgType): PgColumnRows { + fun addColumn(name: String, type: PgType): PgRows { clearCache() val existing = columnByName[name] if (existing == null) { - val column = PgColumnEntry(columns.size, name, type).withSize(size) + val column = PgColumnWithValues(columns.size, name, type).withSize(size) columns.add(column) columnByName[column.name] = column } return this } - fun getColumn(name: String): PgColumnEntry? = columnByName[name] - fun getColumn(index: Int): PgColumnEntry? = if (index in 0 until columns.size) columns[index] else null + fun getColumn(name: String): PgColumnWithValues? = columnByName[name] + fun getColumn(index: Int): PgColumnWithValues? = if (index in 0 until columns.size) columns[index] else null fun hasColumn(name: String): Boolean = getColumn(name) != null fun hasColumn(index: Int): Boolean = getColumn(index) != null @@ -220,7 +245,7 @@ internal class PgColumnRows { for (i in 0 until members.size) { val member: Member = members[i] ?: throw NakshaException(ILLEGAL_STATE, "Member #$i of collection ${collection.id} is null") val name = member.name - val column: PgColumnEntry = getColumn(name) ?: throw NakshaException(ILLEGAL_STATE, "Missing member '$name' at index $i of collection ${collection.id}") + val column: PgColumnWithValues = getColumn(name) ?: throw NakshaException(ILLEGAL_STATE, "Missing member '$name' at index $i of collection ${collection.id}") val value = column.values[row] if (StandardMembers.Feature.name == name) { // Special case, root feature. @@ -246,20 +271,6 @@ internal class PgColumnRows { return false } - /** - * Adds one [PgColumnEntry] per declared [naksha.model.objects.Member]. - * - * Idempotent — built-in names are never re-added by addColumns(allColumns), and members are checked individually. - */ - fun addCustomMembers(members: naksha.model.objects.MemberList?): PgColumnRows { - if (members == null) return this - for (m in members) { - if (m == null) continue - addColumn(PgMemberHelper.pgColumnName(m.name), PgMemberHelper.pgTypeFor(m.dataType)) - } - return this - } - operator fun set(row: Int, tuple: Tuple) { withMinSize(row) val membersBook = tuple.membersBook @@ -310,7 +321,7 @@ internal class PgColumnRows { * ``` * @since 3.0 */ - fun addAll(cursor: PgCursor): PgColumnRows { + fun addAll(cursor: PgCursor): PgRows { while (add(cursor)) cursor.next() return this } @@ -319,7 +330,7 @@ internal class PgColumnRows { * Read all rows from cursor, expects the cursor to be at first result and that for each column, there is an array of values, so an aggregate generated via `ARRAY_AGG`. * @since 3.0 */ - fun addAggregated(cursor: PgCursor): PgColumnRows { + fun addAggregated(cursor: PgCursor): PgRows { if (cursor.isRow()) { for (column in columns) { if (cursor.contains(column.name)) { @@ -336,19 +347,6 @@ internal class PgColumnRows { return this } - /** - * Returns the names of all columns as comma separated string. - * @return the names of all columns as comma separated string. - * @since 3.0 - */ - fun names(): String { - val cached = this.names - if (cached != null) return cached - val names = columns.joinToString(", ") { PgUtil.quoteIdent(it.name) } - this.names = names - return names - } - /** * Returns the names of all columns as comma separated string, surrounded with aggregation instruction, _(like `ARRAY_AGG(id)`)_, usage: * @@ -368,13 +366,26 @@ internal class PgColumnRows { val cached = this.namesAggregate if (cached != null) return cached val names = columns.joinToString(", ") { - val q = PgUtil.quoteIdent(it.name) - "ARRAY_AGG($q) AS $q" - } + val q = PgUtil.quoteIdent(it.name) + "ARRAY_AGG($q) AS $q" + } this.namesAggregate = names return names } + /** + * Returns the names of all columns as comma separated string. + * @return the names of all columns as comma separated string. + * @since 3.0 + */ + fun names(): String { + val cached = this.names + if (cached != null) return cached + val names = columns.joinToString(", ") { PgUtil.quoteIdent(it.name) } + this.names = names + return names + } + /** * Returns the placeholders of all columns as comma separated string _($1, $2, ...)_, usage: * diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgSession.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgSession.kt index 20a1f9de6..423810f1a 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgSession.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgSession.kt @@ -233,8 +233,8 @@ open class PgSession( assertOpen() var tx: StorageTx? = this.tx if (tx == null) { - val txn = storage.newConnection(options, false, null).use { conn -> storage.adminMap.newTxn(conn) } - tx = StorageTx(storage, txn.version, options.appId, options.author, storage.adminMap, this) + val txn = storage.newConnection(options, false, null).use { conn -> storage.adminCatalog.newTxn(conn) } + tx = StorageTx(storage, txn.version, options.appId, options.author, storage.adminCatalog, this) this.tx = tx } return tx @@ -380,9 +380,9 @@ open class PgSession( (if (mayReadParallel) newReadConnection() else readConnection()).use { readConn -> val conn = readConn.conn val byCollection = mutableMapOf>() - val adminMap = storage.adminMap + val adminCatalog = storage.adminCatalog for (featureTuple in missing) { - val read = PgRead(conn, adminMap, featureTuple.tupleNumber) + val read = PgRead(conn, adminCatalog, featureTuple.tupleNumber) read.featureTuple = featureTuple var reads = byCollection[read.groupId] if (reads == null) { @@ -398,15 +398,6 @@ open class PgSession( } } - /** - * Returns the effective HEAD-table column list for the given collection. - * - * For backward-compatible collections ([NakshaCollection.members] is `null`) this is the full - * Delegates to [PgCollection.effectiveHeadColumns]. - */ - private fun effectiveHeadColumns(collection: PgCollection): List = - collection.effectiveHeadColumns - /** * Load [Tuple] from a specific collection, can be executed in parallel, when multiple collections are needed. We should make parallel reading optional, we experienced that when used for example in EMR, too many connections can harm. However, the cache could keep objects in Redis or alike, and then read perfectly fine in parallel! * @@ -421,23 +412,14 @@ open class PgSession( // Maybe this is already fast enough? if (reads.isEmpty()) throw illegalState("Reads must not be empty") val first = reads.first() - val map = first.map + val map = first.catalog val collection = first.collection val historyTables = first.historyTables // When history tables are included in the read, we need `next_version` in the result set // (for the UNION ALL to have matching columns and so history tuples carry their next_version). // For HEAD-only reads it is absent from the physical table, so we skip it and getTuple // reads it as null (correct for live HEAD rows). - val effectiveCols = if (historyTables != null) - collection.effectiveHistoryColumns - else - collection.effectiveHeadColumns - val rows = PgColumnRows() - .withDatabaseNumber(map.storage.number) - .withCatalogNumber(map.number) - .withCollectionNumber(collection.number) - .withDefaultDataEncoding(collection.head.dataEncoding ?: Naksha.DEFAULT_DATA_ENCODING) - .addColumns(effectiveCols) + val rows = PgRows().useHeadTable(historyTables == null).withCollection(collection.head) map.setSearchPath(conn) val headTables = first.headTables val sql = StringBuilder() @@ -492,80 +474,80 @@ open class PgSession( override fun getMapById(mapId: String): NakshaCatalog? { assertOpen() return (if (mayReadParallel) newReadConnection() else readConnection()).use { - storage.adminMap.getPgMapById(it.conn, mapId)?.head + storage.adminCatalog.getPgCatalogById(it.conn, mapId)?.head } } /** - * Returns the [PgMap] for the given id. + * Returns the [PgCatalog] for the given id. * @param mapId the map-id. - * @return the [PgMap]; _null_ if the map does not yet exist. + * @return the [PgCatalog]; _null_ if the map does not yet exist. */ - fun getPgMapById(mapId: String): PgMap? { + fun getPgMapById(mapId: String): PgCatalog? { assertOpen() return (if (mayReadParallel) newReadConnection() else readConnection()).use { - storage.adminMap.getPgMapById(it.conn, mapId) + storage.adminCatalog.getPgCatalogById(it.conn, mapId) } } override fun getMapByNumber(mapNumber: Int): NakshaCatalog? { assertOpen() return (if (mayReadParallel) newReadConnection() else readConnection()).use { - storage.adminMap.getPgMapByNumber(it.conn, mapNumber)?.head + storage.adminCatalog.getPgCatalogByNumber(it.conn, mapNumber)?.head } } /** - * Returns the [PgMap] for the given number. + * Returns the [PgCatalog] for the given number. * @param mapNumber the map-number. - * @return the [PgMap]; _null_ if the map does not yet exist. + * @return the [PgCatalog]; _null_ if the map does not yet exist. */ - fun getPgMapByNumber(mapNumber: Int): PgMap? { + fun getPgMapByNumber(mapNumber: Int): PgCatalog? { assertOpen() return (if (mayReadParallel) newReadConnection() else readConnection()).use { - storage.adminMap.getPgMapByNumber(it.conn, mapNumber) + storage.adminCatalog.getPgCatalogByNumber(it.conn, mapNumber) } } override fun getCollectionById(map: NakshaCatalog, collectionId: String): NakshaCollection? { assertOpen() return (if (mayReadParallel) newReadConnection() else readConnection()).use { - val pgMap = storage.adminMap.getPgMapById(it.conn, map.id) ?: return null + val pgMap = storage.adminCatalog.getPgCatalogById(it.conn, map.id) ?: return null pgMap.getPgCollectionById(it.conn, collectionId)?.head } } /** * Returns the [PgCollection] for the given id. - * @param pgMap the [PgMap] in which to search for the collection. + * @param pgCatalog the [PgCatalog] in which to search for the collection. * @param collectionId the collection-id. * @return the [PgCollection]; _null_ if the collection does not yet exist. */ - fun getPgCollectionById(pgMap: PgMap, collectionId: String): PgCollection? { + fun getPgCollectionById(pgCatalog: PgCatalog, collectionId: String): PgCollection? { assertOpen() return (if (mayReadParallel) newReadConnection() else readConnection()).use { - pgMap.getPgCollectionById(it.conn, collectionId) + pgCatalog.getPgCollectionById(it.conn, collectionId) } } override fun getCollectionByNumber(map: NakshaCatalog, collectionNumber: Int): NakshaCollection? { assertOpen() return (if (mayReadParallel) newReadConnection() else readConnection()).use { - val pgMap = storage.adminMap.getPgMapById(it.conn, map.id) ?: return null + val pgMap = storage.adminCatalog.getPgCatalogById(it.conn, map.id) ?: return null pgMap.getPgCollectionByNumber(it.conn, collectionNumber)?.head } } /** * Returns the [PgCollection] for the given number. - * @param pgMap the [PgMap] in which to search for the collection. + * @param pgCatalog the [PgCatalog] in which to search for the collection. * @param collectionNumber the collection-number. * @return the [PgCollection]; _null_ if the collection does not yet exist. */ - fun getPgCollectionByNumber(pgMap: PgMap, collectionNumber: Int): PgCollection? { + fun getPgCollectionByNumber(pgCatalog: PgCatalog, collectionNumber: Int): PgCollection? { assertOpen() return (if (mayReadParallel) newReadConnection() else readConnection()).use { - pgMap.getPgCollectionByNumber(it.conn, collectionNumber) + pgCatalog.getPgCollectionByNumber(it.conn, collectionNumber) } } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgStorage.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgStorage.kt index 34814796d..f3dd07767 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgStorage.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgStorage.kt @@ -75,20 +75,20 @@ import kotlin.js.JsExport @JsExport abstract class PgStorage protected constructor() : AbstractStorage() { - private var _adminMap: PgAdminMap? = null + private var _adminMap: PgAdminCatalog? = null /** * The admin-map, set by [initStorage]. * @since 3.0 */ - open val adminMap: PgAdminMap + open val adminCatalog: PgAdminCatalog get() = _adminMap ?: throw NakshaException(UNINITIALIZED, "Storage uninitialized") /** * Private setter for the admin map, to be used in the deriving storage class. * @since 3.0 */ - protected fun setAdminMap(adminMap: PgAdminMap) { + protected fun setAdminMap(adminMap: PgAdminCatalog) { _adminMap = adminMap } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgTable.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgTable.kt index f26579775..50766bdcd 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgTable.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgTable.kt @@ -2,11 +2,6 @@ package naksha.psql -import naksha.model.illegalState -import naksha.model.objects.Index -import naksha.model.objects.IndexType -import naksha.model.objects.MemberList -import naksha.model.objects.MemberType import naksha.psql.PgUtil.PgUtilCompanion.quoteIdent import kotlin.js.JsExport import kotlin.js.JsStatic @@ -15,122 +10,69 @@ import kotlin.jvm.JvmStatic /** * Information about a single database table. - * @see [PgHead] - * @see [PgTransactions] - * @see [PgDeleted] - * @see [PgHistory] - * @see [PgMeta] + * @see [PgHeadTable] + * @see [PgHistoryTable] + * @see [PgMetaTable] */ @JsExport -open class PgTable( +abstract class PgTable( /** * The collection to which the table belongs. * @since 3.0 */ @JvmField val collection: PgCollection, + /** * The table-name. * @since 3.0 */ @JvmField val name: String, - /** - * The storage-class where the table is located. - * @since 3.0 - */ - @JvmField val storageClass: PgStorageClass, - /** - * If the table is volatile (is updated often); `false` only for history tables. - * @since 3.0 - */ - @JvmField val isVolatile: Boolean, + /** * The parent table, if this is a partition of it. * @since 3.0 */ - @JvmField val partitionOfTable: PgTable? = null, - /** - * If this is a partition of a [history table][PgHistory], the year; otherwise, if this is a performance partition, the index in the partitions array, so a value between 0 and n, with n being `partitionOf.partitionCount - 1`. - * @since 3.0 - */ - @JvmField val partitionOfValue: Int = -1, - /** - * The column by which to partition, if this is a partitioned table; must be either [PgColumn.fn] or [PgColumn.next_version]. - * @since 3.0 - */ - @JvmField val partitionByColumn: PgColumn? = null, - /** - * If this table is performance-partitioned, the amount of partitions, therefore - * - `0` when [partitionByColumn] is `null` - * - `0` when [partitionByColumn] is [PgColumn.next_version], so the partitions are years. - * - `2 .. 1000` when [partitionByColumn] is [PgColumn.fn], so this is a performance-partitioned table. - * @since 3.0 - */ - @JvmField val partitionCount: Int = 0 + @JvmField val parent: PgTable? = null, ) { companion object PgTableCompanion { /** - * Tests if this is any HEAD table _(either root or a performance-partition)_. + * Tests if this is any HEAD table, either root or a distribution-partition. * @param name the table name. * @return _true_ if this is any HEAD table. */ @JvmStatic @JsStatic - fun isAnyHead(name: String): Boolean = isHead(name) || isHeadPartition(name) + fun isAnyHead(name: String): Boolean = isHead(name) || isHeadDistributionPartition(name) /** - * Tests if this is the root HEAD table. + * Tests if this is the root HEAD table, i.e. `foo`. * @param name the relation name. * @return _true_ if this is the root HEAD table. */ @JvmStatic @JsStatic - fun isHead(name: String): Boolean = name.indexOf(PG_S) < 0 // does not contain a separator + fun isHead(name: String): Boolean = name.indexOf('$') < 0 /** - * Tests if this is a performance-partition of the HEAD table. + * Tests if this is a distribution-partition of the HEAD table, i.e. `foo$12`. * @param name the table name. * @return _true_ if this is a performance-partition of the HEAD table. */ @JvmStatic @JsStatic - fun isHeadPartition(name: String): Boolean = name.indexOf(PG_PART) > 0 // {name}$a??? - - /** - * Tests if this is any DELETED table. - * @param name the table name. - * @return _true_ if this is any DELETED table. - */ - @JvmStatic - @JsStatic - fun isAnyDeleted(name: String): Boolean = isDeleted(name) || isDeletedPartition(name) - - /** - * Tests if this is the root DELETED table. - * @param name the table name. - * @return _true_ if this is the root DELETED table. - */ - @JvmStatic - @JsStatic - fun isDeleted(name: String): Boolean = name.endsWith(PG_DEL) // {name}$del + fun isHeadDistributionPartition(name: String): Boolean = name.indexOf('$') > 0 + && !name.contains("\$hst") + && !name.contains("\$meta") /** - * Tests if this is a performance-partition of the DELETED table. - * @param name the table name. - * @return _true_ if this is a performance-partition of the DELETED table. - */ - @JvmStatic - @JsStatic - fun isDeletedPartition(name: String): Boolean = name.indexOf("${PG_DEL}${PG_PART}") > 0 // {name}$del$a??? - - /** - * Tests if this is the META table. + * Tests if this is the META table, i.e. `foo$meta`. * @param name the table name. * @return _true_ if this is the META table. */ @JvmStatic @JsStatic - fun isMeta(name: String): Boolean = name.endsWith(PG_META) // {name}$meta + fun isMeta(name: String): Boolean = name.endsWith("\$meta") /** * Tests if this is any HISTORY table. @@ -139,7 +81,7 @@ open class PgTable( */ @JvmStatic @JsStatic - fun isAnyHistory(name: String): Boolean = name.indexOf(PG_HST) > 0 // {name}$hst... + fun isAnyHistory(name: String): Boolean = name.indexOf("\$hst") > 0 /** * Tests if this is the root HISTORY table. @@ -148,48 +90,27 @@ open class PgTable( */ @JvmStatic @JsStatic - fun isHistory(name: String): Boolean = name.endsWith("${PG_S}${PG_HST}") // {name}$hst + fun isHistory(name: String): Boolean = name.endsWith("\$hst") /** - * Tests if this is a year-partition of HISTORY, but not a performance-partition. - * New naming: `{collection}$hst$` at end (no `$p` after). + * Tests if this is a partition of HISTORY, but not a distribution-partition, i.e. `foo$hst$2026` * @param name the table name. - * @return _true_ if this is a year-partition of HISTORY. + * @return _true_ if this is a partition of HISTORY. */ @JvmStatic @JsStatic - fun isHistoryYear(name: String): Boolean { - val hstIdx = name.indexOf(PG_HST) - if (hstIdx < 0) return false - val after = hstIdx + PG_HST.length - if (after >= name.length || name[after] != '$') return false - val keyStart = after + 1 - val partIdx = name.indexOf(PG_PART, keyStart) - val keyEnd = if (partIdx < 0) name.length else partIdx - if (keyEnd <= keyStart) return false - return name.substring(keyStart, keyEnd).all { it.isDigit() } && partIdx < 0 - } + fun isHistoryPartition(name: String): Boolean = name.indexOf("\$hst") > 0 + && name.count { it.code == '$'.code } == 2 /** - * Tests if this is a performance-partition of a HISTORY year-partition. - * New naming: `{collection}$hst$$p`. + * Tests if this is a distribution-partition of a HISTORY partition, i.e. `foo$hst$2026$1` * @param name the table name. * @return _true_ if this is a performance-partition of a HISTORY year-partition. */ @JvmStatic @JsStatic - fun isHistoryPartition(name: String): Boolean { - val hstIdx = name.indexOf(PG_HST) - if (hstIdx < 0) return false - val after = hstIdx + PG_HST.length - if (after >= name.length || name[after] != '$') return false - val keyStart = after + 1 - val partIdx = name.indexOf(PG_PART, keyStart) - if (partIdx < 0) return false - val keyEnd = partIdx - if (keyEnd <= keyStart) return false - return name.substring(keyStart, keyEnd).all { it.isDigit() } - } + fun isHistoryDistributionPartition(name: String): Boolean = name.indexOf("\$hst") > 0 + && name.count { it.code == '$'.code } == 3 /** * An indicator if this is an internal Naksha collection. Very special rules apply to these tables. @@ -198,263 +119,67 @@ open class PgTable( */ @JvmStatic @JsStatic - fun isInternal(name: String): Boolean = name.startsWith(PG_INTERNAL_PREFIX) + fun isInternal(name: String): Boolean = name.startsWith("naksha~") } + /** + * The `TOAST` management code is triggered only when a row value to be stored in a table is wider than `TOAST_TUPLE_THRESHOLD` bytes (normally 2 kB). The `TOAST` code will compress and/or move field values out-of-line until the row value is shorter than `TOAST_TUPLE_TARGET` bytes (also normally 2 kB, adjustable) or no more gains can be had. During an `UPDATE` operation, values of unchanged fields are normally preserved as-is; so an `UPDATE` of a row with out-of-line values incurs no `TOAST` costs if none of the out-of-line values change. + * + * **Note**: The value of `TOAST_TUPLE_THRESHOLD` is actually via compile switch fixed to 2 kB. + */ + val toast_tuple_target: Int + get() = 2048 // collection.catalog.storage.adminCatalog.maxTupleSize + /** * The table identifier, optionally quoted in double quotes. */ @JvmField val quotedName: String = quoteIdent(name) + @JvmField + val distributionPartitions: Array = if (collection.partitions <= 1) emptyArray() + else if (this is PgHistoryPartition) Array(collection.partitions) { PgDistributionPartition(this, it) } + else if (this is PgHeadTable) Array(collection.partitions) { PgDistributionPartition(this, it) } + else emptyArray() + /** - * Generate [CREATE_SQL] and [TABLESPACE]. - * - * Actually, history is always partitioned by year, all other tables are optionally performance partitioned. + * Generates the `CREATE_TABLE` string and the `TABLESPACE` string. Use like: + * ```kotlin + * val (CREATE_TABLE, TABLESPACE) = CREATE_TABLE_and_TABLESPACE() + * ``` + * The returned `CREATE_TABLE` is something like `"CREATE TABLE IF NOT EXISTS "`, the `TABLESPACE` a string like `""` or `" TABLESPACE foo"`, so that the tablespace can be appended to the end of a table creation statement, and the create-table string is the prefix for the table creation. + * @return the `CREATE_TABLE` string and the `TABLESPACE` string. + * @since 3.0 */ - private fun doInit(): Pair { - // About TOAST_TUPLE_TARGET - // - // Each page in PostgresQL looks like: - // Page header: ~24 bytes - // ItemId array: 4 bytes per tuple - // Line pointer: part of ItemId array - // Tuple header: 23 bytes (for a normal heap tuple) - // NULL bitmap: variable (~1 byte per 8 columns) - // Actual data: variable - // - // Now, PostgresQL maximum page size is 32768 (configurable at compile time), therefore, setting TOAST_TUPLE_TARGET to 32767 - // will ensure that whatever size a page is, PostgresQL will try to insert the row completely, before falling back to TOAST ! - val map = collection.map; - val toast_tuple_target = if (map is PgAdminMap) map.maxTupleSize else map.storage.adminMap.maxTupleSize; - - // Copy to stack, makes possible for the compiler to remember when values are not null! - val shift = collection.head.shift - val partitionCount = this.partitionCount - val partitionColumn = this.partitionByColumn - val partitionValue = partitionOfValue - val parentTable = partitionOfTable - val superTable: PgTable? = parentTable?.partitionOfTable - - // Verify state, if this is a child table. - if (parentTable != null) { - require(parentTable.partitionByColumn != null) { - "The table '${name}' is a partition of '${parentTable.name}', but the parent does not declare 'partitionBy'" - } - when (parentTable.partitionByColumn) { - PgColumn.fn -> { - require(partitionValue >= 0 && partitionValue < parentTable.partitionCount) { - """The table '$name' is a partition of '${parentTable.name}', but does not declare a valid 'partitionOfValue' (0 to ${parentTable.partitionCount}): $partitionValue""" - } - } - - PgColumn.next_version -> { - require(partitionValue > 0) { - """The table '$name' is a partition of '${parentTable.name}', but does not declare a valid 'partitionOfValue' (expect a positive integer from next_version >> $shift): $partitionValue""" - } - } - - else -> throw IllegalArgumentException( - """The table '$name' is partitioned by invalid column: ${parentTable.partitionByColumn} (must be ${PgColumn.fn.name} or ${PgColumn.next_version.name})""" - ) - } - } - - // If this table is partitioned. - if (partitionColumn != null) { - when (partitionColumn) { - // The children are performance tables, partitioned by partition-number. - PgColumn.fn -> { - require(partitionCount in 2..1000) { "Invalid partition-count, expect 2 .. 1000, found : $partitionCount" } - } - // The children are yearly tables, either in transaction table or history table, partition by year. - PgColumn.next_version -> { - require(partitionCount == 0) { "Invalid partition-count, expect 0, found : $partitionCount" } - } - - else -> throw IllegalArgumentException("Unsupported partitionByColumn: '$partitionColumn'") - } - } else { - // This table is not partitioned, so we need to create it. - require(partitionCount == 0) { - "No partitioning, but partitionCount ($partitionCount) given for table '$name'" - } - } - - val storage = collection.map.storage - val CREATE_TABLE: String - val TABLESPACE: String - when (storageClass) { - PgStorageClass.Ephemeral -> { - CREATE_TABLE = "CREATE TABLE IF NOT EXISTS " - TABLESPACE = - if (storage.adminMap.ephemeralTableSpace != null) " TABLESPACE ${storage.adminMap.ephemeralTableSpace}" else "" - } - - PgStorageClass.Brittle -> { - CREATE_TABLE = "CREATE UNLOGGED TABLE IF NOT EXISTS " - TABLESPACE = if (storage.adminMap.brittleTableSpace != null) " TABLESPACE ${storage.adminMap.brittleTableSpace}" else "" - } - - PgStorageClass.Temporary -> { - CREATE_TABLE = "CREATE UNLOGGED TABLE IF NOT EXISTS " - TABLESPACE = if (storage.adminMap.tempTableSpace != null) " TABLESPACE ${storage.adminMap.tempTableSpace}" else "" - } - - else -> { - CREATE_TABLE = "CREATE TABLE IF NOT EXISTS " - TABLESPACE = "" - } - } - - // If this table is a child table (a partition). - if (parentTable != null) { - // If this table has a parent and a super-table, then this must be a performance partition of history, - // as this is the only situation where we have two levels above us, we have: - // HISTORY -> YEARLY -> PARTITION (this) - if (superTable != null) { - val superPartValue = parentTable.partitionOfValue - if (superPartValue <= 0) { - throw illegalState("The table '${parentTable.name}' is a yearly partition with an illegal partition key: $superPartValue") - } - val parentPartCount = parentTable.partitionCount - if (partitionValue !in 0 ..< parentPartCount) { - throw illegalState("The table '${name}' is performance-partitioned with an partition-number being illegal: $partitionValue; expected 0 till $parentPartCount") - } - // HISTORY year + perf-partition leaf: PK includes (fn, version, next_version) because next_version is the year-partition key. - val SQL = """$CREATE_TABLE $quotedName PARTITION OF ${parentTable.quotedName} ( - CONSTRAINT ${quoteIdent(PgIndex.fn_pkey.id(this))} PRIMARY KEY (${PgColumn.fn}, ${PgColumn.version}, ${PgColumn.next_version}), - CONSTRAINT ${quoteIdent(name + PG_TN_NEXT_CONSTRAINT)} CHECK (next_version IS NOT NULL AND ((next_version >> $shift)::int4)=${superPartValue}), - CONSTRAINT ${quoteIdent(name + PG_ID_CONSTRAINT)} CHECK (((fn & 65535)::int4 % $parentPartCount)=$partitionValue) -) FOR VALUES FROM ($partitionValue) TO (${partitionValue+1}) -WITH (fillfactor=100,toast_tuple_target=$toast_tuple_target) -$TABLESPACE""" - return Pair(SQL, TABLESPACE) - } - - // If the parent table is partitioned by year, but there is no super table, this is a yearly table. - if (parentTable.partitionByColumn == PgColumn.next_version) { - if (partitionValue <= 0) { - throw illegalState("The table '$name' is a yearly partition with an illegal partition key: $partitionValue") - } - - // If this table is further partitioned, this must be part of history. - // HISTORY -> YEARLY (this) -> PARTITION - if (partitionColumn != null) { - if (partitionColumn != PgColumn.fn) { - throw illegalState("The history table '$name' must be partitioned only by `fn`, but ${partitionColumn.name} was used!") - } - val SQL = """$CREATE_TABLE $quotedName PARTITION OF ${parentTable.quotedName} ( - CONSTRAINT ${quoteIdent(name + PG_TN_NEXT_CONSTRAINT)} CHECK (next_version IS NOT NULL AND ((next_version >> $shift)::int4)=$partitionValue) -) FOR VALUES FROM ($partitionValue) TO (${partitionValue+1}) -PARTITION BY RANGE (((fn & 65535)::int4 % $partitionCount)) -$TABLESPACE""" - return Pair(SQL, TABLESPACE) - } - - // HISTORY/TX yearly leaf (no further partitioning). PK includes (fn, version, next_version). - val SQL = """$CREATE_TABLE $quotedName PARTITION OF ${parentTable.quotedName} ( - CONSTRAINT ${quoteIdent(PgIndex.fn_pkey.id(this))} PRIMARY KEY (${PgColumn.fn}, ${PgColumn.version}, ${PgColumn.next_version}), - CONSTRAINT ${quoteIdent(name + PG_TN_NEXT_CONSTRAINT)} CHECK (next_version IS NOT NULL AND ((next_version >> $shift)::int4)=$partitionValue) -) FOR VALUES FROM ($partitionValue) TO (${partitionValue+1}) -WITH (fillfactor=100,toast_tuple_target=$toast_tuple_target) -$TABLESPACE""" - return Pair(SQL, TABLESPACE) - } - - // If parent is partitioned by `fn`, then this is a performance partition either of HEAD or DELETED: - // HEAD/DELETED -> PARTITION (this) - if (parentTable.partitionByColumn == PgColumn.fn) { - if (partitionColumn != null) { - throw illegalState("The performance partition '$name' must not be partitioned further, but is by $partitionColumn") - } - val parentPartCount = parentTable.partitionCount - if (partitionValue !in 0 ..< parentPartCount) { - throw illegalState("The table '${name}' is performance-partitioned with an partition-number being illegal: $partitionValue; expected 0 till $parentPartCount") - } - val nextVersionConstraint = if (isDeleted(parentTable.name)) - " CONSTRAINT ${quoteIdent(name + PG_TN_NEXT_CONSTRAINT)} CHECK (next_version = version),\n" - else "" - val SQL = """$CREATE_TABLE $quotedName PARTITION OF ${parentTable.quotedName} ( - CONSTRAINT ${quoteIdent(PgIndex.fn_pkey.id(this))} PRIMARY KEY (${PgColumn.fn}) INCLUDE (${PgColumn.version}), -$nextVersionConstraint CONSTRAINT ${quoteIdent(name + PG_ID_CONSTRAINT)} CHECK (((fn & 65535)::int4 % $parentPartCount)=$partitionValue) -) FOR VALUES FROM ($partitionValue) TO (${partitionValue+1}) -WITH (fillfactor=100,toast_tuple_target=$toast_tuple_target) -$TABLESPACE""" - return Pair(SQL, TABLESPACE) - } - - // This is an unsupported setup! - throw illegalState("Table '${name}' is partition by '${parentTable.partitionByColumn}', this is not supported") - } - // We do not have a parent table, this is a root table. - - // This is table is partitioned by year, it must be either transaction or history! - if (partitionColumn == PgColumn.next_version) { - // TX (this) -> YEARLY - // HISTORY (this) -> YEARLY - // HISTORY (this) -> YEARLY -> PARTITION - val columns = PgColumn.allColumns - val SQL = """$CREATE_TABLE $quotedName ( -${columnDefinitions(columns)}, -CONSTRAINT ${quoteIdent(name + PG_TN_NEXT_CONSTRAINT)} CHECK (next_version IS NOT NULL), -CONSTRAINT ${quoteIdent(name + PG_ID_CONSTRAINT)} CHECK ((fn < 0 AND id IS NOT NULL) OR (fn >= 0 AND id IS NULL)) -) PARTITION BY RANGE (((next_version >> $shift)::int4)) -$TABLESPACE""" - return Pair(SQL, TABLESPACE) + @Suppress("FunctionName") + protected fun CREATE_TABLE_and_TABLESPACE(): Pair { + val adminCatalog = collection.catalog.storage.adminCatalog + return when (collection.storageClass) { + PgStorageClass.Ephemeral -> Pair( + "CREATE TABLE IF NOT EXISTS ", + if (adminCatalog.ephemeralTableSpace != null) " TABLESPACE ${adminCatalog.ephemeralTableSpace}" else "" + ) + + PgStorageClass.Brittle -> Pair( + "CREATE UNLOGGED TABLE IF NOT EXISTS ", + if (adminCatalog.brittleTableSpace != null) " TABLESPACE ${adminCatalog.brittleTableSpace}" else "" + ) + + PgStorageClass.Temporary -> Pair( + "CREATE UNLOGGED TABLE IF NOT EXISTS ", + if (adminCatalog.tempTableSpace != null) " TABLESPACE ${adminCatalog.tempTableSpace}" else "" + ) + + else -> Pair("CREATE TABLE IF NOT EXISTS ", "") } - - // If the root table is not partitioned. - if (partitionColumn == null) { - // HEAD, META, or DELETED. One row per feature; PK = (fn) INCLUDE (version). - // All tables always use the full head column set; custom members are appended via - // columnDefinitions(). - val columns = if (isAnyHead(name) || isMeta(name)) PgColumn.headColumns else PgColumn.allColumns - val SQL = """$CREATE_TABLE $quotedName ( -${columnDefinitions(columns)}, -CONSTRAINT ${quoteIdent(PgIndex.fn_pkey.id(this))} PRIMARY KEY (${PgColumn.fn}) INCLUDE (${PgColumn.version}), -CONSTRAINT ${quoteIdent(name + PG_ID_CONSTRAINT)} CHECK ((fn < 0 AND id IS NOT NULL) OR (fn >= 0 AND id IS NULL)) -) -WITH (fillfactor=100,toast_tuple_target=$toast_tuple_target) -$TABLESPACE""" - return Pair(SQL, TABLESPACE) - } - - // If performance partitioned and not META. - if (partitionColumn == PgColumn.fn && !isMeta(name)) { - // HEAD (this) -> PARTITION - // DELETED (this) -> PARTITION - val columns = if (isDeleted(name)) PgColumn.allColumns else PgColumn.headColumns - val nextVersionConstraint = if (isDeleted(name)) - "CONSTRAINT ${quoteIdent(name + PG_TN_NEXT_CONSTRAINT)} CHECK (next_version = version)" - else "" - val SQL = """$CREATE_TABLE $quotedName ( -${columnDefinitions(columns)}${if (nextVersionConstraint.isNotEmpty()) ",\n$nextVersionConstraint" else ""}, -CONSTRAINT ${quoteIdent(name + PG_ID_CONSTRAINT)} CHECK ((fn < 0 AND id IS NOT NULL) OR (fn >= 0 AND id IS NULL)) -) PARTITION BY RANGE (((fn & 65535)::int4 % $partitionCount)) -$TABLESPACE""" - return Pair(SQL, TABLESPACE) - } - - // Illegal partitioning. - throw illegalState("The table '$name' is partitioned by $partitionColumn, this is not supported") } /** * The SQL code needed to create the table. * @return the SQL code needed to create the table. */ - @JvmField - internal val CREATE_SQL: String? - - @JvmField - internal val TABLESPACE: String? - - init { - val (CREATE_SQL, TABLESPACE) = doInit() - this.CREATE_SQL = CREATE_SQL - this.TABLESPACE = TABLESPACE - } + @Suppress("FunctionName") + abstract fun CREATE_SQL(): String /** * All existing and declared indices. @@ -462,59 +187,11 @@ $TABLESPACE""" var indices: List = emptyList() internal set - /** - * If this table is partitioned by year. - */ - @JvmField - val hasYearPartitions: Boolean = partitionByColumn == PgColumn.next_version - - /** - * If this table is performance partitioned, so features are stored based upon their ID. - */ - @JvmField - val hasTnPartitions: Boolean = (partitionByColumn == PgColumn.fn) && partitionCount >= 2 - /** * Create the table and its partitions. */ internal open fun create(conn: PgConnection) { - if (CREATE_SQL != null) conn.execute(CREATE_SQL).close() - } - - /** - * Add any custom member columns that are declared in [NakshaCollection.members] but do not yet - * exist in the physical table. This is the migration path for collections that have gained new - * members after their initial creation. - * - * For each declared member whose name is not already a known [PgColumn], issues: - * ```sql - * ALTER TABLE ADD COLUMN IF NOT EXISTS - * ``` - * For known built-in [PgColumn] names the same statement is issued so that existing tables - * gain optional columns added in later releases (e.g. `pn`, `pt`, `gv`). - */ - internal open fun addMissingCustomColumns(conn: PgConnection) { - // Always ensure all standard head columns are present (covers tables created with the old - // lean schema that predates the full-schema approach). - for (col in PgColumn.headColumns) { - conn.execute("ALTER TABLE $quotedName ADD COLUMN IF NOT EXISTS ${col.sqlDefinition}").close() - } - // Also add any explicitly declared members that go beyond the standard set. - val members = collection.head.members ?: return - val knownCols = PgColumn.allColumnsByName - val standardSet = PgColumn.headColumns.toSet() - for (m in members) { - if (m == null) continue - val knownPgCol = knownCols[m.name] - // For known PgColumn entries not in the standard headColumns (e.g. pn, pt, gv), - // use the PgColumn sql definition; for truly custom members use the member type mapping. - val colDef = when { - knownPgCol != null && knownPgCol !in standardSet -> knownPgCol.sqlDefinition - knownPgCol != null -> continue // already in headColumns loop above - else -> PgMemberHelper.sqlDefinitionFor(m) - } - conn.execute("ALTER TABLE $quotedName ADD COLUMN IF NOT EXISTS $colDef").close() - } + conn.execute(CREATE_SQL()).close() } /** @@ -524,7 +201,7 @@ $TABLESPACE""" */ open fun createIndex(conn: PgConnection, index: PgIndex) { if (!indices.contains(index)) { - index.create(conn, this) + index.create(conn, name) indices = indices + index } } @@ -556,38 +233,11 @@ $TABLESPACE""" */ open fun dropIndex(conn: PgConnection, index: PgIndex) { if (indices.contains(index)) { - index.drop(conn, this) + index.drop(conn, name) indices = indices - index } } - /** - * Returns the SQL fragment with extra column definitions for user-declared [members][naksha.model.objects.NakshaCollection.members]. - * - * The result is empty when there are no members, otherwise it is comma-prefixed and ready to be appended to the built-in column list in `CREATE TABLE`. - */ - internal fun extraColumnDefinitions(): String { - val members = collection.head.members ?: return "" - if (members.isEmpty()) return "" - val knownCols = PgColumn.allColumnsByName - val standardSet = PgColumn.headColumns.toSet() - val sb = StringBuilder() - for (member in members) { - if (member == null) continue - val knownPgCol = knownCols[member.name] - val colDef = when { - // Known PgColumn not in the standard head set (e.g. pn, pt, gv): use the canonical definition. - knownPgCol != null && knownPgCol !in standardSet -> knownPgCol.sqlDefinition - // Standard head columns are already in baseColumns; skip to avoid duplicates. - knownPgCol != null -> continue - // Truly custom member: generate from type mapping. - else -> PgMemberHelper.sqlDefinitionFor(member) - } - sb.append(",\n").append(colDef) - } - return sb.toString() - } - /** * Maps a [PgType] to the same sort-order bucket used by [PgMemberHelper_C.columnSortOrder], * so that standard and custom columns can be interleaved into the correct type-alignment group. @@ -605,230 +255,16 @@ $TABLESPACE""" } /** - * Builds the full comma-separated column-definition block for a `CREATE TABLE` statement, merging - * [baseColumns] with any custom/SPECIAL members declared on the collection. - * - * Extra members (SPECIAL like `pn`/`pt`/`gv`, plus truly custom ones) are inserted **within - * their correct type-alignment bucket** — immediately after the last standard column of the same - * type group. The result follows the canonical layout: - * - * ``` - * [INT64 std cols] [INT64 extras] [FLOAT64 std] [FLOAT64 extras] ... [BYTE_ARRAY std] [BYTE_ARRAY extras] [TAGS extras] - * ``` - * - * When [NakshaCollection.members] is `null` (backward-compatible mode) no custom columns exist and - * [baseColumns] are emitted as-is. + * Builds the full comma-separated column-definition block for a `CREATE TABLE` statement. The reads the [PgCollection]. + * @return the SQL column declarations to be used inside a `CREATE TABLE` statement. */ - internal fun columnDefinitions(baseColumns: List): String { - val members = collection.head.members - if (members.isNullOrEmpty()) { - return baseColumns.joinToString(",\n") { it.sqlDefinition } - } - val knownCols = PgColumn.allColumnsByName - val standardSet = baseColumns.toSet() - - // Collect extra (SPECIAL + custom) column definitions, grouped by sort-order bucket. - // Bucket → ordered list of SQL column definition strings. - val extrasByBucket = mutableMapOf>() - for (m in members) { - if (m == null) continue - val knownPgCol = knownCols[m.name] - val colDef = when { - knownPgCol != null && knownPgCol !in standardSet -> knownPgCol.sqlDefinition - knownPgCol != null -> continue // already in baseColumns — skip - else -> PgMemberHelper.sqlDefinitionFor(m) - } - val bucket = PgMemberHelper.columnSortOrder(m.dataType) - extrasByBucket.getOrPut(bucket) { mutableListOf() }.add(colDef) - } - - if (extrasByBucket.isEmpty()) { - return baseColumns.joinToString(",\n") { it.sqlDefinition } - } - - // Determine the index of the last standard column in each bucket. - val lastIndexForBucket = mutableMapOf() - for ((idx, col) in baseColumns.withIndex()) { - val bucket = pgTypeSortOrder(col.type) - lastIndexForBucket[bucket] = idx - } - // Also build a sorted list of standard bucket values for gap detection. - val standardBuckets = lastIndexForBucket.keys.sorted() - + internal fun columnDefinitions(): String { val sb = StringBuilder() - for ((idx, col) in baseColumns.withIndex()) { + for (column in collection.columns) { if (sb.isNotEmpty()) sb.append(",\n") - sb.append(col.sqlDefinition) - // After the last standard column of this bucket, flush: - // 1. Extras for this exact bucket. - // 2. Extras for any intermediate buckets that have no standard columns and fall - // between this bucket and the next standard bucket (or end-of-list). - val bucket = pgTypeSortOrder(col.type) - if (lastIndexForBucket[bucket] == idx) { - // Flush this bucket's extras. - val extras = extrasByBucket.remove(bucket) - if (extras != null) for (def in extras) sb.append(",\n").append(def) - // Determine the next standard bucket (if any). - val nextStdBucket = standardBuckets.firstOrNull { it > bucket } - // Flush all intermediate extra-only buckets that belong before the next standard bucket. - for (extraBucket in extrasByBucket.keys.sorted()) { - if (nextStdBucket != null && extraBucket >= nextStdBucket) break - val gapExtras = extrasByBucket.remove(extraBucket) ?: continue - for (def in gapExtras) sb.append(",\n").append(def) - } - } - } - // Flush any remaining extras (buckets beyond the last standard bucket, e.g. TAGS). - for (bucket in extrasByBucket.keys.sorted()) { - for (def in extrasByBucket[bucket]!!) sb.append(",\n").append(def) + sb.append(column.sql) } return sb.toString() } - - /** - * Returns the unique identifier for a user-defined index on this table. - * - * The result follows the pattern `{table-name}$ci_{index-name}` and is truncated to 63 bytes (Postgres identifier limit). - */ - internal fun customIndexId(indexName: String): String { - val id = "${name}${PG_CUSTOM_IDX}${indexName}" - return if (id.length > 63) id.substring(0, 63) else id - } - - /** - * Creates a [CustomIndex][naksha.model.objects.CustomIndex] on this table. - * - * Internal naming follows `{table-name}$ci_{index.name}`; truncated to Postgres's 63-byte identifier limit. - * @param conn the connection to use to execute the creation. - * @param index the index to create. - */ - open fun createCustomIndex(conn: PgConnection, index: Index) { - val sql = buildCustomIndexSql(index) ?: return - conn.execute(sql).close() - } - - /** - * Drops a custom [Index] from this table. - */ - open fun dropCustomIndex(conn: PgConnection, indexName: String) { - val id = customIndexId(indexName) - conn.execute("DROP INDEX IF EXISTS ${quoteIdent(id)} CASCADE").close() - } - - private fun buildCustomIndexSql(index: Index): String? { - val on = index.on - if (on.isEmpty()) return null - val id = quoteIdent(customIndexId(index.name)) - val unique = if (index.unique) "UNIQUE INDEX" else "INDEX " - val fillFactor = if (isVolatile) "WITH (deduplicate_items=OFF, fillfactor=80)" else "WITH (deduplicate_items=OFF, fillfactor=100)" - val members = collection.head.members - val firstCol = on[0] ?: return null - return when (index.type) { - IndexType.BTREE -> { - val cols = on.filterNotNull().joinToString(", ") { col -> - val pgIdent = quoteIdent(physicalColumnName(col, members)) - val opclass = if (isTextColumn(col, members)) " text_pattern_ops" else "" - "$pgIdent$opclass DESC" - } - val include = index.include?.takeIf { !it.isEmpty() }?.let { inc -> - " INCLUDE (${inc.filterNotNull().joinToString(", ") { quoteIdent(physicalColumnName(it, members)) }})" - } ?: "" - """ -CREATE $unique IF NOT EXISTS $id ON ${quotedName} -USING btree ($cols)$include -$fillFactor ${TABLESPACE};""".trim() - } - IndexType.SPATIAL -> { - if (!isSpatialColumn(firstCol, members)) { - throw naksha.model.illegalArg("SPATIAL index '${index.name}' must target a member of dataType SPATIAL (column '$firstCol' is not)") - } - val cCol = quoteIdent(physicalColumnName(firstCol, members)) - """ -CREATE INDEX IF NOT EXISTS $id ON ${quotedName} -USING gist (naksha_2d($cCol)) -WITH (fillfactor=${if (isVolatile) 80 else 100}) ${TABLESPACE} -WHERE naksha_2d($cCol) IS NOT NULL;""".trim() - } - IndexType.TAGS -> { - if (!isFlatMapColumn(firstCol, members)) { - throw naksha.model.illegalArg("TAGS custom index '${index.name}' must target a member of dataType TAGS or TAGS_FROM_ARRAY") - } - val pgIdent = quoteIdent(physicalColumnName(firstCol, members)) - """ -CREATE INDEX IF NOT EXISTS $id ON ${quotedName} -USING gin ($pgIdent) -${TABLESPACE};""".trim() - } - IndexType.SET -> { - if (!isSetColumn(firstCol, members)) { - throw naksha.model.illegalArg("SET custom index '${index.name}' must target a member of dataType SET") - } - val pgIdent = quoteIdent(physicalColumnName(firstCol, members)) - """ -CREATE INDEX IF NOT EXISTS $id ON ${quotedName} -USING gin ($pgIdent) -${TABLESPACE};""".trim() - } - else -> null - } - } - - private fun physicalColumnName(name: String, members: MemberList?): String { - if (members != null) { - for (m in members) { - if (m != null && m.name == name) return PgMemberHelper.pgColumnName(name) - } - } - return name - } - - private fun isTextColumn(name: String, members: MemberList?): Boolean { - if (members != null) { - for (m in members) { - if (m != null && m.name == name) { - return m.dataType == MemberType.STRING - } - } - } - if (name == "id" || name == "app_id" || name == "author" || name == "ft" || name == "origin" || name == "target") return true - return false - } - - private fun isFlatMapColumn(name: String, members: MemberList?): Boolean { - val type = memberTypeOf(name, members) ?: return false - return type == MemberType.TAGS || type == MemberType.TAGS_FROM_ARRAY - } - - private fun isSetColumn(name: String, members: MemberList?): Boolean = - memberTypeOf(name, members) == MemberType.SET - - /** - * Resolves the [MemberType] of a member by name: explicitly declared members take precedence, - * otherwise the standard member contract ([StandardMembers.ALL]) is consulted (e.g. the built-in - * `tags` member, which is a [MemberType.SET]). - */ - private fun memberTypeOf(name: String, members: MemberList?): MemberType? { - if (members != null) { - for (m in members) { - if (m != null && m.name == name) return m.dataType - } - } - for (sm in naksha.model.objects.StandardMembers.ALL) { - if (sm.name == name) return sm.dataType - } - return null - } - - private fun isSpatialColumn(name: String, members: MemberList?): Boolean { - // Built-in spatial columns are always valid targets. - if (name == PgColumn.geo.name || name == PgColumn.ref_point.name) return true - // Custom members declared as SPATIAL are valid. - if (members != null) { - for (m in members) { - if (m != null && m.name == name) return m.dataType == MemberType.SPATIAL - } - } - return false - } } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgType.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgType.kt index 48a3fc2ce..ec48e2606 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgType.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgType.kt @@ -156,15 +156,25 @@ class PgType : JsEnum() { @JvmStatic fun of(name: String?): PgType? = getDefined(name, PgType::class) + /** + * Returns the database column type to be used for a specific [Member]. + * @param member the [Member] to lookup. + * @return the database column type to be used for a specific [Member]. + * @since 3.0 + */ + @JsStatic + @JvmStatic + fun ofMember(member: Member): PgType = ofMemberType(member.dataType) + /** * Returns the database column type to be used for a specific [MemberType]. - * @param member the [MemberType] to lookup. + * @param memberType the [MemberType] to lookup. * @return the database column type to be used for a specific [MemberType]. * @since 3.0 */ @JsStatic @JvmStatic - fun ofMemberType(member: Member): PgType = when (member.dataType) { + fun ofMemberType(memberType: MemberType): PgType = when (memberType) { MemberType.BOOLEAN -> BOOLEAN MemberType.INT8 -> SHORT MemberType.INT16 -> SHORT diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWrite.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWrite.kt index 0671532bc..2ddd29590 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWrite.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWrite.kt @@ -18,16 +18,16 @@ internal data class PgWrite(val original: Write, val i: Int) { /** * The map into which to write. * - * - If a map is modified, this is [Naksha.ADMIN_MAP][naksha.model.Naksha.ADMIN_CATALOG_ID], [asPgMap] and [asNakshaMap] will be set. + * - If a map is modified, this is [Naksha.ADMIN_MAP][naksha.model.Naksha.ADMIN_CATALOG_ID], [asPgCatalog] and [asNakshaMap] will be set. * - If a collection is modified, this is the map in which [Naksha.COLLECTIONS_COL][naksha.model.Naksha.COLLECTIONS_COL_ID] is located, [asPgCollection] and [asNakshaCollection] will be set. * @since 3.0 */ - lateinit var map: PgMap + lateinit var map: PgCatalog /** * The collection into which to write. * - * - If a map is modified, this is [Naksha.CATALOGS_COL][naksha.model.Naksha.CATALOGS_COL_ID], [asPgMap] and [asNakshaMap] will be set. + * - If a map is modified, this is [Naksha.CATALOGS_COL][naksha.model.Naksha.CATALOGS_COL_ID], [asPgCatalog] and [asNakshaMap] will be set. * - If a collection is modified, this is [Naksha.COLLECTIONS_COL][naksha.model.Naksha.COLLECTIONS_COL_ID], [asPgCollection] and [asNakshaCollection] will be set. * @since 3.0 */ @@ -126,10 +126,10 @@ internal data class PgWrite(val original: Write, val i: Int) { get() = !isTransactionModification && !isMapModification && !isCollectionModification /** - * If the feature is a map, the [PgMap] representation. + * If the feature is a map, the [PgCatalog] representation. * @since 3.0 */ - var asPgMap: PgMap? = null + var asPgCatalog: PgCatalog? = null /** * If this modifies a map, the feature cast to [NakshaCatalog]. diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt index c822cf9df..e93ef4c70 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt @@ -97,11 +97,11 @@ open class PgWriter internal constructor( * @since 3.0 */ private fun addPgWrite( - map: PgMap, + map: PgCatalog, collection: PgCollection, writeOp: WriteOp, write: PgWrite, - byMap: MutableMap>?>>>, + byMap: MutableMap>?>>>, writeListCapacity: Int) { var byCollection = byMap[map] if (byCollection == null) { @@ -136,8 +136,8 @@ open class PgWriter internal constructor( * @since 3.0 */ private fun groupOperations(writes: ArrayList) - : MutableMap>?>>> { - val byMap = mutableMapOf>?>>>() + : MutableMap>?>>> { + val byMap = mutableMapOf>?>>>() var writeListCapacity = writes.size for (i in writes.indices) { val write = writes[i] @@ -248,7 +248,7 @@ open class PgWriter internal constructor( } featuresModified += 1 } else if (write.isMapModification) { - val map = write.asPgMap + val map = write.asPgCatalog if (map != null) transaction.useMap(map.id, map.number, write.action) } else if (write.isCollectionModification) { val map = write.map @@ -276,7 +276,7 @@ open class PgWriter internal constructor( // Detect tbe map into which to write. val mapId = write.original.mapId ?: throw illegalArg("The given write does not have a map-id") - val map = storage.adminMap.getPgMapById(conn, mapId) ?: + val map = storage.adminCatalog.getPgCatalogById(conn, mapId) ?: throw mapNotFound("The write #${write.i} refers to not existing map '$mapId'") write.map = map @@ -289,7 +289,7 @@ open class PgWriter internal constructor( // If this operation modifies a map. if (write.isMapModification) { val op = write.op - var pgMap = storage.adminMap.getPgMapById(null, write.id) ?: storage.adminMap.getPgMapById(conn, write.id) + var pgMap = storage.adminCatalog.getPgCatalogById(null, write.id) ?: storage.adminCatalog.getPgCatalogById(conn, write.id) val nakshaMap: NakshaCatalog? if (op == WriteOp.CREATE || op == WriteOp.UPSERT || op == WriteOp.UPDATE) { @@ -300,7 +300,7 @@ open class PgWriter internal constructor( if (op == WriteOp.UPDATE) { throw mapNotFound("The UPDATE (write #${write.i}) failed, because the map '$featureId' does not exist") } - pgMap = PgMap(storage, nakshaMap) + pgMap = PgCatalog(storage, nakshaMap) createPgMap(pgMap) } else if (op == WriteOp.CREATE) { throw mapExists("The write #${write.i} failed, because the map '$featureId' does exist already") @@ -313,7 +313,7 @@ open class PgWriter internal constructor( } else { throw illegalState("The write #${write.i} refers to an unsupported operation: '$op'") } - write.asPgMap = pgMap + write.asPgCatalog = pgMap write.asNakshaMap = nakshaMap } @@ -367,7 +367,7 @@ open class PgWriter internal constructor( return list } - private fun executeWrite(map: PgMap, collection: PgCollection, partition: Int, byWriteOp: Map>) { + private fun executeWrite(map: PgCatalog, collection: PgCollection, partition: Int, byWriteOp: Map>) { // DELETE val deletes = byWriteOp[WriteOp.DELETE] if (deletes != null) { @@ -409,8 +409,8 @@ open class PgWriter internal constructor( * @param map the map that should be physically created. * @since 3.0 */ - protected open fun createPgMap(map: PgMap) { - storage.adminMap.createPgMap(conn, map) + protected open fun createPgMap(map: PgCatalog) { + storage.adminCatalog.createPgCatalog(conn, map) } /** @@ -418,8 +418,8 @@ open class PgWriter internal constructor( * @param map the map that was just created. * @since 3.0 */ - protected open fun deletePgMap(map: PgMap) { - storage.adminMap.deletePgMap(conn, map) + protected open fun deletePgMap(map: PgCatalog) { + storage.adminCatalog.deletePgCatalog(conn, map) } /** @@ -544,7 +544,7 @@ open class PgWriter internal constructor( * @since 3.0 */ protected open fun createPgCollection(collection: PgCollection) { - collection.map.createPgCollection(conn, collection) + collection.catalog.createPgCollection(conn, collection) } /** @@ -553,6 +553,6 @@ open class PgWriter internal constructor( * @since 3.0 */ protected open fun deletePgCollection(collection: PgCollection) { - collection.map.deletePgCollection(conn, collection) + collection.catalog.deletePgCollection(conn, collection) } } \ No newline at end of file diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterBase.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterBase.kt index 76265e279..fe3d45f27 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterBase.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterBase.kt @@ -47,7 +47,7 @@ internal abstract class PgWriterBase protected constructor( get() = collection.storage.number val mapNumber: Int - get() = collection.map.number + get() = collection.catalog.number val collectionNumber: Int get() = collection.number @@ -72,7 +72,7 @@ internal abstract class PgWriterBase protected constructor( * The rows to write. * @since 3.0 */ - val inRows = PgColumnRows() + val inRows = PgRows() .withDatabaseNumber(storageNumber) .withCatalogNumber(mapNumber) .withCollectionNumber(collectionNumber) @@ -143,7 +143,7 @@ internal abstract class PgWriterBase protected constructor( val hst = collection.historyTable ?: return null var yearTable: PgHistoryYear? = hst.years[year] if (yearTable == null) { - hst.addYear(year) + hst.addPartition(year) yearTable = hst.years[year] if (yearTable == null) { throw illegalState("Internal error, failed to add history year $year") @@ -163,7 +163,7 @@ internal abstract class PgWriterBase protected constructor( * @since 3.0 */ fun execute(conn: PgConnection) { - collection.map.setSearchPath(conn) + collection.catalog.setSearchPath(conn) return doExecute(conn) } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterDelete.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterDelete.kt index 4dcfa2c96..7f0204e87 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterDelete.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterDelete.kt @@ -144,7 +144,7 @@ ${if (purge) "LEFT JOIN head_deleted ON head_deleted.id = query.id" else ""} override fun doExecute(conn: PgConnection) { if (writes.isEmpty()) return - val outRows = PgColumnRows() + val outRows = PgRows() .withDatabaseNumber(storageNumber) .withCatalogNumber(mapNumber) .withCollectionNumber(collectionNumber) diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt index 87c37c0dc..a87c07182 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt @@ -133,7 +133,7 @@ LEFT JOIN inserted ON inserted.id = new_row.id // All nullable BYTE_ARRAY columns may carry the "keep if undefined" sentinel and must be // read back from the DB so the in-memory tuple reflects the final stored value. val keepableByteCols = collection.effectiveHeadColumns.filter { it.type == PgType.BYTE_ARRAY && it !== PgColumn.feature } - val rows = PgColumnRows() + val rows = PgRows() .withDatabaseNumber(storageNumber) .withCatalogNumber(mapNumber) .withCollectionNumber(collectionNumber) diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt index 1d818189f..922f3001b 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt @@ -146,7 +146,7 @@ ${if (head_to_history.isNotEmpty()) "LEFT JOIN head_to_history ON head_to_histor override fun doExecute(conn: PgConnection) { val keepableByteCols = collection.effectiveHeadColumns.filter { it.type == PgType.BYTE_ARRAY && it !== PgColumn.feature } - val outRows = PgColumnRows() + val outRows = PgRows() .withDatabaseNumber(storageNumber) .withCatalogNumber(mapNumber) .withCollectionNumber(collectionNumber) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByMetadataTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByMetadataTest.kt index a781ec057..8ca71e6d1 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByMetadataTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByMetadataTest.kt @@ -31,7 +31,7 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { // And: val featuresByAppId = executeMetaQuery( - MetaQuery( + MemberQuery( column = MetaColumn.appId(), op = StringOp.EQUALS, value = sessionOptions.appId @@ -58,7 +58,7 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { // And: val featuresByAppIdPrefix = executeMetaQuery( - MetaQuery( + MemberQuery( column = MetaColumn.appId(), op = StringOp.STARTS_WITH, value = "prefixed_test_app" @@ -85,7 +85,7 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { // And: val featuresByAuthor = executeMetaQuery( - MetaQuery( + MemberQuery( column = MetaColumn.author(), op = StringOp.EQUALS, value = sessionOptions.author @@ -112,7 +112,7 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { // And: val featuresByAuthorPrefix = executeMetaQuery( - MetaQuery( + MemberQuery( column = MetaColumn.author(), op = StringOp.STARTS_WITH, value = "Jacky" @@ -136,7 +136,7 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { // And: val featuresById = executeMetaQuery( - MetaQuery( + MemberQuery( column = MetaColumn.id(), op = StringOp.EQUALS, value = inputFeature.id @@ -160,7 +160,7 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { // And: val featuresByIdPrefix = executeMetaQuery( - MetaQuery( + MemberQuery( column = MetaColumn.id(), op = StringOp.STARTS_WITH, value = TEST_FEATURE_ID.substring(0..4) @@ -186,7 +186,7 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { // And: val featuresByType = executeMetaQuery( - MetaQuery(MetaColumn.featureType(), StringOp.EQUALS, inputFeature.featureType) + MemberQuery(MetaColumn.featureType(), StringOp.EQUALS, inputFeature.featureType) ).features // Then: @@ -208,7 +208,7 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { // And: val featuresByTypePrefix = executeMetaQuery( - MetaQuery(MetaColumn.featureType(), StringOp.STARTS_WITH, "quite") + MemberQuery(MetaColumn.featureType(), StringOp.STARTS_WITH, "quite") ).features // Then: @@ -235,9 +235,9 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { val featuresByAppIdAndAuthor = executeRead(ReadFeatures().apply { mapId = collection.catalogId collectionIds += collection.id - query.metadata = MetaAnd( - MetaQuery(MetaColumn.author(), StringOp.EQUALS, author), - MetaQuery(MetaColumn.appId(), StringOp.STARTS_WITH, appId.substring(0, 2)) + query.metadata = MemberAnd( + MemberQuery(MetaColumn.author(), StringOp.EQUALS, author), + MemberQuery(MetaColumn.appId(), StringOp.STARTS_WITH, appId.substring(0, 2)) ) }) @@ -257,7 +257,7 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { val insertedFeatureXyz = insertFeatureAndGetXyz(inputFeature) // And: - val featuresByCreatedAt = executeMetaQuery(MetaQuery( + val featuresByCreatedAt = executeMetaQuery(MemberQuery( MetaColumn.createdAt(), DoubleOp.EQ, insertedFeatureXyz.createdAt @@ -279,7 +279,7 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { val insertedFeatureXyz = insertFeatureAndGetXyz(inputFeature) // And: - val featuresByUpdatedAt = executeMetaQuery(MetaQuery( + val featuresByUpdatedAt = executeMetaQuery(MemberQuery( MetaColumn.updatedAt(), DoubleOp.EQ, insertedFeatureXyz.updatedAt @@ -304,13 +304,13 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { // And: val featuresCreatedInFrame = executeMetaQuery( - MetaAnd( - MetaQuery( + MemberAnd( + MemberQuery( MetaColumn.createdAt(), DoubleOp.GT, insertedFeatureXyz.createdAt - 100 ), - MetaQuery( + MemberQuery( MetaColumn.createdAt(), DoubleOp.LT, insertedFeatureXyz.createdAt + 100 @@ -337,13 +337,13 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { // And: val featuresUpdatedInFrame = executeMetaQuery( - MetaAnd( - MetaQuery( + MemberAnd( + MemberQuery( MetaColumn.updatedAt(), DoubleOp.GTE, insertedFeatureXyz.updatedAt ), - MetaQuery( + MemberQuery( MetaColumn.updatedAt(), DoubleOp.LTE, insertedFeatureXyz.updatedAt + 100 @@ -367,7 +367,7 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { val insertedFeatureXyz = insertFeatureAndGetXyz(inputFeature) // And: - val featuresByAuthorTs = executeMetaQuery(MetaQuery( + val featuresByAuthorTs = executeMetaQuery(MemberQuery( MetaColumn.authorTs(), DoubleOp.EQ, insertedFeatureXyz.authorTs @@ -393,9 +393,9 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { val featuresByAppIdAndAuthor = executeRead(ReadFeatures().apply { mapId = collection.catalogId collectionIds += collection.id - query.metadata = MetaOr( - MetaQuery(MetaColumn.author(), StringOp.EQUALS, "this_is_totally_off"), - MetaQuery(MetaColumn.appId(), StringOp.STARTS_WITH, appId.substring(0, 2)) + query.metadata = MemberOr( + MemberQuery(MetaColumn.author(), StringOp.EQUALS, "this_is_totally_off"), + MemberQuery(MetaColumn.appId(), StringOp.STARTS_WITH, appId.substring(0, 2)) ) }).features @@ -426,7 +426,7 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { queryHistory = true queryDeleted = true query = RequestQuery().apply { - metadata = MetaQuery(MetaColumn.action(), DoubleOp.NE, Action.CREATE.intValue) + metadata = MemberQuery(MetaColumn.action(), DoubleOp.NE, Action.CREATE.intValue) } } val response = executeRead(getHistoryWithoutUpdates) @@ -452,7 +452,7 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { return persistedFeature.properties.xyz } - private fun executeMetaQuery(metaQuery: IMetaQuery): SuccessResponse { + private fun executeMetaQuery(metaQuery: IMemberQuery): SuccessResponse { return executeRead(ReadFeatures().apply { mapId = collection.catalogId collectionIds += collection.id diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByOtherTns.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByOtherTns.kt index f2580602c..f4fff91b2 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByOtherTns.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByOtherTns.kt @@ -8,7 +8,7 @@ import naksha.model.request.Write import naksha.model.request.WriteRequest import naksha.model.request.query.AnyOp import naksha.model.request.query.MetaColumn -import naksha.model.request.query.MetaQuery +import naksha.model.request.query.MemberQuery import naksha.psql.assertions.NakshaFeatureFluidAssertions.Companion.assertThatFeature import kotlin.test.Test import kotlin.test.assertEquals @@ -43,7 +43,7 @@ class ReadFeaturesByOtherTns : PgTestBase( val updatedVersion: Int64 = updateResp.features[0]!!.tupleNumber.version.value // When: querying for features whose `next_version` matches that version - val nextVersionQuery = MetaQuery( + val nextVersionQuery = MemberQuery( MetaColumn.nextVersion(), AnyOp.IS_ANY_OF, arrayOf(updatedVersion) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/TupleNumberPersistenceTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/TupleNumberPersistenceTest.kt index fd7af33bb..5a732f523 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/TupleNumberPersistenceTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/TupleNumberPersistenceTest.kt @@ -67,7 +67,7 @@ class TupleNumberPersistenceTest : PgTestBase(collection = null, mapId = "") { // And: `storeNumber` checks out storage.adminConnection().use { conn -> - val pgMap = storage.adminMap.getPgMapById(conn, collection.catalogId!!) + val pgMap = storage.adminCatalog.getPgCatalogById(conn, collection.catalogId!!) require(pgMap != null) { "Missing map ${collection.catalogId}" } val pgCollection = pgMap.getPgCollectionById(conn, collection.id) require(pgCollection != null) { "Missing collection ${collection.id}" } diff --git a/here-naksha-lib-psql/src/jsMain/kotlin/naksha/psql/Plv8Trigger.kt b/here-naksha-lib-psql/src/jsMain/kotlin/naksha/psql/Plv8Trigger.kt index 7f2c7257e..a910348a0 100644 --- a/here-naksha-lib-psql/src/jsMain/kotlin/naksha/psql/Plv8Trigger.kt +++ b/here-naksha-lib-psql/src/jsMain/kotlin/naksha/psql/Plv8Trigger.kt @@ -20,7 +20,7 @@ internal const val TG_LEVEL_ROW = "ROW" internal const val TG_LEVEL_STATEMENT = "STATEMENT" /** - * The raw row as returned by PostgresQL triggers. Check [PgColumn.allColumns], it should match this. + * The raw row as returned by PostgresQL triggers. Check [PgColumn_C.allColumns], it should match this. */ @JsPlainObject external interface Plv8Row { diff --git a/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlAdminMap.kt b/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlAdminCatalog.kt similarity index 94% rename from here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlAdminMap.kt rename to here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlAdminCatalog.kt index 5771ad466..f183d9bf7 100644 --- a/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlAdminMap.kt +++ b/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlAdminCatalog.kt @@ -13,12 +13,12 @@ import naksha.psql.PgUtil.PgUtilCompanion.quoteLiteral * @since 3.0 */ @Suppress("MemberVisibilityCanBePrivate") -class PsqlAdminMap internal constructor( +class PsqlAdminCatalog internal constructor( storage: PgStorage, config: PgConfig, create: Boolean?, upgrade: Boolean? -) : PgAdminMap(storage, config, create, upgrade) { +) : PgAdminCatalog(storage, config, create, upgrade) { override val storage: PsqlStorage get() = super.storage as PsqlStorage @@ -26,17 +26,17 @@ class PsqlAdminMap internal constructor( override fun getDataEncoding(feature: Any?, context: Any?): DataEncoding { if (context is PgCollection) { var encoding = context.head.dataEncoding - if (encoding == null) encoding = context.map.head.dataEncoding + if (encoding == null) encoding = context.catalog.head.dataEncoding return encoding ?: Naksha.DEFAULT_DATA_ENCODING } - if (context is PgMap) { + if (context is PgCatalog) { return context.head.dataEncoding ?: Naksha.DEFAULT_DATA_ENCODING } if (context is NakshaCollection) { val collectionEncoding = context.dataEncoding if (collectionEncoding != null) return collectionEncoding val mapId = context.catalogId ?: return Naksha.DEFAULT_DATA_ENCODING - val pgMap = getPgMapById(null, mapId) + val pgMap = getPgCatalogById(null, mapId) return pgMap?.head?.dataEncoding ?: Naksha.DEFAULT_DATA_ENCODING } if (context is NakshaCatalog) { @@ -55,11 +55,11 @@ class PsqlAdminMap internal constructor( return null } - override fun createAdminMap(conn: PgConnection, config: PgConfig, storageId: String, storageNumber: Int64, psqlVersion: NakshaVersion): Int { + override fun createAdminCatalog(conn: PgConnection, config: PgConfig, storageId: String, storageNumber: Int64, psqlVersion: NakshaVersion): Int { return upsertAdminMap(conn, storageId, storageNumber, psqlVersion, null, null) } - override fun upgradeAdminMap(conn: PgConnection, config: PgConfig, storageId: String, storageNumber: Int64, psqlVersion: NakshaVersion, schemaOid: Int, installedVersion: NakshaVersion?) { + override fun upgradeAdminCatalog(conn: PgConnection, config: PgConfig, storageId: String, storageNumber: Int64, psqlVersion: NakshaVersion, schemaOid: Int, installedVersion: NakshaVersion?) { upsertAdminMap(conn, storageId, storageNumber, psqlVersion, schemaOid, installedVersion) } diff --git a/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlMap.kt b/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlMap.kt index f2e5d3c68..bcec3b0c0 100644 --- a/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlMap.kt +++ b/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlMap.kt @@ -14,14 +14,14 @@ import java.util.concurrent.TimeUnit * @since 3.0 */ data class PsqlMap( - val adminMap: PsqlAdminMap, - val pgMap: PgMap? = null, - val id: String = pgMap?.id ?: throw illegalArg("PsqlMap without valid id"), - val number: Int = pgMap?.number ?: throw illegalArg("PsqlMap without valid number") + val adminMap: PsqlAdminCatalog, + val pgCatalog: PgCatalog? = null, + val id: String = pgCatalog?.id ?: throw illegalArg("PsqlMap without valid id"), + val number: Int = pgCatalog?.number ?: throw illegalArg("PsqlMap without valid number") ): Expiry { /** - * Tests if the underlying [PgMap] still exist. + * Tests if the underlying [PgCatalog] still exist. * @return `true` if the map exists; `false` if this is a tombstone cache entry. */ fun exists(): Boolean = head.get() != null @@ -29,7 +29,7 @@ data class PsqlMap( /** * The current HEAD state, _null_ if the map does not exist _(after being deleted)_. */ - val head = AtomicRef(pgMap) + val head = AtomicRef(pgCatalog) // ----------------------------< Children management aka collection caching >------------------------------------------ diff --git a/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlStorage.kt b/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlStorage.kt index c7f592a0c..69ab6246f 100644 --- a/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlStorage.kt +++ b/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlStorage.kt @@ -15,8 +15,8 @@ open class PsqlStorage : PgStorage(), IStorage { override val configKlass: KClass = PgConfig::class - override val adminMap: PsqlAdminMap - get() = super.adminMap as PsqlAdminMap + override val adminCatalog: PsqlAdminCatalog + get() = super.adminCatalog as PsqlAdminCatalog private var _cluster: PgCluster? = null @@ -55,22 +55,22 @@ open class PsqlStorage : PgStorage(), IStorage { _cluster = c } setAdminMap(newAdminMap(config, create, upgrade)) - adminMap.start() + adminCatalog.start() } - protected open fun newAdminMap(config: PgConfig, create: Boolean?, upgrade: Boolean?): PsqlAdminMap - = PsqlAdminMap(this, config, create, upgrade) + protected open fun newAdminMap(config: PgConfig, create: Boolean?, upgrade: Boolean?): PsqlAdminCatalog + = PsqlAdminCatalog(this, config, create, upgrade) override fun newSession(options: SessionOptions, readOnly: Boolean): PgSession { useInitialized() return PgSession(this, options, readOnly) } - override fun getDataEncoding(feature: Any?, context: Any?): DataEncoding = adminMap.getDataEncoding(feature, context) + override fun getDataEncoding(feature: Any?, context: Any?): DataEncoding = adminCatalog.getDataEncoding(feature, context) - override fun getDictionary(id: String): JbDictionary? = adminMap.getDictionary(id) + override fun getDictionary(id: String): JbDictionary? = adminCatalog.getDictionary(id) - override fun getEncodingDictionary(feature: Any?, context: Any?): JbDictionary? = adminMap.getEncodingDictionary(feature, context) + override fun getEncodingDictionary(feature: Any?, context: Any?): JbDictionary? = adminCatalog.getEncodingDictionary(feature, context) override fun newConnection(options: SessionOptions, readOnly: Boolean, init: Fx2?): PgConnection = cluster.newConnection(options, readOnly, init) diff --git a/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlStorageListener.kt b/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlStorageListener.kt index abaee2c79..22be5f99c 100644 --- a/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlStorageListener.kt +++ b/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlStorageListener.kt @@ -13,7 +13,7 @@ internal class PsqlStorageListener(storage: PsqlStorage) : Thread("lib-psql-list private val storageRef: WeakReference = WeakReference(storage) private val shutdown = AtomicBoolean(false) private val adminOptions = Naksha.adminOptions - private val adminMap = storage.adminMap as PsqlAdminMap + private val adminMap = storage.adminCatalog as PsqlAdminCatalog private val cluster = storage.cluster private val e = Exception() diff --git a/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlTestStorage.kt b/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlTestStorage.kt index f89fc1224..42a443902 100644 --- a/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlTestStorage.kt +++ b/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlTestStorage.kt @@ -169,7 +169,7 @@ class PsqlTestStorage : PsqlStorage() { super.initStorage(config, create, upgrade) } - override fun newAdminMap(config: PgConfig, create: Boolean?, upgrade: Boolean?): PsqlAdminMap { - return PsqlAdminMap(this, config, create, upgrade) + override fun newAdminMap(config: PgConfig, create: Boolean?, upgrade: Boolean?): PsqlAdminCatalog { + return PsqlAdminCatalog(this, config, create, upgrade) } } \ No newline at end of file From 6b7cb81c75f345b8606f800de4a9a33e89c550cd Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Thu, 18 Jun 2026 11:14:44 +0200 Subject: [PATCH 18/57] Fix more issues in PgRows and related. Signed-off-by: Alexander Lowey-Weber --- .../activitylog/ActivityLogHandler.java | 2 +- .../activitylog/ActivityLogHandlerTest.java | 2 +- .../lib/hub/mock/NHAdminReaderMock.java | 2 +- .../kotlin/naksha/jbon/IMemberEncoder.kt | 3 +- .../kotlin/naksha/model/TupleNumber.kt | 2 +- .../kotlin/naksha/model/objects/Member.kt | 14 + .../naksha/model/objects/StandardMembers.kt | 14 + .../kotlin/naksha/model/request/OrderBy.kt | 47 +-- .../naksha/model/request/ReadFeatures.kt | 30 +- .../naksha/model/request/RequestQuery.kt | 24 +- .../naksha/model/request/query/MemberQuery.kt | 3 +- .../kotlin/naksha/psql/PgCatalog.kt | 2 +- .../kotlin/naksha/psql/PgCollection.kt | 12 +- .../commonMain/kotlin/naksha/psql/PgColumn.kt | 2 +- .../kotlin/naksha/psql/PgColumnWithValues.kt | 11 +- .../kotlin/naksha/psql/PgQueryWhereBuilder.kt | 2 +- .../commonMain/kotlin/naksha/psql/PgRows.kt | 321 +++++++++--------- .../commonMain/kotlin/naksha/psql/PgTable.kt | 6 +- .../kotlin/naksha/psql/PgWriterBase.kt | 2 +- .../kotlin/naksha/psql/PgWriterInsert.kt | 4 +- .../kotlin/naksha/psql/PgWriterUpdate.kt | 2 +- .../kotlin/naksha/psql/PgWriterUpsert.kt | 4 +- .../naksha/psql/ReadFeaturesByMetadataTest.kt | 8 +- .../naksha/psql/ReadFeaturesByOtherTns.kt | 2 +- 24 files changed, 281 insertions(+), 240 deletions(-) diff --git a/here-naksha-handler-activitylog/src/jvmMain/java/com/here/naksha/handler/activitylog/ActivityLogHandler.java b/here-naksha-handler-activitylog/src/jvmMain/java/com/here/naksha/handler/activitylog/ActivityLogHandler.java index 65c7548d7..cf356efc3 100644 --- a/here-naksha-handler-activitylog/src/jvmMain/java/com/here/naksha/handler/activitylog/ActivityLogHandler.java +++ b/here-naksha-handler-activitylog/src/jvmMain/java/com/here/naksha/handler/activitylog/ActivityLogHandler.java @@ -177,7 +177,7 @@ private ReadFeatures featuresWhereNextVersionIsOneOf(List tupleNumb ReadFeatures requestPredecessors = new ReadFeatures(); requestPredecessors.setCollectionIds(StringList.of(properties.getSpaceId())); requestPredecessors.setQueryHistory(true); - requestPredecessors.getQuery().setMetadata(nextVersionQuery); + requestPredecessors.getQuery().setMembers(nextVersionQuery); return requestPredecessors; } diff --git a/here-naksha-handler-activitylog/src/jvmTest/java/com/here/naksha/handler/activitylog/ActivityLogHandlerTest.java b/here-naksha-handler-activitylog/src/jvmTest/java/com/here/naksha/handler/activitylog/ActivityLogHandlerTest.java index 71acace3c..d1a87dd57 100644 --- a/here-naksha-handler-activitylog/src/jvmTest/java/com/here/naksha/handler/activitylog/ActivityLogHandlerTest.java +++ b/here-naksha-handler-activitylog/src/jvmTest/java/com/here/naksha/handler/activitylog/ActivityLogHandlerTest.java @@ -469,7 +469,7 @@ private boolean isHistoryAwareReadFeatures(ReadRequest readRequest) { } private boolean containsNextVersionMetaQuery(ReadFeatures readFeatures, TupleNumber... expectedTns) { - IMemberQuery metaQuery = readFeatures.getQuery().getMetadata(); + IMemberQuery metaQuery = readFeatures.getQuery().getMembers(); if (!(metaQuery instanceof MemberQuery mq)) return false; boolean basicCheck = mq.getColumn().equals(MetaColumn.nextVersion()) && mq.getOp().equals(AnyOp.IS_ANY_OF); diff --git a/here-naksha-lib-hub/src/jvmTest/java/com/here/naksha/lib/hub/mock/NHAdminReaderMock.java b/here-naksha-lib-hub/src/jvmTest/java/com/here/naksha/lib/hub/mock/NHAdminReaderMock.java index 845ddb20e..98c141210 100644 --- a/here-naksha-lib-hub/src/jvmTest/java/com/here/naksha/lib/hub/mock/NHAdminReaderMock.java +++ b/here-naksha-lib-hub/src/jvmTest/java/com/here/naksha/lib/hub/mock/NHAdminReaderMock.java @@ -101,7 +101,7 @@ public NHAdminReaderMock(final @NotNull Map getFeatures(List collectionIds, List featureIds, RequestQuery query) { - if (query.getProperties() != null || query.getMetadata() != null || !query.getRefTiles().isEmpty()) { + if (query.getProperties() != null || query.getMembers() != null || !query.getRefTiles().isEmpty()) { throw new NakshaException(new NakshaError(NakshaError.ILLEGAL_ARGUMENT, "Mock supports only tags and spatial query")); } final List allFeaturesFromCollections = getAllFeaturesFromCollections(collectionIds); diff --git a/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/IMemberEncoder.kt b/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/IMemberEncoder.kt index 08fc1a90d..9d8d6285a 100644 --- a/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/IMemberEncoder.kt +++ b/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/IMemberEncoder.kt @@ -8,8 +8,7 @@ package naksha.jbon */ fun interface IMemberEncoder { /** - * @param path The current path segments (root is empty). The current key/index is at - * `path[pathEnd - 1]` when `pathEnd > 0`. + * @param path The current path segments (root is empty). The current key/index is at `path[pathEnd - 1]` when `pathEnd > 0`. * @param pathEnd The amount of valid path segments in [path]. * @param value The value that is about to be encoded. * @return members-book index (>= 0) or -1 to continue normal encoding. diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumber.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumber.kt index e7b9904f1..25312c047 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumber.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumber.kt @@ -31,7 +31,7 @@ import kotlin.jvm.JvmStatic * urn:naksha:tn:{database-number}:{catalog-number}:{collection-number}:{feature-number}:{version} * ``` * - * - There are no two [tuples][Tuple] with the same [tuple-number][TupleNumber]; world-wide.\ + * There are no two [tuple][Tuple] with the same [tuple-number][TupleNumber]; world-wide. * @since 3.0 */ @JsExport diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt index 0a007d81b..d6b13fb82 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt @@ -11,6 +11,7 @@ import naksha.base.NullableProperty import naksha.base.Proxy import naksha.geo.SpGeometry import naksha.model.Naksha +import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_ARGUMENT import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_STATE import naksha.model.NakshaException import naksha.model.TupleNumber @@ -44,6 +45,7 @@ class Member() : AnyObject(), Comparator { */ @JsName("of") constructor(name: String, dataType: MemberType = MemberType.STRING, path: JsonPath? = null) : this() { + Naksha.verifyInternalId(name) this.name = name this.dataType = dataType this.path = path ?: JsonPath(listOf("properties", name)) @@ -60,6 +62,7 @@ class Member() : AnyObject(), Comparator { */ @JsName("relocate") constructor(origin: Member, path: JsonPath) : this() { + if (origin.isVirtual()) throw NakshaException(ILLEGAL_ARGUMENT, "Virtual members can't be copied") this.name = origin.name this.dataType = origin.dataType this.path = path @@ -147,6 +150,17 @@ class Member() : AnyObject(), Comparator { /** True if this member is mandatory. */ fun isMandatory(): Boolean = mandatory + internal var virtual: Boolean = false + internal fun withVirtual(): Member { + this.virtual = true + return this + } + + /** + * True if this member is a virtual member. There are only + */ + fun isVirtual(): Boolean = mandatory + /** Remove [mandatory] from the underlying map; returns this for chaining. */ internal fun removeMandatory(): Member { removeRaw("mandatory") diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt index e487dad85..72b5a812d 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt @@ -33,6 +33,20 @@ class StandardMembers private constructor() { @JvmField @JsStatic val Tn = Member("_tn", MemberType.TUPLE_NUMBER, JsonPath("tn")).withMandatory() + /** + * Virtual member that refers to the feature-number of the [tuple-number][Tn], the only purpose is for queries and result ordering. + * @since 3.0 + */ + @JvmField @JsStatic + val FeatureNumber = Member("_fn", MemberType.INT64, null).withMandatory().withVirtual() + + /** + * Virtual member that refers to the version of the [tuple-number][Tn], the only purpose is for queries and result ordering. + * @since 3.0 + */ + @JvmField @JsStatic + val Version = Member("_version", MemberType.INT64, null).withMandatory().withVirtual() + /** * `_nv` — **next-version** (`INT64`). The version at which this tuple was superseded by the next state. Present only in _HISTORY_; in _HEAD_ the value is intrinsically the current [HEAD version][naksha.model.Version.HEAD]. * @since 3.0 diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/OrderBy.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/OrderBy.kt index eb0fce628..ebe1d5bad 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/OrderBy.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/OrderBy.kt @@ -5,10 +5,10 @@ package naksha.model.request import naksha.base.NotNullEnum import naksha.base.NullableProperty import naksha.base.AnyObject -import naksha.model.request.query.MetaColumn +import naksha.model.objects.Member +import naksha.model.objects.StandardMembers import naksha.model.request.query.SortOrder import naksha.model.request.query.SortOrder.SortOrderCompanion.ANY -import naksha.model.request.query.SortOrder.SortOrderCompanion.DESCENDING import kotlin.js.JsExport import kotlin.js.JsName import kotlin.js.JsStatic @@ -18,27 +18,23 @@ import kotlin.jvm.JvmStatic /** * Describes a sort order in a [result-set][naksha.model.request.IResultSet]. * - * **Warning**: Using custom ordering may not be supported by the storage. The best is to only use the pre-defined sort orders: - * - [deterministic] - * - [version] - * - [id] - * - [author] - * - * @constructor Creating an ordering, where the details + * @constructor Create an ordering. */ @JsExport class OrderBy() : AnyObject() { /** - * Create a new order. - * @param column the column by which to order by, if _null_, any column is okay, just a deterministic order is requested. + * Create an order. + * + * If [member] is `null`, [next] must be `null` as well. + * @param member the member by which to order by, if _null_, any member is accepted, only a deterministic order is requested. * @param order the sort order, if [ANY][SortOrder.ANY] is given, then the storage can pick whatever is faster. - * @param next if a second-level order is requested, for example order by `id` and then by `txn`, and finally by `uid`. + * @param next if a second-level order is requested; i.e. order by `id`, then by `version`. */ @JsName("of") @JvmOverloads - constructor(column: MetaColumn?, order: SortOrder = ANY, next: OrderBy? = null) : this() { - this.column = column + constructor(member: Member?, order: SortOrder = ANY, next: OrderBy? = null) : this() { + this.column = member this.sortOrder = order this.next = next } @@ -56,44 +52,37 @@ class OrderBy() : AnyObject() { */ @JsStatic @JvmStatic - fun version(): OrderBy = OrderBy(MetaColumn.version()) + fun version(): OrderBy = OrderBy(StandardMembers.Version) /** * Supported ordering by `tuple-number` _(so by storage, map, collection, feature, version, uid). */ @JsStatic @JvmStatic - fun tupleNumber(): OrderBy = OrderBy(column=MetaColumn.tupleNumber()) + fun tupleNumber(): OrderBy = OrderBy(StandardMembers.Tn) /** * Supported ordering by `id` and `version`. */ @JsStatic @JvmStatic - fun id(): OrderBy = OrderBy(MetaColumn.id(), next = version()) - - /** - * Supported ordering by `author`, `updatedAt`, `id`, and `version`. - */ - @JsStatic - @JvmStatic - fun author(): OrderBy = OrderBy(MetaColumn.author(), next = OrderBy(MetaColumn.updatedAt(), DESCENDING, id())) + fun id(): OrderBy = OrderBy(StandardMembers.Id, next = version()) - private val COLUMN_OR_NULL = NullableProperty(MetaColumn::class) + private val MEMBER_OR_NULL = NullableProperty(Member::class) private val SORT_ORDER = NotNullEnum(SortOrder::class) { _, _ -> ANY } private val NEXT_OR_NULL = NullableProperty(OrderBy::class) } /** - * The [MetaColumn] by which to order, if `null`, then deterministic ordering is requested. + * The [Member] by which to order, if `null`, then deterministic ordering is requested. * @since 3.0 */ - var column by COLUMN_OR_NULL + var column: Member? by MEMBER_OR_NULL /** * @see [column] */ - fun withColumn(value: MetaColumn?): OrderBy { + fun withMember(value: Member?): OrderBy { column = value return this } @@ -113,7 +102,7 @@ class OrderBy() : AnyObject() { } /** - * Optionally next order, so after ordering by this [MetaColumn], order those that are equal by the given next one. If `null`, the order will switch to just be deterministic, when the [MetaColumn] values are equal so far _(internally storages are recommended to use the [TupleNumber][naksha.model.TupleNumber] to the final ordering)_. + * Optionally next order, so after ordering by this [Member], order those that are equal by the given next one. If `null`, the order will switch to just be deterministic, when the [Member] values are equal so far _(internally storages are recommended to use the [TupleNumber][naksha.model.TupleNumber] to the final ordering)_. * @since 3.0 */ var next by NEXT_OR_NULL diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadFeatures.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadFeatures.kt index 498aa53ed..41d342c1c 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadFeatures.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadFeatures.kt @@ -23,10 +23,7 @@ open class ReadFeatures : ReadRequest() { companion object ReadFeatures_C { private val STRING_OR_NULL = NullableProperty(String::class) - private val BOOLEAN_OR_FALSE = - NotNullProperty(Boolean::class) { _, _ -> false } - private val BOOLEAN_OR_TRUE = - NotNullProperty(Boolean::class) { _, _ -> true } + private val BOOLEAN_OR_FALSE = NotNullProperty(Boolean::class) { _, _ -> false } private val INT_OR_1 = NotNullProperty(Int::class) { _, _ -> 1 } private val VERSION_OR_NULL = NullableProperty(Version::class) private val STRING_LIST = NotNullProperty(StringList::class) @@ -131,7 +128,7 @@ open class ReadFeatures : ReadRequest() { /** * Extend the request to include features that are in a deleted state _(defaults to `false`)_. */ - var queryDeleted by BOOLEAN_OR_FALSE + var queryDeleted: Boolean by BOOLEAN_OR_FALSE /** * Extend the request to search through historic states of features _(defaults to `false`)_. @@ -140,7 +137,7 @@ open class ReadFeatures : ReadRequest() { * [versions] is greater than `1`, results are ordered automatically by the storage in reverse * version order, so the most recent state is returned first. */ - var queryHistory by BOOLEAN_OR_FALSE + var queryHistory: Boolean by BOOLEAN_OR_FALSE /** * Defines how many states (versions) of each matching feature should be returned _(defaults to `1`)_. @@ -158,7 +155,7 @@ open class ReadFeatures : ReadRequest() { * Requesting multiple versions can have a significant performance impact and should be used with care. * @since 3.0.0 */ - var versions by INT_OR_1 + var versions: Int by INT_OR_1 /** * Limit the read to all states at or after the given minimum version, `null` if no limit. @@ -167,7 +164,8 @@ open class ReadFeatures : ReadRequest() { * will be rejected with [ILLEGAL_ARGUMENT][naksha.model.NakshaError.ILLEGAL_ARGUMENT]. * @since 3.0.0 */ - var minVersion by VERSION_OR_NULL + // TODO: Change to Int64 aka Long! + var minVersion: Version? by VERSION_OR_NULL /** * Limit the read to states at or before the given maximum version, `null` if no limit @@ -180,21 +178,23 @@ open class ReadFeatures : ReadRequest() { * will be rejected with [ILLEGAL_ARGUMENT][naksha.model.NakshaError.ILLEGAL_ARGUMENT]. * @since 3.0.0 */ - var version by VERSION_OR_NULL + // TODO: Change to Int64 aka Long! + var version: Version? by VERSION_OR_NULL /** * Order the result-set like given; this is an expensive operation and should be avoided. * * If an order is required, but no specific one, then it is strongly recommended to stick with the [deterministic order][OrderBy.deterministic], which is produced by creating a blank empty [OrderBy] object or through the static helper method [OrderBy.deterministic]. Ordering by anything else can have a drastic performance impact. */ - var orderBy by ORDER_BY_OR_NULL + var orderBy: OrderBy? by ORDER_BY_OR_NULL /** * Add all features that match the given IDs into the result-set. + * + * If more complex queries are need, please use a [MemberQuery][naksha.model.request.query.MemberQuery], see [query]. * @since 3.0.0 */ - //TODO CASL-1149 should support custom queries - var featureIds by STRING_LIST + var featureIds: StringList by STRING_LIST /** * Add all features that match the given [GUIDs][naksha.model.Guid] into the result-set. @@ -202,13 +202,15 @@ open class ReadFeatures : ReadRequest() { * This can be used to load features in specific states. * @since 3.0.0 */ - var guids by GUID_LIST + // TODO: We should replace this with `tupleNumbers`, because that is what we will encode into `uuid` and that is what the clients need. + // Is there any use-case for the GUID any longer? + var guids: GuidList by GUID_LIST /** * Add all features that match the given query into the result-set. * @since 3.0.0 */ - var query by QUERY + var query: RequestQuery by QUERY /** * Tests whether this request is effectively a query for all features in their current **HEAD** state, diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/RequestQuery.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/RequestQuery.kt index 4dad4db4e..18f4594ae 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/RequestQuery.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/RequestQuery.kt @@ -35,7 +35,7 @@ open class RequestQuery : AnyObject() { private val SPATIAL_QUERY_OR_NULL = NullableProperty(ISpatialQuery::class) private val TAG_QUERY_OR_NULL = NullableProperty(ITagQuery::class) private val PROPERTIES_QUERY_OR_NULL = NullableProperty(IPropertyQuery::class) - private val METADATA_QUERY_OR_NULL = NullableProperty(IMemberQuery::class) + private val MEMBER_QUERY_OR_NULL = NullableProperty(IMemberQuery::class) } /** @@ -43,6 +43,9 @@ open class RequestQuery : AnyObject() { * @since 3.0.0 * @see ISpatialQuery */ + // TODO: We need to replace this with MemberQueries. + // Actually, in the members we can store multiple geometries, all of them can be searched. + @Deprecated("Use member queries, there can be multiple spatial members that can be searched and combined.") var spatial by SPATIAL_QUERY_OR_NULL /** @@ -50,6 +53,9 @@ open class RequestQuery : AnyObject() { * @since 3.0.0 * @see ITagQuery */ + // TODO: We need to replace this with MemberQueries. + // Actually, in the members we can store multiple tag-like members, all of them can be searched. + @Deprecated("Use member queries, there can be multiple tag-like members that can be searched and combined.") var tags by TAG_QUERY_OR_NULL /** @@ -57,14 +63,20 @@ open class RequestQuery : AnyObject() { * @since 3.0.0 * @see IPropertyQuery */ + // TODO: Remove this completely, we should only allow to actually search for members. + // Not members must be post-filtered by the client, we can offer the helper we have for this case. + // This makes it as well very clear to the client and user, what can found fast, and what will be slow. + @Deprecated("Remove this completely, we only allow to actually search for members.") var properties by PROPERTIES_QUERY_OR_NULL /** - * Search for features matching the given metadata query. + * Search for features matching the given member query. * @since 3.0.0 * @see IMemberQuery */ - var metadata by METADATA_QUERY_OR_NULL + // TODO: Because actually everything boils down to member-queries only, we should move this into the ReadFeatures directly. + // We only need this property in ReadFeaturs, so that clients can defined how indices they have created are used. + var members by MEMBER_QUERY_OR_NULL /** * Search for features that have a reference point in one of the given tiles. @@ -72,6 +84,8 @@ open class RequestQuery : AnyObject() { * If the list is empty, no limit is applied. * @since 3.0.0 */ + // TODO: Remove this completely, clients that need spatial queries should use spatial members. + @Deprecated("Please use spatial members instead") var refTiles by INT_LIST /** @@ -80,6 +94,7 @@ open class RequestQuery : AnyObject() { * @return this. * @since 3.0.0 */ + @Deprecated("Please use spatial members instead") fun addRefTile(tile: HereTile): RequestQuery { refTiles.add(tile.intKey) return this @@ -91,6 +106,7 @@ open class RequestQuery : AnyObject() { * @return this. * @since 3.0.0 */ + @Deprecated("Please use spatial members instead") fun removeRefTile(tile: HereTile): RequestQuery { refTiles.remove(tile.intKey) return this @@ -105,6 +121,6 @@ open class RequestQuery : AnyObject() { && spatial == null && tags == null && properties == null - && metadata == null + && members == null } } \ No newline at end of file diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MemberQuery.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MemberQuery.kt index bc6faadfb..c8c1a05e4 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MemberQuery.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MemberQuery.kt @@ -5,6 +5,7 @@ package naksha.model.request.query import naksha.base.AnyObject import naksha.base.NotNullProperty import naksha.base.NullableProperty +import naksha.model.objects.Member import kotlin.js.JsExport import kotlin.js.JsName @@ -22,7 +23,7 @@ open class MemberQuery() : AnyObject(), IMemberQuery { * @since 3.0 */ @JsName("of") - constructor(column: MetaColumn, op: AnyOp, value: Any? = null) : this() { + constructor(column: Member, op: AnyOp, value: Any? = null) : this() { this.column = column this.op = op this.value = value diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCatalog.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCatalog.kt index 6e5ee83aa..7e37437e9 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCatalog.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCatalog.kt @@ -360,7 +360,7 @@ open class PgCatalog internal constructor( // Read from database val outRows = PgRows().withCollection(collections.head) setSearchPath(conn) - val SQL = """SELECT ${outRows.names()} + val SQL = """SELECT ${outRows.aliases()} FROM ${collections.headTable.quotedName} WHERE id = $1 AND (version & 3) < 2""" val plan = conn.prepare(SQL, arrayOf(PgType.STRING.text)) diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt index d48c0c6ae..f3b1a19dc 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt @@ -271,24 +271,22 @@ open class PgCollection internal constructor( /** * Returns the [PgColumn] that corresponds to the given member. * @param member the [Member] for which to return the column. - * @return the [PgColumn] that corresponds to the given member. - * @throws NakshaException if the given `member` does not have a dedicated [PgColumn]. + * @return the [PgColumn] that with the given name; `null` if no such column exists. * @since 3.0 */ - fun column(member: Member): PgColumn = column(member.name) + fun column(member: Member): PgColumn? = column(member.name) /** * Returns the [PgColumn] that has the given name. * @param name the name of the column to return. - * @return the [PgColumn] that with the given name. - * @throws NakshaException if the given `name` does not have a dedicated [PgColumn]. + * @return the [PgColumn] that with the given name; `null` if no such column exists. * @since 3.0 */ - fun column(name: String): PgColumn { + fun column(name: String): PgColumn? { for (column in columns) { if (column.name == name) return column } - throw NakshaException(INTERNAL_ERROR, "headColumn($name) called, but no such columns exists") + return null } /** diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumn.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumn.kt index d5509b8e1..7c50db741 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumn.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumn.kt @@ -62,7 +62,7 @@ data class PgColumn( * @since 3.0 */ @JvmField - val extra: String?, + val extra: String? = null, /** * The type of the column. diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumnWithValues.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumnWithValues.kt index 06dd8f5ad..6cd6d6848 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumnWithValues.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumnWithValues.kt @@ -13,16 +13,16 @@ internal data class PgColumnWithValues( * The database column. * @since 3.0 */ - val column: PgColumn, + val pgColumn: PgColumn, /** - * The index of the [PgRows], can differ from the physical indexing. + * An optional alias, if the column is mapped to a different name in the result-set. * @since 3.0 */ - val index: Int = column.index, + val alias: String = pgColumn.name, /** - * When used in [PgRows], the values of the column for each row. + * The values of the column for each row. * @since 3.0 */ val values: AnyList = AnyList() @@ -32,6 +32,7 @@ internal data class PgColumnWithValues( return this } fun anyValues(): MutableList = values + /** Returns all values of this column as array. */ fun anyArray(): Array = values.toArray() fun intValues(): MutableList = values as MutableList fun intArray(): Array = values.toArray() as Array @@ -43,4 +44,6 @@ internal data class PgColumnWithValues( fun stringArray(): Array = values.toArray() as Array fun byteArrayValues(): MutableList = values as MutableList fun byteArrayArray(): Array = values.toArray() as Array + + override fun toString(): String = alias } \ No newline at end of file diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt index 75f92e1e6..4f3a998e2 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt @@ -248,7 +248,7 @@ internal class PgQueryWhereBuilder(private val request: ReadFeatures) { } private fun whereMetadata() { - val metaQuery = request.query.metadata + val metaQuery = request.query.members if (metaQuery != null) { if (where.isNotEmpty()) { where.append(" AND (") diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRows.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRows.kt index 5927618db..8887e2c00 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRows.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRows.kt @@ -1,15 +1,13 @@ package naksha.psql import naksha.base.Int64 +import naksha.geo.SpGeometry import naksha.jbon.BookType import naksha.jbon.HeapBook import naksha.model.* -import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_ARGUMENT import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_STATE -import naksha.model.NakshaError.NakshaErrorCompanion.INTERNAL_ERROR -import naksha.model.objects.Member -import naksha.model.objects.NakshaCollection -import naksha.model.objects.StandardMembers +import naksha.model.objects.MemberType +import naksha.model.objects.StandardMembers.StandardMembers_C.Feature /** * Helper class to convert rows into arrays of column-data and vice versa. The main purpose is to read and write full tuples, but it supports basically as well virtual columns. @@ -21,13 +19,12 @@ internal class PgRows { * @since 3.0 */ val columns = mutableListOf() - internal val columnByName = mutableMapOf() - private var names: String? = null + private var aliases: String? = null private var namesAggregate: String? = null private var placeholders: String? = null private var arrayTypeNames: Array? = null private fun clearCache(): PgRows { - names = null + aliases = null namesAggregate = null placeholders = null arrayTypeNames = null @@ -48,70 +45,48 @@ internal class PgRows { } } - fun withMinSize(size: Int): PgRows { - if (this.size < size) this.size = size - return this - } - /** - * If set to true, then the [StandardMembers.NextVersion] is not added when setting the [collection] or calling [withCollection]. + * Ensures that in all columns have at least this amount of values, if too short, adds `null` values until the minimal size is reached. * @since 3.0 */ - var isHead: Boolean = false - - /** - * Disables the [StandardMembers.NextVersion], which does not exist in the _HEAD_ table. - * @param useHead if the _HEAD_ table is used; defaults to _true_. - * @since 3.0 - */ - fun useHeadTable(useHead: Boolean = true): PgRows { - isHead = useHead + fun withMinRows(rowsCount: Int): PgRows { + if (this.size < rowsCount) { + this.size = rowsCount + for (column in columns) { + column.values.size = rowsCount + } + } return this } /** - * When set, clear the [columns] and add all columns of the given [PgCollection]. + * When set, clear the [columns], add all columns of the given [PgCollection], and set the [collectionNumber], [catalogNumber], and [databaseNumber]. Eventually this will read all columns, so a full member-book, from a specific database table, no matter if from _HISTORY_ or _HEAD_. * @since 3.0 */ var collection: PgCollection? = null set(collection) { if (collection != null) { clearCache() + columns.clear() collectionNumber = collection.collectionNumber catalogNumber = collection.catalog.catalogNumber databaseNumber = collection.catalog.storage.number - - val members = collection.useMembers() - if (!members.isSortedByIndex()) throw NakshaException(ILLEGAL_ARGUMENT, "The members of the given collection are not sorted by index") - // We add the internal `~fn` (feature-number) and `~version` first. - columns.clear() - var index = 0 - columns.add(PgColumnWithValues(index++, "~fn", PgType.INT64)) - columns.add(PgColumnWithValues(index++, "~version", PgType.INT64)) - for (i in 0 ..< members.size) { - val member = members[i] ?: throw NakshaException(INTERNAL_ERROR, "The member at index $i is null; this must not happen") - if (i != member.index) throw NakshaException(INTERNAL_ERROR, "The member at index $i has an member-index $index; this must not happen, expected $i") - // We store the tuple-number in the "~fn" and "~version" columns! - if (StandardMembers.Tn.name == member.name) continue - // In the HEAD table there is no next-version! - if (isHead && StandardMembers.NextVersion.name == member.name) continue - // Everything else as declared. - columns.add(PgColumnWithValues(index++, member.name, PgType.ofMemberType(member))) + for (pgColumn in collection.columns) { + columns.add(PgColumnWithValues(pgColumn)) } } field = collection } /** - * Add the members of the given [NakshaCollection] to the row-set. - * - * The members of the given collection must be sorted by index. - * @param collection the [NakshaCollection] of which to add the members. + * Clear the members, then add all members of the given [PgCollection] to the row-set. + * @param col the [PgCollection] of which to add the members. * @return this + * @see [collection] * @since 3.0 */ - fun withCollection(collection: NakshaCollection): PgRows { - this.collection = collection + fun withCollection(col: PgCollection): PgRows { + this.collection = col return this } @@ -169,43 +144,65 @@ internal class PgRows { return this } - fun addColumn(name: String, type: PgType): PgRows { + fun addColumn(column: PgColumn, alias: String = column.name): PgRows { + clearCache() + val existing = getColumn(alias) + if (existing == null) { + val column = PgColumnWithValues(column, alias).withSize(size) + columns.add(column) + withMinRows(size) + } + return this + } + fun addColumn(alias: String, type: MemberType): PgRows { clearCache() - val existing = columnByName[name] + val existing = getColumn(alias) if (existing == null) { - val column = PgColumnWithValues(columns.size, name, type).withSize(size) + val column = PgColumnWithValues(PgColumn(-1, alias, type)).withSize(size) columns.add(column) - columnByName[column.name] = column + withMinRows(size) } return this } - fun getColumn(name: String): PgColumnWithValues? = columnByName[name] + fun getColumn(alias: String): PgColumnWithValues? { + for (column in columns) if (column.alias == alias) return column + return null + } fun getColumn(index: Int): PgColumnWithValues? = if (index in 0 until columns.size) columns[index] else null - fun hasColumn(name: String): Boolean = getColumn(name) != null + fun hasColumn(alias: String): Boolean = getColumn(alias) != null fun hasColumn(index: Int): Boolean = getColumn(index) != null - fun getAny(row: Int, columnName: String): Any? = columnByName[columnName]?.values?.get(row) - fun getInt(row: Int, columnName: String): Int? { - val value = getAny(row, columnName) - return if (value is Int) value else null - } - fun getInt64(row: Int, columnName: String): Int64? { - val value = getAny(row, columnName) - return if (value is Int64) value else null - } - fun getDouble(row: Int, columnName: String): Double? { - val value = getAny(row, columnName) - return if (value is Double) value else null + fun getAny(row: Int, alias: String): Any? = getColumn(alias)?.values?.get(row) + fun getInt(row: Int, alias: String): Int? = getAny(row, alias) as? Int + fun getInt64(row: Int, alias: String): Int64? = getAny(row, alias) as Int64? + fun getDouble(row: Int, alias: String): Double? = getAny(row, alias) as Double? + fun getString(row: Int, alias: String): String? = getAny(row, alias) as String? + fun getByteArray(row: Int, alias: String): ByteArray? = getAny(row, alias) as ByteArray? + fun getSpatial(row: Int, alias: String): SpGeometry? { + val raw = getByteArray(row, alias) ?: return null + return try { + Naksha.decodeGeometry(raw) + } catch (_: Exception) { + null + } } - fun getString(row: Int, columnName: String): String? { - val value = getAny(row, columnName) - return if (value is String) value else null + fun getTags(row: Int, alias: String): TagMap? { + val raw = getString(row, alias) ?: return null + return try { + Naksha.decodeTags(raw) + } catch (_: Exception) { + null + } } - fun getByteArray(row: Int, columnName: String): ByteArray? { - val value = getAny(row, columnName) - return if (value is ByteArray) value else null + fun getTagList(row: Int, alias: String): TagList? { + val raw = getString(row, alias) ?: return null + return try { + Naksha.decodeTagList(raw) + } catch (_: Exception) { + null + } } fun getB64(row: Int, columnName: String, featureNumber: Int64): TupleNumber? { val raw = getByteArray(row, columnName) ?: return null @@ -237,25 +234,23 @@ internal class PgRows { * @since 3.0 */ fun getTuple(row: Int): Tuple? { - if (row < 0 || row >= size) return null + if (row !in 0..< size) return null val collection = this.collection ?: return null - val members = collection.members ?: return null val membersBook = HeapBook(BookType.MEMBER_BOOK) var featureBytes: ByteArray? = null - for (i in 0 until members.size) { - val member: Member = members[i] ?: throw NakshaException(ILLEGAL_STATE, "Member #$i of collection ${collection.id} is null") - val name = member.name - val column: PgColumnWithValues = getColumn(name) ?: throw NakshaException(ILLEGAL_STATE, "Missing member '$name' at index $i of collection ${collection.id}") + for (column in columns) { val value = column.values[row] - if (StandardMembers.Feature.name == name) { + if (Feature.name == column.pgColumn.name) { // Special case, root feature. if (value !is ByteArray) throw NakshaException(ILLEGAL_STATE, "The feature root is no byte-array") featureBytes = value } else { - membersBook.put(name, value) + val pgColumn = collection.column(column.pgColumn.name) + if (pgColumn != null) membersBook.put(pgColumn.name, value) + // else: We have and additional row in the result set that we can ignore for tuple fetching. } } - if (featureBytes == null) throw NakshaException(ILLEGAL_STATE, "Missing mandatory member '${StandardMembers.Feature.name}'!") + if (featureBytes == null) throw NakshaException(ILLEGAL_STATE, "Missing mandatory member '${Feature.name}'!") return Tuple(featureBytes = featureBytes, membersBook = membersBook) } @@ -264,7 +259,7 @@ internal class PgRows { fun set(row: Int, columnName: String, value: Any?): Boolean { val column = getColumn(columnName) if (column != null) { - withMinSize(row) + withMinRows(row) column.values[row] = value return true } @@ -272,50 +267,52 @@ internal class PgRows { } operator fun set(row: Int, tuple: Tuple) { - withMinSize(row) + withMinRows(row) val membersBook = tuple.membersBook val END = membersBook.namesLength() for (i in 0 until END) { val memberName = membersBook.getNameAt(i) ?: continue val column = getColumn(memberName) ?: continue val value = membersBook[memberName] - set(row, column.name, value) + column.values[row] = value } + val featureColumn = getColumn(Feature.name) ?: return + featureColumn.values[row] = tuple.featureBytes } + /** + * Copies the columns from the cursor to the given position. The method does nothing, if the given cursor is not positioned at a row ([PgCursor.isRow] is _false_). + * @param row the row-number to set, if the given cursor is at a valid row. + * @param cursor the cursor to read from. + * @since 3.0 + */ operator fun set(row: Int, cursor: PgCursor) { - withMinSize(row) - for (column in columns) { - if (cursor.contains(column.name)) { - val value = cursor.column(column.name) - column.values[row] = value - } + if (!cursor.isRow()) return + withMinRows(row) + val columnNames = cursor.columnNames() + for (columnName in columnNames) { + val column = getColumn(columnName) ?: continue + val value = cursor.column(columnName) + column.values[row] = value } } /** - * Add the current row of the cursor. + * Add the current row of the cursor to the end of the rows list. * @param cursor the cursor from which to read. - * @return `true` if a rows was read; `false` if the cursor is not at a valid row. + * @return `true` if a rows was read; `false` if the cursor is not at a valid row _([PgCursor.isRow] is _false_). * @since 3.0 */ fun add(cursor: PgCursor): Boolean { if (cursor.isRow()) { - val row = size - size += 1 - for (column in columns) { - if (cursor.contains(column.name)) { - val value = cursor.column(column.name) - column.values[row] = value - } - } + set(size ,cursor) return true } return false } /** - * Read all rows from cursor, expects the cursor to be at first result, usage: + * Read all rows from cursor to the end of the rows, expects the cursor to be at first result, usage: * ```kotlin * plan.execute(queryValues).fetch().use { resultRows.addAll(it) } * ``` @@ -326,64 +323,67 @@ internal class PgRows { return this } - /** - * Read all rows from cursor, expects the cursor to be at first result and that for each column, there is an array of values, so an aggregate generated via `ARRAY_AGG`. - * @since 3.0 - */ - fun addAggregated(cursor: PgCursor): PgRows { - if (cursor.isRow()) { - for (column in columns) { - if (cursor.contains(column.name)) { - val values = cursor.column(column.name) - if (values is Array<*>) { - withMinSize(values.size) - for (i in 0 until values.size) { - set(i, column.name, values[i]) - } - } - } - } - } - return this - } - - /** - * Returns the names of all columns as comma separated string, surrounded with aggregation instruction, _(like `ARRAY_AGG(id)`)_, usage: - * - * ```kotlin - * val rows = PgColumnRows().addColumns(allColumns) - * val SQL = """SELECT ${rows.namesAggregate()} - * FROM "naksha~admin".${collections.head.quotedName} - * WHERE id = ANY($1)""" - * val plan = conn.prepare(SQL, rows.typeNames()) - * val cursor = plan.execute(rows.valuesExecutable()) - * ``` - * - * @return the names of all columns as comma separated string, surrounded with aggregation instruction. - * @since 3.0 - */ - fun namesAggregate(): String { - val cached = this.namesAggregate - if (cached != null) return cached - val names = columns.joinToString(", ") { - val q = PgUtil.quoteIdent(it.name) - "ARRAY_AGG($q) AS $q" - } - this.namesAggregate = names - return names - } +// /** +// * Read all rows from cursor, expects the cursor to be at first result and that for each column, there is an array of values, so an aggregate generated via `ARRAY_AGG`. +// * @since 3.0 +// */ +// fun addAggregated(cursor: PgCursor): PgRows { +// if (cursor.isRow()) { +// for (column in columns) { +// if (cursor.contains(column.name)) { +// val values = cursor.column(column.name) +// if (values is Array<*>) { +// withMinRows(values.size) +// for (i in 0 until values.size) { +// set(i, column.name, values[i]) +// } +// } +// } +// } +// } +// return this +// } + +// /** +// * Returns the names of all columns as comma separated string, surrounded with aggregation instruction, _(like `ARRAY_AGG(id)`)_, usage: +// * +// * ```kotlin +// * val rows = PgColumnRows().addColumns(allColumns) +// * val SQL = """SELECT ${rows.aliasesAggregate()} +// * FROM "naksha~admin".${collections.head.quotedName} +// * WHERE id = ANY($1)""" +// * val plan = conn.prepare(SQL, rows.typeNames()) +// * val cursor = plan.execute(rows.valuesExecutable()) +// * ``` +// * +// * @return the aliases of all columns as comma separated string, surrounded with aggregation instruction, example: +// * ```sql +// * ARRAY_AGG("foo") AS "foo", ARRAY_AGG("bar") AS "bar", ... +// * ``` +// * @since 3.0 +// */ +// fun aliasesAggregate(): String { +// val cached = this.namesAggregate +// if (cached != null) return cached +// val names = columns.joinToString(", ") { +// val q = PgUtil.quoteIdent(it.alias) +// "ARRAY_AGG($q) AS $q" +// } +// this.namesAggregate = names +// return names +// } /** - * Returns the names of all columns as comma separated string. - * @return the names of all columns as comma separated string. + * Returns the aliases of all columns as comma separated string, optionally quoted. + * @return the aliases of all columns as comma separated string. * @since 3.0 */ - fun names(): String { - val cached = this.names - if (cached != null) return cached - val names = columns.joinToString(", ") { PgUtil.quoteIdent(it.name) } - this.names = names - return names + fun aliases(): String { + var aliases = this.aliases + if (aliases != null) return aliases + aliases = columns.joinToString(", ") { PgUtil.quoteIdent(it.alias) } + this.aliases = aliases + return aliases } /** @@ -402,9 +402,14 @@ internal class PgRows { * @since 3.0 */ fun placeholders(): String { - val cached = this.placeholders - if (cached != null) return cached - val placeholders = columns.joinToString(", ") { "\$${(it.index + 1)}" } + var placeholders = this.placeholders + if (placeholders != null) return placeholders + val sb = StringBuilder() + for (i in 0 ..< columns.size) { + if (!sb.isEmpty()) sb.append(',') + sb.append('$').append(i) + } + placeholders = sb.toString() this.placeholders = placeholders return placeholders } @@ -424,7 +429,7 @@ internal class PgRows { * @return the array type-names of all columns. * @since 3.0 */ - fun typeNames(): Array = Array(columns.size) { columns[it].type.text + "[]" } + fun typeNames(): Array = Array(columns.size) { columns[it].pgColumn.pgType.text + "[]" } /** * Returns the values of all columns cast to a type that is supported by [PgPlan.execute], usage: diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgTable.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgTable.kt index 50766bdcd..366fd556b 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgTable.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgTable.kt @@ -239,8 +239,7 @@ abstract class PgTable( } /** - * Maps a [PgType] to the same sort-order bucket used by [PgMemberHelper_C.columnSortOrder], - * so that standard and custom columns can be interleaved into the correct type-alignment group. + * Maps a [PgType] to a sort-order. */ private fun pgTypeSortOrder(type: PgType): Int = when (type) { PgType.INT64 -> 0 // INT64 @@ -251,7 +250,8 @@ abstract class PgTable( PgType.BOOLEAN -> 6 // BOOLEAN PgType.STRING -> 7 // STRING PgType.BYTE_ARRAY -> 8 // BYTE_ARRAY / SPATIAL - else -> 11 + PgType.JSONB -> 9 // JSONB, aka TAGS and SET + else -> 10 // should not happen! } /** diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterBase.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterBase.kt index fe3d45f27..361cfa301 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterBase.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterBase.kt @@ -76,7 +76,7 @@ internal abstract class PgWriterBase protected constructor( .withDatabaseNumber(storageNumber) .withCatalogNumber(mapNumber) .withCollectionNumber(collectionNumber) - .withMinSize(writes.size) + .withMinRows(writes.size) /** * Generates a live mapping between the write instructions and the partition-index into which they will write. diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterInsert.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterInsert.kt index d6ccee787..bb0291c10 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterInsert.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterInsert.kt @@ -41,7 +41,7 @@ internal class PgWriterInsert(writer: PgWriter, collection: PgCollection, partit val insert_into_history = if (historyTable != null && collection.head.storeHistory == StoreMode.ON) historyTable else null val new_row = """WITH new_row AS ( - SELECT * FROM UNNEST(${inRows.placeholders()}) AS t(${inRows.names()}) + SELECT * FROM UNNEST(${inRows.placeholders()}) AS t(${inRows.aliases()}) )""" // Detect any existing tombstone in HEAD for the same id (auto-purge target). @@ -78,7 +78,7 @@ internal class PgWriterInsert(writer: PgWriter, collection: PgCollection, partit // Plain INSERT for features that have no tombstone in HEAD (the normal case). val head_inserted = """, head_inserted AS ( - INSERT INTO ${headTable.quotedName} (${inRows.names()}) + INSERT INTO ${headTable.quotedName} (${inRows.aliases()}) SELECT * FROM new_row WHERE new_row.id NOT IN (SELECT id FROM head_tombstone) RETURNING id, fn, version diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt index a87c07182..dfae13d03 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt @@ -42,7 +42,7 @@ internal class PgWriterUpdate(writer: PgWriter, collection: PgCollection, partit // All input provided by client (the updates) val query = """WITH new_row AS ( - SELECT * FROM UNNEST(${inRows.placeholders()}) AS t(${inRows.names()}) + SELECT * FROM UNNEST(${inRows.placeholders()}) AS t(${inRows.aliases()}) )""" val effHead = collection.effectiveHeadColumns diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt index 922f3001b..f981afb51 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt @@ -38,7 +38,7 @@ internal class PgWriterUpsert(writer: PgWriter, collection: PgCollection, partit // This is what we should INSERT or UPDATE. val new_row = """WITH new_row AS ( - SELECT * FROM UNNEST(${inRows.placeholders()}) AS t(${inRows.names()}) + SELECT * FROM UNNEST(${inRows.placeholders()}) AS t(${inRows.aliases()}) )""" // Select existing. @@ -73,7 +73,7 @@ internal class PgWriterUpsert(writer: PgWriter, collection: PgCollection, partit // Insert new rows for which there was no existing HEAD version. // Sentinel "undefined" on any BYTE_ARRAY column is treated as NULL on insert (no prior value to retain). val head_inserted = """, head_inserted AS ( - INSERT INTO ${headTable.quotedName} (${inRows.names()}) + INSERT INTO ${headTable.quotedName} (${inRows.aliases()}) SELECT ${inRows.columns.joinToString(", ") { col -> val q = PgUtil.quoteIdent(col.name) if (keepableByteCols.any { it.name == col.name }) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByMetadataTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByMetadataTest.kt index 8ca71e6d1..0fe62e5c4 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByMetadataTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByMetadataTest.kt @@ -235,7 +235,7 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { val featuresByAppIdAndAuthor = executeRead(ReadFeatures().apply { mapId = collection.catalogId collectionIds += collection.id - query.metadata = MemberAnd( + query.members = MemberAnd( MemberQuery(MetaColumn.author(), StringOp.EQUALS, author), MemberQuery(MetaColumn.appId(), StringOp.STARTS_WITH, appId.substring(0, 2)) ) @@ -393,7 +393,7 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { val featuresByAppIdAndAuthor = executeRead(ReadFeatures().apply { mapId = collection.catalogId collectionIds += collection.id - query.metadata = MemberOr( + query.members = MemberOr( MemberQuery(MetaColumn.author(), StringOp.EQUALS, "this_is_totally_off"), MemberQuery(MetaColumn.appId(), StringOp.STARTS_WITH, appId.substring(0, 2)) ) @@ -426,7 +426,7 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { queryHistory = true queryDeleted = true query = RequestQuery().apply { - metadata = MemberQuery(MetaColumn.action(), DoubleOp.NE, Action.CREATE.intValue) + members = MemberQuery(MetaColumn.action(), DoubleOp.NE, Action.CREATE.intValue) } } val response = executeRead(getHistoryWithoutUpdates) @@ -456,7 +456,7 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { return executeRead(ReadFeatures().apply { mapId = collection.catalogId collectionIds += collection.id - query.metadata = metaQuery + query.members = metaQuery }) } } \ No newline at end of file diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByOtherTns.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByOtherTns.kt index f4fff91b2..810426a25 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByOtherTns.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByOtherTns.kt @@ -51,7 +51,7 @@ class ReadFeaturesByOtherTns : PgTestBase( val byNextTnResp = executeRead(ReadFeatures().apply { mapId = collection.catalogId collectionIds += collection.id - query.metadata = nextVersionQuery + query.members = nextVersionQuery queryHistory = true }) From b0067422cba8121fd24e55085b6ade0427483cef Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Thu, 18 Jun 2026 12:38:36 +0200 Subject: [PATCH 19/57] Some more fixes. Signed-off-by: Alexander Lowey-Weber --- .../activitylog/ActivityLogHandlerTest.java | 2 +- .../commonMain/kotlin/naksha/model/Naksha.kt | 1 - .../kotlin/naksha/model/objects/Member.kt | 2 +- .../naksha/model/objects/StandardIndices.kt | 13 ++--- .../kotlin/naksha/model/objects/XyzIndices.kt | 6 +-- .../naksha/model/request/PropertyFilter.kt | 48 +++++++++++++------ .../naksha/model/request/query/MemberQuery.kt | 20 ++++---- .../naksha/model/request/query/Property.kt | 13 ++--- .../kotlin/naksha/psql/PgQueryWhereBuilder.kt | 6 +-- .../naksha/psql/ReadFeaturesByMetadataTest.kt | 12 ++--- 10 files changed, 64 insertions(+), 59 deletions(-) diff --git a/here-naksha-handler-activitylog/src/jvmTest/java/com/here/naksha/handler/activitylog/ActivityLogHandlerTest.java b/here-naksha-handler-activitylog/src/jvmTest/java/com/here/naksha/handler/activitylog/ActivityLogHandlerTest.java index d1a87dd57..6b3033cc4 100644 --- a/here-naksha-handler-activitylog/src/jvmTest/java/com/here/naksha/handler/activitylog/ActivityLogHandlerTest.java +++ b/here-naksha-handler-activitylog/src/jvmTest/java/com/here/naksha/handler/activitylog/ActivityLogHandlerTest.java @@ -471,7 +471,7 @@ private boolean isHistoryAwareReadFeatures(ReadRequest readRequest) { private boolean containsNextVersionMetaQuery(ReadFeatures readFeatures, TupleNumber... expectedTns) { IMemberQuery metaQuery = readFeatures.getQuery().getMembers(); if (!(metaQuery instanceof MemberQuery mq)) return false; - boolean basicCheck = mq.getColumn().equals(MetaColumn.nextVersion()) + boolean basicCheck = mq.getMember().equals(MetaColumn.nextVersion()) && mq.getOp().equals(AnyOp.IS_ANY_OF); if (!basicCheck) return false; if (expectedTns.length == 0) return mq.getValue() != null; diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt index 8c678ceaf..413c31d52 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt @@ -201,7 +201,6 @@ class Naksha private constructor() { return id!! } - @JsStatic @JvmStatic private fun verifyId(id: String?, internal: Boolean, throwOnError: Boolean): Boolean { if (id.isNullOrEmpty()) { diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt index d6b13fb82..da72a96a8 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt @@ -34,7 +34,7 @@ import kotlin.js.JsName * @since 3.0 */ @JsExport -class Member() : AnyObject(), Comparator { +open class Member() : AnyObject(), Comparator { /** * Construct a member with a name and the given data type. diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardIndices.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardIndices.kt index f8e00d144..5d0c51471 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardIndices.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardIndices.kt @@ -10,7 +10,7 @@ import kotlin.jvm.JvmField * The canonical, storage-managed indices that every Naksha storage understands. * * These are flavour-independent: the [MANDATORY] indices are always present, the standard optional - * [GistGeometry] indexes the standard [StandardMembers.Geometry] member, and the [SPECIAL] indices + * [Geometry] indexes the standard [StandardMembers.Geometry] member, and the [SPECIAL] indices * are declared explicitly per collection (e.g. `naksha~transactions`). The default index set for a * Data-Hub (XYZ) compatible collection lives in [XyzIndices], the index counterpart of [XyzMembers]. * @since 3.0 @@ -82,18 +82,13 @@ class StandardIndices private constructor() { // ------------------------------------------------------------------------- /** - * `gist_geo` — spatial ([IndexType.SPATIAL]) GIST index over the geometry member - * (WHERE `geo IS NOT NULL`). + * `geo` — spatial ([IndexType.SPATIAL]) GIST index over the geometry member (WHERE `geo IS NOT NULL`). * - * Geometry is a **standard** member (part of the GeoJSON standard, see - * [StandardMembers.Geometry]), so its index is defined here. An index refers to a member by - * its identity (name + type), not by JSON path, so this same index applies to the XYZ - * geometry member ([XyzMembers.XyzGeometry], also named `geo`); [XyzIndices.ALL] references - * it rather than redeclaring it. + * Geometry is a **standard** member (part of the GeoJSON standard, see [StandardMembers.Geometry]). * @since 3.0 */ @JvmField @JsStatic - val GistGeometry = Index("gist_geo", IndexType.SPATIAL, "geo") + val Geometry = Index("geo", IndexType.SPATIAL, "geo") // ------------------------------------------------------------------------- // Special indices — not added automatically; declared explicitly per collection diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzIndices.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzIndices.kt index 2d7e69156..0eee33658 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzIndices.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzIndices.kt @@ -14,7 +14,7 @@ import kotlin.jvm.JvmField * [NakshaCollection.withXyzIndices]. * * An index refers to a member by its identity (name + type), not by JSON path, so indices that - * target a member which is also standard (e.g. the geometry member, see [StandardIndices.GistGeometry]) + * target a member which is also standard (e.g. the geometry member, see [StandardIndices.Geometry]) * are **referenced** from [StandardIndices] rather than redeclared here. The storage-managed indices * that every collection always has live in [StandardIndices.MANDATORY]. * @since 3.0 @@ -131,7 +131,7 @@ class XyzIndices private constructor() { /** * All indices for a default XYZ collection, in declaration order: the [StandardIndices.MANDATORY] * indices (always present), followed by the XYZ default indices, followed by the geometry index - * (referenced from [StandardIndices.GistGeometry], since geometry is a standard member). + * (referenced from [StandardIndices.Geometry], since geometry is a standard member). * * Does **not** include the [StandardIndices.SPECIAL] indices (`pn`/`pt`/`gv`), which are declared * explicitly only where needed (e.g. `naksha~transactions`). @@ -147,7 +147,7 @@ class XyzIndices private constructor() { XyzCustomValue0, XyzCustomValue1, XyzCustomValue2, XyzCustomValue3, XyzCustomString0, XyzCustomString1, XyzCustomString2, XyzCustomString3, XyzReferencePoint, - StandardIndices.GistGeometry, + StandardIndices.Geometry, ) } } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/PropertyFilter.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/PropertyFilter.kt index e7f459886..db403b518 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/PropertyFilter.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/PropertyFilter.kt @@ -3,11 +3,17 @@ package naksha.model.request import naksha.base.AnyList import naksha.base.AnyObject import naksha.base.Platform +import naksha.base.Platform.PlatformCompanion.UNDEFINED +import naksha.base.PlatformList +import naksha.base.PlatformListApi +import naksha.base.PlatformListApi.PlatformListApiCompanion.array_get +import naksha.base.PlatformListApi.PlatformListApiCompanion.array_get_length +import naksha.base.PlatformMap +import naksha.base.PlatformMapApi +import naksha.base.PlatformMapApi.PlatformMapApiCompanion.map_get import naksha.base.PlatformUtil import naksha.base.Proxy -import naksha.model.Naksha -import naksha.model.Naksha.NakshaCompanion.cache -import naksha.model.Naksha.NakshaCompanion.getStorageByNumber +import naksha.model.objects.JsonPath import naksha.model.objects.NakshaFeature import naksha.model.request.query.* @@ -35,8 +41,7 @@ class PropertyFilter(val req: ReadFeatures) : ResultFilter { is POr -> return pQuery.any { resolvePropsQueryOnFeature(it, feature) } is PNot -> return !resolvePropsQueryOnFeature(pQuery.query, feature) is PQuery -> { - val path = pQuery.property.path.filterNotNull() - val propValue = walkPath(feature, path) + val propValue = walkPath(feature, pQuery.property.path) return resolveEachOp(pQuery.op, propValue, pQuery.value) } } @@ -44,31 +49,44 @@ class PropertyFilter(val req: ReadFeatures) : ResultFilter { } /** - * Walk a property path on an object. Returns [Platform.UNDEFINED] if the path does not exist. + * Walk a property path on an object or array. + * @return [Platform.UNDEFINED] if the path does not exist. */ - private fun walkPath(root: Any?, path: List): Any? { - var current: Any? = root + private fun walkPath(objectOrArray: Any?, path: JsonPath): Any? { + var current: Any? = objectOrArray for (key in path) { + if (key == null) return UNDEFINED current = when (current) { is AnyList -> { - val index = key.toIntOrNull() ?: return Platform.UNDEFINED - if (index < 0 || index >= current.size) return Platform.UNDEFINED + val index: Int = (key as Number?)?.toInt() ?: return UNDEFINED + if (index < 0 || index >= current.size) return UNDEFINED current[index] } - is AnyObject -> if (current.containsKey(key)) current[key] else return Platform.UNDEFINED is NakshaFeature -> { val raw = current.getRaw(key) - if (raw === Platform.UNDEFINED) return Platform.UNDEFINED else raw + if (raw === UNDEFINED) return UNDEFINED else raw } - else -> return Platform.UNDEFINED + is AnyObject -> if (current.containsKey(key)) current[key] else return UNDEFINED + is PlatformList -> { + val index: Int = (key as Number?)?.toInt() ?: return UNDEFINED + if (index < 0 || index >= array_get_length(current)) return UNDEFINED + array_get(current, index) + } + is PlatformMap -> { + if (key !is String) return UNDEFINED + map_get(current, key) + } + else -> return UNDEFINED } } + if (current is PlatformList) return current.proxy(AnyList::class) + if (current is PlatformMap) return current.proxy(AnyObject::class) return current } private fun resolveEachOp(op: AnyOp, featureProperty: Any?, queryProperty: Any?) : Boolean { return when (op) { - AnyOp.EXISTS -> featureProperty != Platform.UNDEFINED + AnyOp.EXISTS -> featureProperty != UNDEFINED AnyOp.IS_NULL -> featureProperty == null AnyOp.IS_NOT_NULL -> featureProperty != null AnyOp.IS_TRUE -> featureProperty == true @@ -146,7 +164,7 @@ class PropertyFilter(val req: ReadFeatures) : ResultFilter { return try { Proxy.box(Platform.fromJSON(json), Any::class) } catch (e: Exception) { - Platform.PlatformCompanion.logger.warn("JSON parsing failed for string that appeared to be JSON: $json") + Platform.logger.warn("JSON parsing failed for string that appeared to be JSON: $json") json } } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MemberQuery.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MemberQuery.kt index c8c1a05e4..4c4c22b32 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MemberQuery.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/MemberQuery.kt @@ -10,27 +10,27 @@ import kotlin.js.JsExport import kotlin.js.JsName /** - * A query about a specific [member][naksha.model.objects.Member]. + * Query a [member][naksha.model.objects.Member]. * @since 3.0 */ @JsExport open class MemberQuery() : AnyObject(), IMemberQuery { /** - * Create an initialized property query. - * @param column the column of the metadata to query. + * Create an initialized member query. + * @param member the member to query. * @param op the operation to execute. * @param value the parameter value of the operation. * @since 3.0 */ @JsName("of") - constructor(column: Member, op: AnyOp, value: Any? = null) : this() { - this.column = column + constructor(member: Member, op: AnyOp, value: Any? = null) : this() { + this.member = member this.op = op this.value = value } - companion object PropertyQueryCompanion { - private val COLUMNS = NotNullProperty(MetaColumn::class) + companion object MemberQuery_C { + private val MEMBERS = NotNullProperty(Member::class) private val QUERY_OP = NotNullProperty(AnyOp::class) private val ANY = NullableProperty(Any::class) } @@ -38,15 +38,15 @@ open class MemberQuery() : AnyObject(), IMemberQuery { /** * The column to query. */ - var column by COLUMNS + var member: Member by MEMBERS /** * The operation to execute. */ - var op by QUERY_OP + var op: AnyOp by QUERY_OP /** * The parameter value of the operation; if any. */ - var value by ANY + var value: Any? by ANY } \ No newline at end of file diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/Property.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/Property.kt index 564e46efa..d109d3e01 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/Property.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/Property.kt @@ -2,11 +2,11 @@ package naksha.model.request.query -import naksha.base.NotNullProperty import naksha.base.PlatformListApi.PlatformListApiCompanion.array_get import naksha.base.PlatformListApi.PlatformListApiCompanion.array_get_length -import naksha.base.StringList import naksha.base.fn.Fn1 +import naksha.model.objects.Member +import naksha.model.objects.StandardMembers import kotlin.js.JsExport import kotlin.js.JsName @@ -20,7 +20,7 @@ import kotlin.js.JsName * @see ITagQuery */ @JsExport -open class Property() : MetaColumn(FEATURE) { +open class Property() : Member(StandardMembers.Feature.name) { /** * Create a property from a path given as variable argument list. @@ -43,15 +43,8 @@ open class Property() : MetaColumn(FEATURE) { const val XYZ = "@ns:com:here:xyz" const val TAGS = "tags" - - private val PATH = NotNullProperty(StringList::class) } - /** - * The path inside the feature. - */ - val path by PATH - private var array: Array? = null private var string: String? = null diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt index 4f3a998e2..39bf08fda 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt @@ -278,14 +278,14 @@ internal class PgQueryWhereBuilder(private val request: ReadFeatures) { ) is MemberQuery -> { - val isActionQuery = metaQuery.column == MetaColumn.action() + val isActionQuery = metaQuery.member == MetaColumn.action() val pgColumn = if (isActionQuery) { PgColumn.version } else { - PgColumn.ofRowColumn(metaQuery.column) ?: throw NakshaException( + PgColumn.ofRowColumn(metaQuery.member) ?: throw NakshaException( NakshaError.ILLEGAL_STATE, - "Couldn't find PgColumn for TupleColumn: ${metaQuery.column.name}" + "Couldn't find PgColumn for TupleColumn: ${metaQuery.member.name}" ) } val leftOperand = if (isActionQuery) { diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByMetadataTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByMetadataTest.kt index 0fe62e5c4..6dfd64f02 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByMetadataTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByMetadataTest.kt @@ -32,7 +32,7 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { // And: val featuresByAppId = executeMetaQuery( MemberQuery( - column = MetaColumn.appId(), + member = MetaColumn.appId(), op = StringOp.EQUALS, value = sessionOptions.appId ) @@ -59,7 +59,7 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { // And: val featuresByAppIdPrefix = executeMetaQuery( MemberQuery( - column = MetaColumn.appId(), + member = MetaColumn.appId(), op = StringOp.STARTS_WITH, value = "prefixed_test_app" ) @@ -86,7 +86,7 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { // And: val featuresByAuthor = executeMetaQuery( MemberQuery( - column = MetaColumn.author(), + member = MetaColumn.author(), op = StringOp.EQUALS, value = sessionOptions.author ) @@ -113,7 +113,7 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { // And: val featuresByAuthorPrefix = executeMetaQuery( MemberQuery( - column = MetaColumn.author(), + member = MetaColumn.author(), op = StringOp.STARTS_WITH, value = "Jacky" ) @@ -137,7 +137,7 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { // And: val featuresById = executeMetaQuery( MemberQuery( - column = MetaColumn.id(), + member = MetaColumn.id(), op = StringOp.EQUALS, value = inputFeature.id ) @@ -161,7 +161,7 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { // And: val featuresByIdPrefix = executeMetaQuery( MemberQuery( - column = MetaColumn.id(), + member = MetaColumn.id(), op = StringOp.STARTS_WITH, value = TEST_FEATURE_ID.substring(0..4) ) From 5c562279b2c71e5427a4f350bc5974ac30d2111f Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Thu, 18 Jun 2026 15:15:07 +0200 Subject: [PATCH 20/57] Fix catalog, add some comments where needed, deprecate usage of encoding. Signed-off-by: Alexander Lowey-Weber --- .../naksha/cli/copy/service/CopyService.java | 2 +- .../cli/copy/service/CopyServiceTest.java | 4 +- .../kotlin/naksha/model/IStorage.kt | 11 +++ .../kotlin/naksha/model/NakshaError.kt | 6 +- .../kotlin/naksha/model/NakshaException.kt | 6 +- .../kotlin/naksha/psql/PgAdminCatalog.kt | 83 ++++++++++--------- .../kotlin/naksha/psql/PgCatalog.kt | 22 +++-- .../commonMain/kotlin/naksha/psql/PgRows.kt | 16 ++-- .../commonMain/kotlin/naksha/psql/PgWrite.kt | 4 +- .../commonMain/kotlin/naksha/psql/PgWriter.kt | 26 +++--- 10 files changed, 102 insertions(+), 78 deletions(-) diff --git a/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/copy/service/CopyService.java b/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/copy/service/CopyService.java index 7351d9db1..0ac11db0c 100644 --- a/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/copy/service/CopyService.java +++ b/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/copy/service/CopyService.java @@ -158,7 +158,7 @@ private void createMapIfAbsent(IStorage storage, String mapId) throws CopyServic ); case ErrorResponse errorResponse -> { NakshaError nakshaError = errorResponse.getError(); - if (!nakshaError.getCode().equals(NakshaError.MAP_EXISTS)) { + if (!nakshaError.getCode().equals(NakshaError.CATALOG_EXISTS)) { throw new CopyServiceException("Problem with creating map!", new NakshaException(nakshaError)); } logger.info("Map(id: \"{}\") is already present on storage(id: \"{}\")!", mapId, storage.getId()); diff --git a/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/copy/service/CopyServiceTest.java b/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/copy/service/CopyServiceTest.java index 9c31061be..a9a858e99 100644 --- a/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/copy/service/CopyServiceTest.java +++ b/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/copy/service/CopyServiceTest.java @@ -160,7 +160,7 @@ void shouldSucceedWithAutoCreateTargetAndAbsentTargetMapAndCollection() throws F void shouldSucceedWithAutoCreateTargetAndAbsentTargetCollection() throws FeaturesWriteExecutorException { // Given: valid target storage with write sessions IStorage targetStorage = createTargetStorage(sessionOptions); - IWriteSession createMapWriteSession = createWriteSessionReturningErrorResponse(NakshaError.MAP_EXISTS); + IWriteSession createMapWriteSession = createWriteSessionReturningErrorResponse(NakshaError.CATALOG_EXISTS); IWriteSession createCollectionWriteSession = createWriteSessionReturningSuccessResponse(); when(targetStorage.newWriteSession(sessionOptions)) .thenReturn(createMapWriteSession) @@ -220,7 +220,7 @@ void shouldSucceedWithAutoCreateTargetAndAbsentTargetCollection() throws Feature void shouldSucceedWithAutoCreateTargetAndExistingTargetMapAndCollection() throws FeaturesWriteExecutorException { // Given: valid target storage with write sessions IStorage targetStorage = createTargetStorage(sessionOptions); - IWriteSession createMapWriteSession = createWriteSessionReturningErrorResponse(NakshaError.MAP_EXISTS); + IWriteSession createMapWriteSession = createWriteSessionReturningErrorResponse(NakshaError.CATALOG_EXISTS); IWriteSession createCollectionWriteSession = createWriteSessionReturningErrorResponse(NakshaError.COLLECTION_EXISTS); when(targetStorage.newWriteSession(sessionOptions)) .thenReturn(createMapWriteSession) diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/IStorage.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/IStorage.kt index 1f1771271..d45683931 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/IStorage.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/IStorage.kt @@ -72,6 +72,10 @@ interface IStorage : IDictReader { */ val hardCap: Int + // TODO: fun createDatabase(databaseId: String): NakshaDatabase + // fun upgradeDatabase(database: NakshaDatabase) + // fun deleteDatabase(database: NakshaDatabase) + /** * Open a new write session. * @@ -80,6 +84,7 @@ interface IStorage : IDictReader { * @return the write session. * @since 2.0.7 */ + // TODO: Modify: fun newWriteSession(database: NakshaDatabase, options: SessionOptions? = null): IWriteSession fun newWriteSession(options: SessionOptions? = null): IWriteSession /** @@ -88,6 +93,7 @@ interface IStorage : IDictReader { * @param lambda the lambda to execute in a try block, ensuring that the session is closed. * @return the result of the lambda. */ + // TODO: Modify: fun useWriteSession(database: NakshaDatabase, options: SessionOptions? = null, lambda: Fn1): T fun useWriteSession(options: SessionOptions? = null, lambda: Fn1): T { val session = newWriteSession(options) return session.use { lambda.call(session) } @@ -99,6 +105,7 @@ interface IStorage : IDictReader { * @param options the session-options. * @param lambda the void lambda to execute in a try block, ensuring that the session is closed. */ + // TODO: Modify: fun runInWriteSession(database: NakshaDatabase, options: SessionOptions? = null, lambda: Fx1): T fun runInWriteSession(options: SessionOptions? = null, lambda: Fx1) { val session = newWriteSession(options) session.use { lambda.call(session) } @@ -112,6 +119,7 @@ interface IStorage : IDictReader { * @return the read-only session. * @since 2.0.7 */ + // TODO: Modify: fun newReadSession(database: NakshaDatabase, options: SessionOptions? = null): IReadSession fun newReadSession(options: SessionOptions? = null): IReadSession /** @@ -120,6 +128,7 @@ interface IStorage : IDictReader { * @param lambda the lambda to execute in a try block, ensuring that the session is closed. * @return the result of the lambda. */ + // TODO: Modify: fun useReadSession(database: NakshaDatabase, options: SessionOptions? = null, useReadSession): T fun useReadSession(options: SessionOptions? = null, lambda: Fn1): T { val session = newReadSession(options) return session.use { lambda.call(session) } @@ -131,6 +140,7 @@ interface IStorage : IDictReader { * @param options the session-options. * @param lambda the void lambda to execute in a try block, ensuring that the session is closed. */ + // TODO: Modify: fun runInReadSession(database: NakshaDatabase, options: SessionOptions? = null, lambda: Fx1): T fun runInReadSession(options: SessionOptions? = null, lambda: Fx1) { val session = newReadSession(options) session.use { lambda.call(session) } @@ -145,5 +155,6 @@ interface IStorage : IDictReader { * @return best [DataEncoding] to use. * @since 3.0 */ + @Deprecated("Will be removed in a future release.") fun getDataEncoding(feature: Any?, context: Any? = null): DataEncoding } \ No newline at end of file diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/NakshaError.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/NakshaError.kt index 0e8f06633..ce04c94e8 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/NakshaError.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/NakshaError.kt @@ -108,7 +108,7 @@ open class NakshaError() : AnyObject() { * A not further specified conflict occurred when performing an operation, for example when the database reports a unique index violation, and the storage is not able to give a more specific reason. * @since 3.0.0 * @see [isConflict] - * @see [MAP_EXISTS] + * @see [CATALOG_EXISTS] * @see [MAP_NOT_FOUND] * @see [COLLECTION_EXISTS] * @see [COLLECTION_NOT_FOUND] @@ -216,7 +216,7 @@ open class NakshaError() : AnyObject() { * * @since 3.0.0 */ - const val MAP_EXISTS = "MapExists" + const val CATALOG_EXISTS = "MapExists" /** * A map does not exist, but is expected to exist. @@ -281,7 +281,7 @@ open class NakshaError() : AnyObject() { * @since 3.0 */ fun isConflict(): Boolean = when(code) { - MAP_EXISTS, + CATALOG_EXISTS, MAP_NOT_FOUND, COLLECTION_EXISTS, COLLECTION_NOT_FOUND, diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/NakshaException.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/NakshaException.kt index 504b5b7c1..ec2e19f84 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/NakshaException.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/NakshaException.kt @@ -12,7 +12,7 @@ import naksha.model.NakshaError.NakshaErrorCompanion.FORBIDDEN import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_ARGUMENT import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_ID import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_STATE -import naksha.model.NakshaError.NakshaErrorCompanion.MAP_EXISTS +import naksha.model.NakshaError.NakshaErrorCompanion.CATALOG_EXISTS import naksha.model.NakshaError.NakshaErrorCompanion.MAP_NOT_FOUND import naksha.model.NakshaError.NakshaErrorCompanion.UNSUPPORTED_OPERATION @@ -110,12 +110,12 @@ fun generalException(msg: String, cause: Throwable? = null): NakshaException = N fun mapNotFound(msg: String): NakshaException = NakshaException(MAP_NOT_FOUND, msg) /** - * Create [MAP_EXISTS] exception. + * Create [CATALOG_EXISTS] exception. * @param msg the message. * @return the [NakshaException]. * @since 3.0 */ -fun mapExists(msg: String): NakshaException = NakshaException(MAP_EXISTS, msg) +fun mapExists(msg: String): NakshaException = NakshaException(CATALOG_EXISTS, msg) /** * Create [COLLECTION_NOT_FOUND] exception. diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgAdminCatalog.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgAdminCatalog.kt index 191c4fb66..6328c34c3 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgAdminCatalog.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgAdminCatalog.kt @@ -443,7 +443,7 @@ SELECT basics.*, procs.* FROM basics, procs; * @param catalog the catalog to store, must have a valid _HEAD_ state **and** must have a valid [TupleNumber]. * @since 3.0 */ - protected fun storeCatalog(catalog: PgCatalog) { + protected fun cacheCatalog(catalog: PgCatalog) { do { val id = catalog.id val newCatalog = catalog.head @@ -468,27 +468,36 @@ SELECT basics.*, procs.* FROM basics, procs; } while (true) } - protected fun invalidateCatalog(catalog: PgCatalog) { - catalogCache.remove(catalog.head.catalogNumber, catalog) + /** + * Remove the catalog with the given catalog-number from the cache. + */ + protected fun invalidateCatalog(catalog: PgCatalog, atomic: Boolean = true) { + if (atomic) catalogCache.remove(catalog.head.catalogNumber, catalog) else catalogCache.remove(catalog.head.catalogNumber) } /** - * Create a new [map][PgCatalog] using the given connection, and return it. + * Create a new [catalog][PgCatalog] using the given connection, and return it. * * ### Note - * Does not commit the given connection, therefore the map is not yet persisted, but can be used through the given connection. The method neither creates the corresponding entry in the collection's collection of the admin-map, it only creates the schema and collection-number sequence counter! - * - * - Throws [NakshaError.MAP_EXISTS] if such a map exists already. + * Does not commit the given connection, therefore the catalog _(aka schema)_ is not yet persisted, but can be used through the given connection. The method neither creates the corresponding entry in the collection's collection of the admin-catalog, it only creates the schema. * @param conn the connection to use to access the database. - * @param map the map to create. - * @return the created map. + * @param catalog the catalog to create. + * @throws NakshaException with [NakshaError.CATALOG_EXISTS] if a catalog with the same `id` exists, or if a catalog with a different `id`, but same feature-number exists _(hash collision)_. * @since 3.0.0 */ - fun createPgCatalog(conn: PgConnection, map: PgCatalog) { - if (Naksha.isInternalId(map.id)) throw NakshaException(ILLEGAL_ARGUMENT, "Can't create internal maps: ${map.id}") - conn.execute("CREATE SCHEMA IF NOT EXISTS ${map.quotedId}").close() - map.createPgCollection(conn, map.collections) // 0 - invalidateCatalog(map) + fun createPgCatalog(conn: PgConnection, catalog: PgCatalog) { + if (Naksha.isInternalId(catalog.id)) throw NakshaException(ILLEGAL_ARGUMENT, "Can't create internal catalogs: ${catalog.id}") + if (!Naksha.isValidId(catalog.id)) throw NakshaException(ILLEGAL_ARGUMENT, "Invalid catalog identifier: ${catalog.id}") + // TODO: We need to ensure that there is no other schema with the same feature-number: + // Write: `ALTER SCHEMA ${catalog.quotedId} SET SCHEMA OPTION 'featureNumber' 'stringifiedFN';` + // Read: + // SELECT nspname, nspconfig FROM pg_namespace WHERE nspname = '${catalog.id}'; + // SELECT nspname, split_part(kv, '=', 2) AS feature_number + // FROM pg_namespace, unnest(nspconfig) AS kv + // WHERE nspname = '${catalog.id}' AND split_part(kv, '=', 1) = 'featureNumber'; + conn.execute("CREATE SCHEMA IF NOT EXISTS ${catalog.quotedId}").close() + catalog.createPgCollection(conn, catalog.collections) // 0 + invalidateCatalog(catalog) } /** @@ -507,9 +516,9 @@ SELECT basics.*, procs.* FROM basics, procs; } /** - * Returns the existing map with the given identifier; if any. + * Returns the existing catalog with the given identifier; if any. * @param conn the connection to use to access the database. - * @param id the map-id to query. + * @param id the catalog-id to query. * @return the map, if it exists; _null_ otherwise. * @since 3.0.0 */ @@ -519,20 +528,20 @@ SELECT basics.*, procs.* FROM basics, procs; val existing = catalogCache[catalogNumber] if (existing != null || conn==null) return existing - val outRows = PgRows().useHeadTable().withCollection(catalogs.head) - val SQL = """SELECT ${outRows.names()} + val outRows = PgRows().withCollection(catalogs) + val SQL = """SELECT ${outRows.aliases()} FROM "naksha~admin".${catalogs.headTable.quotedName} WHERE id = $1 AND (version & 3) < 2""" val plan = conn.prepare(SQL, arrayOf(PgType.STRING.text)) - plan.execute(arrayOf(id)).fetch().use { cursor: PgCursor -> - + plan.execute(arrayOf(id)).fetch().use { cursor -> + if (!outRows.read(cursor)) return null } if (outRows.size == 0) return null - val tuple = outRows[0] ?: return null + val tuple: Tuple = outRows[0] ?: return null Naksha.cache.store(tuple) - val nakshaMap = tuple.decodeFeature(null).proxy(NakshaCatalog::class) - val pgCatalog = PgCatalog(storage, nakshaMap) - storeCatalog(pgCatalog) + val nakshaCatalog = tuple.decodeFeature(null).proxy(NakshaCatalog::class) + val pgCatalog = PgCatalog(storage, nakshaCatalog) + cacheCatalog(pgCatalog) return pgCatalog } @@ -550,23 +559,19 @@ WHERE id = $1 AND (version & 3) < 2""" if (conn == null) return null // Read from database - val outRows = PgRows().useHeadTable().withCollection(catalogs.head) - val SQL = """ - SELECT ${outRows.names()} - FROM "naksha~admin".${catalogs.headTable.quotedName} - WHERE fn = $1 AND (version & 3) < 2 - """.trimIndent() + val rows = PgRows().withCollection(catalogs) + val SQL = """SELECT ${rows.aliases()} +FROM "naksha~admin".${catalogs.headTable.quotedName} +WHERE fn = $1 AND (version & 3) < 2""" + setSearchPath(conn) val plan = conn.prepare(SQL, arrayOf(PgType.INT64.text)) - conn.execute(getSearchPath()) - plan.execute(arrayOf(number)).fetch().use { - outRows.addAll(cursor = it) - } - if (outRows.size == 0) return null - val tuple = outRows[0] ?: return null + plan.execute(arrayOf(number)).fetch().use { rows.readAll(it) } + if (rows.size == 0) return null + val tuple = rows[0] ?: return null Naksha.cache.store(tuple) - val nakshaMap = tuple.decodeFeature(null).proxy(NakshaCatalog::class) - val pgCatalog = PgCatalog(storage, nakshaMap) - storeCatalog(pgCatalog) + val nakshaCatalog = tuple.decodeFeature(null).proxy(NakshaCatalog::class) + val pgCatalog = PgCatalog(storage, nakshaCatalog) + cacheCatalog(pgCatalog) return pgCatalog } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCatalog.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCatalog.kt index 7e37437e9..b56574dcb 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCatalog.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCatalog.kt @@ -137,23 +137,29 @@ open class PgCatalog internal constructor( } /** - * Returns the `search_path` so that this map is on the top, followed by `naksha~admin`, `topology`, `hint_plan`, `public`. - * @return the `search_path` so that this map is on the top, followed by `naksha~admin`, `topology`, `hint_plan`, `public`. + * Returns the `search_path` so that this schema _(catalog)_ is on the top, followed by `naksha~admin`, `topology`, `hint_plan`, `public`. + * @return the `search_path` so that this schema _(catalog)_ is on the top, followed by `naksha~admin`, `topology`, `hint_plan`, `public`. */ - fun getSearchPath(): String = if (this is PgAdminCatalog) { - "SET search_path = \"naksha~admin\", topology, hint_plan, public" + fun searchPath(): String = if (this is PgAdminCatalog) { + "\"naksha~admin\", topology, hint_plan, public" } else { - "SET search_path = ${quotedId}, \"naksha~admin\", topology, hint_plan, public" + "${quotedId}, \"naksha~admin\", topology, hint_plan, public" } /** * Sets the `search_path` for the current transaction, so until `commit` or `rollback`. + * + * Actually: + * ```sql + * SET search_path = ${searchPath()} + * ``` + * * @param conn the connection where to set the search path. * @since 3.0 - * @see [getSearchPath] + * @see [searchPath] */ fun setSearchPath(conn: PgConnection) { - conn.execute(getSearchPath()).close() + conn.execute("SET search_path = ${searchPath()}").close() } /** @@ -365,7 +371,7 @@ FROM ${collections.headTable.quotedName} WHERE id = $1 AND (version & 3) < 2""" val plan = conn.prepare(SQL, arrayOf(PgType.STRING.text)) plan.execute(arrayOf(id)).fetch().use { - outRows.addAll(cursor = it) + outRows.readAll(cursor = it) } if (outRows.size == 0) return null val tuple = outRows[0] ?: return null diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRows.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRows.kt index 8887e2c00..97413ff50 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRows.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRows.kt @@ -298,12 +298,12 @@ internal class PgRows { } /** - * Add the current row of the cursor to the end of the rows list. + * Read from the given cursor and add a row to the end of the row-set. Requires that the cursor is positioned on a row _([PgCursor.isRow])_. Does not move the cursor forward. * @param cursor the cursor from which to read. - * @return `true` if a rows was read; `false` if the cursor is not at a valid row _([PgCursor.isRow] is _false_). + * @return `true` if a row was read; `false` if the cursor is not at a valid row _([PgCursor.isRow] is _false_). * @since 3.0 */ - fun add(cursor: PgCursor): Boolean { + fun read(cursor: PgCursor): Boolean { if (cursor.isRow()) { set(size ,cursor) return true @@ -312,14 +312,16 @@ internal class PgRows { } /** - * Read all rows from cursor to the end of the rows, expects the cursor to be at first result, usage: + * Read all rows from cursor to the end of the rows, expects the cursor to be positioned at the first row that should be read, usage: * ```kotlin - * plan.execute(queryValues).fetch().use { resultRows.addAll(it) } + * val rows = PgRows().withCollection(pgCollection) + * plan.execute(query).fetch().use { rows.readAll(it) } + * // Process the rows * ``` * @since 3.0 */ - fun addAll(cursor: PgCursor): PgRows { - while (add(cursor)) cursor.next() + fun readAll(cursor: PgCursor): PgRows { + while (read(cursor)) cursor.next() return this } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWrite.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWrite.kt index 2ddd29590..86941f84d 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWrite.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWrite.kt @@ -115,7 +115,7 @@ internal data class PgWrite(val original: Write, val i: Int) { */ var tuple: Tuple? = null - val isMapModification: Boolean + val isCatalogModification: Boolean get() = original.isMapModification() val isCollectionModification: Boolean get() = original.isCollectionModification() @@ -123,7 +123,7 @@ internal data class PgWrite(val original: Write, val i: Int) { get() = Naksha.ADMIN_CATALOG_ID == map.id && Naksha.TRANSACTIONS_COL_ID == collection.id // This variant differs from write.isFeatureModification, because it includes dictionaries, which are just features for us! val isFeatureModification: Boolean - get() = !isTransactionModification && !isMapModification && !isCollectionModification + get() = !isTransactionModification && !isCatalogModification && !isCollectionModification /** * If the feature is a map, the [PgCatalog] representation. diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt index e93ef4c70..de22e6058 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt @@ -247,7 +247,7 @@ open class PgWriter internal constructor( txCol.add(tupleNumber, col.partitions) } featuresModified += 1 - } else if (write.isMapModification) { + } else if (write.isCatalogModification) { val map = write.asPgCatalog if (map != null) transaction.useMap(map.id, map.number, write.action) } else if (write.isCollectionModification) { @@ -286,34 +286,34 @@ open class PgWriter internal constructor( throw collectionNotFound("The write #${write.i} refers to not existing collection '$colId'") write.collection = collection - // If this operation modifies a map. - if (write.isMapModification) { + // If this operation modifies a catalog. + if (write.isCatalogModification) { val op = write.op - var pgMap = storage.adminCatalog.getPgCatalogById(null, write.id) ?: storage.adminCatalog.getPgCatalogById(conn, write.id) + var pgCatalog = storage.adminCatalog.getPgCatalogById(null, write.id) ?: storage.adminCatalog.getPgCatalogById(conn, write.id) val nakshaMap: NakshaCatalog? if (op == WriteOp.CREATE || op == WriteOp.UPSERT || op == WriteOp.UPDATE) { val feature = write.feature ?: throw illegalArg("The write #${write.i} is $op, but the feature is null") nakshaMap = if (feature is NakshaCatalog) feature else feature.proxy(NakshaCatalog::class) nakshaMap.storageId = storage.id - if (pgMap == null) { + if (pgCatalog == null) { if (op == WriteOp.UPDATE) { throw mapNotFound("The UPDATE (write #${write.i}) failed, because the map '$featureId' does not exist") } - pgMap = PgCatalog(storage, nakshaMap) - createPgMap(pgMap) + pgCatalog = PgCatalog(storage, nakshaMap) + createPgCatalog(pgCatalog) } else if (op == WriteOp.CREATE) { throw mapExists("The write #${write.i} failed, because the map '$featureId' does exist already") } } else if (op == WriteOp.DELETE || op == WriteOp.PURGE) { - if (pgMap != null) { - deletePgMap(pgMap) + if (pgCatalog != null) { + deletePgMap(pgCatalog) } nakshaMap = null } else { throw illegalState("The write #${write.i} refers to an unsupported operation: '$op'") } - write.asPgCatalog = pgMap + write.asPgCatalog = pgCatalog write.asNakshaMap = nakshaMap } @@ -406,11 +406,11 @@ open class PgWriter internal constructor( /** * Invoked when a [NakshaMap][naksha.model.objects.NakshaCatalog] should be physically created. - * @param map the map that should be physically created. + * @param catalog the catalog that should be physically created. * @since 3.0 */ - protected open fun createPgMap(map: PgCatalog) { - storage.adminCatalog.createPgCatalog(conn, map) + protected open fun createPgCatalog(catalog: PgCatalog) { + storage.adminCatalog.createPgCatalog(conn, catalog) } /** From 4db5ad681d4058d8a4215786cc654a0bd5945544 Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Thu, 18 Jun 2026 17:07:58 +0200 Subject: [PATCH 21/57] More fixes in PgCatalog, session, transaction, aso. Signed-off-by: Alexander Lowey-Weber --- .../kotlin/naksha/model/ISession.kt | 2 +- .../naksha/model/MemberProcessorList.kt | 12 +- .../kotlin/naksha/model/MemberProcessorMap.kt | 143 +++++++++++- .../naksha/model/{StorageTx.kt => PgTx.kt} | 7 +- .../commonMain/kotlin/naksha/model/Version.kt | 4 +- .../naksha/model/objects/XyzProcessors.kt | 82 +++++++ .../kotlin/naksha/psql/PgCatalog.kt | 208 +++++------------- .../kotlin/naksha/psql/PgCollection.kt | 27 ++- .../kotlin/naksha/psql/PgHistoryTable.kt | 10 +- .../kotlin/naksha/psql/PgSession.kt | 14 +- .../commonMain/kotlin/naksha/psql/PgWriter.kt | 4 +- .../kotlin/naksha/psql/PgWriterBase.kt | 2 +- 12 files changed, 324 insertions(+), 191 deletions(-) rename here-naksha-lib-model/src/commonMain/kotlin/naksha/model/{StorageTx.kt => PgTx.kt} (94%) create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzProcessors.kt diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/ISession.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/ISession.kt index a7478ceda..1d415690f 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/ISession.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/ISession.kt @@ -56,7 +56,7 @@ interface ISession : AutoCloseable { * @return the member processor map. * @since 3.0 */ - fun processors(): MemberProcessorMap + val processors: MemberProcessorMap // TODO: Define a streaming API (full table scan) to consume all features from a collection. // This API is designed to backup data, or to execute a read request with a huge cardinality, diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/MemberProcessorList.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/MemberProcessorList.kt index cace1b8c1..72a90f44b 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/MemberProcessorList.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/MemberProcessorList.kt @@ -12,4 +12,14 @@ import kotlin.js.JsExport * @since 3.0 */ @JsExport -open class MemberProcessorList(private val delegate: MutableList = ArrayList()) : MutableList by delegate +open class MemberProcessorList(private val delegate: MutableList = ArrayList()) : MutableList by delegate { + /** + * Create a shallow copy of this list. + * @return the shallow copy. + */ + fun copy(): MemberProcessorList { + val copy = MemberProcessorList() + for (e in delegate) copy.add(e) + return copy + } +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/MemberProcessorMap.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/MemberProcessorMap.kt index 24163a0a7..f19b0e424 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/MemberProcessorMap.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/MemberProcessorMap.kt @@ -2,6 +2,7 @@ package naksha.model +import naksha.model.objects.Member import kotlin.js.JsExport import kotlin.js.JsName @@ -16,7 +17,7 @@ import kotlin.js.JsName * @since 3.0 */ @JsExport -open class MemberProcessorMap : MutableMap { +class MemberProcessorMap : MutableMap { private val delegate: MutableMap = mutableMapOf() @@ -25,15 +26,15 @@ open class MemberProcessorMap : MutableMap { // ------------------------------------------------------------------------- /** - * Add a processor for the given member name. + * Add a processor for the member with the given name. * - * If the processor is already registered for this member, the call is a no-op. - * Processors are invoked in the order in which they were added. - * @param name The name of the member as specified in the collection. + * If the processor is already registered for this member, the call is a no-op. Processors are invoked in the order in which they were added. + * @param name The name of the member as specified in the [NakshaCollection][naksha.model.objects.NakshaCollection]. * @param processor The processor to add. + * @return this. */ @JsName("addProcessor") - open fun addProcessor(name: String, processor: IMemberProcessor) { + fun addProcessor(name: String, processor: IMemberProcessor): MemberProcessorMap { var list = delegate[name] if (list == null) { list = MemberProcessorList() @@ -42,18 +43,65 @@ open class MemberProcessorMap : MutableMap { if (!list.contains(processor)) { list.add(processor) } + return this } /** - * Remove a processor for the given member name. + * Add a processor for the given member. + * + * If the processor is already registered for this member, the call is a no-op. Processors are invoked in the order in which they were added. + * @param member The member for which to add a processor. + * @param processor The processor to add. + * @return this. + */ + @JsName("addMemberProcessor") + fun addProcessor(member: Member, processor: IMemberProcessor): MemberProcessorMap = addProcessor(member.name, processor) + + /** + * Add multiple processor for the member with the given name. + * + * If a processor is already registered for this member, the call is a no-op. Processors are invoked in the order in which they were added. + * @param name The name of the member as specified in the [NakshaCollection][naksha.model.objects.NakshaCollection]. + * @param processors The processors to add. + * @return this. + */ + @JsName("addProcessors") + fun addProcessors(name: String, processors: List): MemberProcessorMap { + if (processors.isEmpty()) return this + var list = delegate[name] + if (list == null) { + list = MemberProcessorList() + delegate[name] = list + } + for (processor in processors) { + if (!list.contains(processor)) { + list.add(processor) + } + } + return this + } + + /** + * Add multiple processor for the member with the given name. + * + * If a processor is already registered for this member, the call is a no-op. Processors are invoked in the order in which they were added. + * @param member The member for which to add processors. + * @param processors The processors to add. + * @return this. + */ + @JsName("addMemberProcessors") + fun addProcessors(member: Member, processors: List): MemberProcessorMap = addProcessors(member.name, processors) + + /** + * Remove a processor for the member with the given name. * * If the list becomes empty after removal, the entry is removed from the map. - * @param name The name of the member as specified in the collection. + * @param name The name of the member as specified in the [NakshaCollection][naksha.model.objects.NakshaCollection].. * @param processor The processor to remove. - * @return `true` if the processor was found and removed, `false` otherwise. + * @return _true_ if the processor was found and removed, _false_ otherwise. */ @JsName("removeProcessor") - open fun removeProcessor(name: String, processor: IMemberProcessor): Boolean { + fun removeProcessor(name: String, processor: IMemberProcessor): Boolean { val list = delegate[name] ?: return false val removed = list.remove(processor) if (list.isEmpty()) { @@ -63,11 +111,35 @@ open class MemberProcessorMap : MutableMap { } /** - * Returns the list of processors for the given member name, or `null` if none are registered. + * Remove a processor for the given member. + * + * If the list becomes empty after removal, the entry is removed from the map. + * @param member The member for which to remove the processor. + * @param processor The processor to remove. + * @return _true_ if the processor was found and removed, _false_ otherwise. + */ + @JsName("removeMemberProcessor") + fun removeProcessor(member: Member, processor: IMemberProcessor): Boolean = removeProcessor(member.name, processor) + + /** + * Returns the list of processors for the member with the given name, or `null` if none are registered. + * + * Mutation of the returned list will modify the processor map too. * @param name The member name. + * @return the processor list or `null`, if no processor for the given name is added. */ @JsName("getProcessors") - open fun getProcessors(name: String): MemberProcessorList? = delegate[name] + fun getProcessors(name: String): MemberProcessorList? = delegate[name] + + /** + * Returns the list of processors for the member or `null` if none are registered. + * + * Mutation of the returned list will modify the processor map too. + * @param member The member. + * @return the processor list or `null`, if no processor for the given member is added. + */ + @JsName("getMemberProcessors") + fun getProcessors(member: Member): MemberProcessorList? = delegate[member.name] // ------------------------------------------------------------------------- // MutableMap delegation @@ -109,4 +181,51 @@ open class MemberProcessorMap : MutableMap { * Returns `true` if there are no processors registered for any member. */ fun isEmptyProcessors(): Boolean = delegate.isEmpty() + + /** + * Create a backup of the current processor map, useful when temporary different processors are needed. + * @param clear if the map should be cleared after backup, makes the backup faster. + * @return the backup of the processor map. + * @since 3.0 + */ + fun backup(clear: Boolean = true): Map { + val backup = mutableMapOf() + for ((key, value) in delegate.entries) { + // When we clear, we can just copy the reference, otherwise we need a copy of the MemberList. + if (clear) backup[key] = value else backup[key] = value.copy() + } + if (clear) delegate.clear() + return backup + } + + /** + * Restores a backup, useful when temporary different processors are needed. + * @param backup the backup to restore. + * @param clear if the map should be cleared, before restoring. + * @param consume if the backup should be consumed, then only references need to be copied. + * @return this. + */ + fun restore(backup: Map, clear: Boolean = true, consume: Boolean = false): MemberProcessorMap { + if (clear) delegate.clear() + for ((key, backupList) in backup) { + if (clear && consume) { + // We can copy the reference, the backup is not need anymore anyway. + delegate[key] = backupList + } else if (clear) { + // Copy from backup, because the backup should stay intact and may be reused. + // If we would not do this, any modification after restore would modify the backup too! + delegate[key] = backupList.copy() + } else { // no clear, no consume + val existing = delegate[key] + if (existing == null) { + if (consume) delegate[key] = backupList else delegate[key] = backupList.copy() + } else { + for (e in backupList) { + if (!existing.contains(e)) existing.add(e) + } + } + } + } + return this + } } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/StorageTx.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/PgTx.kt similarity index 94% rename from here-naksha-lib-model/src/commonMain/kotlin/naksha/model/StorageTx.kt rename to here-naksha-lib-model/src/commonMain/kotlin/naksha/model/PgTx.kt index e70dd911d..84f1f9962 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/StorageTx.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/PgTx.kt @@ -4,7 +4,6 @@ package naksha.model import naksha.base.Int64 import naksha.jbon.IDictReader -import naksha.jbon.HeapBook import naksha.model.objects.* import kotlin.js.JsExport import kotlin.js.JsName @@ -19,7 +18,7 @@ import kotlin.js.JsName * @since 3.0 */ @JsExport -open class StorageTx private constructor( +open class PgTx private constructor( /** * The storage instance for which this transaction is done. Does not have to be supplied. * @since 3.0 @@ -94,12 +93,12 @@ open class StorageTx private constructor( * The statistical transaction information, updated while this class is being used, should eventually be writted into the transaction-log of the storage. * @since 3.0 */ - open val transaction: NakshaTx = NakshaTx().setVersion(version) + open val nakshaTx: NakshaTx = NakshaTx().setVersion(version) /** * The `updated_at` value being used for all [Tuple] created, basically just reads `transaction.time`. * @since 3.0 */ open val updatedAt: Int64 - get() = transaction.time + get() = nakshaTx.time } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Version.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Version.kt index 8e9d36640..d456b9009 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Version.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Version.kt @@ -194,7 +194,7 @@ open class Version(@JvmField val number: Int64) : Comparable { /** * The _HEAD_ sentinel version _(`9_007_199_254_740_991` aka `2^53-1`)_. Can be used as well to mask version to ensure valid version number, like `version & Version.HEAD`. * - * When a [Tuple] is the _HEAD_ state its next-version is synthesized as this value. + * When a [Tuple] is the _HEAD_ state its next-version is synthesized as this value or as `null`, which has by definition the same meaning. * @since 3.0 */ @JvmField @@ -234,7 +234,7 @@ open class Version(@JvmField val number: Int64) : Comparable { /** * The maximum value of the 30-bit sequence field (`0x3FFF_FFFF` = 1073741823). - * Also usable as a bitmask to extract the sequence from a shifted value. + * Also, usable as a bitmask to extract the sequence from a shifted value. * @since 3.0 */ @JvmField diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzProcessors.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzProcessors.kt new file mode 100644 index 000000000..3513df65e --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzProcessors.kt @@ -0,0 +1,82 @@ +package naksha.model.objects + +import naksha.base.Int64 +import naksha.base.Platform +import naksha.model.IMemberProcessor +import naksha.model.ISession +import kotlin.jvm.JvmStatic + +/** + * A singleton with standard processors to restore Xyz-Hub compatibility. + * + * To restore the functionality of Xyz-Hub, the session needs to be patched like: + * ```kotlin + * val processors = session.processors + * val backup = processors.backup(clear=true) + * try { + * processors.addProcessor(XyzMembers.XyzCreatedAt, XyzProcessors.xyzCreatedAt) + * processors.addProcessor(XyzMembers.XyzUpdatedAt, XyzProcessors.xyzUpdatedAt) + * processors.addProcessor(XyzMembers.XyzAppId, XyzProcessors.xyzAppId) + * processors.addProcessor(XyzMembers.XyzAuthor, XyzProcessors.xyzAuthor) + * processors.addProcessor(XyzMembers.XyzAuthorTimestamp, XyzProcessors.xyzAuthorTimestamp) + * // ... + * // perform all normal session operations. + * // the added processors will ensure that the members having correct values. + * } finally { + * processors.restore(backup, clear=true, consume=true) + * } + * ``` + * @since 3.0 + */ +class XyzProcessors private constructor() { + companion object XyzProcessor_C { + /** + * Ensures that [XyzCreatedAt][naksha.model.objects.XyzMembers.XyzMembers_C.XyzCreatedAt] is set correctly. + * @since 3.0 + */ + @JvmStatic + val xyzCreatedAt = fun(session: ISession, collection: NakshaCollection, feature: NakshaFeature, member: Member, value: Any?): Int64 { + if (value is Int64) return value + if (value is Number) return Int64(value.toLong()) + return Platform.currentMillis() + } as IMemberProcessor + + /** + * Ensures that [XyzUpdatedAt][naksha.model.objects.XyzMembers.XyzMembers_C.XyzUpdatedAt] is set correctly. + * @since 3.0 + */ + @JvmStatic + val xyzUpdatedAt = fun(session: ISession, collection: NakshaCollection, feature: NakshaFeature, member: Member, value: Any?): Int64 { + return Platform.currentMillis() + } as IMemberProcessor + + /** + * Ensures that [XyzAppId][naksha.model.objects.XyzMembers.XyzMembers_C.XyzAppId] is set correctly. + * @since 3.0 + */ + @JvmStatic + val xyzAppId = fun(session: ISession, collection: NakshaCollection, feature: NakshaFeature, member: Member, value: Any?): String { + return session.options.appId + } as IMemberProcessor + + /** + * Ensures that [XyzAuthor][naksha.model.objects.XyzMembers.XyzMembers_C.XyzAuthor] is set correctly. + * @since 3.0 + */ + @JvmStatic + val xyzAuthor = fun(session: ISession, collection: NakshaCollection, feature: NakshaFeature, member: Member, value: Any?): String? { + val author = session.options.author + return author ?: value as String? + } as IMemberProcessor + + /** + * Ensures that [XyzAuthorTimestamp][naksha.model.objects.XyzMembers.XyzMembers_C.XyzAuthorTimestamp] is set correctly. + * @since 3.0 + */ + @JvmStatic + val xyzAuthorTimestamp = fun(session: ISession, collection: NakshaCollection, feature: NakshaFeature, member: Member, value: Any?): Int64? { + val author = session.options.author + return if (author != null) Platform.currentMillis() else value as Int64? + } as IMemberProcessor + } +} \ No newline at end of file diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCatalog.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCatalog.kt index b56574dcb..3b9e5d403 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCatalog.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCatalog.kt @@ -5,6 +5,7 @@ package naksha.psql import naksha.base.* import naksha.base.Platform.PlatformCompanion.logger import naksha.model.Action +import naksha.model.IWriteSession import naksha.model.Naksha import naksha.model.Naksha.NakshaCompanion.COLLECTIONS_COL_ID import naksha.model.Naksha.NakshaCompanion.COLLECTIONS_COL_FN @@ -16,9 +17,14 @@ import naksha.model.Naksha.NakshaCompanion.TRANSACTIONS_COL_ID import naksha.model.Naksha.NakshaCompanion.TRANSACTIONS_COL_FN import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_STATE import naksha.model.NakshaException +import naksha.model.PgTx import naksha.model.TupleNumber import naksha.model.objects.NakshaCollection import naksha.model.objects.NakshaCatalog +import naksha.model.objects.StandardMembers.StandardMembers_C.Id +import naksha.model.objects.XyzMembers +import naksha.model.objects.XyzProcessors +import naksha.psql.PgColumn.PgColumn_C.FN import naksha.psql.PgUtil.PgUtilCompanion.quoteIdent import kotlin.js.JsExport import kotlin.jvm.JvmField @@ -107,7 +113,7 @@ open class PgCatalog internal constructor( * @param collection the collection to store, must have a valid _HEAD_ state **and** must have a valid [TupleNumber]. * @since 3.0 */ - protected fun storeCollection(collection: PgCollection) { + protected fun cacheCollection(collection: PgCollection) { do { val id = collection.id val newCollection = collection.head @@ -168,67 +174,40 @@ open class PgCatalog internal constructor( * ### Note * - This method does not commit the given connection, therefore the collection is not yet persisted, but can be used through the given connection. * - The method does not insert the corresponding entry into the collection's collection, this must be done upfront by the caller. - * - * - Throws [NakshaError.MAP_NOT_FOUND] if the given map does not exist _(anymore)_. - * - Throws [NakshaError.COLLECTION_EXISTS] if such a collection exists already in the given map. * @param conn the connection to use to access the database. * @param collection the collection to create. * @return the created map. * @since 3.0 */ - open fun createPgCollection(conn: PgConnection, collection: PgCollection) { - // Ensure that all tables and indices are created in the correct map! + open fun createPgCollection(session: IWriteSession, conn: PgConnection, collection: PgCollection) { + // Ensure that all tables and indices are created in the correct schema! setSearchPath(conn) - val NOW = Epoch() - - // The indices list drives which optional indices are created: - // - null → backward-compatible: all DEFAULT_INDICES (non-internal), but only when members is also null - // (when members is explicitly set, only custom indices from the indices list are created) - // - non-null → only the client-declared (already stripped of internal entries by normalizeCollection) - // Mandatory/internal indices (id_unique, txn_unique, id, version, gbn) are always created below - // and are not present in either list. - val optionalIndices = collection.head.indices - val membersExplicit = collection.head.members != null - val defaultPgIndices: List = if (optionalIndices == null && !membersExplicit) PgIndex.DEFAULT_INDICES.filter { !it.internal } else emptyList() - - /** Creates one optional index on [table]: delegates to createIndex for known PgIndex names, - * or to createCustomIndex for user-defined names. */ - fun createOptionalIndex(table: PgTable, idx: naksha.model.objects.Index) { - val pgIdx = PgIndex.of(idx.name) - if (pgIdx != null) table.createIndex(conn, pgIdx) - else table.createCustomIndex(conn, idx) - } + val processors = session.processors + val backup = processors.backup(true) + try { + processors.addProcessor(XyzMembers.XyzCreatedAt, XyzProcessors.xyzCreatedAt) + processors.addProcessor(XyzMembers.XyzUpdatedAt, XyzProcessors.xyzUpdatedAt) + processors.addProcessor(XyzMembers.XyzAppId, XyzProcessors.xyzAppId) + processors.addProcessor(XyzMembers.XyzAuthor, XyzProcessors.xyzAuthor) + processors.addProcessor(XyzMembers.XyzAuthorTimestamp, XyzProcessors.xyzAuthorTimestamp) - val head = collection.headTable - head.create(conn) - head.createIndex(conn, PgIndex.id_unique) - head.createIndex(conn, PgIndex.version) - head.createIndex(conn, PgIndex.gbn_idx) - for (index in defaultPgIndices) head.createIndex(conn, index) - if (optionalIndices != null) for (idx in optionalIndices) if (idx != null) createOptionalIndex(head, idx) + val pgSession = session as PgSession + val tx: PgTx = pgSession.useTx() - val meta = collection.metaTable - if (meta != null) { - meta.create(conn) - meta.createIndex(conn, PgIndex.id_unique) - meta.createIndex(conn, PgIndex.version) - meta.createIndex(conn, PgIndex.gbn_idx) - for (index in defaultPgIndices) meta.createIndex(conn, index) - if (optionalIndices != null) for (idx in optionalIndices) if (idx != null) createOptionalIndex(meta, idx) - } + val headTable = collection.headTable + headTable.create(conn) + for (index in collection.headIndices) headTable.createIndex(conn, index) - val history = collection.historyTable - if (history != null) { + val history = collection.historyTable history.create(conn) - history.createPartition(conn, NOW.year) - history.createPartition(conn, NOW.year + 1) - history.createIndex(conn, PgIndex.id) - history.createIndex(conn, PgIndex.version) - history.createIndex(conn, PgIndex.gbn_idx) - for (index in defaultPgIndices) history.createIndex(conn, index) - if (optionalIndices != null) for (idx in optionalIndices) if (idx != null) createOptionalIndex(history, idx) + history.createPartition(conn, collection.historyPartitionNumberOf(tx.version.number)) + for (index in collection.historyIndices) headTable.createIndex(conn, index) + + // TODO: Fix cache by adding 2nd level cache in session, we only want to update in 2nd level cache and move to 1rst level when committed! + invalidateCollection(collection) + } finally { + processors.restore(backup, clear = true, consume = true) } - invalidateCollection(collection) } /** @@ -238,86 +217,8 @@ open class PgCatalog internal constructor( * @since 3.0.0 */ private fun refreshPgCollection(conn: PgConnection, collection: PgCollection): PgCollection { - // TODO: Fix me! - val cursor = PgRelation.select(conn, collection.catalog.id, id) - cursor.use { - // - // NOTE: We ignore all unknown relations, that allows users to add some own indices and relations! - // - var headRelation: PgRelation? = null - val headIndices: MutableList = mutableListOf() - val headPartitions: MutableMap = mutableMapOf() - val headYears: MutableMap = mutableMapOf() - var historyRelation: PgRelation? = null - val historyIndices: MutableList = mutableListOf() - val historyYears: MutableMap = mutableMapOf() - val historyPartitions: MutableMap = mutableMapOf() - var metaRelation: PgRelation? = null - val metaIndices: MutableList = mutableListOf() - while (cursor.next()) { - val rel = PgRelation(cursor) - if (rel.isAnyHeadRelation()) { - if (rel.isHeadRootRelation()) { - headRelation = rel - } else if (rel.isTable()) { - val i = rel.partitionNumber() - if (i >= 0) headPartitions[i] = rel - } else if (rel.isIndex()) { - val index = PgIndex.of(rel.name) - if (index != null && index !in headIndices) headIndices.add(index) - } - } - if (rel.isAnyHistoryRelation()) { - if (rel.isHistoryRootRelation()) { - historyRelation = rel - } else if (rel.isHistoryYearRelation()) { - val year = rel.year() - if (year > 0) historyYears[year] = rel - } else if (rel.isHistoryPartition()) { - val i = rel.partitionNumber() - if (i >= 0) historyPartitions[i] = rel - } else if (rel.isIndex()) { - val index = PgIndex.of(rel.name) - if (index != null && index !in historyIndices) historyIndices.add(index) - } - } - if (rel.isAnyMetaRelation()) { - if (rel.isMetaRootRelation()) { - metaRelation = rel - } else if (rel.isIndex()) { - val index = PgIndex.of(rel.name) - if (index != null && index !in metaIndices) metaIndices.add(index) - } - } - } - - if (headRelation != null) { - if (headRelation.isPartition()) { - val parts = headPartitions.size - if (parts < 2 || parts > 256) { - throw NakshaException( - ILLEGAL_STATE, - "Invalid amount of HEAD partitions found, must be 2..256, but is ${headPartitions.size}" - ) - } - collection.headTable = PgHeadTable(collection, headRelation.storageClass, parts) - } else { - collection.headTable = PgHeadTable(collection, headRelation.storageClass, 0) - } - for (index in headIndices) collection.headTable.addIndex(index) - } - if (historyRelation != null) { - val history = PgHistoryTable(collection.headTable) - collection.historyTable = history - for (entry in historyYears) history.years[entry.key] = PgHistoryYear(history, entry.key) - } - if (metaRelation != null) { - val meta = PgMetaTable(collection.headTable) - collection.metaTable = meta - for (index in metaIndices) meta.addIndex(index) - } - } - return collection + // TODO: Implement me, but only if needed! + throw UnsupportedOperationException() } /** @@ -331,13 +232,15 @@ open class PgCatalog internal constructor( val builder = StringBuilder() val head = collection.headTable builder.append("DROP TABLE IF EXISTS ${head.quotedName} CASCADE;\n") - val meta = collection.metaTable - if (meta != null) builder.append("DROP TABLE IF EXISTS ${meta.quotedName} CASCADE;\n") + val history = collection.historyTable - if (history != null) builder.append("DROP TABLE IF EXISTS ${history.quotedName} CASCADE;\n") + builder.append("DROP TABLE IF EXISTS ${history.quotedName} CASCADE;\n") + val SQL = builder.toString() - logger.info("Dropped collection '{}' with collection-number {}", collection.id, collection.head.collectionNumber) conn.execute(SQL).close() + logger.info("Dropped collection '{}' with collection-number {}", collection.id, collection.head.collectionNumber) + + // TODO: Fix cache by adding 2nd level cache in session, we only want to update in 2nd level cache and move to 1rst level when committed! invalidateCollection(collection) } @@ -364,21 +267,22 @@ open class PgCatalog internal constructor( if (existing != null || conn == null) return existing // Read from database - val outRows = PgRows().withCollection(collections.head) setSearchPath(conn) - val SQL = """SELECT ${outRows.aliases()} -FROM ${collections.headTable.quotedName} -WHERE id = $1 AND (version & 3) < 2""" + val TABLE = collections.headTable.quotedName + val ID = collections.column(Id) + val SQL = "SELECT * FROM $TABLE WHERE $ID = $1" val plan = conn.prepare(SQL, arrayOf(PgType.STRING.text)) + val rows = PgRows().withCollection(collections) plan.execute(arrayOf(id)).fetch().use { - outRows.readAll(cursor = it) + rows.readAll(cursor = it) } - if (outRows.size == 0) return null - val tuple = outRows[0] ?: return null + if (rows.size == 0) return null + val tuple = rows[0] ?: return null Naksha.cache.store(tuple) val nakshaCollection = tuple.decodeFeature(null).proxy(NakshaCollection::class) val pgCollection = PgCollection(this, nakshaCollection) - storeCollection(pgCollection) + // TODO: Fix cache by adding 2nd level cache in session, we only want to update in 2nd level cache and move to 1rst level when committed! + cacheCollection(pgCollection) return pgCollection } @@ -404,21 +308,19 @@ WHERE id = $1 AND (version & 3) < 2""" if (existing != null || conn == null) return existing // Read from database - val outRows = PgRows().useHeadTable().withCollection(collections.head) setSearchPath(conn) - val SQL = """SELECT ${outRows.names()} -FROM ${collections.headTable.quotedName} -WHERE fn = $1 AND (version & 3) < 2""" + val TABLE = collections.headTable.quotedName + val SQL = "SELECT * FROM $TABLE WHERE $FN = $1" val plan = conn.prepare(SQL, arrayOf(PgType.INT64.text)) - plan.execute(arrayOf(number)).fetch().use { - outRows.addAll(cursor = it) - } - if (outRows.size == 0) return null - val tuple = outRows[0] ?: return null + val rows = PgRows().withCollection(collections) + plan.execute(arrayOf(number)).fetch().use { rows.readAll(it) } + if (rows.size == 0) return null + val tuple = rows[0] ?: return null Naksha.cache.store(tuple) val nakshaCollection = tuple.decodeFeature(null).proxy(NakshaCollection::class) val pgCollection = PgCollection(this, nakshaCollection) - storeCollection(pgCollection) + // TODO: Fix cache by adding 2nd level cache in session, we only want to update in 2nd level cache and move to 1rst level when committed! + cacheCollection(pgCollection) return pgCollection } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt index f3b1a19dc..dbc32c568 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt @@ -7,6 +7,7 @@ import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_STATE import naksha.model.NakshaError.NakshaErrorCompanion.INTERNAL_ERROR import naksha.model.objects.IndexList import naksha.model.objects.Member +import naksha.model.objects.MemberList import naksha.model.objects.MemberType.MemberType_C.BYTE_ARRAY import naksha.model.objects.MemberType.MemberType_C.INT64 import naksha.model.objects.MemberType.MemberType_C.STRING @@ -70,18 +71,24 @@ open class PgCollection internal constructor( @JvmField val shift: Int = nakshaCollection.shift + /** + * Returns the partition index of the [PgHistoryPartition] in which features can be found, that are modified in the given version. + * @param version the version in which features are modified. + * @return the partition index of the [PgHistoryPartition] into which _HEAD_ features will be moved, when modified in the given version. + * @since 3.0 + */ + fun historyPartitionNumberOf(version: Int64): Int = (version shr shift).toInt() + /** * Convert the given member into a [PgColumn], only fails for the standard member [Tn]. - * @param member the member to convert, must not me [Tuple-Number][StandardMembers.Tn]. + * @param member the member to convert, must not me [Tuple-Number][Tn]. * @param index the real index in the physical table at which to place the member. * @return the [PgColumn] for the member. - * @throws NakshaException with error [ILLEGAL_ARGUMENT], if the given member is [Tn]. + * @throws NakshaException with error [INTERNAL_ERROR], if the given member is [Tn]. * @since 3.0 */ private fun fromMember(member: Member, index: Int): PgColumn { - if (Tn.name == member.name) { - throw NakshaException(ILLEGAL_ARGUMENT, "The tuple-number can't be converted using PgColumn.fromMember") - } + if (Tn.name == member.name) throw NakshaException(INTERNAL_ERROR, "The tuple-number can't be converted using fromMember") val memberName = member.name // These mandatory members get special storage handling. when (memberName) { @@ -98,13 +105,19 @@ open class PgCollection internal constructor( } } + /** + * Read the [members][NakshaCollection.members] from the given [NakshaCollection] and turn them into a [PgColumn] list. If the members of the given [NakshaCollection] and not yet [ordered by index][MemberList.isSortedByIndex], then the method will invoke a [sort by type][MemberList.sortByDataTypeAndAssignIndex]. + * @param nakshaCollection the collection from which to extract the [PgColumn]'s. + * @return an array with all extracted [PgColumn]'s. + */ private fun generateColumns(nakshaCollection: NakshaCollection): Array { - val members = nakshaCollection.useMembers() + val members: MemberList = nakshaCollection.useMembers() if (!members.isSortedByIndex()) { members.sortByDataTypeAndAssignIndex() } var i = 0 - return Array(members.size + 1) { // we split tuple-number into `fn` and `version`, therefore +1 + // We split tuple-number into `fn` and `version`, therefore we need size + 1. + return Array(members.size + 1) { when (it) { // The first three members are fixed to: 0 -> PgColumn.FN diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgHistoryTable.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgHistoryTable.kt index b915ed6c5..86c3796ce 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgHistoryTable.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgHistoryTable.kt @@ -82,7 +82,14 @@ $TABLESPACE""" for (entry in partitions) entry.value.create(conn) } - fun createPartition(conn: PgConnection, partitionNumber: Int) { + /** + * Create a new [PgHistoryPartition], if no such partition exists already. + * @param conn the connection to be used to modify the database. + * @param partitionNumber the partition-number of the partition to create. + * @return the [PgHistoryPartition] created or already existing. + * @see [PgCollection.historyPartitionNumberOf] + */ + fun createPartition(conn: PgConnection, partitionNumber: Int): PgHistoryPartition { var partition = partitions[partitionNumber] if (partition == null) { partition = PgHistoryPartition(this, partitionNumber) @@ -92,6 +99,7 @@ $TABLESPACE""" for (index in indices) { partition.createIndex(conn, index) } + return partition } fun addPartition(partitionNumber: Int) { diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgSession.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgSession.kt index 423810f1a..6bfe72365 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgSession.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgSession.kt @@ -220,7 +220,7 @@ open class PgSession( * The current transaction wrapper; if any. * @since 3.0 */ - internal var tx: StorageTx? = null + internal var tx: PgTx? = null private set /** @@ -228,19 +228,19 @@ open class PgSession( * @return the current transaction. * @since 3.0 */ - internal fun useTx(): StorageTx { + internal fun useTx(): PgTx { assertMutable() assertOpen() - var tx: StorageTx? = this.tx + var tx: PgTx? = this.tx if (tx == null) { val txn = storage.newConnection(options, false, null).use { conn -> storage.adminCatalog.newTxn(conn) } - tx = StorageTx(storage, txn.version, options.appId, options.author, storage.adminCatalog, this) + tx = PgTx(storage, txn.version, options.appId, options.author, storage.adminCatalog, this) this.tx = tx } return tx } - override fun getTransaction(): NakshaTx? = tx?.transaction + override fun getTransaction(): NakshaTx? = tx?.nakshaTx /** * Return the current transaction, if no transaction started yet, starts a new one. @@ -248,7 +248,7 @@ open class PgSession( * - Throws [NakshaError.ILLEGAL_STATE] if this is session is [readOnly] or [closed][isClosed]. * @return the current transaction. */ - override fun useTransaction(): NakshaTx = useTx().transaction + override fun useTransaction(): NakshaTx = useTx().nakshaTx private var executionCount: Int = 0 @@ -306,7 +306,7 @@ open class PgSession( val tx = tx if (tx != null) { try { - val transaction = tx.transaction + val transaction = tx.nakshaTx val writeTx = Write().createFeature(Naksha.ADMIN_CATALOG_ID, TRANSACTIONS_COL, transaction) val writeRequest = WriteRequest().add(writeTx) // TODO: Should we use a savepoint here? diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt index de22e6058..3172daccb 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt @@ -63,7 +63,7 @@ open class PgWriter internal constructor( * @since 3.0 */ val transaction: NakshaTx - get() = tx.transaction + get() = tx.nakshaTx /** * Performs the given writes. @@ -233,7 +233,7 @@ open class PgWriter internal constructor( val tupleNumbers = TupleNumberList() tupleNumbers.setCapacity(writes.size) val tupleList = ArrayList(writes.size) - val transaction = tx.transaction + val transaction = tx.nakshaTx var featuresModified = 0 for (write in targetWrites) { val tupleNumber = write.tupleNumber diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterBase.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterBase.kt index 361cfa301..0ca5e382e 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterBase.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterBase.kt @@ -66,7 +66,7 @@ internal abstract class PgWriterBase protected constructor( * @since 3.0 */ val transaction: NakshaTx - get() = tx.transaction + get() = tx.nakshaTx /** * The rows to write. From 5ae2aa72e299478fdbf38a780b2b5236ffc7c81a Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Fri, 19 Jun 2026 09:47:00 +0200 Subject: [PATCH 22/57] Fix more compiler issues. Signed-off-by: Alexander Lowey-Weber --- .../kotlin/naksha/model/objects/NakshaTx.kt | 2 +- .../kotlin/naksha/psql/PgCollection.kt | 6 + .../kotlin/naksha/psql/PgSession.kt | 159 ++++++++++-------- .../commonMain/kotlin/naksha/psql/PgWriter.kt | 6 +- 4 files changed, 100 insertions(+), 73 deletions(-) diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaTx.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaTx.kt index 44cfebad4..dc2cd9066 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaTx.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaTx.kt @@ -199,7 +199,7 @@ open class NakshaTx : NakshaFeature() { * @since 3.0 */ @JvmOverloads - fun useMap(id: String, number: Int, action: Action? = null): NakshaTxMap { + fun useCatalog(id: String, number: Int, action: Action? = null): NakshaTxMap { val existing = maps[id] if (existing != null) { if (action != null) existing.action = action diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt index dbc32c568..ecb1802ac 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt @@ -309,4 +309,10 @@ open class PgCollection internal constructor( */ @JvmField var internal: Boolean = id.startsWith("naksha~") + + // TODO: We need information from the database which history partitions exist. + // Reading from history must be done using the root table, not individual partitions. + // Only writing is done through individual partitions, and only for writing we need to know what exists! + // We should add a method like this: + // internal fun update(conn: PgConnection) {} } \ No newline at end of file diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgSession.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgSession.kt index 6bfe72365..e0dcc89d1 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgSession.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgSession.kt @@ -6,13 +6,17 @@ import naksha.base.* import naksha.base.Platform.PlatformCompanion.logger import naksha.base.Platform.PlatformCompanion.longToInt64 import naksha.base.Platform.PlatformCompanion.newAtomicInt64 +import naksha.base.PlatformDataViewApi.PlatformDataViewApiCompanion.dataview_set_int64 import naksha.model.* import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_STATE +import naksha.model.NakshaError.NakshaErrorCompanion.INTERNAL_ERROR import naksha.model.objects.NakshaCollection import naksha.model.objects.NakshaCatalog +import naksha.model.objects.NakshaFeature import naksha.model.request.* import naksha.model.request.WriteRequest import naksha.model.objects.NakshaTx +import naksha.psql.PgColumn.PgColumn_C.FN import kotlin.js.JsExport import kotlin.jvm.JvmField import kotlin.math.min @@ -346,9 +350,7 @@ open class PgSession( * Processors are invoked in the order in which they were added. * @since 3.0 */ - private val _processors = MemberProcessorMap() - - override fun processors(): MemberProcessorMap = _processors + override val processors = MemberProcessorMap() override fun isClosed(): Boolean = _closed @@ -372,6 +374,7 @@ open class PgSession( return PgLock(this, useConnection(), lockId, false) } + // TODO: We should only have one method being this one! override fun loadTuples(featureTuples: List) = loadTuples(featureTuples, 0, featureTuples.size, FETCH_ALL) override fun loadTuples(featureTuples: List, from: Int, to: Int, mode: FetchMode) { @@ -379,20 +382,20 @@ open class PgSession( if (missing.isNotEmpty()) { (if (mayReadParallel) newReadConnection() else readConnection()).use { readConn -> val conn = readConn.conn - val byCollection = mutableMapOf>() val adminCatalog = storage.adminCatalog + val byCollection: MutableMap = mutableMapOf() for (featureTuple in missing) { - val read = PgRead(conn, adminCatalog, featureTuple.tupleNumber) - read.featureTuple = featureTuple - var reads = byCollection[read.groupId] - if (reads == null) { - reads = ArrayList(min(1000, missing.size)) - byCollection[read.groupId] = reads + val tn = featureTuple.tupleNumber + val collection: PgCollection = adminCatalog.getPgCollectionByNumber(conn, tn.collectionNumber) ?: continue + var list = byCollection[collection] + if (list == null) { + list = FeatureTupleList() + byCollection[collection] = list } - reads.add(read) + list.add(featureTuple) } - for (entry in byCollection) { - loadTuplesFromCollection(conn, entry.value, mode) + for ((collection, featureTuples) in byCollection) { + loadTuplesFromCollection(conn, collection, featureTuples) } } } @@ -402,73 +405,91 @@ open class PgSession( * Load [Tuple] from a specific collection, can be executed in parallel, when multiple collections are needed. We should make parallel reading optional, we experienced that when used for example in EMR, too many connections can harm. However, the cache could keep objects in Redis or alike, and then read perfectly fine in parallel! * * @param conn the connection to use for this read. - * @param reads the reads to perform. - * @param mode the load-mode + * @param collection the collection to read. + * @param featureTuples the tuple to load. */ - private fun loadTuplesFromCollection(conn: PgConnection, reads: List, mode: FetchMode) { - // TODO: We can improve this to load the results as GZIP compressed binary! - // Read BINARY.md for more information. - // For the sake of delivery, we take the shortcut, and only us ARRAY_AGG - // Maybe this is already fast enough? - if (reads.isEmpty()) throw illegalState("Reads must not be empty") - val first = reads.first() - val map = first.catalog - val collection = first.collection - val historyTables = first.historyTables - // When history tables are included in the read, we need `next_version` in the result set - // (for the UNION ALL to have matching columns and so history tuples carry their next_version). - // For HEAD-only reads it is absent from the physical table, so we skip it and getTuple - // reads it as null (correct for live HEAD rows). - val rows = PgRows().useHeadTable(historyTables == null).withCollection(collection.head) - map.setSearchPath(conn) - val headTables = first.headTables - val sql = StringBuilder() - // Prefix selected columns with `t.` so they don't collide with `fn` / `version` from the lookup CTE. - val prefixedRowNames = rows.columns.joinToString(", ") { "t.${it.name}" } - // HEAD has no `next_version` column; substitute NULL for live rows, but for tombstones - // (version & 3 == 2) set next_version = version so the tombstone is self-referential (terminal). - val prefixedRowNamesForHead = rows.columns.joinToString(", ") { - if (it.name == PgColumn.next_version.name) - "CASE WHEN (t.version & 3) >= 2 THEN t.version ELSE NULL::int8 END AS ${it.name}" - else "t.${it.name}" - } - sql.append("WITH ").append(TUPLE_LOOKUP_CTE).append(",\nresult AS(\n") - var unionAll = false - for (headTable in headTables) { - if (unionAll) sql.append("\tUNION ALL\n") else unionAll = true - sql.append("SELECT ").append(prefixedRowNamesForHead) - .append(" FROM ").append(headTable.quotedName) - .append(" t JOIN lookup l ON (t.fn, t.version) = (l.fn, l.version)\n") + private fun loadTuplesFromCollection(conn: PgConnection, collection: PgCollection, featureTuples: FeatureTupleList): Int { + if (featureTuples.isEmpty()) return 0 + val HEAD_TABLE = collection.headTable.quotedName + val HISTORY_TABLE = collection.historyTable.quotedName + // Generate input array + val fn_version_bytes = ByteArray(featureTuples.size * 16) + val view = Platform.newDataView(fn_version_bytes) + for (i in 0..< featureTuples.size) { + val tn = featureTuples[i]?.tupleNumber ?: throw NakshaException(INTERNAL_ERROR, "featureTuples[$i] is null") + // Note: We do the math by intention. The CPU is very good at math, it's basically free. + // However, memory access is really slow, by doing it this way, the CPU can reorder + // the memory access, and the JIT can unroll the loop. + dataview_set_int64(view, i*16, tn.featureNumber) + dataview_set_int64(view, i*16 + 8, tn.version) } - if (historyTables != null) { - for (hstTable in historyTables) { - if (unionAll) sql.append("\tUNION ALL\n") else unionAll = true - sql.append("SELECT ").append(prefixedRowNames) - .append(" FROM ").append(hstTable.quotedName) - .append(" t JOIN lookup l ON (t.fn, t.version) = (l.fn, l.version)\n") - } - } else { - // No history tables: HEAD contains tombstones for deleted rows; no separate shadow table. - } - sql.append(")\nSELECT ").append(rows.namesAggregate()).append(" FROM result") - val SQL = sql.toString() - val tupleNumbers: Array = reads.map { it.tupleNumber!!.toB128() }.toTypedArray() + val SQL = """ +WITH lookup AS ( + SELECT + ((get_byte(b, 0) & 255)::bigint << 56) | + ((get_byte(b, 1) & 255)::bigint << 48) | + ((get_byte(b, 2) & 255)::bigint << 40) | + ((get_byte(b, 3) & 255)::bigint << 32) | + ((get_byte(b, 4) & 255)::bigint << 24) | + ((get_byte(b, 5) & 255)::bigint << 16) | + ((get_byte(b, 6) & 255)::bigint << 8) | + (get_byte(b, 7) & 255)::bigint AS fn, + + ((get_byte(b, 8) & 255)::bigint << 56) | + ((get_byte(b, 9) & 255)::bigint << 48) | + ((get_byte(b, 10) & 255)::bigint << 40)| + ((get_byte(b, 11) & 255)::bigint << 32)| + ((get_byte(b, 12) & 255)::bigint << 24)| + ((get_byte(b, 13) & 255)::bigint << 16)| + ((get_byte(b, 14) & 255)::bigint << 8) | + (get_byte(b, 15) & 255)::bigint AS version + FROM unnest($1::bytea[]) AS t(b) +), from_head AS ( + SELECT head.* FROM $HEAD_TABLE head + JOIN lookup l ON (head.fn, head.version) = (l.fn, l.version) +), from_hst AS ( + SELECT hst.* FROM $HISTORY_TABLE hst + JOIN lookup l ON (hst.fn, hst.version) = (l.fn, l.version) +) +SELECT * FROM from_head +UNION ALL +SELECT * FROM from_hst""" + collection.catalog.setSearchPath(conn) + val rows = PgRows().withCollection(collection) conn.prepare(SQL, arrayOf(PgType.BYTE_ARRAY_ARRAY.text)).use { plan -> - plan.execute(arrayOf(tupleNumbers)).fetch().use { cursor -> - rows.addAggregated(cursor) + plan.execute(arrayOf(fn_version_bytes)).fetch().use { cursor -> + rows.readAll(cursor) } } + // Copy tuples into cache, which allows us in the next loop to read the tuple back from the cache. for (i in 0 until rows.size) { val tuple = rows[i] ?: continue Naksha.cache.store(tuple) - val tupleNumber = tuple.tupleNumber - for (read in reads) { - if (read.tupleNumber == tupleNumber) { - read.featureTuple?.tuple = tuple - break + } + var found = 0 + for (i in 0..< featureTuples.size) { + val featureTuple = featureTuples[i] ?: throw NakshaException(INTERNAL_ERROR, "featureTuples[$i] is null") + val tn = featureTuple.tupleNumber + val tuple = Naksha.cache[tn] + if (tuple != null) { + // TODO: We need a cache for global books! + // We can simply say that the global books need to be committed, before they can be used. + // Additionally, they are immutable, onces stored, they can not be deleted nor mutated. + // Therefore, if a + val feature: NakshaFeature = tuple.decodeFeature(null) + featureTuple.feature = feature + if (featureTuple.id != feature.id) { + // This must not happen! + val tn = featureTuple.tupleNumber + throw NakshaException( + INTERNAL_ERROR, + "The `id` of feature-tuple '${feature.id}' does not match that of the loaded tuple: ${feature.id}, feature-number: '${tn.featureNumber}', version: ${tn.version}, collection: ${collection.id}" + ) } + found++ } } + return found } override fun getMapById(mapId: String): NakshaCatalog? { diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt index 3172daccb..fa972f1b9 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt @@ -242,19 +242,19 @@ open class PgWriter internal constructor( if (write.isFeatureModification) { val map = write.map val col = write.collection - val txCol = transaction.useMap(map.id, map.number).useCollection(col.id, col.number) + val txCol = transaction.useCatalog(map.id, map.number).useCollection(col.id, col.number) if (tupleNumber != null) { txCol.add(tupleNumber, col.partitions) } featuresModified += 1 } else if (write.isCatalogModification) { val map = write.asPgCatalog - if (map != null) transaction.useMap(map.id, map.number, write.action) + if (map != null) transaction.useCatalog(map.id, map.number, write.action) } else if (write.isCollectionModification) { val map = write.map val col = write.asPgCollection if (col != null) { - transaction.useMap(map.id, map.number).useCollection(col.id, col.number, write.action) + transaction.useCatalog(map.id, map.number).useCollection(col.id, col.number, write.action) map.invalidateCollection(col) } } From 739d1cc8c9e5c96d5ed7f3c2b0417f504956d806 Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Fri, 19 Jun 2026 11:31:09 +0200 Subject: [PATCH 23/57] More minor fixes, mainly naming and comments. Signed-off-by: Alexander Lowey-Weber --- .../cli/storages/GeneratingSession.java | 4 +- .../hub/storages/NHAdminStorageReader.java | 10 +-- .../hub/storages/NHSpaceStorageReader.java | 4 +- .../lib/hub/mock/NHAdminReaderMock.java | 4 +- .../kotlin/naksha/jbon/IDictReader.kt | 1 + .../kotlin/naksha/model/ISession.kt | 30 ++++---- .../kotlin/naksha/model/IStorage.kt | 12 --- .../commonMain/kotlin/naksha/model/Naksha.kt | 6 +- .../commonMain/kotlin/naksha/model/Tuple.kt | 2 +- .../kotlin/naksha/model/objects/Member.kt | 8 ++ .../kotlin/naksha/model/objects/MemberList.kt | 6 +- .../naksha/model/objects/NakshaCollection.kt | 8 +- .../naksha/model/objects/StandardMembers.kt | 3 + .../kotlin/naksha/psql/PgAdminCatalog.kt | 4 +- .../kotlin/naksha/psql/PgCatalog.kt | 36 +++------ .../kotlin/naksha/psql/PgCollection.kt | 3 + .../naksha/psql/PgDistributionPartition.kt | 3 + .../kotlin/naksha/psql/PgHeadTable.kt | 3 + .../kotlin/naksha/psql/PgHistoryPartition.kt | 5 ++ .../kotlin/naksha/psql/PgMemberHelper.kt | 56 +------------- .../naksha/psql/PgNakshaTransactions.kt | 7 +- .../kotlin/naksha/psql/PgSession.kt | 18 ++--- .../commonMain/kotlin/naksha/psql/PgUtil.kt | 76 ------------------- .../kotlin/naksha/psql/PsqlAdminCatalog.kt | 22 ------ .../psql/{PsqlMap.kt => PsqlCatalog.kt} | 10 +-- .../kotlin/naksha/psql/PsqlCollection.kt | 2 +- .../jvmMain/kotlin/naksha/psql/PsqlStorage.kt | 2 - .../here/naksha/lib/view/ViewReadSession.java | 4 +- .../here/naksha/lib/view/MockReadSession.java | 4 +- .../storage/http/HttpStorageReadSession.java | 4 +- 30 files changed, 110 insertions(+), 247 deletions(-) rename here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/{PsqlMap.kt => PsqlCatalog.kt} (96%) diff --git a/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/storages/GeneratingSession.java b/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/storages/GeneratingSession.java index 297b924d2..4d706f584 100644 --- a/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/storages/GeneratingSession.java +++ b/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/storages/GeneratingSession.java @@ -115,7 +115,7 @@ public NakshaCatalog getMapById(@NotNull String mapId) { @Nullable @Override - public NakshaCatalog getMapByNumber(int mapNumber) { + public NakshaCatalog getMapByNumber(int catalogNumber) { throw new NakshaException(NakshaError.UNSUPPORTED_OPERATION, ""); } @@ -127,7 +127,7 @@ public NakshaCollection getCollectionById(@NotNull NakshaCatalog map, @NotNull S @Nullable @Override - public NakshaCollection getCollectionByNumber(@NotNull NakshaCatalog map, int collectionNumber) { + public NakshaCollection getCollectionByNumber(@NotNull NakshaCatalog catalog, int collectionNumber) { throw new NakshaException(NakshaError.UNSUPPORTED_OPERATION, ""); } diff --git a/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHAdminStorageReader.java b/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHAdminStorageReader.java index 2dc551bc3..c5d479eb5 100644 --- a/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHAdminStorageReader.java +++ b/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHAdminStorageReader.java @@ -114,12 +114,12 @@ public Response executeParallel(@NotNull Request request) { @Override public @Nullable NakshaCatalog getMapById(@NotNull String mapId) { - return session.getMapById(mapId); + return session.getCatalogById(mapId); } @Override - public @Nullable NakshaCatalog getMapByNumber(int mapNumber) { - return session.getMapByNumber(mapNumber); + public @Nullable NakshaCatalog getMapByNumber(int catalogNumber) { + return session.getMapByNumber(catalogNumber); } @Override @@ -133,8 +133,8 @@ public void loadTuples(@NotNull List featureTuples, int } @Override - public @Nullable NakshaCollection getCollectionByNumber(@NotNull NakshaCatalog map, int collectionNumber) { - return session.getCollectionByNumber(map, collectionNumber); + public @Nullable NakshaCollection getCollectionByNumber(@NotNull NakshaCatalog catalog, int collectionNumber) { + return session.getCollectionByNumber(catalog, collectionNumber); } @Override diff --git a/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHSpaceStorageReader.java b/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHSpaceStorageReader.java index 061fe2555..5b3de093e 100644 --- a/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHSpaceStorageReader.java +++ b/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHSpaceStorageReader.java @@ -342,7 +342,7 @@ public Response executeParallel(@NotNull Request request) { } @Override - public @Nullable NakshaCatalog getMapByNumber(int mapNumber) { + public @Nullable NakshaCatalog getMapByNumber(int catalogNumber) { throw NOT_SUPPORTED_ERROR; } @@ -357,7 +357,7 @@ public void loadTuples(@NotNull List featureTuples, int } @Override - public @Nullable NakshaCollection getCollectionByNumber(@NotNull NakshaCatalog map, int collectionNumber) { + public @Nullable NakshaCollection getCollectionByNumber(@NotNull NakshaCatalog catalog, int collectionNumber) { throw NOT_SUPPORTED_ERROR; } diff --git a/here-naksha-lib-hub/src/jvmTest/java/com/here/naksha/lib/hub/mock/NHAdminReaderMock.java b/here-naksha-lib-hub/src/jvmTest/java/com/here/naksha/lib/hub/mock/NHAdminReaderMock.java index 98c141210..04c668cfd 100644 --- a/here-naksha-lib-hub/src/jvmTest/java/com/here/naksha/lib/hub/mock/NHAdminReaderMock.java +++ b/here-naksha-lib-hub/src/jvmTest/java/com/here/naksha/lib/hub/mock/NHAdminReaderMock.java @@ -284,7 +284,7 @@ public Response executeParallel(@NotNull Request request) { } @Override - public @Nullable NakshaCatalog getMapByNumber(int mapNumber) { + public @Nullable NakshaCatalog getMapByNumber(int catalogNumber) { throw new NakshaException(new NakshaError(NakshaError.UNSUPPORTED_OPERATION, "Not supported by mock yet")); } @@ -294,7 +294,7 @@ public Response executeParallel(@NotNull Request request) { } @Override - public @Nullable NakshaCollection getCollectionByNumber(@NotNull NakshaCatalog map, int collectionNumber) { + public @Nullable NakshaCollection getCollectionByNumber(@NotNull NakshaCatalog catalog, int collectionNumber) { throw new NakshaException(new NakshaError(NakshaError.UNSUPPORTED_OPERATION, "Not supported by mock yet")); } diff --git a/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/IDictReader.kt b/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/IDictReader.kt index 56bde9a0d..d998d5ce8 100644 --- a/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/IDictReader.kt +++ b/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/IDictReader.kt @@ -9,6 +9,7 @@ import kotlin.js.JsExport * @since 3.0 */ @JsExport +@Deprecated("To be removed") interface IDictReader { /** * Retrieve the dictionary with the given identifier. diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/ISession.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/ISession.kt index 1d415690f..3c354a7b0 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/ISession.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/ISession.kt @@ -110,44 +110,44 @@ interface ISession : AutoCloseable { /** * Returns the map for the given identifier. * - * This method does only access the internal caching, and may not be up-to-date. If invoked on a [write session][IWriteSession] before committing changes, it will return maps that were created in the current session, but beware that these maps may eventually fail to commit. - * @param mapId the map-id for which to return the latest HEAD state. - * @return the map; _null_ if no such map exists. + * This method does only access the internal caching, and may not be up-to-date. If invoked on a [write session][IWriteSession] before committing changes, it will return catalogs that were created in the current session, but beware that these catalogs may eventually fail to commit. + * @param catalogId the catalog-id for which to return the latest HEAD state. + * @return the catalog; _null_ if no such catalog exists. * @since 3.0 */ - fun getMapById(mapId: String): NakshaCatalog? + fun getCatalogById(catalogId: String): NakshaCatalog? /** - * Returns the map for the given number. + * Returns the catalog for the given number. * - * This method does only access the internal caching, and may not be up-to-date. If invoked on a [write session][IWriteSession] before committing changes, it will return maps that were created in the current session, but beware that these maps may eventually fail to commit. - * @param mapNumber the map-number for which to return the latest HEAD state. - * @return the map; _null_ if no such map exists. + * This method does only access the internal caching, and may not be up-to-date. If invoked on a [write session][IWriteSession] before committing changes, it will return catalogs that were created in the current session, but beware that these catalogs may eventually fail to commit. + * @param catalogNumber the catalog-number for which to return the latest HEAD state. + * @return the catalog; _null_ if no such catalog exists. * @since 3.0 */ - fun getMapByNumber(mapNumber: Int): NakshaCatalog? + fun getMapByNumber(catalogNumber: Int): NakshaCatalog? /** * Returns the collection for the given identifier. * - * This method does only access the internal caching, and may not be up-to-date. If invoked on a [write session][IWriteSession] before committing changes, it will return maps that were created in the current session, but beware that these collections may eventually fail to commit. - * @param map the map to query. + * This method does only access the internal caching, and may not be up-to-date. If invoked on a [write session][IWriteSession] before committing changes, it will return collections that were created in the current session, but beware that these collections may eventually fail to commit. + * @param catalog the catalog to query. * @param collectionId the collection-id for which to return the latest HEAD state. * @return the collection; _null_ if no such collection exists. * @since 3.0 */ - fun getCollectionById(map: NakshaCatalog, collectionId: String): NakshaCollection? + fun getCollectionById(catalog: NakshaCatalog, collectionId: String): NakshaCollection? /** * Returns the collection for the given number. * - * This method does only access the internal caching, and may not be up-to-date. If invoked on a [write session][IWriteSession] before committing changes, it will return maps that were created in the current session, but beware that these collections may eventually fail to commit. - * @param map the map to query. + * This method does only access the internal caching, and may not be up-to-date. If invoked on a [write session][IWriteSession] before committing changes, it will return collections that were created in the current session, but beware that these collections may eventually fail to commit. + * @param catalog the catalog to query. * @param collectionNumber the collection-number for which to return the latest HEAD state. * @return the collection; _null_ if no such collection exists. * @since 3.0 */ - fun getCollectionByNumber(map: NakshaCatalog, collectionNumber: Int): NakshaCollection? + fun getCollectionByNumber(catalog: NakshaCatalog, collectionNumber: Int): NakshaCollection? /** * Load all tuples into the given [feature-tuples][FeatureTuple]. diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/IStorage.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/IStorage.kt index d45683931..2ad3e6e2b 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/IStorage.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/IStorage.kt @@ -145,16 +145,4 @@ interface IStorage : IDictReader { val session = newReadSession(options) session.use { lambda.call(session) } } - - /** - * The best feature encoding for the given feature. - * - * - Throws [NakshaError.UNINITIALIZED], if the storage failed to initialize. - * @param feature the feature to encode; _null_ if no specific one is available. - * @param context the context in which the encoding happens (for example the [map][naksha.model.objects.NakshaCatalog] or [collection][naksha.model.objects.NakshaCollection]); _null_ if none is available. - * @return best [DataEncoding] to use. - * @since 3.0 - */ - @Deprecated("Will be removed in a future release.") - fun getDataEncoding(feature: Any?, context: Any? = null): DataEncoding } \ No newline at end of file diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt index 413c31d52..922449d3e 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt @@ -17,6 +17,7 @@ import kotlin.js.JsExport import kotlin.js.JsName import kotlin.js.JsStatic import kotlin.jvm.JvmField +import kotlin.jvm.JvmOverloads import kotlin.jvm.JvmStatic /** @@ -140,14 +141,17 @@ class Naksha private constructor() { * * **Beware**: Identifiers must not contain upper-case letters, because many storages does not make a difference between upper- and lower-cased letters. * @param id the identifier. + * @param internal if _true_, then extends the allowed character set to `[a-z_][a-z0-9_:-~$]{31}`. * @return _true_ if the identifier is valid; _false_ otherwise. * @since 3.0 * @see [verifyId] + * @see [verifyInternalId] * @see [MAX_ID_LENGTH] */ @JsStatic @JvmStatic - fun isValidId(id: String?): Boolean { + @JvmOverloads + fun isValidId(id: String?, internal: Boolean = false): Boolean { if (id.isNullOrEmpty() || "naksha" == id || id.length > MAX_ID_LENGTH) return false var i = 0 var c = id[i++] diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Tuple.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Tuple.kt index 970683b99..87c061a3c 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Tuple.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Tuple.kt @@ -84,7 +84,7 @@ data class Tuple @JvmOverloads constructor( globalBook: IBook? ): Tuple { val members = collection.useMembers() - val processors = session.processors() + val processors = session.processors // Update the tuple-number. val tnMember = collection.useMember(StandardMembers.Tn) diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt index da72a96a8..5be323683 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt @@ -18,6 +18,8 @@ import naksha.model.TupleNumber import kotlin.js.JsExport import kotlin.js.JsName +// TODO: We need an immutable version of this, because this actually allows mutation. + /** * A materialized part of a feature. * @@ -191,6 +193,12 @@ open class Member() : AnyObject(), Comparator { /** * Ensures that the given `other` member is the same as this. + * + * The purpose of the method is generally to do: + * ```kotlin + * val member = StandardMember.Id.asSame(someCustomMember) + * ``` + * Which will return the given custom member, ensuring that it still is the standard `id` member, but allows different paths. * @param other The member to compare this with. * @param comparePath If the path must be the same as well, defaults to _false_. * @return The `other` member, if it is the same as this. diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberList.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberList.kt index 42a2f6f16..94ade30a5 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberList.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberList.kt @@ -3,6 +3,7 @@ package naksha.model.objects import naksha.base.ListProxy +import naksha.model.Naksha import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_STATE import naksha.model.NakshaError.NakshaErrorCompanion.INTERNAL_ERROR import naksha.model.NakshaException @@ -120,12 +121,15 @@ open class MemberList() : ListProxy(Member::class) { } /** - * Test whether this member list is valid, so does not have `null` entries and all members have unique names. Throws a [NakshaException], if any error is found. + * Test whether this member list is valid, so does not have `null` entries and all members have unique valid names. Throws a [NakshaException], if any error is found. */ fun validate() { for (i in 0 until this.size) { val member = this[i] ?: throw NakshaException(ILLEGAL_STATE, "Member at index $i is null") val memberName = member.name + if (!Naksha.isValidId(memberName, internal = true)) { + throw NakshaException(ILLEGAL_STATE, "Member at index $i has invalid name: $memberName") + } for (j in (i + 1) until this.size) { val later = this[j] ?: throw NakshaException(ILLEGAL_STATE, "Member at index $j is null") if (memberName == later.name) { diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt index e09be2d93..5057fa1ae 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt @@ -361,9 +361,12 @@ open class NakshaCollection() : NakshaFeature() { } /** - * Returns the members list. If the member list is currently `null`, it creates it from [XyzMembers.ALL]. If the list does not contain the mandatory members, they will be added. + * Returns the validated members list. + * + * If the member list is currently `null`, it creates it from [XyzMembers.ALL]. If the list does not contain the mandatory members, they will be added. * @return the members list of this collection. * @since 3.0 + * @throws NakshaException with [ILLEGAL_STATE] */ open fun useMembers(): MemberList { var write = false @@ -378,9 +381,10 @@ open class NakshaCollection() : NakshaFeature() { for (mandatory in StandardMembers.MANDATORY) { val found: Member? = list.get(mandatory.name) if (found != null) { - mandatory.asSame(found) + mandatory.asSame(found, comparePath = false) } else { list.add(mandatory) + write = true } } if (write) this.members = list diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt index 72b5a812d..4baa27614 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt @@ -6,6 +6,9 @@ import kotlin.js.JsExport import kotlin.js.JsStatic import kotlin.jvm.JvmField +// TODO: We need immutability for the standard members and other pre-declared members. +// If not, we could encounter a situation where someone accidentally modified a standard member. + /** * The canonical set of standard members that every Naksha storage understands. * @since 3.0 diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgAdminCatalog.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgAdminCatalog.kt index 6328c34c3..2d11e9fd7 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgAdminCatalog.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgAdminCatalog.kt @@ -585,9 +585,9 @@ WHERE fn = $1 AND (version & 3) < 2""" = PgMapList().withAll(catalogCache.mapNotNull { it.value }) // TODO: This only reads the cache, but we need to load from database! - abstract fun getDataEncoding(feature: Any?, context: Any?): DataEncoding - + @Deprecated("Will be replaced with global books") abstract override fun getDictionary(id: String): JbDictionary? + @Deprecated("Will be replaced with global books") abstract override fun getEncodingDictionary(feature: Any?, context: Any?): JbDictionary? } \ No newline at end of file diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCatalog.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCatalog.kt index 3b9e5d403..e84395c10 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCatalog.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCatalog.kt @@ -179,35 +179,23 @@ open class PgCatalog internal constructor( * @return the created map. * @since 3.0 */ - open fun createPgCollection(session: IWriteSession, conn: PgConnection, collection: PgCollection) { + open fun createPgCollection(conn: PgConnection, collection: PgCollection) { // Ensure that all tables and indices are created in the correct schema! setSearchPath(conn) - val processors = session.processors - val backup = processors.backup(true) - try { - processors.addProcessor(XyzMembers.XyzCreatedAt, XyzProcessors.xyzCreatedAt) - processors.addProcessor(XyzMembers.XyzUpdatedAt, XyzProcessors.xyzUpdatedAt) - processors.addProcessor(XyzMembers.XyzAppId, XyzProcessors.xyzAppId) - processors.addProcessor(XyzMembers.XyzAuthor, XyzProcessors.xyzAuthor) - processors.addProcessor(XyzMembers.XyzAuthorTimestamp, XyzProcessors.xyzAuthorTimestamp) - val pgSession = session as PgSession - val tx: PgTx = pgSession.useTx() + val headTable = collection.headTable + headTable.create(conn) + for (index in collection.headIndices) headTable.createIndex(conn, index) - val headTable = collection.headTable - headTable.create(conn) - for (index in collection.headIndices) headTable.createIndex(conn, index) - - val history = collection.historyTable - history.create(conn) - history.createPartition(conn, collection.historyPartitionNumberOf(tx.version.number)) - for (index in collection.historyIndices) headTable.createIndex(conn, index) + val history = collection.historyTable + history.create(conn) + // Note: We do not create history partitions, because doing so would require a session. + // The reason is, that we need to know the version to know which history partition the HEAD will be moved into. + // history.createPartition(conn, collection.historyPartitionNumberOf(tx.version.number)) + // for (index in collection.historyIndices) headTable.createIndex(conn, index) - // TODO: Fix cache by adding 2nd level cache in session, we only want to update in 2nd level cache and move to 1rst level when committed! - invalidateCollection(collection) - } finally { - processors.restore(backup, clear = true, consume = true) - } + // TODO: Fix cache by adding 2nd level cache in session, we only want to update in 2nd level cache and move to 1rst level when committed! + invalidateCollection(collection) } /** diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt index ecb1802ac..58f130125 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt @@ -24,6 +24,7 @@ import naksha.psql.PgColumn.PgColumn_C.EXTERNAL import naksha.psql.PgColumn.PgColumn_C.MAIN import naksha.psql.PgColumn.PgColumn_C.PLAIN import kotlin.js.JsExport +import kotlin.js.JsName import kotlin.jvm.JvmField /** @@ -287,6 +288,7 @@ open class PgCollection internal constructor( * @return the [PgColumn] that with the given name; `null` if no such column exists. * @since 3.0 */ + @JsName("getColumnByMember") fun column(member: Member): PgColumn? = column(member.name) /** @@ -295,6 +297,7 @@ open class PgCollection internal constructor( * @return the [PgColumn] that with the given name; `null` if no such column exists. * @since 3.0 */ + @JsName("getColumnByName") fun column(name: String): PgColumn? { for (column in columns) { if (column.name == name) return column diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgDistributionPartition.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgDistributionPartition.kt index dd85d2de5..2cf90a6fc 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgDistributionPartition.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgDistributionPartition.kt @@ -10,6 +10,7 @@ import naksha.psql.PgColumn.PgColumn_C.FN import naksha.psql.PgColumn.PgColumn_C.VERSION import naksha.psql.PgUtil.PgUtilCompanion.quoteIdent import kotlin.js.JsExport +import kotlin.js.JsName import kotlin.jvm.JvmField /** @@ -38,11 +39,13 @@ class PgDistributionPartition private constructor( /** * Create a distribution partition in the [PgHeadTable]. */ + @JsName("newHeadDistributionPartition") constructor(parent: PgHeadTable, partitionNumber: Int) : this(parent as PgTable, partitionNumber) /** * Create a distribution partition in the history aka [PgHistoryPartition]. */ + @JsName("newHistoryDistributionPartition") constructor(parent: PgHistoryPartition, partitionNumber: Int) : this(parent as PgTable, partitionNumber) override fun CREATE_SQL(): String { diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgHeadTable.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgHeadTable.kt index d68aef6fd..f47c56943 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgHeadTable.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgHeadTable.kt @@ -8,6 +8,7 @@ import naksha.psql.PgColumn.PgColumn_C.VERSION import naksha.psql.PgUtil.PgUtilCompanion.partitionNumber import naksha.psql.PgUtil.PgUtilCompanion.quoteIdent import kotlin.js.JsExport +import kotlin.js.JsName import kotlin.jvm.JvmField /** @@ -67,9 +68,11 @@ PARTITION BY RANGE ((($FN & 65535)::int4 % ${collection.partitions})$TABLESPACE; * @param featureId the ID of the feature to locate the performance partition for. * @return either the performance partition to put the feature into; _null_ if the table is not partitioned, features need to be written into the table itself. */ + @JsName("getByFeatureId") operator fun get(featureId: String): PgDistributionPartition? = if (partitions.isEmpty()) null else partitions[partitionNumber(featureId) % partitions.size] + @JsName("getByFeatureNumber") operator fun get(featureNumber: Int64): PgDistributionPartition? = if (partitions.isEmpty()) null else partitions[featureNumber.toInt() % partitions.size] diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgHistoryPartition.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgHistoryPartition.kt index f505678a7..b92aca53e 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgHistoryPartition.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgHistoryPartition.kt @@ -12,6 +12,7 @@ import naksha.psql.PgColumn.PgColumn_C.FN import naksha.psql.PgColumn.PgColumn_C.VERSION import naksha.psql.PgUtil.PgUtilCompanion.quoteIdent import kotlin.js.JsExport +import kotlin.js.JsName import kotlin.jvm.JvmField /** @@ -72,6 +73,7 @@ WITH (fillfactor=50,toast_tuple_target=$toast_tuple_target)$TABLESPACE""" * @return the calculated partition-number. * @since 3.0 */ + @JsName("partitionNumberForFeatureNumber") fun partitionNumber(featureNumber: Int64): Int = Naksha.partitionNumber(featureNumber) % collection.partitions /** @@ -80,6 +82,7 @@ WITH (fillfactor=50,toast_tuple_target=$toast_tuple_target)$TABLESPACE""" * @return the calculated partition-number. * @since 3.0 */ + @JsName("partitionNumberForFeatureId") fun partitionNumber(featureId: String): Int = Naksha.partitionNumber(featureId) % collection.partitions /** @@ -87,6 +90,7 @@ WITH (fillfactor=50,toast_tuple_target=$toast_tuple_target)$TABLESPACE""" * @param featureNumber the feature-number of the feature to return the distribution-partition for. * @return either the distribution-partition to put the feature into or `null` if the table is not partitioned, features need to be written into the table itself. */ + @JsName("getByFeatureNumber") operator fun get(featureNumber: Int64): PgDistributionPartition? { val partitions = this.partitions if (partitions.isEmpty()) return null @@ -100,6 +104,7 @@ WITH (fillfactor=50,toast_tuple_target=$toast_tuple_target)$TABLESPACE""" * @param featureId the feature-id of the feature to return the distribution-partition for. * @return either the distribution-partition to put the feature into or `null` if the table is not partitioned, features need to be written into the table itself. */ + @JsName("getByFeatureId") operator fun get(featureId: String): PgDistributionPartition? { val partitions = this.partitions if (partitions.isEmpty()) return null diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMemberHelper.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMemberHelper.kt index dd6f19908..5888ff35e 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMemberHelper.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMemberHelper.kt @@ -9,14 +9,14 @@ import naksha.base.ListProxy import naksha.base.PlatformList import naksha.base.Platform.PlatformCompanion.logger import naksha.base.Platform.PlatformCompanion.toJSON -import naksha.model.NakshaError -import naksha.model.NakshaException import naksha.model.TagList import naksha.model.objects.Member -import naksha.model.objects.MemberList import naksha.model.objects.MemberType import naksha.model.objects.NakshaFeature +// TODO: This is AI generated slop, we need to review and only use what we really need! +// What is really used should go into static Member methods! + /** * Helpers to map [CustomMember] values from a [NakshaFeature] into a [PgRows] row. * @@ -35,22 +35,6 @@ class PgMemberHelper private constructor() { */ fun pgColumnName(memberName: String): String = memberName - /** - * Validates that none of the members in [members] use a reserved built-in column name. - * Throws [NakshaException] with [NakshaError.ILLEGAL_ARGUMENT] on the first conflict found. - * Must be called before creating a new collection. - */ - fun validateMemberNames(members: MemberList) { - for (member in members) { - if (member != null && member.name in reservedColumnNames) { - throw NakshaException( - NakshaError.ILLEGAL_ARGUMENT, - "Custom member name '${member.name}' conflicts with a built-in column name" - ) - } - } - } - fun pgTypeFor(type: MemberType): PgType = when (type) { MemberType.BOOLEAN -> PgType.BOOLEAN MemberType.INT8 -> PgType.SHORT @@ -332,40 +316,6 @@ class PgMemberHelper private constructor() { else -> 12 } - /** - * The set of member names that correspond to pre-defined optional [PgColumn]s (e.g. `geo`, `tags`, `cc`). - * When two members have the same type sort-order, pre-defined members are placed before user-invented ones. - */ - private val predefinedMemberNames: Set by lazy { - val mandatory = PgColumn.mandatoryColumns.map { it.name }.toSet() - PgColumn.headColumns.map { it.name }.filter { it !in mandatory }.toSet() - } - - /** - * Sorts [members] in-place for optimal PostgreSQL column layout. - * - * Ordering rules (applied only at **collection-creation** time; never on updates): - * 1. Primary: type alignment group ([columnSortOrder]) - * 2. Secondary: pre-defined members (matching a built-in optional column name) before user-invented ones - * 3. Tertiary: member name lexicographically ascending - * - * The sort is stable within each group so that the caller's intent is preserved as a tie-breaker. - */ - fun sortMembersForStorage(members: MemberList) { - // TODO: Move this into MemberList as `sortForStorage()` - // Use: members.sortedBy { it?.dataType?.sortOrder ?: Int.MAX_VALUE } - if (members.size <= 1) return - val snapshot = (0 until members.size).map { members[it]!! } - val sorted = snapshot.sortedWith( - compareBy( - { columnSortOrder(it.dataType) }, - { if (it.name in predefinedMemberNames) 0 else 1 }, - { it.name } - )) - members.clear() - members.addAll(sorted) - } - private fun warnMismatch(featureId: String, memberName: String, expected: String, value: Any) { logger.warn("Custom member '$memberName' on feature '$featureId': expected $expected, got ${value::class.simpleName}") } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaTransactions.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaTransactions.kt index a21f996ec..c48a62de3 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaTransactions.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgNakshaTransactions.kt @@ -11,7 +11,7 @@ import naksha.model.objects.StoreMode import kotlin.js.JsExport /** - * The internal collection in the admin-map, that keeps track of the transactions of the storage. + * The internal collection in the admin-catalog, that keeps track of the transactions of the storage. * * This is a standard partitioned collection with: * - 16 HEAD partitions (partitioned by `fn`) @@ -27,7 +27,7 @@ import kotlin.js.JsExport * HERE global sequencer populates them. */ @JsExport -class PgNakshaTransactions internal constructor(adminMap: PgAdminCatalog) : PgCollection(adminMap, NakshaCollection() +class PgNakshaTransactions internal constructor(adminCatalog: PgAdminCatalog) : PgCollection(adminCatalog, NakshaCollection() .withCatalogId(ADMIN_CATALOG_ID) .withId(TRANSACTIONS_COL_ID) .withStoreDeleted(StoreMode.OFF) @@ -40,10 +40,11 @@ class PgNakshaTransactions internal constructor(adminMap: PgAdminCatalog) : PgCo StandardMembers.GlobalVersion, ) .withIndices( - naksha.model.objects.Index().withName(PgIndex.txn_unique.name), StandardIndices.PublishNumber, StandardIndices.PublishTime, StandardIndices.GlobalVersion, ) ), PgInternalCollection +// TODO: We need to fix this, we want all internal collections to use XYZ members and indices +// For this case, we additionally want the publication members and indices. \ No newline at end of file diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgSession.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgSession.kt index e0dcc89d1..2529b3ef9 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgSession.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgSession.kt @@ -16,10 +16,8 @@ import naksha.model.objects.NakshaFeature import naksha.model.request.* import naksha.model.request.WriteRequest import naksha.model.objects.NakshaTx -import naksha.psql.PgColumn.PgColumn_C.FN import kotlin.js.JsExport import kotlin.jvm.JvmField -import kotlin.math.min /** * A session linked to a PostgresQL database. @@ -492,10 +490,10 @@ SELECT * FROM from_hst""" return found } - override fun getMapById(mapId: String): NakshaCatalog? { + override fun getCatalogById(catalogId: String): NakshaCatalog? { assertOpen() return (if (mayReadParallel) newReadConnection() else readConnection()).use { - storage.adminCatalog.getPgCatalogById(it.conn, mapId)?.head + storage.adminCatalog.getPgCatalogById(it.conn, catalogId)?.head } } @@ -511,10 +509,10 @@ SELECT * FROM from_hst""" } } - override fun getMapByNumber(mapNumber: Int): NakshaCatalog? { + override fun getMapByNumber(catalogNumber: Int): NakshaCatalog? { assertOpen() return (if (mayReadParallel) newReadConnection() else readConnection()).use { - storage.adminCatalog.getPgCatalogByNumber(it.conn, mapNumber)?.head + storage.adminCatalog.getPgCatalogByNumber(it.conn, catalogNumber)?.head } } @@ -530,10 +528,10 @@ SELECT * FROM from_hst""" } } - override fun getCollectionById(map: NakshaCatalog, collectionId: String): NakshaCollection? { + override fun getCollectionById(catalog: NakshaCatalog, collectionId: String): NakshaCollection? { assertOpen() return (if (mayReadParallel) newReadConnection() else readConnection()).use { - val pgMap = storage.adminCatalog.getPgCatalogById(it.conn, map.id) ?: return null + val pgMap = storage.adminCatalog.getPgCatalogById(it.conn, catalog.id) ?: return null pgMap.getPgCollectionById(it.conn, collectionId)?.head } } @@ -551,10 +549,10 @@ SELECT * FROM from_hst""" } } - override fun getCollectionByNumber(map: NakshaCatalog, collectionNumber: Int): NakshaCollection? { + override fun getCollectionByNumber(catalog: NakshaCatalog, collectionNumber: Int): NakshaCollection? { assertOpen() return (if (mayReadParallel) newReadConnection() else readConnection()).use { - val pgMap = storage.adminCatalog.getPgCatalogById(it.conn, map.id) ?: return null + val pgMap = storage.adminCatalog.getPgCatalogById(it.conn, catalog.id) ?: return null pgMap.getPgCollectionByNumber(it.conn, collectionNumber)?.head } } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgUtil.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgUtil.kt index 6b8e295c0..eeb38459c 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgUtil.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgUtil.kt @@ -205,81 +205,5 @@ class PgUtil private constructor() { @JsStatic @JvmStatic fun lockId(name: String): Int64 = Fnv1a64.string(Fnv1a64.start(), name) - - /** - * Decode the Naksha feature, auto-detecting the encoding from header bytes. - * @param bytes the bytes to decode. - * @param dictManager the dictionary manager to use for legacy JBON1 decoding; if any. - * @return the Naksha feature. - * @since 3.0.0 - */ - @JsStatic - @JvmStatic - @Deprecated( - message = "Please use Naksha class instead", - replaceWith = ReplaceWith("Naksha.decodeFeature(bytes, dictManager)"), - level = DeprecationLevel.WARNING - ) - fun decodeFeature(bytes: ByteArray?, dictManager: IDictManager? = null): NakshaFeature? = Naksha.decodeFeature(bytes, dictManager) - - /** - * Decode the Naksha tags from their raw `jsonb` text form. - * @param json the JSON text to decode (value of the `tags` `jsonb` column). - * @return the Naksha tags. - * @since 3.0.0 - */ - @JsStatic - @JvmStatic - @Deprecated( - message = "Please use Naksha class instead", - replaceWith = ReplaceWith("Naksha.decodeTags(json)"), - level = DeprecationLevel.WARNING - ) - fun decodeTags(json: String?): TagMap? = Naksha.decodeTags(json) - - /** - * Encodes the given tags into raw `jsonb` text. - * @param tags the tags to encode. - * @return the JSON text representation. - * @since 3.0.0 - */ - @JsStatic - @JvmStatic - @Deprecated( - message = "Please use Naksha class instead", - replaceWith = ReplaceWith("Naksha.encodeTags(tags)"), - level = DeprecationLevel.WARNING - ) - fun encodeTags(tags: TagMap?): String? = Naksha.encodeTags(tags) - - /** - * Decode a GeoJSON geometry from `TWKB` encoded bytes. All Naksha geometries are stored as raw TWKB. - * @param bytes the bytes to decode. - * @return the geometry. - * @since 3.0.0 - */ - @JsStatic - @JvmStatic - @Deprecated( - message = "Please use Naksha class instead", - replaceWith = ReplaceWith("Naksha.decodeGeometry(bytes)"), - level = DeprecationLevel.WARNING - ) - fun decodeGeometry(bytes: ByteArray?): SpGeometry? = Naksha.decodeGeometry(bytes) - - /** - * Encodes the given GeoJSON geometry into `TWKB` bytes. All Naksha geometries are stored as raw TWKB. - * @param geometry the geometry to encode. - * @return the encoded GeoJSON geometry. - * @since 3.0.0 - */ - @JsStatic - @JvmStatic - @Deprecated( - message = "Please use Naksha class instead", - replaceWith = ReplaceWith("Naksha.encodeGeometry(geometry)"), - level = DeprecationLevel.WARNING - ) - fun encodeGeometry(geometry: SpGeometry?): ByteArray? = Naksha.encodeGeometry(geometry) } } \ No newline at end of file diff --git a/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlAdminCatalog.kt b/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlAdminCatalog.kt index f183d9bf7..4014b6c2b 100644 --- a/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlAdminCatalog.kt +++ b/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlAdminCatalog.kt @@ -23,28 +23,6 @@ class PsqlAdminCatalog internal constructor( override val storage: PsqlStorage get() = super.storage as PsqlStorage - override fun getDataEncoding(feature: Any?, context: Any?): DataEncoding { - if (context is PgCollection) { - var encoding = context.head.dataEncoding - if (encoding == null) encoding = context.catalog.head.dataEncoding - return encoding ?: Naksha.DEFAULT_DATA_ENCODING - } - if (context is PgCatalog) { - return context.head.dataEncoding ?: Naksha.DEFAULT_DATA_ENCODING - } - if (context is NakshaCollection) { - val collectionEncoding = context.dataEncoding - if (collectionEncoding != null) return collectionEncoding - val mapId = context.catalogId ?: return Naksha.DEFAULT_DATA_ENCODING - val pgMap = getPgCatalogById(null, mapId) - return pgMap?.head?.dataEncoding ?: Naksha.DEFAULT_DATA_ENCODING - } - if (context is NakshaCatalog) { - return context.dataEncoding ?: Naksha.DEFAULT_DATA_ENCODING - } - return Naksha.DEFAULT_DATA_ENCODING - } - override fun getDictionary(id: String): JbDictionary? { // TODO: Implement me! return null diff --git a/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlMap.kt b/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlCatalog.kt similarity index 96% rename from here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlMap.kt rename to here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlCatalog.kt index bcec3b0c0..f1d7b2dad 100644 --- a/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlMap.kt +++ b/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlCatalog.kt @@ -8,16 +8,16 @@ import java.util.concurrent.TimeUnit /** * A cache for a specific map, which by itself will cache the collections. * - * @property adminMap the admin-map to which this cache entry belongs. + * @property adminCatalog the admin-map to which this cache entry belongs. * @property id the map-id. * @property number the map-number. * @since 3.0 */ -data class PsqlMap( - val adminMap: PsqlAdminCatalog, +data class PsqlCatalog( + val adminCatalog: PsqlAdminCatalog, val pgCatalog: PgCatalog? = null, - val id: String = pgCatalog?.id ?: throw illegalArg("PsqlMap without valid id"), - val number: Int = pgCatalog?.number ?: throw illegalArg("PsqlMap without valid number") + val id: String = pgCatalog?.id ?: throw illegalArg("PsqlCatalog without valid id"), + val number: Int = pgCatalog?.catalogNumber ?: throw illegalArg("PsqlCatalog without valid number") ): Expiry { /** diff --git a/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlCollection.kt b/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlCollection.kt index ce2135c53..f2010770b 100644 --- a/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlCollection.kt +++ b/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlCollection.kt @@ -6,7 +6,7 @@ import naksha.base.AtomicRef * A cache entry for a [PgCollection]. * @since 3.0 */ -data class PsqlCollection(val psqlMap: PsqlMap, val id: String, val number: Number) { +data class PsqlCollection(val psqlCatalog: PsqlCatalog, val id: String, val number: Number) { /** * Tests if the underlying [PgCollection] exist. * @return `true` if the collection exists; `false` if this is a tombstone cache entry. diff --git a/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlStorage.kt b/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlStorage.kt index 69ab6246f..1a28a3524 100644 --- a/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlStorage.kt +++ b/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlStorage.kt @@ -66,8 +66,6 @@ open class PsqlStorage : PgStorage(), IStorage { return PgSession(this, options, readOnly) } - override fun getDataEncoding(feature: Any?, context: Any?): DataEncoding = adminCatalog.getDataEncoding(feature, context) - override fun getDictionary(id: String): JbDictionary? = adminCatalog.getDictionary(id) override fun getEncodingDictionary(feature: Any?, context: Any?): JbDictionary? = adminCatalog.getEncodingDictionary(feature, context) diff --git a/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/ViewReadSession.java b/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/ViewReadSession.java index 52a07367c..837d739af 100644 --- a/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/ViewReadSession.java +++ b/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/ViewReadSession.java @@ -261,7 +261,7 @@ public void loadTuples(@NotNull List featureTuples, int } @Override - public @Nullable NakshaCatalog getMapByNumber(int mapNumber) { + public @Nullable NakshaCatalog getMapByNumber(int catalogNumber) { throw new UnsupportedOperationException(); } @@ -271,7 +271,7 @@ public void loadTuples(@NotNull List featureTuples, int } @Override - public @Nullable NakshaCollection getCollectionByNumber(@NotNull NakshaCatalog map, int collectionNumber) { + public @Nullable NakshaCollection getCollectionByNumber(@NotNull NakshaCatalog catalog, int collectionNumber) { throw new UnsupportedOperationException(); } diff --git a/here-naksha-lib-view/src/jvmTest/java/com/here/naksha/lib/view/MockReadSession.java b/here-naksha-lib-view/src/jvmTest/java/com/here/naksha/lib/view/MockReadSession.java index 733bfb792..397b5f48d 100644 --- a/here-naksha-lib-view/src/jvmTest/java/com/here/naksha/lib/view/MockReadSession.java +++ b/here-naksha-lib-view/src/jvmTest/java/com/here/naksha/lib/view/MockReadSession.java @@ -108,7 +108,7 @@ public boolean isClosed() { } @Override - public @Nullable NakshaCatalog getMapByNumber(int mapNumber) { + public @Nullable NakshaCatalog getMapByNumber(int catalogNumber) { return null; } @@ -118,7 +118,7 @@ public boolean isClosed() { } @Override - public @Nullable NakshaCollection getCollectionByNumber(@NotNull NakshaCatalog map, int collectionNumber) { + public @Nullable NakshaCollection getCollectionByNumber(@NotNull NakshaCatalog catalog, int collectionNumber) { return null; } diff --git a/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/HttpStorageReadSession.java b/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/HttpStorageReadSession.java index f7ed05d50..9d2e9c55a 100644 --- a/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/HttpStorageReadSession.java +++ b/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/HttpStorageReadSession.java @@ -141,7 +141,7 @@ public Response executeParallel(@NotNull Request request) { } @Override - public @Nullable NakshaCatalog getMapByNumber(int mapNumber) { + public @Nullable NakshaCatalog getMapByNumber(int catalogNumber) { return null; } @@ -151,7 +151,7 @@ public void loadTuples(@NotNull List featureTuples, int } @Override - public @Nullable NakshaCollection getCollectionByNumber(@NotNull NakshaCatalog map, int collectionNumber) { + public @Nullable NakshaCollection getCollectionByNumber(@NotNull NakshaCatalog catalog, int collectionNumber) { throw new NotImplementedException("Not supported by HTTP storage"); } From 998e8906e95bffe529cb072cb2804aeaa50edc1e Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Fri, 19 Jun 2026 13:27:35 +0200 Subject: [PATCH 24/57] Rename more maps into catalogs Signed-off-by: Alexander Lowey-Weber --- .../http/tasks/EventHandlerApiTask.java | 4 +- .../app/service/http/tasks/SpaceApiTask.java | 4 +- .../service/http/tasks/StorageApiTask.java | 4 +- .../service/util/NakshaAdminRequestUtil.java | 2 +- .../naksha/cli/copy/service/CopyService.java | 2 +- .../cli/storages/GeneratingSession.java | 2 +- .../cli/copy/service/CopyServiceTest.java | 2 +- .../cli/copy/service/psql/PsqlCopyTest.java | 2 +- .../lib/handlers/DefaultStorageHandler.java | 2 +- .../handlers/DefaultStorageHandlerTest.java | 2 +- .../hub/storages/NHAdminStorageReader.java | 4 +- .../hub/storages/NHSpaceStorageReader.java | 2 +- .../lib/hub/mock/NHAdminReaderMock.java | 2 +- .../kotlin/naksha/model/ISession.kt | 2 +- .../naksha/model/request/ReadCollections.kt | 2 +- .../naksha/model/request/ReadFeatures.kt | 6 +-- .../kotlin/naksha/model/request/ReadMaps.kt | 2 +- .../naksha/model/request/ReadTransactions.kt | 2 +- .../java/naksha/model/util/RequestHelper.java | 4 +- .../kotlin/naksha/psql/PgSession.kt | 54 +++++++++---------- .../kotlin/naksha/psql/AttachmentTest.kt | 6 +-- .../kotlin/naksha/psql/ChainCollectionTest.kt | 4 +- .../kotlin/naksha/psql/CollectionTests.kt | 10 ++-- .../kotlin/naksha/psql/DeleteFeatureBase.kt | 8 +-- .../kotlin/naksha/psql/HistoryUuidTest.kt | 4 +- .../kotlin/naksha/psql/InsertFeatureTest.kt | 14 ++--- .../kotlin/naksha/psql/PartitioningTest.kt | 2 +- .../naksha/psql/PgPropertyFilterTest.kt | 8 +-- .../kotlin/naksha/psql/ReadFeaturesAll.kt | 2 +- .../naksha/psql/ReadFeaturesByGeometryTest.kt | 6 +-- .../naksha/psql/ReadFeaturesByGuuidTest.kt | 2 +- .../naksha/psql/ReadFeaturesByMetadataTest.kt | 10 ++-- .../naksha/psql/ReadFeaturesByOtherTns.kt | 2 +- .../naksha/psql/ReadFeaturesByRefTilesTest.kt | 4 +- .../naksha/psql/ReadFeaturesByTagsTest.kt | 2 +- .../kotlin/naksha/psql/ReadHistoryTest.kt | 6 +-- .../kotlin/naksha/psql/ReadLimitTest.kt | 2 +- .../kotlin/naksha/psql/ReadOrderedTest.kt | 4 +- .../naksha/psql/RecreateAfterDeleteTest.kt | 12 ++--- .../kotlin/naksha/psql/UpdateFeatureTest.kt | 4 +- .../kotlin/naksha/psql/UpsertFeatureTest.kt | 2 +- .../jsMain/kotlin/naksha/psql/Plv8Storage.kt | 4 -- .../here/naksha/lib/view/ViewReadSession.java | 2 +- .../naksha/lib/view/ViewWriteSession.java | 2 +- .../lib/view/concurrent/LayerReadRequest.java | 2 +- .../concurrent/ParallelQueryExecutor.java | 4 +- .../here/naksha/lib/view/MockReadSession.java | 2 +- .../com/here/naksha/lib/view/ViewTest.java | 2 +- .../storage/http/HttpStorageReadSession.java | 2 +- 49 files changed, 114 insertions(+), 126 deletions(-) diff --git a/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/EventHandlerApiTask.java b/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/EventHandlerApiTask.java index f5cc676ed..302457596 100644 --- a/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/EventHandlerApiTask.java +++ b/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/EventHandlerApiTask.java @@ -107,7 +107,7 @@ protected void init() { private @NotNull XyzResponse executeGetHandlers() { // Create ReadFeatures Request to read all handlers from Admin DB final ReadFeatures request = new ReadFeatures().addCollectionId(EVENT_HANDLERS); - request.setMapId(naksha().getAdminMapId()); + request.setCatalogId(naksha().getAdminMapId()); // Submit request to NH Space Storage Response response = executeReadRequestFromSpaceStorage(request); // transform Response to Http FeatureCollection response @@ -118,7 +118,7 @@ protected void init() { // Create ReadFeatures Request to read the handler with the specific ID from Admin DB final String handlerId = routingContext.pathParam(HANDLER_ID); final ReadFeatures request = new ReadFeatures().addCollectionId(EVENT_HANDLERS); - request.setMapId(naksha().getAdminMapId()); + request.setCatalogId(naksha().getAdminMapId()); request.setFeatureIds(StringList.of(handlerId)); // Submit request to NH Space Storage Response response = executeReadRequestFromSpaceStorage(request); diff --git a/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/SpaceApiTask.java b/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/SpaceApiTask.java index 27f24636f..e2dd9efc5 100644 --- a/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/SpaceApiTask.java +++ b/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/SpaceApiTask.java @@ -133,7 +133,7 @@ private XyzResponse executeDeleteSpace() { private @NotNull XyzResponse executeGetSpaces() { final ReadFeatures request = new ReadFeatures().addCollectionId(SPACES); - request.setMapId(naksha().getAdminMapId()); + request.setCatalogId(naksha().getAdminMapId()); Response response = executeReadRequestFromSpaceStorage(request); return transformResponseToXyzCollectionResponse(response, Space.class, 0, DEF_ADMIN_FEATURE_LIMIT, null, null); } @@ -141,7 +141,7 @@ private XyzResponse executeDeleteSpace() { private @NotNull XyzResponse executeGetSpaceById() { final String spaceId = extractMandatoryPathParam(routingContext, SPACE_ID); final ReadFeatures request = new ReadFeatures().addCollectionId(SPACES); - request.setMapId(naksha().getAdminMapId()); + request.setCatalogId(naksha().getAdminMapId()); request.setFeatureIds(StringList.of(spaceId)); Response response = executeReadRequestFromSpaceStorage(request); return transformResponseToXyzFeatureResponse(response, Space.class, NOT_FOUND_ON_NO_ELEMENTS); diff --git a/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/StorageApiTask.java b/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/StorageApiTask.java index 36cd9df3e..cf509f1ee 100644 --- a/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/StorageApiTask.java +++ b/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/StorageApiTask.java @@ -104,7 +104,7 @@ protected void init() { private @NotNull XyzResponse executeGetStorages() { final ReadFeatures request = new ReadFeatures().addCollectionId(STORAGES); - request.setMapId(naksha().getAdminMapId()); + request.setCatalogId(naksha().getAdminMapId()); Response response = executeReadRequestFromSpaceStorage(request); return transformResponseToXyzCollectionResponse(response, NakshaStorage.class, STORAGE_MASKING); } @@ -112,7 +112,7 @@ protected void init() { private @NotNull XyzResponse executeGetStorageById() { final String storageId = ApiParams.extractMandatoryPathParam(routingContext, STORAGE_ID); final ReadFeatures request = new ReadFeatures().addCollectionId(STORAGES); - request.setMapId(naksha().getAdminMapId()); + request.setCatalogId(naksha().getAdminMapId()); request.setFeatureIds(StringList.of(storageId)); return transformedResponseTo(request); } diff --git a/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/util/NakshaAdminRequestUtil.java b/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/util/NakshaAdminRequestUtil.java index 206a53847..8b8b41b52 100644 --- a/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/util/NakshaAdminRequestUtil.java +++ b/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/util/NakshaAdminRequestUtil.java @@ -48,7 +48,7 @@ public static WriteRequest deleteEventHandlerRequest(INaksha naksha, String hand private static ReadFeatures getAdminResourcesRequest(INaksha naksha, String resourceCollection) { ReadFeatures readFeatures = new ReadFeatures(); - readFeatures.setMapId(naksha.getAdminMapId()); + readFeatures.setCatalogId(naksha.getAdminMapId()); readFeatures.addCollectionId(resourceCollection); return readFeatures; } diff --git a/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/copy/service/CopyService.java b/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/copy/service/CopyService.java index 0ac11db0c..5d686e14d 100644 --- a/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/copy/service/CopyService.java +++ b/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/copy/service/CopyService.java @@ -115,7 +115,7 @@ private ReadFeatures createReadFeaturesRequest(CopyElement source) { readFeatures.setCollectionIds( StringList.of(source.getCollectionId()) ); - readFeatures.setMapId(source.getMapId()); + readFeatures.setCatalogId(source.getMapId()); return readFeatures; } diff --git a/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/storages/GeneratingSession.java b/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/storages/GeneratingSession.java index 4d706f584..182012a37 100644 --- a/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/storages/GeneratingSession.java +++ b/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/storages/GeneratingSession.java @@ -115,7 +115,7 @@ public NakshaCatalog getMapById(@NotNull String mapId) { @Nullable @Override - public NakshaCatalog getMapByNumber(int catalogNumber) { + public NakshaCatalog getCatalogByNumber(int catalogNumber) { throw new NakshaException(NakshaError.UNSUPPORTED_OPERATION, ""); } diff --git a/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/copy/service/CopyServiceTest.java b/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/copy/service/CopyServiceTest.java index a9a858e99..c32f599aa 100644 --- a/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/copy/service/CopyServiceTest.java +++ b/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/copy/service/CopyServiceTest.java @@ -798,7 +798,7 @@ private void assertReadFeatures(List readFeaturesList) { ReadFeatures readFeatures = readFeaturesList.getFirst(); assertEquals(1, readFeatures.getCollectionIds().getSize()); assertEquals(srcCopyElement.getCollectionId(), readFeatures.getCollectionIds().getFirst()); - assertEquals(srcCopyElement.getMapId(), readFeatures.getMapId()); + assertEquals(srcCopyElement.getMapId(), readFeatures.getCatalogId()); } private IStorage createFailingSrcStorage() { diff --git a/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/copy/service/psql/PsqlCopyTest.java b/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/copy/service/psql/PsqlCopyTest.java index c7adb088b..4ec437f01 100644 --- a/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/copy/service/psql/PsqlCopyTest.java +++ b/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/copy/service/psql/PsqlCopyTest.java @@ -289,7 +289,7 @@ private ReadFeatures createReadFeaturesRequest(String mapId, String collectionId readFeatures.setCollectionIds( StringList.of(collectionId) ); - readFeatures.setMapId(mapId); + readFeatures.setCatalogId(mapId); return readFeatures; } diff --git a/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/DefaultStorageHandler.java b/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/DefaultStorageHandler.java index dc64b274b..34dab3600 100644 --- a/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/DefaultStorageHandler.java +++ b/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/DefaultStorageHandler.java @@ -525,7 +525,7 @@ private void applyMapIdAndCollectionId( ) { if (request instanceof ReadFeatures) { ReadFeatures rf = (ReadFeatures) request; - rf.setMapId(mapId); + rf.setCatalogId(mapId); rf.setCollectionIds(StringList.of(collectionId)); } else if (request instanceof WriteRequest) { WriteRequest wr = (WriteRequest) request; diff --git a/here-naksha-lib-handlers/src/jvmTest/java/com/here/naksha/lib/handlers/DefaultStorageHandlerTest.java b/here-naksha-lib-handlers/src/jvmTest/java/com/here/naksha/lib/handlers/DefaultStorageHandlerTest.java index 949998cfa..ead2fe89d 100644 --- a/here-naksha-lib-handlers/src/jvmTest/java/com/here/naksha/lib/handlers/DefaultStorageHandlerTest.java +++ b/here-naksha-lib-handlers/src/jvmTest/java/com/here/naksha/lib/handlers/DefaultStorageHandlerTest.java @@ -677,7 +677,7 @@ private static Request writeRandomFeature() { private static Request readRandomFeature() { ReadFeatures readFeatures = new ReadFeatures(); - readFeatures.setMapId("random_map_" + RandomUtils.nextInt()); + readFeatures.setCatalogId("random_map_" + RandomUtils.nextInt()); readFeatures.setCollectionIds(new StringList("random_collection_" + RandomUtils.nextInt())); readFeatures.setFeatureIds(new StringList("random_feature_" + RandomUtils.nextInt())); return readFeatures; diff --git a/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHAdminStorageReader.java b/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHAdminStorageReader.java index c5d479eb5..7f7d76a6f 100644 --- a/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHAdminStorageReader.java +++ b/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHAdminStorageReader.java @@ -118,8 +118,8 @@ public Response executeParallel(@NotNull Request request) { } @Override - public @Nullable NakshaCatalog getMapByNumber(int catalogNumber) { - return session.getMapByNumber(catalogNumber); + public @Nullable NakshaCatalog getCatalogByNumber(int catalogNumber) { + return session.getCatalogByNumber(catalogNumber); } @Override diff --git a/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHSpaceStorageReader.java b/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHSpaceStorageReader.java index 5b3de093e..65545512e 100644 --- a/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHSpaceStorageReader.java +++ b/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHSpaceStorageReader.java @@ -342,7 +342,7 @@ public Response executeParallel(@NotNull Request request) { } @Override - public @Nullable NakshaCatalog getMapByNumber(int catalogNumber) { + public @Nullable NakshaCatalog getCatalogByNumber(int catalogNumber) { throw NOT_SUPPORTED_ERROR; } diff --git a/here-naksha-lib-hub/src/jvmTest/java/com/here/naksha/lib/hub/mock/NHAdminReaderMock.java b/here-naksha-lib-hub/src/jvmTest/java/com/here/naksha/lib/hub/mock/NHAdminReaderMock.java index 04c668cfd..045915d74 100644 --- a/here-naksha-lib-hub/src/jvmTest/java/com/here/naksha/lib/hub/mock/NHAdminReaderMock.java +++ b/here-naksha-lib-hub/src/jvmTest/java/com/here/naksha/lib/hub/mock/NHAdminReaderMock.java @@ -284,7 +284,7 @@ public Response executeParallel(@NotNull Request request) { } @Override - public @Nullable NakshaCatalog getMapByNumber(int catalogNumber) { + public @Nullable NakshaCatalog getCatalogByNumber(int catalogNumber) { throw new NakshaException(new NakshaError(NakshaError.UNSUPPORTED_OPERATION, "Not supported by mock yet")); } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/ISession.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/ISession.kt index 3c354a7b0..34cffdcdf 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/ISession.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/ISession.kt @@ -125,7 +125,7 @@ interface ISession : AutoCloseable { * @return the catalog; _null_ if no such catalog exists. * @since 3.0 */ - fun getMapByNumber(catalogNumber: Int): NakshaCatalog? + fun getCatalogByNumber(catalogNumber: Int): NakshaCatalog? /** * Returns the collection for the given identifier. diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadCollections.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadCollections.kt index 25aec5298..08edadd9e 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadCollections.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadCollections.kt @@ -80,7 +80,7 @@ open class ReadCollections : ReadRequest() { */ fun toReadFeatures(): ReadFeatures { val req = ReadFeatures() - req.mapId = mapId + req.catalogId = mapId req.collectionIds.add(Naksha.COLLECTIONS_COL_ID) req.featureIds.addAll(collectionIds) return req diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadFeatures.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadFeatures.kt index 41d342c1c..5c7443138 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadFeatures.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadFeatures.kt @@ -37,13 +37,13 @@ open class ReadFeatures : ReadRequest() { * * @since 3.0 */ - var mapId by STRING_OR_NULL + var catalogId by STRING_OR_NULL /** - * @see [mapId] + * @see [catalogId] */ open fun withMapId(value: String?): ReadFeatures { - mapId = value + catalogId = value return this } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadMaps.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadMaps.kt index 5a5a508cd..ed7aeef91 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadMaps.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadMaps.kt @@ -71,7 +71,7 @@ open class ReadMaps() : ReadRequest() { */ fun toReadFeatures(): ReadFeatures { val req = ReadFeatures() - req.mapId = Naksha.ADMIN_CATALOG_ID + req.catalogId = Naksha.ADMIN_CATALOG_ID req.collectionIds.add(Naksha.CATALOGS_COL_ID) req.featureIds.addAll(mapIds) return req diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadTransactions.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadTransactions.kt index f33e8eb49..2713fd2d7 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadTransactions.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadTransactions.kt @@ -13,7 +13,7 @@ import kotlin.js.JsExport @JsExport open class ReadTransactions : ReadFeatures() { init { - mapId = Naksha.ADMIN_CATALOG_ID + catalogId = Naksha.ADMIN_CATALOG_ID collectionIds.add(Naksha.TRANSACTIONS_COL_ID) } diff --git a/here-naksha-lib-model/src/jvmMain/java/naksha/model/util/RequestHelper.java b/here-naksha-lib-model/src/jvmMain/java/naksha/model/util/RequestHelper.java index 1c0de913c..47911a9ff 100644 --- a/here-naksha-lib-model/src/jvmMain/java/naksha/model/util/RequestHelper.java +++ b/here-naksha-lib-model/src/jvmMain/java/naksha/model/util/RequestHelper.java @@ -45,7 +45,7 @@ private RequestHelper() { final @NotNull String featureId ) { final ReadFeatures readFeatures = new ReadFeatures().addCollectionId(collectionName); - readFeatures.setMapId(mapId); + readFeatures.setCatalogId(mapId); readFeatures.getFeatureIds().add(featureId); return readFeatures; } @@ -63,7 +63,7 @@ private RequestHelper() { final @NotNull List featureIds ) { final ReadFeatures readFeatures = new ReadFeatures().addCollectionId(collectionName); - readFeatures.setMapId(mapId); + readFeatures.setCatalogId(mapId); readFeatures.getFeatureIds().addAll(featureIds); return readFeatures; } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgSession.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgSession.kt index 2529b3ef9..c7b5bb284 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgSession.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgSession.kt @@ -95,9 +95,9 @@ open class PgSession( } /** - * Assert that this session mutable. - * - Throws [NakshaError.ILLEGAL_STATE] if this session is [closed][isClosed]. + * Assert that this session is closed. * @since 3.0 + * @throws NakshaException with [ILLEGAL_STATE] if this session is [closed][isClosed]. */ fun assertOpen() { if (_closed) throw NakshaException(ILLEGAL_STATE, "Connection closed") @@ -490,50 +490,43 @@ SELECT * FROM from_hst""" return found } - override fun getCatalogById(catalogId: String): NakshaCatalog? { - assertOpen() - return (if (mayReadParallel) newReadConnection() else readConnection()).use { - storage.adminCatalog.getPgCatalogById(it.conn, catalogId)?.head - } - } + override fun getCatalogById(catalogId: String): NakshaCatalog? = getPgCatalogById(catalogId)?.head /** * Returns the [PgCatalog] for the given id. - * @param mapId the map-id. + * @param catalogId the catalog-id. * @return the [PgCatalog]; _null_ if the map does not yet exist. */ - fun getPgMapById(mapId: String): PgCatalog? { + fun getPgCatalogById(catalogId: String): PgCatalog? { + val adminCatalog = storage.adminCatalog + val cachedCatalog = adminCatalog.getPgCatalogById(null, catalogId) + if (cachedCatalog != null) return cachedCatalog assertOpen() return (if (mayReadParallel) newReadConnection() else readConnection()).use { - storage.adminCatalog.getPgCatalogById(it.conn, mapId) + adminCatalog.getPgCatalogById(it.conn, catalogId) } } - override fun getMapByNumber(catalogNumber: Int): NakshaCatalog? { - assertOpen() - return (if (mayReadParallel) newReadConnection() else readConnection()).use { - storage.adminCatalog.getPgCatalogByNumber(it.conn, catalogNumber)?.head - } - } + override fun getCatalogByNumber(catalogNumber: Int): NakshaCatalog? = getPgCatalogByNumber(catalogNumber)?.head /** * Returns the [PgCatalog] for the given number. - * @param mapNumber the map-number. + * @param catalogNumber the catalog-number. * @return the [PgCatalog]; _null_ if the map does not yet exist. */ - fun getPgMapByNumber(mapNumber: Int): PgCatalog? { + fun getPgCatalogByNumber(catalogNumber: Int): PgCatalog? { + val adminCatalog = storage.adminCatalog + val cachedCatalog = adminCatalog.getPgCatalogByNumber(null, catalogNumber) + if (cachedCatalog != null) return cachedCatalog assertOpen() return (if (mayReadParallel) newReadConnection() else readConnection()).use { - storage.adminCatalog.getPgCatalogByNumber(it.conn, mapNumber) + adminCatalog.getPgCatalogByNumber(it.conn, catalogNumber) } } override fun getCollectionById(catalog: NakshaCatalog, collectionId: String): NakshaCollection? { - assertOpen() - return (if (mayReadParallel) newReadConnection() else readConnection()).use { - val pgMap = storage.adminCatalog.getPgCatalogById(it.conn, catalog.id) ?: return null - pgMap.getPgCollectionById(it.conn, collectionId)?.head - } + val pgCatalog = getPgCatalogById(catalog.id) ?: return null + return getPgCollectionById(pgCatalog, collectionId)?.head } /** @@ -543,6 +536,8 @@ SELECT * FROM from_hst""" * @return the [PgCollection]; _null_ if the collection does not yet exist. */ fun getPgCollectionById(pgCatalog: PgCatalog, collectionId: String): PgCollection? { + val cachedCollection = pgCatalog.getPgCollectionById(null, collectionId) + if (cachedCollection != null) return cachedCollection assertOpen() return (if (mayReadParallel) newReadConnection() else readConnection()).use { pgCatalog.getPgCollectionById(it.conn, collectionId) @@ -550,11 +545,8 @@ SELECT * FROM from_hst""" } override fun getCollectionByNumber(catalog: NakshaCatalog, collectionNumber: Int): NakshaCollection? { - assertOpen() - return (if (mayReadParallel) newReadConnection() else readConnection()).use { - val pgMap = storage.adminCatalog.getPgCatalogById(it.conn, catalog.id) ?: return null - pgMap.getPgCollectionByNumber(it.conn, collectionNumber)?.head - } + val pgCatalog = getPgCatalogById(catalog.id) ?: return null + return getPgCollectionByNumber(pgCatalog, collectionNumber)?.head } /** @@ -564,6 +556,8 @@ SELECT * FROM from_hst""" * @return the [PgCollection]; _null_ if the collection does not yet exist. */ fun getPgCollectionByNumber(pgCatalog: PgCatalog, collectionNumber: Int): PgCollection? { + val cachedCollection = pgCatalog.getPgCollectionByNumber(null, collectionNumber) + if (cachedCollection != null) return cachedCollection assertOpen() return (if (mayReadParallel) newReadConnection() else readConnection()).use { pgCatalog.getPgCollectionByNumber(it.conn, collectionNumber) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/AttachmentTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/AttachmentTest.kt index 709c2d997..77c3890ab 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/AttachmentTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/AttachmentTest.kt @@ -41,7 +41,7 @@ class AttachmentTest : PgTestBase() { // Read the feature Naksha.cache.clear() executeRead(ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id featureIds += featureToCreate.id }).apply { @@ -96,7 +96,7 @@ class AttachmentTest : PgTestBase() { Naksha.cache.clear() val readFeature: NakshaFeature executeRead(ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id featureIds += featureToCreate.id }).apply { @@ -185,7 +185,7 @@ class AttachmentTest : PgTestBase() { Naksha.cache.clear() val readFeature: NakshaFeature executeRead(ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id featureIds += featureToCreate.id }).apply { diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ChainCollectionTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ChainCollectionTest.kt index 57f08c0e0..06e138623 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ChainCollectionTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ChainCollectionTest.kt @@ -95,7 +95,7 @@ class ChainCollectionTest : PgTestBase( // When: reading all three back by their numeric IDs in one request val response = executeRead(ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id featureIds += headFn.toString() featureIds += midFn.toString() @@ -187,7 +187,7 @@ class ChainCollectionTest : PgTestBase( // When: reading all features from this collection (no ID filter) val all = executeRead(ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id }) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/CollectionTests.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/CollectionTests.kt index b2d76a867..aad8a0e44 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/CollectionTests.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/CollectionTests.kt @@ -52,7 +52,7 @@ class CollectionTests : PgTestBase(collection = null, mapId = "") { // Then: this collection is queryable and empty val readAllFromCollection = ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id } val collectionContent = executeRead(readAllFromCollection) @@ -60,7 +60,7 @@ class CollectionTests : PgTestBase(collection = null, mapId = "") { // And: Virtual Collections contain the created collection val selectCollectionFromVirt = ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += Naksha.COLLECTIONS_COL_ID featureIds += collection.id } @@ -207,7 +207,7 @@ class CollectionTests : PgTestBase(collection = null, mapId = "") { val readFeatureRequest = ReadFeatures() - readFeatureRequest.mapId = map.id + readFeatureRequest.catalogId = map.id readFeatureRequest.collectionIds.add(collectionName) readFeatureRequest.featureIds.add(feature.id) val readFeaturesResponse = executeRead(readFeatureRequest) @@ -280,7 +280,7 @@ class CollectionTests : PgTestBase(collection = null, mapId = "") { feature = featureCreateResponse.features[0]!! val readFeature = ReadFeatures() - readFeature.mapId = map.id + readFeature.catalogId = map.id readFeature.collectionIds.add(collectionId) readFeature.featureIds.add(feature.id) val readFeatureResponse = executeRead(readFeature) @@ -330,7 +330,7 @@ class CollectionTests : PgTestBase(collection = null, mapId = "") { val responseCollection = assertNotNull(updateResponse.features[0]).proxy(NakshaCollection::class) assertEquals(StoreMode.SUSPEND, responseCollection.storeDeleted) val selectCollectionFromVirt = ReadFeatures().apply { - mapId = map.id + catalogId = map.id collectionIds += Naksha.COLLECTIONS_COL_ID featureIds += collection.id } diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/DeleteFeatureBase.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/DeleteFeatureBase.kt index 5f4fb8c86..f1ef0bbc7 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/DeleteFeatureBase.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/DeleteFeatureBase.kt @@ -49,7 +49,7 @@ abstract class DeleteFeatureBase( // Verify that the feature does not exist Naksha.cache.clear() executeRead(ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id featureIds += initialFeature.id }).let { // this = SuccessResponse @@ -60,7 +60,7 @@ abstract class DeleteFeatureBase( // queryHistory=true (without queryDeleted) returns only past states from the history table. // The tombstone is in HEAD and is NOT included unless queryDeleted=true is also set. executeRead(ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id featureIds += initialFeature.id queryHistory = true @@ -73,7 +73,7 @@ abstract class DeleteFeatureBase( // verify if delete table contains element executeRead(ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id featureIds += initialFeature.id queryDeleted = true @@ -136,7 +136,7 @@ abstract class DeleteFeatureBase( // Confirm the tombstone is visible via queryDeleted and has the right action. Naksha.cache.clear() executeRead(ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id featureIds += featureId queryDeleted = true diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/HistoryUuidTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/HistoryUuidTest.kt index 756a4066d..40b0361ef 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/HistoryUuidTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/HistoryUuidTest.kt @@ -40,7 +40,7 @@ class HistoryUuidTest: PgTestBase(NakshaCollection( // And: Naksha.cache.clear() val featureVersions = executeRead(ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id featureIds += feature.id queryHistory = true @@ -87,7 +87,7 @@ class HistoryUuidTest: PgTestBase(NakshaCollection( // And: Naksha.cache.clear() val featureVersions = executeRead(ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id featureIds += feature.id queryHistory = true diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/InsertFeatureTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/InsertFeatureTest.kt index e1ca5a543..0b134e207 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/InsertFeatureTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/InsertFeatureTest.kt @@ -30,7 +30,7 @@ class InsertFeatureTest : PgTestBase() { // And: reading all features from collection val readResponse = executeRead(ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id featureIds += featureToCreate.id }) @@ -104,7 +104,7 @@ class InsertFeatureTest : PgTestBase() { // And: reading all features from collection val readResponse = executeRead(ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id featureIds += featureToCreate.id }) @@ -148,7 +148,7 @@ class InsertFeatureTest : PgTestBase() { // And: reading all features from collection val readResponse = executeRead(ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id featureIds += featureToCreate.id }) @@ -193,7 +193,7 @@ class InsertFeatureTest : PgTestBase() { // And: reading all features from collection val readResponse = executeRead(ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id // this.version = version // this.minVersion = version @@ -238,7 +238,7 @@ class InsertFeatureTest : PgTestBase() { Platform.logger.info("Clear cache and reload feature from database") Naksha.cache.clear(storage) val featuresByIdResponse = executeRead(ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id featureIds.add(firstFeatureToCreate.id) }) @@ -258,7 +258,7 @@ class InsertFeatureTest : PgTestBase() { // Read only one feature by bounding box. val featuresByBBox = executeRead(ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id query.spatial = SpIntersects(SpBoundingBox(firstFeatureToCreate.geometry).addMargin(0.0000001).toPolygon()) @@ -284,7 +284,7 @@ class InsertFeatureTest : PgTestBase() { // When: reading both features in a single request with mixed IDs val readResponse = executeRead(ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id featureIds += numericId featureIds += namedFeature.id diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/PartitioningTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/PartitioningTest.kt index b3d47d56a..5b73770e4 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/PartitioningTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/PartitioningTest.kt @@ -89,7 +89,7 @@ class PartitioningTest : PgTestBase() { // also - should be able to read val readRequest = ReadFeatures() - readRequest.mapId = partitionedCollection.catalogId + readRequest.catalogId = partitionedCollection.catalogId readRequest.collectionIds.add(partitionedCollection.id) readRequest.featureIds.add("f1") val readResponse = executeRead(readRequest) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/PgPropertyFilterTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/PgPropertyFilterTest.kt index 8cd436850..f6d10ce33 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/PgPropertyFilterTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/PgPropertyFilterTest.kt @@ -33,7 +33,7 @@ class PgPropertyFilterTest: PgTestBase() { // And: A read request is created with the property query. val readRequest = ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id }.withPropertyQuery(pQuery) // When: read request is executed @@ -65,7 +65,7 @@ class PgPropertyFilterTest: PgTestBase() { // And: A read request is made with the custom filter manually added. val readRequest = ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id resultFilters.add(IdContainsFilter("keep_this")) } @@ -123,7 +123,7 @@ class PgPropertyFilterTest: PgTestBase() { fun shouldNotTriggerFilteringForRequestWithErrorResponse() { // Given: A read request that is designed to fail by targeting a non-existent collection. val readRequest = ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += "non_existent_collection" } @@ -145,7 +145,7 @@ class PgPropertyFilterTest: PgTestBase() { // When: A read request is made to fetch that specific feature by ID, with no result filters. val readRequest = ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id featureIds += feature.id } diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesAll.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesAll.kt index 274626ede..1c7385b6c 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesAll.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesAll.kt @@ -35,7 +35,7 @@ class ReadFeaturesAll : PgTestBase() { @Test fun shouldReturnAllFeatures() { executeRead(ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id }).apply { assertEquals(COUNT, features.size) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByGeometryTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByGeometryTest.kt index 1a8ac73e8..57550e3c1 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByGeometryTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByGeometryTest.kt @@ -33,7 +33,7 @@ class ReadFeaturesByGeometryTest : PgTestBase(collection = null, mapId = "") { // And: reading feature val retrievedFeatures = executeRead( ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id featureIds += feature.id } @@ -66,7 +66,7 @@ class ReadFeaturesByGeometryTest : PgTestBase(collection = null, mapId = "") { // And: reading feature val retrievedFeatures = executeRead( ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id featureIds += feature.id } @@ -251,7 +251,7 @@ class ReadFeaturesByGeometryTest : PgTestBase(collection = null, mapId = "") { private fun executeSpatialQuery(spatialQuery: ISpatialQuery): SuccessResponse { return executeRead(ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id query.spatial = spatialQuery }) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByGuuidTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByGuuidTest.kt index 082cd9286..1bcad5a02 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByGuuidTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByGuuidTest.kt @@ -25,7 +25,7 @@ class ReadFeaturesByGuuidTest : // When val readByGuid = ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id guids = GuidList().apply { add(guuidById[inputFeature1.id]) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByMetadataTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByMetadataTest.kt index 6dfd64f02..2de5ee854 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByMetadataTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByMetadataTest.kt @@ -233,7 +233,7 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { // And: execute val featuresByAppIdAndAuthor = executeRead(ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id query.members = MemberAnd( MemberQuery(MetaColumn.author(), StringOp.EQUALS, author), @@ -391,7 +391,7 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { // And: execute val featuresByAppIdAndAuthor = executeRead(ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id query.members = MemberOr( MemberQuery(MetaColumn.author(), StringOp.EQUALS, "this_is_totally_off"), @@ -421,7 +421,7 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { // And: History table is queried for everything besides CREATED val getHistoryWithoutUpdates = ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id queryHistory = true queryDeleted = true @@ -441,7 +441,7 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { private fun insertFeatureAndGetXyz(feature: NakshaFeature): XyzNs { insertFeature(feature = feature) val persistedFeatureResponse = executeRead(ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id featureIds += feature.id }) @@ -454,7 +454,7 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { private fun executeMetaQuery(metaQuery: IMemberQuery): SuccessResponse { return executeRead(ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id query.members = metaQuery }) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByOtherTns.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByOtherTns.kt index 810426a25..0792eb0f8 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByOtherTns.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByOtherTns.kt @@ -49,7 +49,7 @@ class ReadFeaturesByOtherTns : PgTestBase( arrayOf(updatedVersion) ) val byNextTnResp = executeRead(ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id query.members = nextVersionQuery queryHistory = true diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByRefTilesTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByRefTilesTest.kt index 69cf993e7..1653b7e1e 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByRefTilesTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByRefTilesTest.kt @@ -46,7 +46,7 @@ class ReadFeaturesByRefTilesTest : PgTestBase(collection = null, mapId = "") { // Given: val getFeaturesFromZagrebAndPrague = ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection!!.id query.refTiles += listOf(zagrebTileLv12.intKey, pragueTileLv12.intKey) } @@ -67,7 +67,7 @@ class ReadFeaturesByRefTilesTest : PgTestBase(collection = null, mapId = "") { // Given: val getFeaturesFromBologna = ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id query.refTiles += bolognaTileLv12.intKey } diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByTagsTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByTagsTest.kt index ae43a17c4..75424349d 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByTagsTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByTagsTest.kt @@ -223,7 +223,7 @@ class ReadFeaturesByTagsTest : PgTestBase() { private fun executeTagsQuery(tagQuery: ITagQuery): SuccessResponse { return executeRead(ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection!!.id query.tags = tagQuery }) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadHistoryTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadHistoryTest.kt index cd0c9535f..96154e02d 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadHistoryTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadHistoryTest.kt @@ -81,7 +81,7 @@ class ReadHistoryTest : PgTestBase() { // Clear cache, and read the history of the feature. Naksha.cache.clear() executeRead(ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds.add(collection.id) featureIds.add(featureId) queryHistory = true @@ -121,7 +121,7 @@ class ReadHistoryTest : PgTestBase() { } executeRead(ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds.add(collection.id) featureIds.add(featureId) queryHistory = true @@ -143,7 +143,7 @@ class ReadHistoryTest : PgTestBase() { } executeRead(ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds.add(collection.id) featureIds.add(featureId) queryHistory = true diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadLimitTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadLimitTest.kt index f834b156d..1654f0442 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadLimitTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadLimitTest.kt @@ -21,7 +21,7 @@ class ReadLimitTest : PgTestBase() { // When val readWithLimit = executeRead(ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id limit = 2 }) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadOrderedTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadOrderedTest.kt index 4e4846173..5e4f3d67c 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadOrderedTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadOrderedTest.kt @@ -48,7 +48,7 @@ class ReadOrderedTest : PgTestBase() { @Test fun searchOrderedById() { executeRead(ReadFeatures().apply { - mapId = TEST_MAP_ID + catalogId = TEST_MAP_ID collectionIds += collection.id orderBy = OrderBy.id() limit = ORDER_BY_ID_LIMIT @@ -62,7 +62,7 @@ class ReadOrderedTest : PgTestBase() { } executeRead(ReadFeatures().apply { - mapId = TEST_MAP_ID + catalogId = TEST_MAP_ID collectionIds += collection.id orderBy = OrderBy(MetaColumn.id(), order = SortOrder.ASCENDING) limit = ORDER_BY_ID_LIMIT diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/RecreateAfterDeleteTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/RecreateAfterDeleteTest.kt index 7541b59c1..bca9eee30 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/RecreateAfterDeleteTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/RecreateAfterDeleteTest.kt @@ -40,7 +40,7 @@ class RecreateAfterDeleteTest : PgTestBase() { ) Naksha.cache.clear() val updated = executeRead(ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id featureIds += featureId }).features.first()!! @@ -54,7 +54,7 @@ class RecreateAfterDeleteTest : PgTestBase() { // Confirm tombstone is visible via queryDeleted=true Naksha.cache.clear() val deleted = executeRead(ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id featureIds += featureId queryDeleted = true @@ -65,7 +65,7 @@ class RecreateAfterDeleteTest : PgTestBase() { // Confirm feature is invisible in a normal read Naksha.cache.clear() val notFound = executeRead(ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id featureIds += featureId }) @@ -85,7 +85,7 @@ class RecreateAfterDeleteTest : PgTestBase() { // Confirm feature is visible again in a normal read Naksha.cache.clear() val found = executeRead(ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id featureIds += featureId }) @@ -96,7 +96,7 @@ class RecreateAfterDeleteTest : PgTestBase() { // History after auto-purge: DELETED (archived tombstone), UPDATED, CREATED (old lifecycle). // Total = 1 (HEAD) + 3 (history) = 4, in descending version order. val historyOnly = executeRead(ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id featureIds += featureId queryHistory = true @@ -111,7 +111,7 @@ class RecreateAfterDeleteTest : PgTestBase() { // queryHistory + queryDeleted: same result — no tombstone in HEAD (was auto-purged), // so queryDeleted=true adds nothing here. val full = executeRead(ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id featureIds += featureId queryHistory = true diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/UpdateFeatureTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/UpdateFeatureTest.kt index ee113eedd..a735f815e 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/UpdateFeatureTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/UpdateFeatureTest.kt @@ -93,7 +93,7 @@ class UpdateFeatureTest : PgTestBase(collection = null, mapId = "") { // READ FEATURE HISTORY Naksha.cache.clear() val readResp = executeRead(ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id featureIds += initialFeature.id queryHistory = true @@ -260,7 +260,7 @@ class UpdateFeatureTest : PgTestBase(collection = null, mapId = "") { private fun fetchSingleFeature(id: String): NakshaFeature { Naksha.cache.clear() val readFeatureResp = executeRead(ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id featureIds += id }) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/UpsertFeatureTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/UpsertFeatureTest.kt index c2b222fa3..3aefa9df6 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/UpsertFeatureTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/UpsertFeatureTest.kt @@ -35,7 +35,7 @@ class UpsertFeatureTest : PgTestBase() { // And: Retrieving feature by id val retrievedFeatures = executeRead(ReadFeatures().apply { - mapId = collection.catalogId + catalogId = collection.catalogId collectionIds += collection.id featureIds += initialFeature.id queryHistory = true diff --git a/here-naksha-lib-psql/src/jsMain/kotlin/naksha/psql/Plv8Storage.kt b/here-naksha-lib-psql/src/jsMain/kotlin/naksha/psql/Plv8Storage.kt index ec1decd84..8e0dfbf08 100644 --- a/here-naksha-lib-psql/src/jsMain/kotlin/naksha/psql/Plv8Storage.kt +++ b/here-naksha-lib-psql/src/jsMain/kotlin/naksha/psql/Plv8Storage.kt @@ -36,10 +36,6 @@ class Plv8Storage : PgStorage() { TODO("Not yet implemented") } - override fun getDataEncoding(feature: Any?, context: Any?): DataEncoding { - TODO("Not yet implemented") - } - override fun getDictionary(id: String): JbDictionary? { TODO("Not yet implemented") } diff --git a/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/ViewReadSession.java b/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/ViewReadSession.java index 837d739af..bd7036942 100644 --- a/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/ViewReadSession.java +++ b/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/ViewReadSession.java @@ -261,7 +261,7 @@ public void loadTuples(@NotNull List featureTuples, int } @Override - public @Nullable NakshaCatalog getMapByNumber(int catalogNumber) { + public @Nullable NakshaCatalog getCatalogByNumber(int catalogNumber) { throw new UnsupportedOperationException(); } diff --git a/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/ViewWriteSession.java b/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/ViewWriteSession.java index c649307ec..158a68bea 100644 --- a/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/ViewWriteSession.java +++ b/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/ViewWriteSession.java @@ -63,7 +63,7 @@ public ViewWriteSession withWriteLayer(ViewLayer viewLayer) { } } else if (request instanceof ReadFeatures) { final ReadFeatures readFeatures = (ReadFeatures) request; - readFeatures.setMapId(writeLayer.getMapId()); + readFeatures.setCatalogId(writeLayer.getMapId()); readFeatures.setCollectionIds(new StringList(writeLayer.getCollectionId())); } else { throw new IllegalArgumentException("Unsupported request type: " + request.getClass()); diff --git a/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/concurrent/LayerReadRequest.java b/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/concurrent/LayerReadRequest.java index 673e5d983..f6f97b104 100644 --- a/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/concurrent/LayerReadRequest.java +++ b/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/concurrent/LayerReadRequest.java @@ -34,7 +34,7 @@ public LayerReadRequest(@NotNull ReadFeatures request, @NotNull ViewLayer viewLa // Note: We need to copy the request, because we need to ignore the map/collection client asked for, // because the view is always fixed to certain map/collection! this.request = request.copy(false); - this.request.setMapId(viewLayer.getMapId()); + this.request.setCatalogId(viewLayer.getMapId()); this.request.setCollectionIds(new StringList(viewLayer.getCollectionId())); this.viewLayer = viewLayer; this.session = session; diff --git a/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/concurrent/ParallelQueryExecutor.java b/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/concurrent/ParallelQueryExecutor.java index 3f1607594..101e38eb1 100644 --- a/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/concurrent/ParallelQueryExecutor.java +++ b/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/concurrent/ParallelQueryExecutor.java @@ -38,12 +38,10 @@ import java.util.stream.Stream; import naksha.base.Int64; -import naksha.base.Platform; import naksha.base.StringList; import naksha.model.*; import naksha.model.request.*; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -114,7 +112,7 @@ private Stream executeSingle( int layerPriority = view.getViewCollection().priorityOf(layer); final String collectionId = layer.getCollectionId(); final ReadFeatures readRequest = request.copy(false); - readRequest.setMapId(layer.getMapId()); + readRequest.setCatalogId(layer.getMapId()); readRequest.setCollectionIds(new StringList(collectionId)); final @NotNull Response readResponse = session.execute(readRequest); diff --git a/here-naksha-lib-view/src/jvmTest/java/com/here/naksha/lib/view/MockReadSession.java b/here-naksha-lib-view/src/jvmTest/java/com/here/naksha/lib/view/MockReadSession.java index 397b5f48d..bf48a319f 100644 --- a/here-naksha-lib-view/src/jvmTest/java/com/here/naksha/lib/view/MockReadSession.java +++ b/here-naksha-lib-view/src/jvmTest/java/com/here/naksha/lib/view/MockReadSession.java @@ -108,7 +108,7 @@ public boolean isClosed() { } @Override - public @Nullable NakshaCatalog getMapByNumber(int catalogNumber) { + public @Nullable NakshaCatalog getCatalogByNumber(int catalogNumber) { return null; } diff --git a/here-naksha-lib-view/src/jvmTest/java/com/here/naksha/lib/view/ViewTest.java b/here-naksha-lib-view/src/jvmTest/java/com/here/naksha/lib/view/ViewTest.java index c3ed90831..fa74d547c 100644 --- a/here-naksha-lib-view/src/jvmTest/java/com/here/naksha/lib/view/ViewTest.java +++ b/here-naksha-lib-view/src/jvmTest/java/com/here/naksha/lib/view/ViewTest.java @@ -351,7 +351,7 @@ void shouldApplyCustomTimeoutsPerLayer() { // And ReadFeatures readFeatures = new ReadFeatures(); - readFeatures.setMapId(TEST_MAP_ID); + readFeatures.setCatalogId(TEST_MAP_ID); readFeatures.setCollectionIds(new StringList(firstLayer.getCollectionId(), secondLayer.getCollectionId(), thirdLayer.getCollectionId())); // When diff --git a/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/HttpStorageReadSession.java b/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/HttpStorageReadSession.java index 9d2e9c55a..310465178 100644 --- a/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/HttpStorageReadSession.java +++ b/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/HttpStorageReadSession.java @@ -141,7 +141,7 @@ public Response executeParallel(@NotNull Request request) { } @Override - public @Nullable NakshaCatalog getMapByNumber(int catalogNumber) { + public @Nullable NakshaCatalog getCatalogByNumber(int catalogNumber) { return null; } From 426d3c6dd2cea40e7de54c32ebc25e2f786ed7fa Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Fri, 19 Jun 2026 13:28:09 +0200 Subject: [PATCH 25/57] Start fixing query builder. Signed-off-by: Alexander Lowey-Weber --- .../commonMain/kotlin/naksha/model/Naksha.kt | 13 +++ .../kotlin/naksha/psql/PgQueryBuilder.kt | 31 ++----- .../kotlin/naksha/psql/PgQueryWhereBuilder.kt | 88 ++++++++++++------- .../kotlin/naksha/psql/PgQueryWhereClause.kt | 13 +-- 4 files changed, 88 insertions(+), 57 deletions(-) diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt index 922449d3e..56acdc961 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt @@ -106,6 +106,19 @@ class Naksha private constructor() { */ const val MAX_ID_LENGTH = 42 // The answer to everything ;-) + // TODO: This shows why we really need a special TupleNumberArray next to TupleNumberList, which stores all tuple-numbers in a single + // byte-array, compressed by adding the database-number, catalog-number and collection-number upfront, then followed by all the + // tuple-numbers. It will reduce memory consumption from 1 GiB to around 256 MiB. + + /** + * The maximum amount of tuple that can be fetched using normal query methods. + * + * This protects the database and client for too big data. When reading tuples, each tuple-number is actually returned from the storage as 16-byte value, so feature-number and version. The Java client then adds database-number, catalog-number and collection-number. Therefore, the maximum amount of data transferred when fetching this amount of tuple is roughly `HARD_READ_LIMIT * 16`. This can already be huge, but when we copy this onto the JVM heap, we expand it, because we add the database-number _(8 byte)_, catalog-number _(4 byte)_ and collection-number _(4 byte)_ to it, plus the overhead of the [TupleNumber] instance (16-byte per instance). Then there is the array into which they are added, this array holds a reference for each [TupleNumber], so another 8-byte. In total, for JVM heap usage, we need to multiple this value with around 56 _(16+16+8+4+4+8)_. For the default value of 16,777,216 this already means around 1 GiB of heap usage, not even thinking about how much more memory will be used, when we start loading all these tuple! + * @since 3.0 + */ + @JvmStatic + var HARD_TUPLE_LIMIT = 16_777_216 + /** * An immutable map between the identifier of an internal collection to the number of that collection. * @since 3.0 diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryBuilder.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryBuilder.kt index 31c6449a4..b5b698bd4 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryBuilder.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryBuilder.kt @@ -31,35 +31,22 @@ class PgQueryBuilder(val session: PgSession, val readRequest: ReadRequest) { private fun readFeatures(req: ReadFeatures): PgQuery { // Collect needed data val pgStorage = session.storage - val mapId = req.mapId ?: throw illegalArg("mapId is missing") - val pgMap = session.getPgMapById(mapId) ?: throw mapNotFound("Map with id '$mapId' does not exist") - // We select what the client wants, maximum is always 16777216 - // Finally, the storage can limit result-size further down below 16777216 (normally we do not expect this to happen). - val REQ_LIMIT = min(max(0, req.limit ?: 16777216), session.storage.hardCap) + val catalogId = req.catalogId ?: throw illegalArg("catalogId is missing") + val pgCatalog = session.getPgCatalogById(catalogId) ?: throw mapNotFound("Catalog with id '$catalogId' does not exist") + val REQ_LIMIT = min(max(0, req.limit ?: Naksha.HARD_TUPLE_LIMIT), session.storage.hardCap) if (REQ_LIMIT == 0) throw illegalArg("Invalid limit given: ${req.limit}, must be 0 to 16777216") val pgCollections: MutableList = mutableListOf() for (collectionId in req.collectionIds) { if (collectionId == null) continue - val pgCollection = session.getPgCollectionById(pgMap, collectionId) ?: - throw collectionNotFound("Collection with id '$collectionId' not found in map '$mapId'") + val pgCollection = session.getPgCollectionById(pgCatalog, collectionId) ?: + throw collectionNotFound("Collection with id '$collectionId' not found in map '$catalogId'") pgCollections.add(pgCollection) } - if (pgCollections.size <= 0) throw illegalArg("Empty collection-ids in request") + if (pgCollections.isEmpty()) throw illegalArg("Empty collection-ids in request") val version = req.version - if (version != null && !req.queryHistory) { - throw illegalArg("Setting 'version' to '$version' requires that 'queryHistory' is enabled!") - } val minVersion = req.minVersion - if (minVersion != null && !req.queryHistory) { - throw illegalArg("Setting 'minVersion' to '$minVersion' requires that 'queryHistory' is enabled!") - } val versions = req.versions - if (versions != 1 && !req.queryHistory) { - throw illegalArg("Setting 'versions' to $versions requires that 'queryHistory' is enabled!") - } - if (versions < 1) { - throw illegalArg("It is not possible to request less than one version of each feature") - } + if (versions < 1) throw illegalArg("It is not possible to request less than one version of each feature") val whereClause = PgQueryWhereBuilder(req).build() val whereQuery = whereClause?.where ?: "" @@ -128,7 +115,7 @@ class PgQueryBuilder(val session: PgSession, val readRequest: ReadRequest) { for (entry in pgCollections.withIndex()) { val pgCollection = entry.value val map = pgCollection.catalog - val read = PgRead(pgMap, pgCollection) + val read = PgRead(pgCatalog, pgCollection) // Note: To simplify queries, we actually always embed the collection-number internally, // eventually, before returning the result, we decide if we put it into the header @@ -216,7 +203,7 @@ SELECT ${if (thePgCollection == null) "col_num, fn, version" else "fn, version"} argValues = whereClause?.argValues?.toTypedArray() ?: emptyArray(), argTypes = whereClause?.argTypeNames ?: emptyArray(), pgStorage.number, - pgMap.catalogNumber, + pgCatalog.catalogNumber, thePgCollection?.collectionNumber ) } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt index 39bf08fda..f7b0a5641 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt @@ -1,32 +1,36 @@ package naksha.psql import naksha.base.AnyList +import naksha.base.Int64 import naksha.base.ListProxy import naksha.base.Platform.PlatformCompanion.toJSON +import naksha.base.StringList import naksha.geo.HereTile import naksha.geo.SpGeometry import naksha.model.* +import naksha.model.objects.StandardMembers import naksha.model.request.ReadFeatures import naksha.model.request.query.* +import naksha.psql.PgColumn.PgColumn_C.FN /** * Helper to convert a [ReadFeatures] request into a sql `WHERE` query. + * + * The collection need to be provided, because they potentially _(when not cached)_ need to be read from the database, therefore, they require a session. We do not want to link this code to a session _(it would break unit testing)_, so we simply expect the collections as input parameter. * @param request the request to wrap. + * @param collections the collections for which to generate the `WHERE` queries. * @since 3.0 * @see [build] */ -internal class PgQueryWhereBuilder(private val request: ReadFeatures) { - - private val argValues: MutableList = mutableListOf() - private val argTypes: MutableList = mutableListOf() - private val where = StringBuilder() +internal class PgQueryWhereBuilder(private val request: ReadFeatures, private val collections: List) { + private val queries: MutableMap = mutableMapOf() /** - * Convert the request into a `WHERE` query. - * @return the [PgQueryWhereClause]. + * Convert the request into `WHERE` queries. + * @return the [PgQueryWhereClause] for each collection given. * @since 3.0 */ - fun build(): PgQueryWhereClause? { + fun build(): Map { whereFeatureId() whereGuids() whereVersion() @@ -34,37 +38,61 @@ internal class PgQueryWhereBuilder(private val request: ReadFeatures) { whereSpatial() whereRefTiles() whereTags() - return if (where.isBlank()) { - null - } else { - PgQueryWhereClause(where = where.toString(), argValues = argValues, argTypes = argTypes) + return queries + } + + private fun queryOf(collection: PgCollection): PgQueryWhereClause { + var query = queries[collection] + if (query == null) { + query = PgQueryWhereClause(collection) + queries[collection] = query } + return query } private fun whereFeatureId() { - val featureIds = request.featureIds.filterNotNull() - if (featureIds.isEmpty()) return - // Partition into numeric IDs (fn >= 0, id stored as NULL in DB) and named IDs (fn < 0, id NOT NULL). - val numericFns = featureIds.mapNotNull { id -> + val reqIds: StringList = request.featureIds + val featureNumbers: MutableList = mutableListOf() + val featureIds: MutableList = mutableListOf() + for (id in reqIds) { + if (id == null) continue val fn = Naksha.featureNumber(id) - if (fn >= naksha.base.Int64(0)) fn else null + if (fn >= Int64(0)) { + featureNumbers.add(fn) + } else { + featureIds.add(id) + } } - val namedIds = featureIds.filter { id -> Naksha.featureNumber(id) < naksha.base.Int64(0) } + if (featureNumbers.isEmpty() && featureIds.isEmpty()) return - val conditions = mutableListOf() - if (namedIds.isNotEmpty()) { - val placeholder = placeholderForArg(namedIds.toTypedArray(), PgType.STRING_ARRAY) - conditions += "${PgColumn.id} = ANY($placeholder)" - } - if (numericFns.isNotEmpty()) { - val placeholder = placeholderForArg(numericFns.toTypedArray(), PgType.INT64_ARRAY) - conditions += "${PgColumn.fn} = ANY($placeholder)" - } - if (conditions.isNotEmpty()) { + // For each collection: + for (collection in collections) { + val query: PgQueryWhereClause = queryOf(collection) + val where: StringBuilder = query.where if (where.isNotEmpty()) where.append(" AND ") - if (conditions.size == 1) where.append(conditions[0]) - else where.append("(${conditions.joinToString(" OR ")})") + + where.append("( ") + if (featureIds.isNotEmpty()) { + val placeholder: String = placeholderForArg(featureIds.toTypedArray(), PgType.STRING_ARRAY) + val ID = collection.column(StandardMembers.Id) ?: throw illegalArg("Collection does not defined `id` column") + where.append(ID.ident).append(" = ANY(").append(placeholder).append(")") + } + if (featureNumbers.isNotEmpty()) { + if (featureIds.isNotEmpty()) where.append(" OR ") + + val placeholder: String = placeholderForArg(featureNumbers.toTypedArray(), PgType.INT64_ARRAY) + if (where.isNotEmpty()) where.append(" AND ") + where.append(FN.ident).append(" = ANY(").append(placeholder).append(")") + + conditions += "${PgColumn.fn} = ANY($placeholder)" + } + if (conditions.isNotEmpty()) { + if (query.isNotEmpty()) query.append(" AND ") + if (conditions.size == 1) query.append(conditions[0]) + else query.append("(${conditions.joinToString(" OR ")})") + } + where.append(")") } } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereClause.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereClause.kt index 9e742f94f..8fc7444f9 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereClause.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereClause.kt @@ -2,31 +2,34 @@ package naksha.psql /** * A SQL `WHERE` query. + * @param collection the collection for which this `WHERE` query applies. * @since 3.0 */ -internal data class PgQueryWhereClause( +internal data class PgQueryWhereClause(val collection: PgCollection) { /** * The `WHERE` query, without the keyword `WHERE` or an empty string, if an empty query (query without conditions). * @since 3.0 */ - val where: String, + val where = StringBuilder() /** * The arguments to used with the WHERE in order. * @since 3.0 */ - val argValues: MutableList, + val argValues: MutableList = mutableListOf() /** * The types of the arguments. * @since 3.0 */ - val argTypes: MutableList, -) { + val argTypes: MutableList = mutableListOf() + /** * Returns the [argTypes] as typed-array _(`Array`)_. * @since 3.0 */ val argTypeNames: Array get() = argTypes.map(PgType::toString).toTypedArray() + + override fun toString(): String = where.toString() } \ No newline at end of file From c63ae992c5ee19935b196728f541a6352d9419d3 Mon Sep 17 00:00:00 2001 From: phmai Date: Fri, 19 Jun 2026 14:38:57 +0200 Subject: [PATCH 26/57] fix Write classes Signed-off-by: phmai --- .../kotlin/naksha/model/request/Write.kt | 35 --- .../commonMain/kotlin/naksha/psql/PgWrite.kt | 9 +- .../commonMain/kotlin/naksha/psql/PgWriter.kt | 19 +- .../kotlin/naksha/psql/AttachmentTest.kt | 240 ------------------ 4 files changed, 10 insertions(+), 293 deletions(-) delete mode 100644 here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/AttachmentTest.kt diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/Write.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/Write.kt index cca8adb51..2acb28f2b 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/Write.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/Write.kt @@ -398,41 +398,6 @@ open class Write : AnyObject() { return this } - /** - * Arbitrary attachment to be stored, if this is [CREATE][WriteOp.CREATE], [UPSERT][WriteOp.UPSERT], or [UPDATE][WriteOp.UPDATE]. - * - * If being [UNDEFINED], then the attachment, in whatever state it is, is left unmodified, _(this is the default value)_. If the value is explicitly set to `null`, an existing attachments is removed, if set to a specific byte-array, then the attachment is updated. - * - * If this is a [CREATE][WriteOp.CREATE] operation, the value [UNDEFINED] has the same meaning as explicitly setting the value to `null`. - * @since 3.0 - * @see [UNDEFINED] - */ - var attachment: ByteArray? = UNDEFINED - - /** - * @see [attachment] - */ - fun withAttachment(value: ByteArray?): Write { - attachment = value - return this - } - - /** - * Ask the storage to keep the attachment in the state in which it currently is. This is the default behavior. - * @return this. - * @since 3.0 - */ - fun keepAttachment(): Write { - attachment = UNDEFINED - return this - } - - /** - * Tests if the attachment should be modified. - * @return `true` if the attachment should be modified; `false` if the attachment should stay unchanged _(default behavior)_. - */ - fun attachmentModified(): Boolean = attachment !== UNDEFINED - /** * If enabled, a missing map is automatically created, when creating or modifying collections; defaults to `false`. * diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWrite.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWrite.kt index 86941f84d..21b9b3085 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWrite.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWrite.kt @@ -90,20 +90,13 @@ internal data class PgWrite(val original: Write, val i: Int) { val partition: Int get() = if (collection.partitions > 1) partitionNumber % collection.partitions else -1 - /** - * The attachment from the [Write] instruction, can be [Write.UNDEFINED]. If the attachment is [Write.UNDEFINED], an existing attachment should be retained. - * @since 3.0 - */ - val attachment: ByteArray? - get() = original.attachment - /** * If the operation is atomic, the version in which the _HEAD_ is expected to be; otherwise `null`. * @since 3.0 */ val version: Version? get() = if (original.atomic && op != WriteOp.CREATE && op != WriteOp.UPSERT) - original.version ?: original.tupleNumber?.version + original.version ?: Version(original.tupleNumber?.version!!) else null diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt index fa972f1b9..24e5d63d5 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt @@ -148,8 +148,7 @@ open class PgWriter internal constructor( WriteOp.CREATE -> { val f = write.feature ?: throw illegalArg("The feature #${write.i} is null") // In a CREATE case, UNDEFINED means the same as `null` - val attachment = if (write.attachment === Write.UNDEFINED) null else write.attachment - val tuple = tx.created(write.map.head, write.collection.head, f, attachment) + val tuple = Tuple.encodeFeature(f, collection.head, Action.CREATE, session, null) write.tuple = tuple val tupleNumber = tuple.tupleNumber write.tupleNumber = tupleNumber @@ -157,14 +156,14 @@ open class PgWriter internal constructor( WriteOp.UPSERT -> { // Note: We first try an INSERT, then, when that fails, we do an on-conflict UPDATE! val f = write.feature ?: throw illegalArg("The feature #${write.i} is null") - val tuple = tx.created(write.map.head, write.collection.head, f, write.attachment) + val tuple = Tuple.encodeFeature(f, collection.head, Action.CREATE, session, null) write.tuple = tuple val tupleNumber = tuple.tupleNumber write.tupleNumber = tupleNumber } WriteOp.UPDATE -> { val f = write.feature ?: throw illegalArg("The feature #${write.i} is null") - val tuple = tx.updated(write.map.head, write.collection.head, f, write.attachment, write.original.atomic) + val tuple = Tuple.encodeFeature(f, collection.head, Action.UPDATE, session, null) write.tuple = tuple val tupleNumber = tuple.tupleNumber write.tupleNumber = tupleNumber @@ -242,19 +241,19 @@ open class PgWriter internal constructor( if (write.isFeatureModification) { val map = write.map val col = write.collection - val txCol = transaction.useCatalog(map.id, map.number).useCollection(col.id, col.number) + val txCol = transaction.useCatalog(map.id, map.catalogNumber).useCollection(col.id, col.collectionNumber) if (tupleNumber != null) { txCol.add(tupleNumber, col.partitions) } featuresModified += 1 } else if (write.isCatalogModification) { val map = write.asPgCatalog - if (map != null) transaction.useCatalog(map.id, map.number, write.action) + if (map != null) transaction.useCatalog(map.id, map.catalogNumber, write.action) } else if (write.isCollectionModification) { val map = write.map val col = write.asPgCollection if (col != null) { - transaction.useCatalog(map.id, map.number).useCollection(col.id, col.number, write.action) + transaction.useCatalog(map.id, map.catalogNumber).useCollection(col.id, col.collectionNumber, write.action) map.invalidateCollection(col) } } @@ -295,7 +294,7 @@ open class PgWriter internal constructor( if (op == WriteOp.CREATE || op == WriteOp.UPSERT || op == WriteOp.UPDATE) { val feature = write.feature ?: throw illegalArg("The write #${write.i} is $op, but the feature is null") nakshaMap = if (feature is NakshaCatalog) feature else feature.proxy(NakshaCatalog::class) - nakshaMap.storageId = storage.id + nakshaMap.databaseId = storage.id if (pgCatalog == null) { if (op == WriteOp.UPDATE) { throw mapNotFound("The UPDATE (write #${write.i}) failed, because the map '$featureId' does not exist") @@ -325,7 +324,7 @@ open class PgWriter internal constructor( val nakshaCollection: NakshaCollection? if (op == WriteOp.CREATE || op == WriteOp.UPSERT || op == WriteOp.UPDATE) { val feature = write.feature ?: throw illegalArg("The write #${write.i} is $op, but the feature is null") - nakshaCollection = if (feature is NakshaCollection) feature else feature.proxy(NakshaCollection::class) + nakshaCollection = feature as? NakshaCollection ?: feature.proxy(NakshaCollection::class) if (pgCollection == null) { if (op == WriteOp.UPDATE) { throw collectionNotFound( @@ -445,7 +444,7 @@ open class PgWriter internal constructor( // ── Members ───────────────────────────────────────────────────────────────── val clientMembers = collection.members if (clientMembers != null) { - val mandatoryByName = PgColumn.mandatoryMembers.associateBy { it.name } + val mandatoryByName = StandardMembers.MANDATORY.associateBy { it.name } val normalizedMembers = MemberList() for (m in clientMembers) { if (m == null) continue diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/AttachmentTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/AttachmentTest.kt deleted file mode 100644 index 77c3890ab..000000000 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/AttachmentTest.kt +++ /dev/null @@ -1,240 +0,0 @@ -package naksha.psql - -import naksha.base.PlatformUtil -import naksha.model.* -import naksha.model.objects.NakshaFeature -import naksha.model.request.* -import naksha.model.RandomFeatures.RandomFeatures_C.randomFeature -import kotlin.test.* - -class AttachmentTest : PgTestBase() { - - @Test - fun insertFeatureWithAttachment() { - val attachmentOriginal = "this is a test" - val attachmentBytes = attachmentOriginal.encodeToByteArray() - val featureToCreate = randomFeature() - val xyz = featureToCreate.properties.xyz - xyz.tags.clear() - xyz.tags.addTag("wicked", false) - - // Write the feature - val writeFeaturesReq = WriteRequest().apply { - add(Write().createFeature(collection.catalogId, collection.id, featureToCreate).withAttachment(attachmentBytes)) - } - executeWrite(writeFeaturesReq).apply { - // Verify the result (will come from cache) - assertEquals(1, length) - assertEquals(1, features.size) - val feature = assertNotNull(features.first()) - assertEquals(featureToCreate.id, feature.id) - assertEquals(Action.CREATE, feature.properties.xyz.guid?.tupleNumber?.action) - - val featureTupleList = this.featureTupleList - assertEquals(1, featureTupleList.size) - val featureTuple = assertNotNull(featureTupleList[0]) - val tuple = assertNotNull(featureTuple.tuple) - assertNotNull(tuple.getByteArray(naksha.model.objects.StandardMembers.XyzAttachment)) - assertContentEquals(attachmentBytes, tuple.getByteArray(naksha.model.objects.StandardMembers.XyzAttachment)) - } - - // Read the feature - Naksha.cache.clear() - executeRead(ReadFeatures().apply { - catalogId = collection.catalogId - collectionIds += collection.id - featureIds += featureToCreate.id - }).apply { - assertEquals(1, length) - assertEquals(1, features.size) - val feature = assertNotNull(features.first()) - assertEquals(featureToCreate.id, feature.id) - assertEquals(Action.CREATE, feature.properties.xyz.guid?.tupleNumber?.action) - - val featureTupleList = this.featureTupleList - assertEquals(1, featureTupleList.size) - val featureTuple = assertNotNull(featureTupleList[0]) - val tuple = assertNotNull(featureTuple.tuple) - assertNotNull(tuple.getByteArray(naksha.model.objects.StandardMembers.XyzAttachment)) - assertContentEquals(attachmentBytes, tuple.getByteArray(naksha.model.objects.StandardMembers.XyzAttachment)) - } - } - - @Test - fun upsertFeatureWithAttachment() { - val attachmentOriginal = "this is a test" - val attachmentBytes = attachmentOriginal.encodeToByteArray() - val featureId = PlatformUtil.randomString() - val featureToCreate = randomFeature(featureId) - featureToCreate.properties["test"] = "start" - val xyz = featureToCreate.properties.xyz - xyz.tags.clear() - xyz.tags.addTag("wicked", false) - - // Write the feature - val writeFeaturesReq = WriteRequest().apply { - add(Write().upsertFeature(collection.catalogId, collection.id, featureToCreate).withAttachment(attachmentBytes)) - } - executeWrite(writeFeaturesReq).apply { - // Verify the result (will come from cache) - assertEquals(1, length) - assertEquals(1, features.size) - val feature = assertNotNull(features.first()) - assertEquals(featureToCreate.id, feature.id) - assertEquals("start", feature.properties["test"]) - assertEquals(Action.CREATE, feature.properties.xyz.guid?.tupleNumber?.action) - - val featureTupleList = this.featureTupleList - assertEquals(1, featureTupleList.size) - val featureTuple = assertNotNull(featureTupleList[0]) - val tuple = assertNotNull(featureTuple.tuple) - assertNotNull(tuple.getByteArray(naksha.model.objects.StandardMembers.XyzAttachment)) - assertContentEquals(attachmentBytes, tuple.getByteArray(naksha.model.objects.StandardMembers.XyzAttachment)) - } - - // Read the feature - Naksha.cache.clear() - val readFeature: NakshaFeature - executeRead(ReadFeatures().apply { - catalogId = collection.catalogId - collectionIds += collection.id - featureIds += featureToCreate.id - }).apply { - assertEquals(1, length) - assertEquals(1, features.size) - val feature = assertNotNull(features.first()) - assertEquals(featureToCreate.id, feature.id) - assertEquals("start", feature.properties["test"]) - assertEquals(Action.CREATE, feature.properties.xyz.guid?.tupleNumber?.action) - - val featureTupleList = this.featureTupleList - assertEquals(1, featureTupleList.size) - val featureTuple = assertNotNull(featureTupleList[0]) - val tuple = assertNotNull(featureTuple.tuple) - assertNotNull(tuple.getByteArray(naksha.model.objects.StandardMembers.XyzAttachment)) - assertContentEquals(attachmentBytes, tuple.getByteArray(naksha.model.objects.StandardMembers.XyzAttachment)) - - readFeature = feature - } - val insertedFeatureGuid = readFeature.properties.xyz.guid - assertNotNull(insertedFeatureGuid) - assertEquals(featureId, insertedFeatureGuid.id) - assertEquals(storage.number, insertedFeatureGuid.tupleNumber.databaseNumber) - assertEquals(map.number, insertedFeatureGuid.tupleNumber.catalogNumber) - assertEquals(collection.number, insertedFeatureGuid.tupleNumber.collectionNumber) - - // Now, update the feature, leave the attachment as it is. - // In other words, for this upsert, we do not provide an attachment, but expect to find it in the response! - Naksha.cache.clear() - val upsertFeature: NakshaFeature = readFeature.copy(true) - upsertFeature.properties["test"] = "end" - val updateFeatureReq = WriteRequest().apply { - // We do not modify attachment, therefore it should be UNDEFINED - add(Write().upsertFeature(collection.catalogId, collection.id, upsertFeature)) - } - executeWrite(updateFeatureReq).apply { - assertEquals(1, length) - assertEquals(1, features.size) - val feature = assertNotNull(features.first()) - assertEquals(featureToCreate.id, feature.id) - assertEquals("end", feature.properties["test"]) - assertEquals(Action.UPDATE, feature.properties.xyz.guid?.tupleNumber?.action) - - val featureTupleList = this.featureTupleList - assertEquals(1, featureTupleList.size) - val featureTuple = assertNotNull(featureTupleList[0]) - val tuple = assertNotNull(featureTuple.tuple) - assertNotNull(tuple.getByteArray(naksha.model.objects.StandardMembers.XyzAttachment)) - assertContentEquals(attachmentBytes, tuple.getByteArray(naksha.model.objects.StandardMembers.XyzAttachment)) - } - } - - @Test - fun updateFeatureWithAttachment() { - val attachmentOriginal = "this is a test" - val attachmentBytes = attachmentOriginal.encodeToByteArray() - val featureId = PlatformUtil.randomString() - val featureToCreate = randomFeature(featureId) - featureToCreate.properties["test"] = "start" - val xyz = featureToCreate.properties.xyz - xyz.tags.clear() - xyz.tags.addTag("wicked", false) - - // Write the feature - val writeFeaturesReq = WriteRequest().apply { - add(Write().createFeature(collection.catalogId, collection.id, featureToCreate).withAttachment(attachmentBytes)) - } - executeWrite(writeFeaturesReq).apply { - // Verify the result (will come from cache) - assertEquals(1, length) - assertEquals(1, features.size) - val feature = assertNotNull(features.first()) - assertEquals(featureToCreate.id, feature.id) - assertEquals(Action.CREATE, feature.properties.xyz.guid?.tupleNumber?.action) - assertEquals("start", feature.properties["test"]) - - val featureTupleList = this.featureTupleList - assertEquals(1, featureTupleList.size) - val featureTuple = assertNotNull(featureTupleList[0]) - val tuple = assertNotNull(featureTuple.tuple) - assertNotNull(tuple.getByteArray(naksha.model.objects.StandardMembers.XyzAttachment)) - assertContentEquals(attachmentBytes, tuple.getByteArray(naksha.model.objects.StandardMembers.XyzAttachment)) - } - - // Read the feature - Naksha.cache.clear() - val readFeature: NakshaFeature - executeRead(ReadFeatures().apply { - catalogId = collection.catalogId - collectionIds += collection.id - featureIds += featureToCreate.id - }).apply { - assertEquals(1, length) - assertEquals(1, features.size) - val feature = assertNotNull(features.first()) - assertEquals(featureToCreate.id, feature.id) - assertEquals(Action.CREATE, feature.properties.xyz.guid?.tupleNumber?.action) - assertEquals("start", feature.properties["test"]) - - val featureTupleList = this.featureTupleList - assertEquals(1, featureTupleList.size) - val featureTuple = assertNotNull(featureTupleList[0]) - val tuple = assertNotNull(featureTuple.tuple) - assertNotNull(tuple.getByteArray(naksha.model.objects.StandardMembers.XyzAttachment)) - assertContentEquals(attachmentBytes, tuple.getByteArray(naksha.model.objects.StandardMembers.XyzAttachment)) - - readFeature = feature - } - val insertedFeatureGuid = readFeature.properties.xyz.guid - assertNotNull(insertedFeatureGuid) - assertEquals(featureId, insertedFeatureGuid.id) - assertEquals(storage.number, insertedFeatureGuid.tupleNumber.databaseNumber) - assertEquals(map.number, insertedFeatureGuid.tupleNumber.catalogNumber) - assertEquals(collection.number, insertedFeatureGuid.tupleNumber.collectionNumber) - - // Now, update the feature, leave the attachment as it is. - // In other words, for this upsert, we do not provide an attachment, but expect to find it in the response! - Naksha.cache.clear() - val updateFeature: NakshaFeature = readFeature.copy(true) - updateFeature.properties["test"] = "end" - val updateFeatureReq = WriteRequest().apply { - // We do not modify attachment, therefore it should be UNDEFINED - add(Write().updateFeature(collection, updateFeature, true)) - } - executeWrite(updateFeatureReq).apply { - assertEquals(1, length) - assertEquals(1, features.size) - val feature = assertNotNull(features.first()) - assertEquals(featureToCreate.id, feature.id) - assertEquals("end", feature.properties["test"]) - assertEquals(Action.UPDATE, feature.properties.xyz.guid?.tupleNumber?.action) - - val featureTupleList = this.featureTupleList - assertEquals(1, featureTupleList.size) - val featureTuple = assertNotNull(featureTupleList[0]) - val tuple = assertNotNull(featureTuple.tuple) - assertNotNull(tuple.getByteArray(naksha.model.objects.StandardMembers.XyzAttachment)) - assertContentEquals(attachmentBytes, tuple.getByteArray(naksha.model.objects.StandardMembers.XyzAttachment)) - } - } -} From 2d0c8755fbbfcdb383fff5e376ad5baaa207b771 Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Fri, 19 Jun 2026 15:45:38 +0200 Subject: [PATCH 27/57] Add dedicated members as helper, specifically for query convertion. Signed-off-by: Alexander Lowey-Weber --- .../http/tasks/EventHandlerApiTask.java | 4 +- .../http/tasks/ReadFeatureApiTask.java | 6 +- .../app/service/http/tasks/SpaceApiTask.java | 4 +- .../service/http/tasks/StorageApiTask.java | 4 +- .../service/util/NakshaAdminRequestUtil.java | 2 +- .../naksha/cli/copy/service/CopyService.java | 2 +- .../cli/copy/service/CopyServiceTest.java | 4 +- .../cli/copy/service/psql/PsqlCopyTest.java | 2 +- .../activitylog/ActivityLogHandler.java | 2 +- .../ActivityLogRequestTranslationUtil.java | 2 +- .../activitylog/ActivityLogHandlerTest.java | 2 +- ...ActivityLogRequestTranslationUtilTest.java | 6 +- .../storage/ReadFeaturesProxyWrapper.java | 2 +- .../lib/handlers/DefaultStorageHandler.java | 2 +- .../IntHandlerForEventHandlerConfigs.java | 4 +- .../internal/IntHandlerForStorageConfigs.java | 4 +- .../handlers/DefaultStorageHandlerTest.java | 2 +- .../internal/IntHandlerForSpacesTest.java | 2 +- .../com/here/naksha/lib/hub/NakshaHub.java | 2 +- .../hub/storages/NHSpaceStorageReader.java | 4 +- .../naksha/lib/hub/NakshaHubWiringTest.java | 6 +- .../lib/hub/mock/NHAdminReaderMock.java | 2 +- .../kotlin/naksha/model/objects/BoolMember.kt | 34 ++++++ .../naksha/model/objects/ByteArrayMember.kt | 34 ++++++ .../naksha/model/objects/Float32Member.kt | 34 ++++++ .../naksha/model/objects/Float64Member.kt | 34 ++++++ .../naksha/model/objects/Int16Member.kt | 34 ++++++ .../naksha/model/objects/Int32Member.kt | 34 ++++++ .../naksha/model/objects/Int64Member.kt | 35 ++++++ .../kotlin/naksha/model/objects/Int8Member.kt | 34 ++++++ .../kotlin/naksha/model/objects/JsonPath.kt | 4 +- .../kotlin/naksha/model/objects/Member.kt | 100 ++++++++++++++++++ .../kotlin/naksha/model/objects/MemberType.kt | 48 ++++++--- .../kotlin/naksha/model/objects/SetMember.kt | 35 ++++++ .../naksha/model/objects/SpatialMember.kt | 35 ++++++ .../naksha/model/objects/StringMember.kt | 34 ++++++ .../kotlin/naksha/model/objects/TagsMember.kt | 34 ++++++ .../naksha/model/objects/TupleNumberMember.kt | 34 ++++++ .../naksha/model/objects/TypedMember.kt | 14 +++ .../naksha/model/request/ReadCollections.kt | 2 +- .../naksha/model/request/ReadFeatures.kt | 51 ++++----- .../kotlin/naksha/model/request/ReadMaps.kt | 2 +- .../naksha/model/request/ReadTransactions.kt | 2 +- .../naksha/model/request/query/AnyOp.kt | 10 +- .../model/request/query/IMemberQuery.kt | 2 +- .../java/naksha/model/util/RequestHelper.java | 4 +- .../commonMain/kotlin/naksha/psql/PgColumn.kt | 1 + .../kotlin/naksha/psql/PgQueryBuilder.kt | 18 ++-- .../kotlin/naksha/psql/PgQueryWhereBuilder.kt | 80 ++++++-------- .../kotlin/naksha/psql/PgQueryWhereClause.kt | 21 ++-- .../kotlin/naksha/psql/ChainCollectionTest.kt | 4 +- .../kotlin/naksha/psql/CollectionTests.kt | 10 +- .../kotlin/naksha/psql/DeleteFeatureBase.kt | 8 +- .../kotlin/naksha/psql/HistoryUuidTest.kt | 4 +- .../kotlin/naksha/psql/InsertFeatureTest.kt | 14 +-- .../kotlin/naksha/psql/PartitioningTest.kt | 2 +- .../naksha/psql/PgPropertyFilterTest.kt | 8 +- .../kotlin/naksha/psql/ReadFeaturesAll.kt | 2 +- .../naksha/psql/ReadFeaturesByGeometryTest.kt | 6 +- .../naksha/psql/ReadFeaturesByGuuidTest.kt | 2 +- .../naksha/psql/ReadFeaturesByMetadataTest.kt | 10 +- .../naksha/psql/ReadFeaturesByOtherTns.kt | 2 +- .../naksha/psql/ReadFeaturesByRefTilesTest.kt | 4 +- .../naksha/psql/ReadFeaturesByTagsTest.kt | 2 +- .../kotlin/naksha/psql/ReadHistoryTest.kt | 6 +- .../kotlin/naksha/psql/ReadLimitTest.kt | 2 +- .../kotlin/naksha/psql/ReadOrderedTest.kt | 4 +- .../naksha/psql/RecreateAfterDeleteTest.kt | 12 +-- .../kotlin/naksha/psql/UpdateFeatureTest.kt | 4 +- .../kotlin/naksha/psql/UpsertFeatureTest.kt | 2 +- .../psql/DeleteFeatureByVersionTest.java | 4 +- .../naksha/lib/view/ViewWriteSession.java | 2 +- .../lib/view/concurrent/LayerReadRequest.java | 2 +- .../concurrent/ParallelQueryExecutor.java | 2 +- .../com/here/naksha/lib/view/ViewTest.java | 2 +- .../ConnectorInterfaceReadExecute.java | 2 +- .../http/ffw/FfwInterfaceReadExecute.java | 2 +- 77 files changed, 789 insertions(+), 209 deletions(-) create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/BoolMember.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/ByteArrayMember.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Float32Member.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Float64Member.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int16Member.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int32Member.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int64Member.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int8Member.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/SetMember.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/SpatialMember.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StringMember.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TagsMember.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TupleNumberMember.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TypedMember.kt diff --git a/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/EventHandlerApiTask.java b/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/EventHandlerApiTask.java index 302457596..e7f99d2a0 100644 --- a/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/EventHandlerApiTask.java +++ b/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/EventHandlerApiTask.java @@ -106,7 +106,7 @@ protected void init() { private @NotNull XyzResponse executeGetHandlers() { // Create ReadFeatures Request to read all handlers from Admin DB - final ReadFeatures request = new ReadFeatures().addCollectionId(EVENT_HANDLERS); + final ReadFeatures request = new ReadFeatures().withCollectionId(EVENT_HANDLERS); request.setCatalogId(naksha().getAdminMapId()); // Submit request to NH Space Storage Response response = executeReadRequestFromSpaceStorage(request); @@ -117,7 +117,7 @@ protected void init() { private @NotNull XyzResponse executeGetHandlerById() { // Create ReadFeatures Request to read the handler with the specific ID from Admin DB final String handlerId = routingContext.pathParam(HANDLER_ID); - final ReadFeatures request = new ReadFeatures().addCollectionId(EVENT_HANDLERS); + final ReadFeatures request = new ReadFeatures().withCollectionId(EVENT_HANDLERS); request.setCatalogId(naksha().getAdminMapId()); request.setFeatureIds(StringList.of(handlerId)); // Submit request to NH Space Storage diff --git a/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/ReadFeatureApiTask.java b/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/ReadFeatureApiTask.java index b7728f402..d26ab1eab 100644 --- a/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/ReadFeatureApiTask.java +++ b/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/ReadFeatureApiTask.java @@ -337,7 +337,7 @@ protected void init() { .withPropertyQuery(propertyQuery) .withTagQuery(tagQuery); rdRequest.setFeatureIds(suppliedFeatureIds); - rdRequest.setCollectionIds(StringList.of(spaceId)); + rdRequest.setCollectionId(StringList.of(spaceId)); rdRequest.setLimit(limit); // Forward request to NH Space Storage reader instance @@ -422,7 +422,7 @@ protected void init() { query.setTags(tagQuery); final ReadFeatures rdRequest = new ReadFeatures(); rdRequest.setFeatureIds(suppliedFeatureIds); - rdRequest.setCollectionIds(StringList.of(spaceId)); + rdRequest.setCollectionId(StringList.of(spaceId)); rdRequest.setQuery(query); rdRequest.withPropertyQuery(propertyQuery); @@ -502,7 +502,7 @@ protected void init() { query.setSpatial(radiusQuery); query.setTags(tagQuery); final ReadFeatures rdRequest = new ReadFeatures(); - rdRequest.setCollectionIds(StringList.of(spaceId)); + rdRequest.setCollectionId(StringList.of(spaceId)); rdRequest.setFeatureIds(suppliedFeatureIds); rdRequest.setQuery(query); rdRequest.withPropertyQuery(propertyQuery); diff --git a/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/SpaceApiTask.java b/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/SpaceApiTask.java index e2dd9efc5..bceee9b20 100644 --- a/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/SpaceApiTask.java +++ b/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/SpaceApiTask.java @@ -132,7 +132,7 @@ private XyzResponse executeDeleteSpace() { } private @NotNull XyzResponse executeGetSpaces() { - final ReadFeatures request = new ReadFeatures().addCollectionId(SPACES); + final ReadFeatures request = new ReadFeatures().withCollectionId(SPACES); request.setCatalogId(naksha().getAdminMapId()); Response response = executeReadRequestFromSpaceStorage(request); return transformResponseToXyzCollectionResponse(response, Space.class, 0, DEF_ADMIN_FEATURE_LIMIT, null, null); @@ -140,7 +140,7 @@ private XyzResponse executeDeleteSpace() { private @NotNull XyzResponse executeGetSpaceById() { final String spaceId = extractMandatoryPathParam(routingContext, SPACE_ID); - final ReadFeatures request = new ReadFeatures().addCollectionId(SPACES); + final ReadFeatures request = new ReadFeatures().withCollectionId(SPACES); request.setCatalogId(naksha().getAdminMapId()); request.setFeatureIds(StringList.of(spaceId)); Response response = executeReadRequestFromSpaceStorage(request); diff --git a/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/StorageApiTask.java b/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/StorageApiTask.java index cf509f1ee..47854cc71 100644 --- a/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/StorageApiTask.java +++ b/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/StorageApiTask.java @@ -103,7 +103,7 @@ protected void init() { } private @NotNull XyzResponse executeGetStorages() { - final ReadFeatures request = new ReadFeatures().addCollectionId(STORAGES); + final ReadFeatures request = new ReadFeatures().withCollectionId(STORAGES); request.setCatalogId(naksha().getAdminMapId()); Response response = executeReadRequestFromSpaceStorage(request); return transformResponseToXyzCollectionResponse(response, NakshaStorage.class, STORAGE_MASKING); @@ -111,7 +111,7 @@ protected void init() { private @NotNull XyzResponse executeGetStorageById() { final String storageId = ApiParams.extractMandatoryPathParam(routingContext, STORAGE_ID); - final ReadFeatures request = new ReadFeatures().addCollectionId(STORAGES); + final ReadFeatures request = new ReadFeatures().withCollectionId(STORAGES); request.setCatalogId(naksha().getAdminMapId()); request.setFeatureIds(StringList.of(storageId)); return transformedResponseTo(request); diff --git a/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/util/NakshaAdminRequestUtil.java b/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/util/NakshaAdminRequestUtil.java index 8b8b41b52..8cca63eac 100644 --- a/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/util/NakshaAdminRequestUtil.java +++ b/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/util/NakshaAdminRequestUtil.java @@ -49,7 +49,7 @@ public static WriteRequest deleteEventHandlerRequest(INaksha naksha, String hand private static ReadFeatures getAdminResourcesRequest(INaksha naksha, String resourceCollection) { ReadFeatures readFeatures = new ReadFeatures(); readFeatures.setCatalogId(naksha.getAdminMapId()); - readFeatures.addCollectionId(resourceCollection); + readFeatures.withCollectionId(resourceCollection); return readFeatures; } diff --git a/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/copy/service/CopyService.java b/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/copy/service/CopyService.java index 5d686e14d..dc90d64ec 100644 --- a/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/copy/service/CopyService.java +++ b/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/copy/service/CopyService.java @@ -112,7 +112,7 @@ private SuccessResponse requireSuccessResponse( private ReadFeatures createReadFeaturesRequest(CopyElement source) { ReadFeatures readFeatures = new ReadFeatures(); - readFeatures.setCollectionIds( + readFeatures.setCollectionId( StringList.of(source.getCollectionId()) ); readFeatures.setCatalogId(source.getMapId()); diff --git a/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/copy/service/CopyServiceTest.java b/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/copy/service/CopyServiceTest.java index c32f599aa..8854e73b5 100644 --- a/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/copy/service/CopyServiceTest.java +++ b/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/copy/service/CopyServiceTest.java @@ -796,8 +796,8 @@ private IStorage createStorageWithFailingReadSession() { private void assertReadFeatures(List readFeaturesList) { assertEquals(1, readFeaturesList.size()); ReadFeatures readFeatures = readFeaturesList.getFirst(); - assertEquals(1, readFeatures.getCollectionIds().getSize()); - assertEquals(srcCopyElement.getCollectionId(), readFeatures.getCollectionIds().getFirst()); + assertEquals(1, readFeatures.getCollectionId().getSize()); + assertEquals(srcCopyElement.getCollectionId(), readFeatures.getCollectionId().getFirst()); assertEquals(srcCopyElement.getMapId(), readFeatures.getCatalogId()); } diff --git a/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/copy/service/psql/PsqlCopyTest.java b/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/copy/service/psql/PsqlCopyTest.java index 4ec437f01..a804e4c34 100644 --- a/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/copy/service/psql/PsqlCopyTest.java +++ b/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/copy/service/psql/PsqlCopyTest.java @@ -286,7 +286,7 @@ private SuccessResponse makeReadRequest(IStorage storage, Request request, Sessi private ReadFeatures createReadFeaturesRequest(String mapId, String collectionId) { ReadFeatures readFeatures = new ReadFeatures(); - readFeatures.setCollectionIds( + readFeatures.setCollectionId( StringList.of(collectionId) ); readFeatures.setCatalogId(mapId); diff --git a/here-naksha-handler-activitylog/src/jvmMain/java/com/here/naksha/handler/activitylog/ActivityLogHandler.java b/here-naksha-handler-activitylog/src/jvmMain/java/com/here/naksha/handler/activitylog/ActivityLogHandler.java index cf356efc3..f74ca7b9e 100644 --- a/here-naksha-handler-activitylog/src/jvmMain/java/com/here/naksha/handler/activitylog/ActivityLogHandler.java +++ b/here-naksha-handler-activitylog/src/jvmMain/java/com/here/naksha/handler/activitylog/ActivityLogHandler.java @@ -175,7 +175,7 @@ private ReadFeatures featuresWhereNextVersionIsOneOf(List tupleNumb } MemberQuery nextVersionQuery = new MemberQuery(MetaColumn.nextVersion(), AnyOp.IS_ANY_OF, versions); ReadFeatures requestPredecessors = new ReadFeatures(); - requestPredecessors.setCollectionIds(StringList.of(properties.getSpaceId())); + requestPredecessors.setCollectionId(StringList.of(properties.getSpaceId())); requestPredecessors.setQueryHistory(true); requestPredecessors.getQuery().setMembers(nextVersionQuery); return requestPredecessors; diff --git a/here-naksha-handler-activitylog/src/jvmMain/java/com/here/naksha/handler/activitylog/ActivityLogRequestTranslationUtil.java b/here-naksha-handler-activitylog/src/jvmMain/java/com/here/naksha/handler/activitylog/ActivityLogRequestTranslationUtil.java index 93937756c..cab733d70 100644 --- a/here-naksha-handler-activitylog/src/jvmMain/java/com/here/naksha/handler/activitylog/ActivityLogRequestTranslationUtil.java +++ b/here-naksha-handler-activitylog/src/jvmMain/java/com/here/naksha/handler/activitylog/ActivityLogRequestTranslationUtil.java @@ -58,7 +58,7 @@ static void transformOriginalRequest(ReadFeatures readFeatures, String spaceId) readFeatures.setQueryHistory(true); readFeatures.setQueryDeleted(true); readFeatures.setVersions(Integer.MAX_VALUE); - readFeatures.setCollectionIds(StringList.of(spaceId)); + readFeatures.setCollectionId(StringList.of(spaceId)); // extract UUIDs from featureIds, reset featureIds StringList rawGuids = readFeatures.getFeatureIds(); diff --git a/here-naksha-handler-activitylog/src/jvmTest/java/com/here/naksha/handler/activitylog/ActivityLogHandlerTest.java b/here-naksha-handler-activitylog/src/jvmTest/java/com/here/naksha/handler/activitylog/ActivityLogHandlerTest.java index 6b3033cc4..5d55f1852 100644 --- a/here-naksha-handler-activitylog/src/jvmTest/java/com/here/naksha/handler/activitylog/ActivityLogHandlerTest.java +++ b/here-naksha-handler-activitylog/src/jvmTest/java/com/here/naksha/handler/activitylog/ActivityLogHandlerTest.java @@ -463,7 +463,7 @@ private ArgumentMatcher predecessorRequestMatcher() { private boolean isHistoryAwareReadFeatures(ReadRequest readRequest) { if (readRequest instanceof ReadFeatures rf) { - return rf.getQueryHistory() && rf.getCollectionIds().size() == 1; + return rf.getQueryHistory() && rf.getCollectionId().size() == 1; } return false; } diff --git a/here-naksha-handler-activitylog/src/jvmTest/java/com/here/naksha/handler/activitylog/ActivityLogRequestTranslationUtilTest.java b/here-naksha-handler-activitylog/src/jvmTest/java/com/here/naksha/handler/activitylog/ActivityLogRequestTranslationUtilTest.java index 58fe7c799..c99a068b9 100644 --- a/here-naksha-handler-activitylog/src/jvmTest/java/com/here/naksha/handler/activitylog/ActivityLogRequestTranslationUtilTest.java +++ b/here-naksha-handler-activitylog/src/jvmTest/java/com/here/naksha/handler/activitylog/ActivityLogRequestTranslationUtilTest.java @@ -9,12 +9,10 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.List; -import java.util.Random; -import naksha.base.JvmInt64; + import naksha.base.StringList; import naksha.model.Guid; import naksha.model.GuidList; -import naksha.model.TupleNumber; import naksha.model.Version; import naksha.model.request.ReadFeatures; import naksha.model.request.query.IPropertyQuery; @@ -172,7 +170,7 @@ void shouldApplyMixedTranslations() { private void verifyAllHistoricalVersionsInCollection(ReadFeatures readFeatures) { assertTrue(readFeatures.getQueryHistory()); - StringList collectionIds = readFeatures.getCollectionIds(); + StringList collectionIds = readFeatures.getCollectionId(); assertEquals(1, collectionIds.size()); assertEquals(TEST_SPACE_ID, collectionIds.get(0)); assertEquals(Integer.MAX_VALUE, readFeatures.getVersions()); diff --git a/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/storage/ReadFeaturesProxyWrapper.java b/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/storage/ReadFeaturesProxyWrapper.java index ca8955242..c2a221447 100644 --- a/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/storage/ReadFeaturesProxyWrapper.java +++ b/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/storage/ReadFeaturesProxyWrapper.java @@ -106,7 +106,7 @@ public ReadFeaturesProxyWrapper withLimit(int limit){ } public ReadFeaturesProxyWrapper withCollection(String collectionId){ - getCollectionIds().add(collectionId); + getCollectionId().add(collectionId); return this; } diff --git a/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/DefaultStorageHandler.java b/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/DefaultStorageHandler.java index 34dab3600..4873cd14b 100644 --- a/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/DefaultStorageHandler.java +++ b/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/DefaultStorageHandler.java @@ -526,7 +526,7 @@ private void applyMapIdAndCollectionId( if (request instanceof ReadFeatures) { ReadFeatures rf = (ReadFeatures) request; rf.setCatalogId(mapId); - rf.setCollectionIds(StringList.of(collectionId)); + rf.setCollectionId(StringList.of(collectionId)); } else if (request instanceof WriteRequest) { WriteRequest wr = (WriteRequest) request; if (isOnlyWriteCollections(wr)) { diff --git a/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/internal/IntHandlerForEventHandlerConfigs.java b/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/internal/IntHandlerForEventHandlerConfigs.java index 287e3a63b..de2d652ce 100644 --- a/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/internal/IntHandlerForEventHandlerConfigs.java +++ b/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/internal/IntHandlerForEventHandlerConfigs.java @@ -261,8 +261,8 @@ private Response noActiveSpaceValidation(Write codec) { // Scan through all spaces with JSON property "eventHandlerIds" containing the targeted handler ID final Property pRef = new Property(EVENT_HANDLER_IDS); final PQuery activeSpacesPOp = new PQuery(pRef, AnyOp.CONTAINS, handlerId); - final ReadFeatures readActiveHandlersRequest = new ReadFeatures().addCollectionId(SPACES) - .withMapId(nakshaHub.getAdminMapId()) + final ReadFeatures readActiveHandlersRequest = new ReadFeatures().withCollectionId(SPACES) + .withCatalogId(nakshaHub.getAdminMapId()) .withPropertyQuery(activeSpacesPOp); return nakshaHub().getAdminStorage().useReadSession(new SessionOptions(), readSession -> { diff --git a/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/internal/IntHandlerForStorageConfigs.java b/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/internal/IntHandlerForStorageConfigs.java index bfb42c370..9032b2899 100644 --- a/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/internal/IntHandlerForStorageConfigs.java +++ b/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/internal/IntHandlerForStorageConfigs.java @@ -108,8 +108,8 @@ private Response noActiveHandlerValidation(Write codec) { final Property property = new Property(NakshaFeature.PROPERTIES_KEY, DefaultStorageHandlerProperties.STORAGE_ID); final PQuery activeHandlersPOp = new PQuery(property, StringOp.EQUALS, storageId); - final ReadFeatures readActiveHandlersRequest = new ReadFeatures().addCollectionId(EVENT_HANDLERS) - .withMapId(nakshaHub.getAdminMapId()) + final ReadFeatures readActiveHandlersRequest = new ReadFeatures().withCollectionId(EVENT_HANDLERS) + .withCatalogId(nakshaHub.getAdminMapId()) .withPropertyQuery(activeHandlersPOp); Response activeHandlersResponse = nakshaHub().getAdminStorage() .useReadSession(SessionOptions.from(NakshaContext.currentContext()), readSession -> readSession.execute(readActiveHandlersRequest)); diff --git a/here-naksha-lib-handlers/src/jvmTest/java/com/here/naksha/lib/handlers/DefaultStorageHandlerTest.java b/here-naksha-lib-handlers/src/jvmTest/java/com/here/naksha/lib/handlers/DefaultStorageHandlerTest.java index ead2fe89d..dce4c6127 100644 --- a/here-naksha-lib-handlers/src/jvmTest/java/com/here/naksha/lib/handlers/DefaultStorageHandlerTest.java +++ b/here-naksha-lib-handlers/src/jvmTest/java/com/here/naksha/lib/handlers/DefaultStorageHandlerTest.java @@ -678,7 +678,7 @@ private static Request writeRandomFeature() { private static Request readRandomFeature() { ReadFeatures readFeatures = new ReadFeatures(); readFeatures.setCatalogId("random_map_" + RandomUtils.nextInt()); - readFeatures.setCollectionIds(new StringList("random_collection_" + RandomUtils.nextInt())); + readFeatures.setCollectionId(new StringList("random_collection_" + RandomUtils.nextInt())); readFeatures.setFeatureIds(new StringList("random_feature_" + RandomUtils.nextInt())); return readFeatures; } diff --git a/here-naksha-lib-handlers/src/jvmTest/java/com/here/naksha/lib/handlers/internal/IntHandlerForSpacesTest.java b/here-naksha-lib-handlers/src/jvmTest/java/com/here/naksha/lib/handlers/internal/IntHandlerForSpacesTest.java index c40f70e53..36844e4a4 100644 --- a/here-naksha-lib-handlers/src/jvmTest/java/com/here/naksha/lib/handlers/internal/IntHandlerForSpacesTest.java +++ b/here-naksha-lib-handlers/src/jvmTest/java/com/here/naksha/lib/handlers/internal/IntHandlerForSpacesTest.java @@ -187,7 +187,7 @@ private void handlersExist(List eventHandlerIds) { } private ArgumentMatcher anyReadHandlersRequest() { - return argument -> argument.getCollectionIds().size() == 1 && EVENT_HANDLERS.equals(argument.getCollectionIds().get(0)); + return argument -> argument.getCollectionId().size() == 1 && EVENT_HANDLERS.equals(argument.getCollectionId().get(0)); } private static SuccessResponse successfulResponseWithIds(List ids) { diff --git a/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/NakshaHub.java b/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/NakshaHub.java index c343d20d1..2e82b0cae 100644 --- a/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/NakshaHub.java +++ b/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/NakshaHub.java @@ -387,7 +387,7 @@ private NakshaHubConfig fetchHubConfigFromDb(String configId, IWriteSession admi @Override public @NotNull ExtensionConfig getExtensionConfig() { - final ReadFeatures readRequest = new ReadFeatures().addCollectionId(EVENT_HANDLERS).withMapId(adminMapId); + final ReadFeatures readRequest = new ReadFeatures().withCollectionId(EVENT_HANDLERS).withCatalogId(adminMapId); final PQuery pQueryExists = new PQuery(new Property(EXTN_ID_PROP_PATH), AnyOp.EXISTS); final PQuery pQueryNotNull = new PQuery(new Property(EXTN_ID_PROP_PATH), AnyOp.IS_NOT_NULL); final IPropertyQuery propertyQuery = new PAnd(pQueryExists, pQueryNotNull); diff --git a/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHSpaceStorageReader.java b/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHSpaceStorageReader.java index 65545512e..5999c6c88 100644 --- a/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHSpaceStorageReader.java +++ b/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHSpaceStorageReader.java @@ -117,7 +117,7 @@ public NHSpaceStorageReader( } private @NotNull Response executeReadFeatures(final @NotNull ReadFeatures rf) { - List collectionIds = rf.getCollectionIds(); + List collectionIds = rf.getCollectionId(); if (collectionIds.size() > 1) { throw new UnsupportedOperationException("Reading from multiple spaces not supported!"); } @@ -161,7 +161,7 @@ public NHSpaceStorageReader( } private @NotNull Response executeReadFeaturesFromCustomSpaces(final @NotNull ReadFeatures rf) { - List collectionIds = rf.getCollectionIds(); + List collectionIds = rf.getCollectionId(); if (collectionIds.size() > 1) { return new ErrorResponse(new NakshaError( NakshaError.UNSUPPORTED_OPERATION, diff --git a/here-naksha-lib-hub/src/jvmTest/java/com/here/naksha/lib/hub/NakshaHubWiringTest.java b/here-naksha-lib-hub/src/jvmTest/java/com/here/naksha/lib/hub/NakshaHubWiringTest.java index 78b178a2d..46275c46f 100644 --- a/here-naksha-lib-hub/src/jvmTest/java/com/here/naksha/lib/hub/NakshaHubWiringTest.java +++ b/here-naksha-lib-hub/src/jvmTest/java/com/here/naksha/lib/hub/NakshaHubWiringTest.java @@ -142,7 +142,7 @@ void testCreateStorageRequestWiring() { @Order(2) void testGetStoragesRequestWiring() throws Exception { // Given: Read Storage request - final ReadFeatures request = new ReadFeatures().addCollectionId(STORAGES); + final ReadFeatures request = new ReadFeatures().withCollectionId(STORAGES); // And: spies and captors in place final EventPipeline spyPipeline = spy(spyPipelineFactory.eventPipeline()); @@ -184,14 +184,14 @@ void testCreateFeatureRequestWiring() throws Exception { final IStorage spyStorageImpl = spy(storageImpl); when(adminStorageReader.execute(argThat(readRequest -> { if (readRequest instanceof ReadFeatures rr) { - return Objects.equals(rr.getCollectionIds().get(0), SPACES); + return Objects.equals(rr.getCollectionId().get(0), SPACES); } return false; }))) .thenReturn(mockResultWithFeature(space)); when(adminStorageReader.execute(argThat(readRequest -> { if (readRequest instanceof ReadFeatures rr) { - return Objects.equals(rr.getCollectionIds().get(0), EVENT_HANDLERS); + return Objects.equals(rr.getCollectionId().get(0), EVENT_HANDLERS); } return false; }))) diff --git a/here-naksha-lib-hub/src/jvmTest/java/com/here/naksha/lib/hub/mock/NHAdminReaderMock.java b/here-naksha-lib-hub/src/jvmTest/java/com/here/naksha/lib/hub/mock/NHAdminReaderMock.java index 045915d74..121b7d273 100644 --- a/here-naksha-lib-hub/src/jvmTest/java/com/here/naksha/lib/hub/mock/NHAdminReaderMock.java +++ b/here-naksha-lib-hub/src/jvmTest/java/com/here/naksha/lib/hub/mock/NHAdminReaderMock.java @@ -94,7 +94,7 @@ public NHAdminReaderMock(final @NotNull Map features = getFeatures(rf.getCollectionIds(), rf.getFeatureIds(), rf.getQuery()); + List features = getFeatures(rf.getCollectionId(), rf.getFeatureIds(), rf.getQuery()); SuccessResponse response = new SuccessResponse(); response.setFeatures(NakshaFeatureList.fromList(features)); return response; diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/BoolMember.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/BoolMember.kt new file mode 100644 index 000000000..d4799b9d8 --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/BoolMember.kt @@ -0,0 +1,34 @@ +package naksha.model.objects + +import naksha.model.illegalArg +import naksha.model.illegalState +import naksha.model.objects.MemberType.MemberType_C.BOOLEAN +import kotlin.js.JsName + +class BoolMember() : TypedMember() { + override fun verify(): BoolMember { + if (dataType != BOOLEAN) { + throw illegalState("The member was illegally cast, expected subtype: $BOOLEAN, found: $dataType") + } + return this + } + + @JsName("of") + constructor(name: String, path: JsonPath? = null) : this() { + this.name = name + this.dataType = BOOLEAN + this.path = path ?: JsonPath(listOf("properties", name)) + this.path.validate() + } + + @JsName("from") + constructor(member: Member, path: JsonPath? = null) : this() { + if (member.dataType != BOOLEAN) throw illegalArg("The given member is not of boolean type") + this.name = member.name + this.dataType = BOOLEAN + this.path = path?.validate() ?: member.path // Only verify modified path. + } + + fun get(feature: NakshaFeature): Boolean? = getBoolean(feature) + fun set(feature: NakshaFeature, value: Boolean): Boolean? = setPath(feature, path, value) as Boolean? +} \ No newline at end of file diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/ByteArrayMember.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/ByteArrayMember.kt new file mode 100644 index 000000000..3a9fcc7ce --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/ByteArrayMember.kt @@ -0,0 +1,34 @@ +package naksha.model.objects + +import naksha.model.illegalArg +import naksha.model.illegalState +import naksha.model.objects.MemberType.MemberType_C.BYTE_ARRAY +import kotlin.js.JsName + +class ByteArrayMember() : TypedMember() { + override fun verify(): ByteArrayMember { + if (dataType != BYTE_ARRAY) { + throw illegalState("The member was illegally cast, expected subtype: $BYTE_ARRAY, found: $dataType") + } + return this + } + + @JsName("of") + constructor(name: String, path: JsonPath? = null) : this() { + this.name = name + this.dataType = BYTE_ARRAY + this.path = path ?: JsonPath(listOf("properties", name)) + this.path.validate() + } + + @JsName("from") + constructor(member: Member, path: JsonPath? = null) : this() { + if (member.dataType != BYTE_ARRAY) throw illegalArg("The given member is not of byte_array type") + this.name = member.name + this.dataType = BYTE_ARRAY + this.path = path?.validate() ?: member.path + } + + fun get(feature: NakshaFeature): ByteArray? = getByteArray(feature) + fun set(feature: NakshaFeature, value: ByteArray): Any? = setPath(feature, path, value) +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Float32Member.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Float32Member.kt new file mode 100644 index 000000000..ab95a317f --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Float32Member.kt @@ -0,0 +1,34 @@ +package naksha.model.objects + +import naksha.model.illegalArg +import naksha.model.illegalState +import naksha.model.objects.MemberType.MemberType_C.FLOAT32 +import kotlin.js.JsName + +class Float32Member() : TypedMember() { + override fun verify(): Float32Member { + if (dataType != FLOAT32) { + throw illegalState("The member was illegally cast, expected subtype: $FLOAT32, found: $dataType") + } + return this + } + + @JsName("of") + constructor(name: String, path: JsonPath? = null) : this() { + this.name = name + this.dataType = FLOAT32 + this.path = path ?: JsonPath(listOf("properties", name)) + this.path.validate() + } + + @JsName("from") + constructor(member: Member, path: JsonPath? = null) : this() { + if (member.dataType != FLOAT32) throw illegalArg("The given member is not of float32 type") + this.name = member.name + this.dataType = FLOAT32 + this.path = path?.validate() ?: member.path + } + + fun get(feature: NakshaFeature): Float? = getDouble(feature)?.toFloat() + fun set(feature: NakshaFeature, value: Float): Any? = setPath(feature, path, value) +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Float64Member.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Float64Member.kt new file mode 100644 index 000000000..15a727c13 --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Float64Member.kt @@ -0,0 +1,34 @@ +package naksha.model.objects + +import naksha.model.illegalArg +import naksha.model.illegalState +import naksha.model.objects.MemberType.MemberType_C.FLOAT64 +import kotlin.js.JsName + +class Float64Member() : TypedMember() { + override fun verify(): Float64Member { + if (dataType != FLOAT64) { + throw illegalState("The member was illegally cast, expected subtype: $FLOAT64, found: $dataType") + } + return this + } + + @JsName("of") + constructor(name: String, path: JsonPath? = null) : this() { + this.name = name + this.dataType = FLOAT64 + this.path = path ?: JsonPath(listOf("properties", name)) + this.path.validate() + } + + @JsName("from") + constructor(member: Member, path: JsonPath? = null) : this() { + if (member.dataType != FLOAT64) throw illegalArg("The given member is not of float64 type") + this.name = member.name + this.dataType = FLOAT64 + this.path = path?.validate() ?: member.path + } + + fun get(feature: NakshaFeature): Double? = getDouble(feature) + fun set(feature: NakshaFeature, value: Double): Any? = setPath(feature, path, value) +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int16Member.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int16Member.kt new file mode 100644 index 000000000..9341c72b2 --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int16Member.kt @@ -0,0 +1,34 @@ +package naksha.model.objects + +import naksha.model.illegalArg +import naksha.model.illegalState +import naksha.model.objects.MemberType.MemberType_C.INT16 +import kotlin.js.JsName + +class Int16Member() : TypedMember() { + override fun verify(): Int16Member { + if (dataType != INT16) { + throw illegalState("The member was illegally cast, expected subtype: $INT16, found: $dataType") + } + return this + } + + @JsName("of") + constructor(name: String, path: JsonPath? = null) : this() { + this.name = name + this.dataType = INT16 + this.path = path ?: JsonPath(listOf("properties", name)) + this.path.validate() + } + + @JsName("from") + constructor(member: Member, path: JsonPath? = null) : this() { + if (member.dataType != INT16) throw illegalArg("The given member is not of int16 type") + this.name = member.name + this.dataType = INT16 + this.path = path?.validate() ?: member.path + } + + fun get(feature: NakshaFeature): Short? = getInt64(feature)?.toShort() + fun set(feature: NakshaFeature, value: Short): Any? = setPath(feature, path, value) +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int32Member.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int32Member.kt new file mode 100644 index 000000000..1628515ad --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int32Member.kt @@ -0,0 +1,34 @@ +package naksha.model.objects + +import naksha.model.illegalArg +import naksha.model.illegalState +import naksha.model.objects.MemberType.MemberType_C.INT32 +import kotlin.js.JsName + +class Int32Member() : TypedMember() { + override fun verify(): Int32Member { + if (dataType != INT32) { + throw illegalState("The member was illegally cast, expected subtype: $INT32, found: $dataType") + } + return this + } + + @JsName("of") + constructor(name: String, path: JsonPath? = null) : this() { + this.name = name + this.dataType = INT32 + this.path = path ?: JsonPath(listOf("properties", name)) + this.path.validate() + } + + @JsName("from") + constructor(member: Member, path: JsonPath? = null) : this() { + if (member.dataType != INT32) throw illegalArg("The given member is not of int32 type") + this.name = member.name + this.dataType = INT32 + this.path = path?.validate() ?: member.path + } + + fun get(feature: NakshaFeature): Int? = getInt64(feature)?.toInt() + fun set(feature: NakshaFeature, value: Int): Any? = setPath(feature, path, value) +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int64Member.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int64Member.kt new file mode 100644 index 000000000..aa9f19294 --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int64Member.kt @@ -0,0 +1,35 @@ +package naksha.model.objects + +import naksha.base.Int64 +import naksha.model.illegalArg +import naksha.model.illegalState +import naksha.model.objects.MemberType.MemberType_C.INT64 +import kotlin.js.JsName + +class Int64Member() : TypedMember() { + override fun verify(): Int64Member { + if (dataType != INT64) { + throw illegalState("The member was illegally cast, expected subtype: $INT64, found: $dataType") + } + return this + } + + @JsName("of") + constructor(name: String, path: JsonPath? = null) : this() { + this.name = name + this.dataType = INT64 + this.path = path ?: JsonPath(listOf("properties", name)) + this.path.validate() + } + + @JsName("from") + constructor(member: Member, path: JsonPath? = null) : this() { + if (member.dataType != INT64) throw illegalArg("The given member is not of int64 type") + this.name = member.name + this.dataType = INT64 + this.path = path?.validate() ?: member.path + } + + fun get(feature: NakshaFeature): Int64? = getInt64(feature) + fun set(feature: NakshaFeature, value: Int64): Any? = setPath(feature, path, value) +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int8Member.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int8Member.kt new file mode 100644 index 000000000..3a5e4873a --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int8Member.kt @@ -0,0 +1,34 @@ +package naksha.model.objects + +import naksha.model.illegalArg +import naksha.model.illegalState +import naksha.model.objects.MemberType.MemberType_C.INT8 +import kotlin.js.JsName + +class Int8Member() : TypedMember() { + override fun verify(): Int8Member { + if (dataType != INT8) { + throw illegalState("The member was illegally cast, expected subtype: $INT8, found: $dataType") + } + return this + } + + @JsName("of") + constructor(name: String, path: JsonPath? = null) : this() { + this.name = name + this.dataType = INT8 + this.path = path ?: JsonPath(listOf("properties", name)) + this.path.validate() + } + + @JsName("from") + constructor(member: Member, path: JsonPath? = null) : this() { + if (member.dataType != INT8) throw illegalArg("The given member is not of int8 type") + this.name = member.name + this.dataType = INT8 + this.path = path?.validate() ?: member.path + } + + fun get(feature: NakshaFeature): Byte? = getInt64(feature)?.toByte() + fun set(feature: NakshaFeature, value: Byte): Any? = setPath(feature, path, value) +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/JsonPath.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/JsonPath.kt index 2b8411cc4..69cd7aafb 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/JsonPath.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/JsonPath.kt @@ -30,14 +30,16 @@ open class JsonPath() : AnyList() { /** * Tests whether the path is valid, if not, throws an [NakshaException] with [ILLEGAL_STATE] error. Actually, the path must only contain strings and integers. + * @return this. * @since 3.0 */ - fun validate() { + fun validate(): JsonPath { for (i in 0 until this.size) { val segment = this[i] ?: throw NakshaException(ILLEGAL_STATE, "Illegal NULL at path position $i") if (segment !is String && segment !is Int) { throw NakshaException(ILLEGAL_STATE, "Illegal value at path position $i, must be string or integer, found: '$segment'") } } + return this } } \ No newline at end of file diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt index 5be323683..4ea0fe936 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt @@ -2,19 +2,38 @@ package naksha.model.objects +import naksha.base.AnyList import naksha.base.AnyObject import naksha.base.Int64 +import naksha.base.ListProxy import naksha.base.MapProxy import naksha.base.NotNullEnum import naksha.base.NotNullProperty import naksha.base.NullableProperty +import naksha.base.PlatformList +import naksha.base.PlatformMap import naksha.base.Proxy +import naksha.base.proxy import naksha.geo.SpGeometry import naksha.model.Naksha import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_ARGUMENT import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_STATE import naksha.model.NakshaException +import naksha.model.TagList +import naksha.model.TagMap import naksha.model.TupleNumber +import naksha.model.objects.ByteArrayMember +import naksha.model.objects.Float32Member +import naksha.model.objects.Float64Member +import naksha.model.objects.Int16Member +import naksha.model.objects.Int32Member +import naksha.model.objects.Int64Member +import naksha.model.objects.Int8Member +import naksha.model.objects.SetMember +import naksha.model.objects.SpatialMember +import naksha.model.objects.StringMember +import naksha.model.objects.TagsMember +import naksha.model.objects.TupleNumberMember import kotlin.js.JsExport import kotlin.js.JsName @@ -301,6 +320,47 @@ open class Member() : AnyObject(), Comparator { return null } + /** + * Helper to read a [TagMap] form the given feature. + * @param feature The feature to read from. + * @return the read value or `null`, if the feature does not store a valid value at the member path. + */ + fun getTagMap(feature: MapProxy<*,*>): TagMap? { + val raw = feature.getPath(path) + if (raw is TagMap) return raw + if (raw is MapProxy<*,*>) return raw.proxy(TagMap::class) + if (raw is PlatformMap) return raw.proxy(TagMap::class) + return null + } + + /** + * Helper to read a [TagMap] form the given feature. + * @param feature The feature to read from. + * @return the read value or `null`, if the feature does not store a valid value at the member path. + */ + fun getTagList(feature: MapProxy<*,*>): TagList? { + val raw = feature.getPath(path) + if (raw is TagList) return raw + if (raw is ListProxy<*>) return raw.proxy(TagList::class) + if (raw is PlatformList) return raw.proxy(TagList::class) + return null + } + + // TODO: We need support for real sets! + + /** + * Helper to read a set form the given feature. + * @param feature The feature to read from. + * @return the read value or `null`, if the feature does not store a valid value at the member path. + */ + fun getSet(feature: MapProxy<*,*>): AnyList? { + val raw = feature.getPath(path) + if (raw is AnyList) return raw + if (raw is ListProxy<*>) return raw.proxy(AnyList::class) + if (raw is PlatformList) return raw.proxy(AnyList::class) + return null + } + /** * Helper to write a member value to the given feature. * @param feature The feature to write to. @@ -318,4 +378,44 @@ open class Member() : AnyObject(), Comparator { private val PATH = NotNullProperty(JsonPath::class) { self, _ -> JsonPath(listOf("properties", self.name)) } private val MANDATORY = NotNullProperty(Boolean::class) { _, _ -> false } } + + /** + * Returns the concrete subtype of this member. + * @return the concrete subtype of this member, for example [BoolMember] + * @since 3.0 + */ + fun subType(): Member { + val klass = this::class + if (klass != Member::class) return this + when (dataType) { + MemberType.BOOLEAN -> proxy(BoolMember::class) + MemberType.INT8 -> proxy(Int8Member::class) + MemberType.INT16 -> proxy(Int16Member::class) + MemberType.INT32 -> proxy(Int32Member::class) + MemberType.INT64 -> proxy(Int64Member::class) + MemberType.FLOAT32 -> proxy(Float32Member::class) + MemberType.FLOAT64 -> proxy(Float64Member::class) + MemberType.STRING -> proxy(StringMember::class) + MemberType.BYTE_ARRAY -> proxy(ByteArrayMember::class) + MemberType.TUPLE_NUMBER -> proxy(TupleNumberMember::class) + MemberType.SPATIAL -> proxy(SpatialMember::class) + MemberType.TAGS, MemberType.TAGS_FROM_ARRAY -> proxy(TagsMember::class) + MemberType.SET -> proxy(SetMember::class) + } + return this + } + + fun asBool(): BoolMember = proxy(BoolMember::class) + fun asInt8(): Int8Member = proxy(Int8Member::class) + fun asInt16(): Int16Member = proxy(Int16Member::class) + fun asInt32(): Int32Member = proxy(Int32Member::class) + fun asInt64(): Int64Member = proxy(Int64Member::class) + fun asFloat32(): Float32Member = proxy(Float32Member::class) + fun asFloat64(): Float64Member = proxy(Float64Member::class) + fun asString(): StringMember = proxy(StringMember::class) + fun asByteArray(): ByteArrayMember = proxy(ByteArrayMember::class) + fun asTupleNumber(): TupleNumberMember = proxy(TupleNumberMember::class) + fun asSpatial(): SpatialMember = proxy(SpatialMember::class) + fun asTags(): TagsMember = proxy(TagsMember::class) + fun asSet(): SetMember = proxy(SetMember::class) } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberType.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberType.kt index 47a380cd3..1483f811d 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberType.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberType.kt @@ -9,6 +9,19 @@ import naksha.model.NakshaError.NakshaErrorCompanion.INITIALIZATION_FAILED import naksha.model.NakshaException import naksha.model.TagMap import naksha.model.TupleNumber +import naksha.model.objects.BoolMember +import naksha.model.objects.ByteArrayMember +import naksha.model.objects.Float32Member +import naksha.model.objects.Float64Member +import naksha.model.objects.Int16Member +import naksha.model.objects.Int32Member +import naksha.model.objects.Int64Member +import naksha.model.objects.Int8Member +import naksha.model.objects.SetMember +import naksha.model.objects.SpatialMember +import naksha.model.objects.StringMember +import naksha.model.objects.TagsMember +import naksha.model.objects.TupleNumberMember import kotlin.js.JsExport import kotlin.jvm.JvmField import kotlin.reflect.KClass @@ -49,70 +62,70 @@ class MemberType : JsEnum() { * @since 3.0 */ @JvmField - val BOOLEAN = defIgnoreCase(MemberType::class, "boolean") { self -> self.sortOrder = 6 } + val BOOLEAN = defIgnoreCase(MemberType::class, "boolean") { self -> self.sortOrder = 6; self.subtype = BoolMember::class } /** * 8-bit signed integer in storage, but when reading from book, decode as long. * @since 3.0 */ @JvmField - val INT8 = defIgnoreCase(MemberType::class, "int8") { self -> self.sortOrder = 5 } + val INT8 = defIgnoreCase(MemberType::class, "int8") { self -> self.sortOrder = 5; self.subtype = Int8Member::class } /** * 16-bit signed integer in storage, but when reading from book, decode as long. * @since 3.0 */ @JvmField - val INT16 = defIgnoreCase(MemberType::class, "int16") { self -> self.sortOrder = 4 } + val INT16 = defIgnoreCase(MemberType::class, "int16") { self -> self.sortOrder = 4; self.subtype = Int16Member::class } /** * 32-bit signed integer in storage, but when reading from book, decode as long. * @since 3.0 */ @JvmField - val INT32 = defIgnoreCase(MemberType::class, "int32") { self -> self.sortOrder = 2 } + val INT32 = defIgnoreCase(MemberType::class, "int32") { self -> self.sortOrder = 2; self.subtype = Int32Member::class } /** * 64-bit signed integer. * @since 3.0 */ @JvmField - val INT64 = defIgnoreCase(MemberType::class, "int64") { self -> self.sortOrder = 0 } + val INT64 = defIgnoreCase(MemberType::class, "int64") { self -> self.sortOrder = 0; self.subtype = Int64Member::class } /** * 32-bit IEEE-754 floating point in storage, but when reading from book, decode as double. * @since 3.0 */ @JvmField - val FLOAT32 = defIgnoreCase(MemberType::class, "float32"){ self -> self.sortOrder = 3 } + val FLOAT32 = defIgnoreCase(MemberType::class, "float32"){ self -> self.sortOrder = 3; self.subtype = Float32Member::class } /** * 64-bit IEEE-754 floating point. * @since 3.0 */ @JvmField - val FLOAT64 = defIgnoreCase(MemberType::class, "float64") { self -> self.sortOrder = 1 } + val FLOAT64 = defIgnoreCase(MemberType::class, "float64") { self -> self.sortOrder = 1; self.subtype = Float64Member::class } /** * Variable-length string. * @since 3.0 */ @JvmField - val STRING = defIgnoreCase(MemberType::class, "string") { self -> self.sortOrder = 7 } + val STRING = defIgnoreCase(MemberType::class, "string") { self -> self.sortOrder = 7; self.subtype = StringMember::class } /** * Raw byte array. * @since 3.0 */ @JvmField - val BYTE_ARRAY = defIgnoreCase(MemberType::class, "byte_array") { self -> self.sortOrder = 13 } + val BYTE_ARRAY = defIgnoreCase(MemberType::class, "byte_array") { self -> self.sortOrder = 13; self.subtype = ByteArrayMember::class } /** * A tuple-number, can be encoded as string or byte-array _(storage decides)_. To be used with [IndexType.BTREE]. * @since 3.0 */ @JvmField - val TUPLE_NUMBER = defIgnoreCase(MemberType::class, "tuple_number") { self -> self.sortOrder = 12 } + val TUPLE_NUMBER = defIgnoreCase(MemberType::class, "tuple_number") { self -> self.sortOrder = 12; self.subtype = TupleNumberMember::class } /** * A geometry stored as raw [TWKB](https://github.com/nicowillis/twkb) bytes. @@ -124,7 +137,7 @@ class MemberType : JsEnum() { * @since 3.0 */ @JvmField - val SPATIAL = defIgnoreCase(MemberType::class, "spatial") { self -> self.sortOrder = 11 } + val SPATIAL = defIgnoreCase(MemberType::class, "spatial") { self -> self.sortOrder = 11; self.subtype = SpatialMember::class } /** * A map whose keys are strings and values are primitives, following the JBON2 tag-map @@ -135,7 +148,7 @@ class MemberType : JsEnum() { * @since 3.0 */ @JvmField - val TAGS = defIgnoreCase(MemberType::class, "tags") { self -> self.sortOrder = 8 } + val TAGS = defIgnoreCase(MemberType::class, "tags") { self -> self.sortOrder = 8; self.subtype = TagsMember::class } /** * A string-array using Naksha tag syntax that is expanded into a [TAGS] map at write time. @@ -149,7 +162,7 @@ class MemberType : JsEnum() { * @since 3.0 */ @JvmField - val TAGS_FROM_ARRAY = defIgnoreCase(MemberType::class, "tags_from_array") { self -> self.sortOrder = 9 } + val TAGS_FROM_ARRAY = defIgnoreCase(MemberType::class, "tags_from_array") { self -> self.sortOrder = 9; self.subtype = TagsMember::class } /** * A JSON array of unique primitive values (booleans, numbers, strings), following the JBON2 @@ -166,9 +179,16 @@ class MemberType : JsEnum() { * @since 3.0 */ @JvmField - val SET = defIgnoreCase(MemberType::class, "set") { self -> self.sortOrder = 10 } + val SET = defIgnoreCase(MemberType::class, "set") { self -> self.sortOrder = 10; self.subtype = SetMember::class } } + /** + * The concrete subtype of this class. + * @since 3.0 + */ + var subtype: KClass = Member::class + private set + /** * Tests if the given value is of this type. * @param value the value to test. diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/SetMember.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/SetMember.kt new file mode 100644 index 000000000..c2c45ce5f --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/SetMember.kt @@ -0,0 +1,35 @@ +package naksha.model.objects + +import naksha.base.AnyList +import naksha.model.illegalArg +import naksha.model.illegalState +import naksha.model.objects.MemberType.MemberType_C.SET +import kotlin.js.JsName + +class SetMember() : TypedMember() { + override fun verify(): SetMember { + if (dataType != SET) { + throw illegalState("The member was illegally cast, expected subtype: $SET, found: $dataType") + } + return this + } + + @JsName("of") + constructor(name: String, path: JsonPath? = null) : this() { + this.name = name + this.dataType = SET + this.path = path ?: JsonPath(listOf("properties", name)) + this.path.validate() + } + + @JsName("from") + constructor(member: Member, path: JsonPath? = null) : this() { + if (member.dataType != SET) throw illegalArg("The given member is not of set type") + this.name = member.name + this.dataType = SET + this.path = path?.validate() ?: member.path + } + + fun get(feature: NakshaFeature): AnyList? = getSet(feature) + fun set(feature: NakshaFeature, value: AnyList): Any? = setPath(feature, path, value) +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/SpatialMember.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/SpatialMember.kt new file mode 100644 index 000000000..40e3d589a --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/SpatialMember.kt @@ -0,0 +1,35 @@ +package naksha.model.objects + +import naksha.geo.SpGeometry +import naksha.model.illegalArg +import naksha.model.illegalState +import naksha.model.objects.MemberType.MemberType_C.SPATIAL +import kotlin.js.JsName + +class SpatialMember() : TypedMember() { + override fun verify(): SpatialMember { + if (dataType != SPATIAL) { + throw illegalState("The member was illegally cast, expected subtype: $SPATIAL, found: $dataType") + } + return this + } + + @JsName("of") + constructor(name: String, path: JsonPath? = null) : this() { + this.name = name + this.dataType = SPATIAL + this.path = path ?: JsonPath(listOf("properties", name)) + this.path.validate() + } + + @JsName("from") + constructor(member: Member, path: JsonPath? = null) : this() { + if (member.dataType != SPATIAL) throw illegalArg("The given member is not of spatial type") + this.name = member.name + this.dataType = SPATIAL + this.path = path?.validate() ?: member.path + } + + fun get(feature: NakshaFeature): SpGeometry? = getGeometry(feature) + fun set(feature: NakshaFeature, value: SpGeometry): Any? = setPath(feature, path, value) +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StringMember.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StringMember.kt new file mode 100644 index 000000000..02080ff21 --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StringMember.kt @@ -0,0 +1,34 @@ +package naksha.model.objects + +import naksha.model.illegalArg +import naksha.model.illegalState +import naksha.model.objects.MemberType.MemberType_C.STRING +import kotlin.js.JsName + +class StringMember() : TypedMember() { + override fun verify(): StringMember { + if (dataType != STRING) { + throw illegalState("The member was illegally cast, expected subtype: $STRING, found: $dataType") + } + return this + } + + @JsName("of") + constructor(name: String, path: JsonPath? = null) : this() { + this.name = name + this.dataType = STRING + this.path = path ?: JsonPath(listOf("properties", name)) + this.path.validate() + } + + @JsName("from") + constructor(member: Member, path: JsonPath? = null) : this() { + if (member.dataType != STRING) throw illegalArg("The given member is not of string type") + this.name = member.name + this.dataType = STRING + this.path = path?.validate() ?: member.path + } + + fun get(feature: NakshaFeature): String? = getString(feature) + fun set(feature: NakshaFeature, value: String): Any? = setPath(feature, path, value) +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TagsMember.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TagsMember.kt new file mode 100644 index 000000000..22d51a632 --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TagsMember.kt @@ -0,0 +1,34 @@ +package naksha.model.objects + +import naksha.model.illegalArg +import naksha.model.illegalState +import naksha.model.objects.MemberType.MemberType_C.TAGS +import kotlin.js.JsName + +class TagsMember() : TypedMember() { + override fun verify(): TagsMember { + if (dataType != TAGS) { + throw illegalState("The member was illegally cast, expected subtype: $TAGS, found: $dataType") + } + return this + } + + @JsName("of") + constructor(name: String, path: JsonPath? = null) : this() { + this.name = name + this.dataType = TAGS + this.path = path ?: JsonPath(listOf("properties", name)) + this.path.validate() + } + + @JsName("from") + constructor(member: Member, path: JsonPath? = null) : this() { + if (member.dataType != TAGS) throw illegalArg("The given member is not of tags type") + this.name = member.name + this.dataType = TAGS + this.path = path?.validate() ?: member.path + } + + fun get(feature: NakshaFeature): TagMap? = getTagMap(feature) + fun set(feature: NakshaFeature, value: TagMap): Any? = setPath(feature, path, value) +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TupleNumberMember.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TupleNumberMember.kt new file mode 100644 index 000000000..b5fd30a1e --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TupleNumberMember.kt @@ -0,0 +1,34 @@ +package naksha.model.objects + +import naksha.model.illegalArg +import naksha.model.illegalState +import naksha.model.objects.MemberType.MemberType_C.TUPLE_NUMBER +import kotlin.js.JsName + +class TupleNumberMember() : TypedMember() { + override fun verify(): TupleNumberMember { + if (dataType != TUPLE_NUMBER) { + throw illegalState("The member was illegally cast, expected subtype: $TUPLE_NUMBER, found: $dataType") + } + return this + } + + @JsName("of") + constructor(name: String, path: JsonPath? = null) : this() { + this.name = name + this.dataType = TUPLE_NUMBER + this.path = path ?: JsonPath(listOf("properties", name)) + this.path.validate() + } + + @JsName("from") + constructor(member: Member, path: JsonPath? = null) : this() { + if (member.dataType != TUPLE_NUMBER) throw illegalArg("The given member is not of tuple_number type") + this.name = member.name + this.dataType = TUPLE_NUMBER + this.path = path?.validate() ?: member.path + } + + fun get(feature: NakshaFeature): TupleNumber? = getTupleNumber(feature) + fun set(feature: NakshaFeature, value: TupleNumber): Any? = setPath(feature, path, value) +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TypedMember.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TypedMember.kt new file mode 100644 index 000000000..fa657df88 --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TypedMember.kt @@ -0,0 +1,14 @@ +package naksha.model.objects + +/** + * Marker class of typed members. + * @since 3.0 + */ +abstract class TypedMember> : Member() { + /** + * Tests if the underlying map has the correct type. + * @return this. + * @throws naksha.model.NakshaException with [ILLEGAL_STATE][naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_STATE], if the type was illegally cast. + */ + abstract fun verify(): T +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadCollections.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadCollections.kt index 08edadd9e..5dea0963d 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadCollections.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadCollections.kt @@ -81,7 +81,7 @@ open class ReadCollections : ReadRequest() { fun toReadFeatures(): ReadFeatures { val req = ReadFeatures() req.catalogId = mapId - req.collectionIds.add(Naksha.COLLECTIONS_COL_ID) + req.collectionId.add(Naksha.COLLECTIONS_COL_ID) req.featureIds.addAll(collectionIds) return req } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadFeatures.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadFeatures.kt index 5c7443138..35b9598ca 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadFeatures.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadFeatures.kt @@ -7,6 +7,7 @@ import naksha.base.NullableProperty import naksha.base.StringList import naksha.model.GuidList import naksha.model.Version +import naksha.model.request.query.IMemberQuery import naksha.model.request.query.IPropertyQuery import naksha.model.request.query.ITagQuery import kotlin.js.JsExport @@ -23,17 +24,17 @@ open class ReadFeatures : ReadRequest() { companion object ReadFeatures_C { private val STRING_OR_NULL = NullableProperty(String::class) + private val STRING_LIST = NotNullProperty(StringList::class) { _, _ -> StringList() } private val BOOLEAN_OR_FALSE = NotNullProperty(Boolean::class) { _, _ -> false } private val INT_OR_1 = NotNullProperty(Int::class) { _, _ -> 1 } private val VERSION_OR_NULL = NullableProperty(Version::class) - private val STRING_LIST = NotNullProperty(StringList::class) private val ORDER_BY_OR_NULL = NullableProperty(OrderBy::class) private val GUID_LIST = NotNullProperty(GuidList::class) private val QUERY = NotNullProperty(RequestQuery::class) } /** - * The id of the map from which to read. + * The id of the catalog from which to read. * * @since 3.0 */ @@ -42,7 +43,7 @@ open class ReadFeatures : ReadRequest() { /** * @see [catalogId] */ - open fun withMapId(value: String?): ReadFeatures { + open fun withCatalogId(value: String?): ReadFeatures { catalogId = value return this } @@ -56,6 +57,7 @@ open class ReadFeatures : ReadRequest() { * * @since 3.0 */ + @Deprecated("Replaced with memberQuery", replaceWith = ReplaceWith("memberQuery")) open fun withPropertyQuery(pQuery: IPropertyQuery?): ReadFeatures { this.query.properties = pQuery this.resultFilters.removeAll { it is PropertyFilter } @@ -72,6 +74,7 @@ open class ReadFeatures : ReadRequest() { * This method comes handy if [IPropertyQuery] was mutated outside of this class scope, * in such cases we need to populate the filter once again so it will be in sync with the query */ + @Deprecated("Replaced with memberQuery", replaceWith = ReplaceWith("memberQuery")) fun refreshPropertyFilter() { this.resultFilters.removeAll { it is PropertyFilter } if(query.properties != null) { @@ -88,6 +91,7 @@ open class ReadFeatures : ReadRequest() { * * @since 3.0 */ + @Deprecated("Replaced with memberQuery", replaceWith = ReplaceWith("memberQuery")) open fun withTagQuery(tQuery: ITagQuery?): ReadFeatures { this.query.tags = tQuery return this @@ -97,31 +101,16 @@ open class ReadFeatures : ReadRequest() { * Ids of collections to read. * @since 3.0 */ - var collectionIds by STRING_LIST + var collectionId: String? by STRING_OR_NULL /** - * Adds the given collection-id into [collectionIds], if it is not already in it. - * @param collectionId the collection-id to add. + * Sets the collection-id into [collectionId]. + * @param collectionId the collection-id to set. * @return this. * @since 3.0 */ - open fun addCollectionId(collectionId: String?): ReadFeatures { - if (!collectionIds.contains(collectionId)) collectionIds.add(collectionId) - return this - } - - /** - * Adds the given collection-ids into [collectionIds], if it is not already in it. - * @param collectionIds the collection-ids to add. - * @return this. - * @since 3.0 - */ - open fun addCollectionIds(vararg collectionIds: String): ReadFeatures { - val ids = this.collectionIds - @Suppress("SENSELESS_COMPARISON") - if (collectionIds != null && collectionIds.isNotEmpty()) { - for (id in collectionIds) if (!ids.contains(id)) ids.add(id) - } + open fun withCollectionId(collectionId: String?): ReadFeatures { + this.collectionId = collectionId return this } @@ -190,10 +179,9 @@ open class ReadFeatures : ReadRequest() { /** * Add all features that match the given IDs into the result-set. - * - * If more complex queries are need, please use a [MemberQuery][naksha.model.request.query.MemberQuery], see [query]. * @since 3.0.0 */ + @Deprecated("Replaced with memberQuery", replaceWith = ReplaceWith("memberQuery")) var featureIds: StringList by STRING_LIST /** @@ -210,8 +198,21 @@ open class ReadFeatures : ReadRequest() { * Add all features that match the given query into the result-set. * @since 3.0.0 */ + @Deprecated("Replaced with memberQuery", replaceWith = ReplaceWith("memberQuery")) var query: RequestQuery by QUERY + /** + * Search for [members][naksha.model.objects.Member]s. + * + * This method now supports to search for all custom defined members. It allows arbitrary combination. + * @since 3.0 + */ + var memberQuery: IMemberQuery? + get() = query.members + set(value) { + query.members = value + } + /** * Tests whether this request is effectively a query for all features in their current **HEAD** state, * i.e. it has no conditions, requests only one state per feature, and does not extend into deleted diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadMaps.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadMaps.kt index ed7aeef91..91f0721d3 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadMaps.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadMaps.kt @@ -72,7 +72,7 @@ open class ReadMaps() : ReadRequest() { fun toReadFeatures(): ReadFeatures { val req = ReadFeatures() req.catalogId = Naksha.ADMIN_CATALOG_ID - req.collectionIds.add(Naksha.CATALOGS_COL_ID) + req.collectionId.add(Naksha.CATALOGS_COL_ID) req.featureIds.addAll(mapIds) return req } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadTransactions.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadTransactions.kt index 2713fd2d7..e1b2cf81c 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadTransactions.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadTransactions.kt @@ -14,7 +14,7 @@ import kotlin.js.JsExport open class ReadTransactions : ReadFeatures() { init { catalogId = Naksha.ADMIN_CATALOG_ID - collectionIds.add(Naksha.TRANSACTIONS_COL_ID) + collectionId.add(Naksha.TRANSACTIONS_COL_ID) } /** diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/AnyOp.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/AnyOp.kt index 87892d6c1..e205cd68a 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/AnyOp.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/AnyOp.kt @@ -26,8 +26,16 @@ open class AnyOp : JsEnum() { } companion object QOpCompanion { + // TODO: Update so that it works for members. + // Actually, we need operations that fit to the dedicated member types. + // CONTAINS_KEY @ Tags + // CONTAINS_ENTRY @ Tags + // CONTAINS @ Set + // IS_ANY_OF @ Primitives, like Int, Long, Double, String + // ... + /** - * Tests if the property exists. + * Tests if the field exists. * @since 3.0.0 */ @JsStatic diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/IMemberQuery.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/IMemberQuery.kt index f1c0ffe6f..c10c7315f 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/IMemberQuery.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/IMemberQuery.kt @@ -12,4 +12,4 @@ import kotlin.js.JsExport * @see MemberQuery */ @JsExport -interface IMemberQuery : IQuery \ No newline at end of file +interface IMemberQuery : IQuery diff --git a/here-naksha-lib-model/src/jvmMain/java/naksha/model/util/RequestHelper.java b/here-naksha-lib-model/src/jvmMain/java/naksha/model/util/RequestHelper.java index 47911a9ff..c7367fcbb 100644 --- a/here-naksha-lib-model/src/jvmMain/java/naksha/model/util/RequestHelper.java +++ b/here-naksha-lib-model/src/jvmMain/java/naksha/model/util/RequestHelper.java @@ -44,7 +44,7 @@ private RequestHelper() { final @Nullable String collectionName, final @NotNull String featureId ) { - final ReadFeatures readFeatures = new ReadFeatures().addCollectionId(collectionName); + final ReadFeatures readFeatures = new ReadFeatures().withCollectionId(collectionName); readFeatures.setCatalogId(mapId); readFeatures.getFeatureIds().add(featureId); return readFeatures; @@ -62,7 +62,7 @@ private RequestHelper() { final @Nullable String collectionName, final @NotNull List featureIds ) { - final ReadFeatures readFeatures = new ReadFeatures().addCollectionId(collectionName); + final ReadFeatures readFeatures = new ReadFeatures().withCollectionId(collectionName); readFeatures.setCatalogId(mapId); readFeatures.getFeatureIds().addAll(featureIds); return readFeatures; diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumn.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumn.kt index 7c50db741..45be17569 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumn.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumn.kt @@ -148,5 +148,6 @@ data class PgColumn( val NEXT_VERSION = PgColumn(2, NextVersion.name, INT64, "STORAGE $PLAIN NOT NULL") } + /** Returns the [ident] of the column, so the quoted [name]. */ override fun toString(): String = ident } \ No newline at end of file diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryBuilder.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryBuilder.kt index b5b698bd4..ea104ebe9 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryBuilder.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryBuilder.kt @@ -1,6 +1,7 @@ package naksha.psql import naksha.model.* +import naksha.model.Naksha.NakshaCompanion.HARD_TUPLE_LIMIT import naksha.model.request.* import naksha.model.request.query.SortOrder.SortOrderCompanion.ASCENDING import kotlin.math.max @@ -31,18 +32,13 @@ class PgQueryBuilder(val session: PgSession, val readRequest: ReadRequest) { private fun readFeatures(req: ReadFeatures): PgQuery { // Collect needed data val pgStorage = session.storage - val catalogId = req.catalogId ?: throw illegalArg("catalogId is missing") + val catalogId = req.catalogId ?: throw illegalArg("Request has not 'catalogId'") val pgCatalog = session.getPgCatalogById(catalogId) ?: throw mapNotFound("Catalog with id '$catalogId' does not exist") - val REQ_LIMIT = min(max(0, req.limit ?: Naksha.HARD_TUPLE_LIMIT), session.storage.hardCap) - if (REQ_LIMIT == 0) throw illegalArg("Invalid limit given: ${req.limit}, must be 0 to 16777216") - val pgCollections: MutableList = mutableListOf() - for (collectionId in req.collectionIds) { - if (collectionId == null) continue - val pgCollection = session.getPgCollectionById(pgCatalog, collectionId) ?: - throw collectionNotFound("Collection with id '$collectionId' not found in map '$catalogId'") - pgCollections.add(pgCollection) - } - if (pgCollections.isEmpty()) throw illegalArg("Empty collection-ids in request") + val REQ_LIMIT = min(max(0, req.limit ?: HARD_TUPLE_LIMIT), session.storage.hardCap) + if (REQ_LIMIT == 0) throw illegalArg("Invalid limit given: ${req.limit}, must be 0 to $HARD_TUPLE_LIMIT") + val collectionId: String = req.collectionId ?: throw illegalArg("Request has no 'collectionId'") + val pgCollection = session.getPgCollectionById(pgCatalog, collectionId) ?: + throw collectionNotFound("Collection with id '$collectionId' not found in catalog '$catalogId'") val version = req.version val minVersion = req.minVersion val versions = req.versions diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt index f7b0a5641..3cd5ffbb7 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt @@ -8,29 +8,34 @@ import naksha.base.StringList import naksha.geo.HereTile import naksha.geo.SpGeometry import naksha.model.* +import naksha.model.objects.Member import naksha.model.objects.StandardMembers import naksha.model.request.ReadFeatures import naksha.model.request.query.* import naksha.psql.PgColumn.PgColumn_C.FN +import naksha.psql.PgColumn.PgColumn_C.VERSION /** * Helper to convert a [ReadFeatures] request into a sql `WHERE` query. * * The collection need to be provided, because they potentially _(when not cached)_ need to be read from the database, therefore, they require a session. We do not want to link this code to a session _(it would break unit testing)_, so we simply expect the collections as input parameter. * @param request the request to wrap. - * @param collections the collections for which to generate the `WHERE` queries. + * @param collection the collection for which to generate the `WHERE` query. * @since 3.0 * @see [build] */ -internal class PgQueryWhereBuilder(private val request: ReadFeatures, private val collections: List) { - private val queries: MutableMap = mutableMapOf() +internal class PgQueryWhereBuilder(private val request: ReadFeatures, private val collection: PgCollection) { + + val where = StringBuilder() + val argValues: MutableList = mutableListOf() + val argTypes: MutableList = mutableListOf() /** * Convert the request into `WHERE` queries. * @return the [PgQueryWhereClause] for each collection given. * @since 3.0 */ - fun build(): Map { + fun build(): PgQueryWhereClause { whereFeatureId() whereGuids() whereVersion() @@ -38,16 +43,7 @@ internal class PgQueryWhereBuilder(private val request: ReadFeatures, private va whereSpatial() whereRefTiles() whereTags() - return queries - } - - private fun queryOf(collection: PgCollection): PgQueryWhereClause { - var query = queries[collection] - if (query == null) { - query = PgQueryWhereClause(collection) - queries[collection] = query - } - return query + return PgQueryWhereClause(collection, where.toString(), argValues, argTypes) } private fun whereFeatureId() { @@ -67,61 +63,51 @@ internal class PgQueryWhereBuilder(private val request: ReadFeatures, private va if (featureNumbers.isEmpty() && featureIds.isEmpty()) return // For each collection: - for (collection in collections) { - val query: PgQueryWhereClause = queryOf(collection) - val where: StringBuilder = query.where - if (where.isNotEmpty()) where.append(" AND ") - - where.append("( ") - if (featureIds.isNotEmpty()) { - val placeholder: String = placeholderForArg(featureIds.toTypedArray(), PgType.STRING_ARRAY) - val ID = collection.column(StandardMembers.Id) ?: throw illegalArg("Collection does not defined `id` column") - where.append(ID.ident).append(" = ANY(").append(placeholder).append(")") - } - if (featureNumbers.isNotEmpty()) { - if (featureIds.isNotEmpty()) where.append(" OR ") + if (where.isNotEmpty()) where.append(" AND ") - val placeholder: String = placeholderForArg(featureNumbers.toTypedArray(), PgType.INT64_ARRAY) - if (where.isNotEmpty()) where.append(" AND ") - where.append(FN.ident).append(" = ANY(").append(placeholder).append(")") + where.append("( ") + if (featureIds.isNotEmpty()) { + val placeholder: String = placeholderForArg(featureIds.toTypedArray(), PgType.STRING_ARRAY) + val ID = collection.column(StandardMembers.Id) ?: throw illegalArg("Collection does not defined `id` column") + where.append(ID.ident).append(" = ANY(").append(placeholder).append(")") + } + if (featureNumbers.isNotEmpty()) { + if (featureIds.isNotEmpty()) where.append(" OR ") - conditions += "${PgColumn.fn} = ANY($placeholder)" - } - if (conditions.isNotEmpty()) { - if (query.isNotEmpty()) query.append(" AND ") - if (conditions.size == 1) query.append(conditions[0]) - else query.append("(${conditions.joinToString(" OR ")})") - } - where.append(")") + val placeholder: String = placeholderForArg(featureNumbers.toTypedArray(), PgType.INT64_ARRAY) + if (where.isNotEmpty()) where.append(" AND ") + where.append(FN.ident).append(" = ANY(").append(placeholder).append(")") } + where.append(")") } private fun whereGuids() { val tupleNumbers = request.guids.mapNotNull { it?.tupleNumber } if (tupleNumbers.isNotEmpty()) { if (where.isNotEmpty()) where.append(" AND ") + val fns = arrayOfNulls(tupleNumbers.size) val versions = arrayOfNulls(tupleNumbers.size) for (i in tupleNumbers.indices) { fns[i] = tupleNumbers[i].featureNumber versions[i] = tupleNumbers[i].version } - val fnPlaceholder = placeholderForArg(fns, PgType.INT64_ARRAY) - val versionPlaceholder = placeholderForArg(versions, PgType.INT64_ARRAY) - where.append("(${PgColumn.fn}, ${PgColumn.version}) IN (SELECT * FROM unnest($fnPlaceholder::int8[], $versionPlaceholder::int8[]))") + val featureNumbersArg = placeholderForArg(fns, PgType.INT64_ARRAY) + val versionsArg = placeholderForArg(versions, PgType.INT64_ARRAY) + where.append("($FN, $VERSION) IN (SELECT * FROM unnest($featureNumbersArg::int8[], $versionsArg::int8[]))") } } private fun whereVersion() { - val txn = request.version - if (txn != null) { + val version = request.version + if (version != null) { if (where.isNotEmpty()) where.append(" AND ") - where.append("${PgColumn.version} <= ${txn.number}") + where.append("$VERSION <= ${version.number}") } - val min_txn = request.minVersion - if (min_txn != null) { + val minVersion = request.minVersion + if (minVersion != null) { if (where.isNotEmpty()) where.append(" AND ") - where.append("${PgColumn.version} >= ${min_txn.number}") + where.append("$VERSION >= ${minVersion.number}") } } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereClause.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereClause.kt index 8fc7444f9..9d0293296 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereClause.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereClause.kt @@ -1,29 +1,38 @@ package naksha.psql +import kotlin.jvm.JvmField + /** * A SQL `WHERE` query. * @param collection the collection for which this `WHERE` query applies. + * @property collection the collection for which this `WHERE` query applies. * @since 3.0 */ -internal data class PgQueryWhereClause(val collection: PgCollection) { +internal data class PgQueryWhereClause( + @JvmField + val collection: PgCollection, + /** * The `WHERE` query, without the keyword `WHERE` or an empty string, if an empty query (query without conditions). * @since 3.0 */ - val where = StringBuilder() + @JvmField + val where: String, /** * The arguments to used with the WHERE in order. * @since 3.0 */ - val argValues: MutableList = mutableListOf() + @JvmField + val argValues: List, /** * The types of the arguments. * @since 3.0 */ - val argTypes: MutableList = mutableListOf() - + @JvmField + val argTypes: List +) { /** * Returns the [argTypes] as typed-array _(`Array`)_. * @since 3.0 @@ -31,5 +40,5 @@ internal data class PgQueryWhereClause(val collection: PgCollection) { val argTypeNames: Array get() = argTypes.map(PgType::toString).toTypedArray() - override fun toString(): String = where.toString() + override fun toString(): String = where } \ No newline at end of file diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ChainCollectionTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ChainCollectionTest.kt index 06e138623..eed809654 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ChainCollectionTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ChainCollectionTest.kt @@ -96,7 +96,7 @@ class ChainCollectionTest : PgTestBase( // When: reading all three back by their numeric IDs in one request val response = executeRead(ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id featureIds += headFn.toString() featureIds += midFn.toString() featureIds += tailFn.toString() @@ -188,7 +188,7 @@ class ChainCollectionTest : PgTestBase( // When: reading all features from this collection (no ID filter) val all = executeRead(ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id }) // Then: find the feature whose right_fn == tailFn (that must be mid) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/CollectionTests.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/CollectionTests.kt index aad8a0e44..b2f0b5a6f 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/CollectionTests.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/CollectionTests.kt @@ -53,7 +53,7 @@ class CollectionTests : PgTestBase(collection = null, mapId = "") { // Then: this collection is queryable and empty val readAllFromCollection = ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id } val collectionContent = executeRead(readAllFromCollection) assertEquals(0, collectionContent.features.size) @@ -61,7 +61,7 @@ class CollectionTests : PgTestBase(collection = null, mapId = "") { // And: Virtual Collections contain the created collection val selectCollectionFromVirt = ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += Naksha.COLLECTIONS_COL_ID + collectionId += Naksha.COLLECTIONS_COL_ID featureIds += collection.id } val virtBeforeDelete = executeRead(selectCollectionFromVirt) @@ -208,7 +208,7 @@ class CollectionTests : PgTestBase(collection = null, mapId = "") { val readFeatureRequest = ReadFeatures() readFeatureRequest.catalogId = map.id - readFeatureRequest.collectionIds.add(collectionName) + readFeatureRequest.collectionId.add(collectionName) readFeatureRequest.featureIds.add(feature.id) val readFeaturesResponse = executeRead(readFeatureRequest) assertEquals(1, readFeaturesResponse.features.size) @@ -281,7 +281,7 @@ class CollectionTests : PgTestBase(collection = null, mapId = "") { val readFeature = ReadFeatures() readFeature.catalogId = map.id - readFeature.collectionIds.add(collectionId) + readFeature.collectionId.add(collectionId) readFeature.featureIds.add(feature.id) val readFeatureResponse = executeRead(readFeature) assertEquals(1, readFeatureResponse.features.size) @@ -331,7 +331,7 @@ class CollectionTests : PgTestBase(collection = null, mapId = "") { assertEquals(StoreMode.SUSPEND, responseCollection.storeDeleted) val selectCollectionFromVirt = ReadFeatures().apply { catalogId = map.id - collectionIds += Naksha.COLLECTIONS_COL_ID + collectionId += Naksha.COLLECTIONS_COL_ID featureIds += collection.id } val colRead = assertNotNull(executeRead(selectCollectionFromVirt).features[0]).proxy(NakshaCollection::class) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/DeleteFeatureBase.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/DeleteFeatureBase.kt index f1ef0bbc7..c2f3c54e5 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/DeleteFeatureBase.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/DeleteFeatureBase.kt @@ -50,7 +50,7 @@ abstract class DeleteFeatureBase( Naksha.cache.clear() executeRead(ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id featureIds += initialFeature.id }).let { // this = SuccessResponse val features = assertNotNull(it.features) @@ -61,7 +61,7 @@ abstract class DeleteFeatureBase( // The tombstone is in HEAD and is NOT included unless queryDeleted=true is also set. executeRead(ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id featureIds += initialFeature.id queryHistory = true versions = 10 @@ -74,7 +74,7 @@ abstract class DeleteFeatureBase( // verify if delete table contains element executeRead(ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id featureIds += initialFeature.id queryDeleted = true }).apply { // this = SuccessResponse @@ -137,7 +137,7 @@ abstract class DeleteFeatureBase( Naksha.cache.clear() executeRead(ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id featureIds += featureId queryDeleted = true }).apply { diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/HistoryUuidTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/HistoryUuidTest.kt index 40b0361ef..91a897a64 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/HistoryUuidTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/HistoryUuidTest.kt @@ -41,7 +41,7 @@ class HistoryUuidTest: PgTestBase(NakshaCollection( Naksha.cache.clear() val featureVersions = executeRead(ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id featureIds += feature.id queryHistory = true queryDeleted = true @@ -88,7 +88,7 @@ class HistoryUuidTest: PgTestBase(NakshaCollection( Naksha.cache.clear() val featureVersions = executeRead(ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id featureIds += feature.id queryHistory = true queryDeleted = true diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/InsertFeatureTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/InsertFeatureTest.kt index 0b134e207..093a135c3 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/InsertFeatureTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/InsertFeatureTest.kt @@ -31,7 +31,7 @@ class InsertFeatureTest : PgTestBase() { // And: reading all features from collection val readResponse = executeRead(ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id featureIds += featureToCreate.id }) val retrievedFeatures = readResponse.features @@ -105,7 +105,7 @@ class InsertFeatureTest : PgTestBase() { // And: reading all features from collection val readResponse = executeRead(ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id featureIds += featureToCreate.id }) val retrievedFeatures = readResponse.features @@ -149,7 +149,7 @@ class InsertFeatureTest : PgTestBase() { // And: reading all features from collection val readResponse = executeRead(ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id featureIds += featureToCreate.id }) val retrievedFeatures = readResponse.features @@ -194,7 +194,7 @@ class InsertFeatureTest : PgTestBase() { // And: reading all features from collection val readResponse = executeRead(ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id // this.version = version // this.minVersion = version }) @@ -239,7 +239,7 @@ class InsertFeatureTest : PgTestBase() { Naksha.cache.clear(storage) val featuresByIdResponse = executeRead(ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id featureIds.add(firstFeatureToCreate.id) }) @@ -259,7 +259,7 @@ class InsertFeatureTest : PgTestBase() { // Read only one feature by bounding box. val featuresByBBox = executeRead(ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id query.spatial = SpIntersects(SpBoundingBox(firstFeatureToCreate.geometry).addMargin(0.0000001).toPolygon()) }) @@ -285,7 +285,7 @@ class InsertFeatureTest : PgTestBase() { // When: reading both features in a single request with mixed IDs val readResponse = executeRead(ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id featureIds += numericId featureIds += namedFeature.id }) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/PartitioningTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/PartitioningTest.kt index 5b73770e4..8aa54cb7d 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/PartitioningTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/PartitioningTest.kt @@ -90,7 +90,7 @@ class PartitioningTest : PgTestBase() { // also - should be able to read val readRequest = ReadFeatures() readRequest.catalogId = partitionedCollection.catalogId - readRequest.collectionIds.add(partitionedCollection.id) + readRequest.collectionId.add(partitionedCollection.id) readRequest.featureIds.add("f1") val readResponse = executeRead(readRequest) assertEquals(1, readResponse.features.size) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/PgPropertyFilterTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/PgPropertyFilterTest.kt index f6d10ce33..be8dac057 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/PgPropertyFilterTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/PgPropertyFilterTest.kt @@ -34,7 +34,7 @@ class PgPropertyFilterTest: PgTestBase() { // And: A read request is created with the property query. val readRequest = ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id }.withPropertyQuery(pQuery) // When: read request is executed val response = executeRead(readRequest) @@ -66,7 +66,7 @@ class PgPropertyFilterTest: PgTestBase() { // And: A read request is made with the custom filter manually added. val readRequest = ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id resultFilters.add(IdContainsFilter("keep_this")) } // When: read request is executed @@ -124,7 +124,7 @@ class PgPropertyFilterTest: PgTestBase() { // Given: A read request that is designed to fail by targeting a non-existent collection. val readRequest = ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += "non_existent_collection" + collectionId += "non_existent_collection" } // When: The failing request is executed. @@ -146,7 +146,7 @@ class PgPropertyFilterTest: PgTestBase() { // When: A read request is made to fetch that specific feature by ID, with no result filters. val readRequest = ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id featureIds += feature.id } val response = storage.newReadSession(newSessionOptions()).use { session -> diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesAll.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesAll.kt index 1c7385b6c..0f45474e5 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesAll.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesAll.kt @@ -36,7 +36,7 @@ class ReadFeaturesAll : PgTestBase() { fun shouldReturnAllFeatures() { executeRead(ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id }).apply { assertEquals(COUNT, features.size) for (feature in features) { diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByGeometryTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByGeometryTest.kt index 57550e3c1..aa128e9bd 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByGeometryTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByGeometryTest.kt @@ -34,7 +34,7 @@ class ReadFeaturesByGeometryTest : PgTestBase(collection = null, mapId = "") { val retrievedFeatures = executeRead( ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id featureIds += feature.id } ).features @@ -67,7 +67,7 @@ class ReadFeaturesByGeometryTest : PgTestBase(collection = null, mapId = "") { val retrievedFeatures = executeRead( ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id featureIds += feature.id } ).features @@ -252,7 +252,7 @@ class ReadFeaturesByGeometryTest : PgTestBase(collection = null, mapId = "") { private fun executeSpatialQuery(spatialQuery: ISpatialQuery): SuccessResponse { return executeRead(ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id query.spatial = spatialQuery }) } diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByGuuidTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByGuuidTest.kt index 1bcad5a02..08444df28 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByGuuidTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByGuuidTest.kt @@ -26,7 +26,7 @@ class ReadFeaturesByGuuidTest : // When val readByGuid = ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id guids = GuidList().apply { add(guuidById[inputFeature1.id]) add(guuidById[inputFeature3.id]) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByMetadataTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByMetadataTest.kt index 2de5ee854..4512363a8 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByMetadataTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByMetadataTest.kt @@ -234,7 +234,7 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { // And: execute val featuresByAppIdAndAuthor = executeRead(ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id query.members = MemberAnd( MemberQuery(MetaColumn.author(), StringOp.EQUALS, author), MemberQuery(MetaColumn.appId(), StringOp.STARTS_WITH, appId.substring(0, 2)) @@ -392,7 +392,7 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { // And: execute val featuresByAppIdAndAuthor = executeRead(ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id query.members = MemberOr( MemberQuery(MetaColumn.author(), StringOp.EQUALS, "this_is_totally_off"), MemberQuery(MetaColumn.appId(), StringOp.STARTS_WITH, appId.substring(0, 2)) @@ -422,7 +422,7 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { // And: History table is queried for everything besides CREATED val getHistoryWithoutUpdates = ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id queryHistory = true queryDeleted = true query = RequestQuery().apply { @@ -442,7 +442,7 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { insertFeature(feature = feature) val persistedFeatureResponse = executeRead(ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id featureIds += feature.id }) val persistedFeatures = persistedFeatureResponse.features @@ -455,7 +455,7 @@ class ReadFeaturesByMetadataTest : PgTestBase(collection = null, mapId = "") { private fun executeMetaQuery(metaQuery: IMemberQuery): SuccessResponse { return executeRead(ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id query.members = metaQuery }) } diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByOtherTns.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByOtherTns.kt index 0792eb0f8..efd1f0338 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByOtherTns.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByOtherTns.kt @@ -50,7 +50,7 @@ class ReadFeaturesByOtherTns : PgTestBase( ) val byNextTnResp = executeRead(ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id query.members = nextVersionQuery queryHistory = true }) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByRefTilesTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByRefTilesTest.kt index 1653b7e1e..4c3b74f8d 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByRefTilesTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByRefTilesTest.kt @@ -47,7 +47,7 @@ class ReadFeaturesByRefTilesTest : PgTestBase(collection = null, mapId = "") { // Given: val getFeaturesFromZagrebAndPrague = ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection!!.id + collectionId += collection!!.id query.refTiles += listOf(zagrebTileLv12.intKey, pragueTileLv12.intKey) } @@ -68,7 +68,7 @@ class ReadFeaturesByRefTilesTest : PgTestBase(collection = null, mapId = "") { // Given: val getFeaturesFromBologna = ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id query.refTiles += bolognaTileLv12.intKey } diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByTagsTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByTagsTest.kt index 75424349d..1ee0da5df 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByTagsTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByTagsTest.kt @@ -224,7 +224,7 @@ class ReadFeaturesByTagsTest : PgTestBase() { private fun executeTagsQuery(tagQuery: ITagQuery): SuccessResponse { return executeRead(ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection!!.id + collectionId += collection!!.id query.tags = tagQuery }) } diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadHistoryTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadHistoryTest.kt index 96154e02d..e9c93382e 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadHistoryTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadHistoryTest.kt @@ -82,7 +82,7 @@ class ReadHistoryTest : PgTestBase() { Naksha.cache.clear() executeRead(ReadFeatures().apply { catalogId = collection.catalogId - collectionIds.add(collection.id) + collectionId.add(collection.id) featureIds.add(featureId) queryHistory = true queryDeleted = true @@ -122,7 +122,7 @@ class ReadHistoryTest : PgTestBase() { executeRead(ReadFeatures().apply { catalogId = collection.catalogId - collectionIds.add(collection.id) + collectionId.add(collection.id) featureIds.add(featureId) queryHistory = true queryDeleted = true @@ -144,7 +144,7 @@ class ReadHistoryTest : PgTestBase() { executeRead(ReadFeatures().apply { catalogId = collection.catalogId - collectionIds.add(collection.id) + collectionId.add(collection.id) featureIds.add(featureId) queryHistory = true version = updatedFeature2.guid!!.tupleNumber.version diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadLimitTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadLimitTest.kt index 1654f0442..51f0460a6 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadLimitTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadLimitTest.kt @@ -22,7 +22,7 @@ class ReadLimitTest : PgTestBase() { // When val readWithLimit = executeRead(ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id limit = 2 }) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadOrderedTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadOrderedTest.kt index 5e4f3d67c..8e34ffbd6 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadOrderedTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadOrderedTest.kt @@ -49,7 +49,7 @@ class ReadOrderedTest : PgTestBase() { fun searchOrderedById() { executeRead(ReadFeatures().apply { catalogId = TEST_MAP_ID - collectionIds += collection.id + collectionId += collection.id orderBy = OrderBy.id() limit = ORDER_BY_ID_LIMIT }).apply { @@ -63,7 +63,7 @@ class ReadOrderedTest : PgTestBase() { executeRead(ReadFeatures().apply { catalogId = TEST_MAP_ID - collectionIds += collection.id + collectionId += collection.id orderBy = OrderBy(MetaColumn.id(), order = SortOrder.ASCENDING) limit = ORDER_BY_ID_LIMIT }).apply { diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/RecreateAfterDeleteTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/RecreateAfterDeleteTest.kt index bca9eee30..1b1486281 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/RecreateAfterDeleteTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/RecreateAfterDeleteTest.kt @@ -41,7 +41,7 @@ class RecreateAfterDeleteTest : PgTestBase() { Naksha.cache.clear() val updated = executeRead(ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id featureIds += featureId }).features.first()!! assertEquals(featureId, updated.id) @@ -55,7 +55,7 @@ class RecreateAfterDeleteTest : PgTestBase() { Naksha.cache.clear() val deleted = executeRead(ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id featureIds += featureId queryDeleted = true }).features.first()!! @@ -66,7 +66,7 @@ class RecreateAfterDeleteTest : PgTestBase() { Naksha.cache.clear() val notFound = executeRead(ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id featureIds += featureId }) assertEquals(0, notFound.features.size) @@ -86,7 +86,7 @@ class RecreateAfterDeleteTest : PgTestBase() { Naksha.cache.clear() val found = executeRead(ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id featureIds += featureId }) assertEquals(1, found.features.size) @@ -97,7 +97,7 @@ class RecreateAfterDeleteTest : PgTestBase() { // Total = 1 (HEAD) + 3 (history) = 4, in descending version order. val historyOnly = executeRead(ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id featureIds += featureId queryHistory = true versions = 10 @@ -112,7 +112,7 @@ class RecreateAfterDeleteTest : PgTestBase() { // so queryDeleted=true adds nothing here. val full = executeRead(ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id featureIds += featureId queryHistory = true queryDeleted = true diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/UpdateFeatureTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/UpdateFeatureTest.kt index a735f815e..4f6bbb5bc 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/UpdateFeatureTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/UpdateFeatureTest.kt @@ -94,7 +94,7 @@ class UpdateFeatureTest : PgTestBase(collection = null, mapId = "") { Naksha.cache.clear() val readResp = executeRead(ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id featureIds += initialFeature.id queryHistory = true }) @@ -261,7 +261,7 @@ class UpdateFeatureTest : PgTestBase(collection = null, mapId = "") { Naksha.cache.clear() val readFeatureResp = executeRead(ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id featureIds += id }) assertEquals(1, readFeatureResp.length) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/UpsertFeatureTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/UpsertFeatureTest.kt index 3aefa9df6..5773a6e9e 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/UpsertFeatureTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/UpsertFeatureTest.kt @@ -36,7 +36,7 @@ class UpsertFeatureTest : PgTestBase() { // And: Retrieving feature by id val retrievedFeatures = executeRead(ReadFeatures().apply { catalogId = collection.catalogId - collectionIds += collection.id + collectionId += collection.id featureIds += initialFeature.id queryHistory = true }).features.sortedBy { it!!.properties.xyz.version!!.number.toLong() } diff --git a/here-naksha-lib-psql/src/jvmTest/java/naksha/psql/DeleteFeatureByVersionTest.java b/here-naksha-lib-psql/src/jvmTest/java/naksha/psql/DeleteFeatureByVersionTest.java index 10fc66f32..957d40641 100644 --- a/here-naksha-lib-psql/src/jvmTest/java/naksha/psql/DeleteFeatureByVersionTest.java +++ b/here-naksha-lib-psql/src/jvmTest/java/naksha/psql/DeleteFeatureByVersionTest.java @@ -151,8 +151,8 @@ void shouldFailEntireRequestIfPartialDeletionFails() { private NakshaFeatureList getFeatureByIds(String... ids) { ReadFeatures readAll = new ReadFeatures() - .withMapId(getCollection().getCatalogId()) - .addCollectionId(getCollection().getId()); + .withCatalogId(getCollection().getCatalogId()) + .withCollectionId(getCollection().getId()); readAll.setFeatureIds(StringList.of(ids)); return executeRead(readAll, newSessionOptions()).getFeatures(); } diff --git a/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/ViewWriteSession.java b/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/ViewWriteSession.java index 158a68bea..78cc3f6d2 100644 --- a/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/ViewWriteSession.java +++ b/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/ViewWriteSession.java @@ -64,7 +64,7 @@ public ViewWriteSession withWriteLayer(ViewLayer viewLayer) { } else if (request instanceof ReadFeatures) { final ReadFeatures readFeatures = (ReadFeatures) request; readFeatures.setCatalogId(writeLayer.getMapId()); - readFeatures.setCollectionIds(new StringList(writeLayer.getCollectionId())); + readFeatures.setCollectionId(new StringList(writeLayer.getCollectionId())); } else { throw new IllegalArgumentException("Unsupported request type: " + request.getClass()); } diff --git a/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/concurrent/LayerReadRequest.java b/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/concurrent/LayerReadRequest.java index f6f97b104..8d44c874d 100644 --- a/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/concurrent/LayerReadRequest.java +++ b/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/concurrent/LayerReadRequest.java @@ -35,7 +35,7 @@ public LayerReadRequest(@NotNull ReadFeatures request, @NotNull ViewLayer viewLa // because the view is always fixed to certain map/collection! this.request = request.copy(false); this.request.setCatalogId(viewLayer.getMapId()); - this.request.setCollectionIds(new StringList(viewLayer.getCollectionId())); + this.request.setCollectionId(new StringList(viewLayer.getCollectionId())); this.viewLayer = viewLayer; this.session = session; } diff --git a/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/concurrent/ParallelQueryExecutor.java b/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/concurrent/ParallelQueryExecutor.java index 101e38eb1..0e60c26f0 100644 --- a/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/concurrent/ParallelQueryExecutor.java +++ b/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/concurrent/ParallelQueryExecutor.java @@ -113,7 +113,7 @@ private Stream executeSingle( final String collectionId = layer.getCollectionId(); final ReadFeatures readRequest = request.copy(false); readRequest.setCatalogId(layer.getMapId()); - readRequest.setCollectionIds(new StringList(collectionId)); + readRequest.setCollectionId(new StringList(collectionId)); final @NotNull Response readResponse = session.execute(readRequest); final FeatureTupleList featureList = getFeatureTuples(readResponse); diff --git a/here-naksha-lib-view/src/jvmTest/java/com/here/naksha/lib/view/ViewTest.java b/here-naksha-lib-view/src/jvmTest/java/com/here/naksha/lib/view/ViewTest.java index fa74d547c..76a59758f 100644 --- a/here-naksha-lib-view/src/jvmTest/java/com/here/naksha/lib/view/ViewTest.java +++ b/here-naksha-lib-view/src/jvmTest/java/com/here/naksha/lib/view/ViewTest.java @@ -352,7 +352,7 @@ void shouldApplyCustomTimeoutsPerLayer() { // And ReadFeatures readFeatures = new ReadFeatures(); readFeatures.setCatalogId(TEST_MAP_ID); - readFeatures.setCollectionIds(new StringList(firstLayer.getCollectionId(), secondLayer.getCollectionId(), thirdLayer.getCollectionId())); + readFeatures.setCollectionId(new StringList(firstLayer.getCollectionId(), secondLayer.getCollectionId(), thirdLayer.getCollectionId())); // When new View(viewLayerCollection).newReadSession(sessionOptions).execute(readFeatures); diff --git a/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/connector/ConnectorInterfaceReadExecute.java b/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/connector/ConnectorInterfaceReadExecute.java index 62d317aa6..39917cf6e 100644 --- a/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/connector/ConnectorInterfaceReadExecute.java +++ b/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/connector/ConnectorInterfaceReadExecute.java @@ -168,7 +168,7 @@ private static Event createFeaturesByTileEvent(ReadFeaturesProxyWrapper readRequ } private static String firstCollectionIdOrThrow(ReadFeaturesProxyWrapper request) { - StringList ids = request.getCollectionIds(); + StringList ids = request.getCollectionId(); if (ids == null || ids.isEmpty()) { throw new NakshaException(NakshaError.ILLEGAL_ARGUMENT, "collectionIds must contain at least one non-empty id"); diff --git a/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/ffw/FfwInterfaceReadExecute.java b/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/ffw/FfwInterfaceReadExecute.java index 7233d6bd7..46db2f3c9 100644 --- a/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/ffw/FfwInterfaceReadExecute.java +++ b/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/ffw/FfwInterfaceReadExecute.java @@ -179,6 +179,6 @@ private static String keysToKeyValuesStrings(ReadFeaturesProxyWrapper readReques } private static String baseEndpoint(ReadFeaturesProxyWrapper request) { - return request.getCollectionIds().get(0); + return request.getCollectionId().get(0); } } From e2ac76920db5aa854e7371a04b1b8f0c67f161cd Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Mon, 22 Jun 2026 09:03:52 +0200 Subject: [PATCH 28/57] Add support for member queries. Signed-off-by: Alexander Lowey-Weber --- .../kotlin/naksha/model/request/ops/And.kt | 29 ++++++++++ .../kotlin/naksha/model/request/ops/Not.kt | 29 ++++++++++ .../kotlin/naksha/model/request/ops/Op.kt | 53 +++++++++++++++++++ .../kotlin/naksha/model/request/ops/OpList.kt | 10 ++++ .../kotlin/naksha/model/request/ops/Or.kt | 29 ++++++++++ 5 files changed, 150 insertions(+) create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/And.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Not.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Op.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/OpList.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Or.kt diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/And.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/And.kt new file mode 100644 index 000000000..9898412b5 --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/And.kt @@ -0,0 +1,29 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model.request.ops + +import naksha.base.NotNullProperty +import kotlin.js.JsExport + +/** + * Logical AND. + * @since 3.0 + */ +@JsExport +class And() : Op() { + companion object And_C { + private val VALUES = NotNullProperty(OpList::class) { _, _ -> OpList() } + } + + constructor(vararg children: Op) : this() { + this.op = AND + val _children = this.children + for (arg in children) _children.add(arg) + } + + /** + * The operation arguments, so the sub-operations to logically AND combine. + * @since 3.0 + */ + var children: OpList by VALUES +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Not.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Not.kt new file mode 100644 index 000000000..cf2042ee2 --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Not.kt @@ -0,0 +1,29 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model.request.ops + +import naksha.base.NotNullProperty +import naksha.base.NullableProperty +import kotlin.js.JsExport + +/** + * Logical NOT. + * @since 3.0 + */ +@JsExport +class Not() : Op() { + companion object Not_C { + private val CHILD = NotNullProperty(Op::class) + } + + constructor(child: Op) : this() { + this.op = NOT + this.child = child + } + + /** + * The operation argument, so the sub-operation to logically NOT. + * @since 3.0 + */ + var child: Op by CHILD +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Op.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Op.kt new file mode 100644 index 000000000..c0b303aea --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Op.kt @@ -0,0 +1,53 @@ +@file:OptIn(ExperimentalJsExport::class, ExperimentalJsStatic::class) + +package naksha.model.request.ops + +import naksha.base.AnyObject +import naksha.base.MapProxy +import naksha.base.NotNullProperty +import naksha.base.NullableProperty +import kotlin.js.ExperimentalJsExport +import kotlin.js.ExperimentalJsStatic +import kotlin.js.JsExport +import kotlin.js.JsStatic +import kotlin.jvm.JvmStatic + +/** + * A member operation. + * @since 3.0 + */ +@JsExport +open class Op : AnyObject() { + companion object MemberOp_C { + private val STRING = NotNullProperty(String::class) + private val STRING_OR_NULL = NullableProperty(String::class) + + const val AND = "and" + const val OR = "or" + const val NOT = "not" + + /** + * Auto-detect the concrete type of member operation and return the cast real type. + * @param op the member-operation to detect the real type. + * @return the real [Op] instance or just `null`, if no real type is known. + */ + @JvmStatic + @JsStatic + fun detect(op: MapProxy<*,*>): Op? = when(op.getRaw("op") as String?) { + AND -> op.proxy(And::class) + OR -> op.proxy(Or::class) + NOT -> op.proxy(Not::class) + else -> null + } + } + + /** + * The operation identifier. + */ + var op: String by STRING + + /** + * The name of the member to query; if any _(some operations do not work upon members)_. + */ + var at: String? by STRING_OR_NULL +} \ No newline at end of file diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/OpList.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/OpList.kt new file mode 100644 index 000000000..080600d11 --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/OpList.kt @@ -0,0 +1,10 @@ +@file:OptIn(ExperimentalJsExport::class) + +package naksha.model.request.ops + +import naksha.base.ListProxy +import kotlin.js.ExperimentalJsExport +import kotlin.js.JsExport + +@JsExport +class OpList : ListProxy(Op::class) \ No newline at end of file diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Or.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Or.kt new file mode 100644 index 000000000..65b81e9c5 --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Or.kt @@ -0,0 +1,29 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model.request.ops + +import naksha.base.NotNullProperty +import kotlin.js.JsExport + +/** + * Logical OR. + * @since 3.0 + */ +@JsExport +class Or() : Op() { + companion object Or_C { + private val VALUES = NotNullProperty(OpList::class) { _, _ -> OpList() } + } + + constructor(vararg children: Op) : this() { + this.op = OR + val _children = this.children + for (arg in children) _children.add(arg) + } + + /** + * The operation arguments, so the sub-operations to logically OR combine. + * @since 3.0 + */ + var children: OpList by VALUES +} \ No newline at end of file From c864b304921ff3464a4d59f4ee6f14e0acd0ba4d Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Mon, 22 Jun 2026 10:07:40 +0200 Subject: [PATCH 29/57] Ensure that we consistently talk about TagList and TagMap. Signed-off-by: Alexander Lowey-Weber --- docs/latest/JBON2.md | 66 +++++++++---------- .../kotlin/naksha/jbon/JbDecoder2.kt | 9 +-- .../kotlin/naksha/jbon/JbEncoder2.kt | 50 +++++++++++++- .../commonMain/kotlin/naksha/jbon/LibJbon2.kt | 9 ++- .../commonMain/kotlin/naksha/model/Naksha.kt | 4 +- .../commonMain/kotlin/naksha/model/TagList.kt | 2 +- .../commonMain/kotlin/naksha/model/TagMap.kt | 2 +- .../kotlin/naksha/model/objects/Index.kt | 2 +- .../kotlin/naksha/model/objects/IndexType.kt | 8 +-- .../kotlin/naksha/model/objects/Member.kt | 6 +- .../kotlin/naksha/model/objects/MemberType.kt | 27 ++++---- .../naksha/model/objects/NakshaCollection.kt | 2 +- .../{SetMember.kt => TagListMember.kt} | 18 ++--- .../kotlin/naksha/model/objects/XyzIndices.kt | 4 +- .../kotlin/naksha/model/objects/XyzMembers.kt | 6 +- .../naksha/model/request/query/TagExists.kt | 2 +- .../naksha/model/request/query/TagQuery.kt | 2 +- .../model/request/query/TagSetContains.kt | 2 +- .../kotlin/naksha/model/MemberTest.kt | 8 +-- .../commonMain/kotlin/naksha/psql/PgIndex.kt | 2 +- .../kotlin/naksha/psql/PgMemberHelper.kt | 14 ++-- .../commonMain/kotlin/naksha/psql/PgType.kt | 4 +- .../commonMain/kotlin/naksha/psql/PgWriter.kt | 2 +- .../kotlin/naksha/psql/CollectionTests.kt | 14 ++-- .../naksha/psql/ReadFeaturesByTagsTest.kt | 2 +- 25 files changed, 160 insertions(+), 107 deletions(-) rename here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/{SetMember.kt => TagListMember.kt} (60%) diff --git a/docs/latest/JBON2.md b/docs/latest/JBON2.md index 3c2abd7d9..e2ad2192c 100644 --- a/docs/latest/JBON2.md +++ b/docs/latest/JBON2.md @@ -28,11 +28,11 @@ As the format name indicates, this format is object-oriented. All **JBON** data - `Primitive`: The following _**units**_ are called _primitives_: `null`, `boolean`, `integer`, `float`, `timestamp`, `string`, and `tuple-number`. - `String`: A special _primitive_ that encodes a list of [UNICODE] code points, optionally including [references] to sub-strings. Strings are split using the [UNICODE] word boundary algorithm from [ICU4J]. - `Array` _(`List`)_: A list of arbitrary _**units**_ with significant order _(changing the order creates a different array)_. -- `Set` _(`List`)_: A list of unique non-null _**primitives**_; the order of the elements is not significant, therefore the encoder will optimize the order by sorting the elements. +- `TagList` _(`List`)_: A list of unique non-null _**primitives**_; the order of the elements is significant, and the list must not have duplicates, null or undefined. - `Object` _(`Map`)_: A list of key-value pairs with all keys being unique non-null _**strings**_; the values can be any _**unit**_. The order of the entries is not significant, therefore the encoder will optimize by sorting the entries by their keys. - `Map` _(`Map`)_: A list of key-value pairs with keys limited to be unique non-null _**primitives**_; values can be any _**unit**_. The order of the entries is not significant, therefore the encoder will optimize by sorting the entries by their keys. - `Dictionary` _(`Map`)_: A list of key-value pairs with keys being unique non-null _**strings**_ and values being non-null _**strings**_. The order of the entries is not significant, therefore the encoder will optimize by sorting the entries by their keys. -- `Tags` _(`Map`)_: A list of key-value pairs with keys being unique non-null _**strings**_, and the values being any _**primitive**_. The order of the entries is not significant, therefore the encoder will optimize by sorting the entries by their keys. +- `TagMap` _(`Map`)_: A list of key-value pairs with keys being unique non-null _**strings**_, and the values being any _**primitive**_. The order of the entries is not significant, therefore the encoder will optimize by sorting the entries by their keys. - `Book` _(`List`)_: An addressable array of _**units**_ loaded into context. - `Tuple`: A special encoding of a _feature_ being an `Object` with some metadata to cooperate with the [Naksha data model]. - `TWKB`: An embedded geometry encoded in [Tiny WKB]. @@ -71,7 +71,7 @@ Whenever data is sorted, the following sort order should be used: - Strings, are converted into their [UTF16 string], then sorted by their UTF-16 code-units, so that the sorting is compatible with JavaScript and Java. - This is not a locale-aware alphabetical sorting. - If a [UTF16 string] is part of the **JBON**, it is treated exactly like a string, except that no conversion is needed. -- All structures (including [Array], [Map], [Set], [Object], [Tags], [Dictionary], [Book], [TupleNumberArray], [Tuple], [TWKB], and [Binary]) sort after all primitives, by their [logical bytes]. +- All structures (including [Array], [Map], [TagList], [Object], [TagMap], [Dictionary], [Book], [TupleNumberArray], [Tuple], [TWKB], and [Binary]) sort after all primitives, by their [logical bytes]. Beware that [references] can't be sorted, they always behave exactly like the value to which they refer. So, when a reference to a [string] is given, the sorting is based on the value of the [string], not on the reference itself. @@ -176,9 +176,9 @@ All _**units**_ start with a **lead-in** byte, which describes the actual type o - ss=3 / `11`: Size is **uint32**, 4 byte unsigned integer size - tttt= 0 / `11ss_0000`: [Array] - tttt= 1 / `11ss_0001`: [Map] - - tttt= 2 / `11ss_0010`: [Set] + - tttt= 2 / `11ss_0010`: [TagList] - tttt= 3 / `11ss_0011`: [Object] - - tttt= 4 / `11ss_0100`: [Tags] + - tttt= 4 / `11ss_0100`: [TagMap] - tttt= 5 / `11ss_0101`: [Dictionary] - tttt= 6 / `11ss_0110`: [Book] - tttt= 7 / `11ss_0111`: [TupleNumberArray] @@ -253,7 +253,7 @@ The following types are indexable: - string - byte[] - tuple-number -- tags +- tagmap ## Timestamp A timestamp, encoded with a **lead-in** byte `0000_1100`. It encodes a unix epoch timestamp (UTC) in milliseconds, stored in big-endian encoding as 7-byte value following the **lead-in**. Therefore, it belongs to the primitives. We choose this encoding, because a year has 31,536,000,000 milliseconds, therefore 36-bit can encode 2 years, 40-bit encode 34 years, 48-bit encode already 8925 years, with 56-bit encoding around 2 million years, more than enough. Reducing the size from full 8 byte to 7 byte, saves one byte per value, but more significant, it allows to read timestamps atomically as a single 64-bit integer, then binary-ANDing with `0x00FF_FFFF_FFFF_FFFF` to get the timestamp in milliseconds. @@ -499,7 +499,7 @@ If it is not empty, then the lowest four bit (`tttt`) encode the type of the str For the `Type` column in the following structure tables the maximum allowed type is used. The **lead-in** is always a single byte of type `byte` with the following size described as `int32`, encoded either as 1 byte, 2 byte or 4 byte unsigned integer in big-endian byte-order. -When `ss=0` _(empty structure)_, the `byte_size` field is omitted; the total size of the structure is exactly 1 byte _(the **lead-in** only)_. All other fields shown in the structure tables below are likewise absent in the empty form. The `ss=00` _(empty form)_ is only meaningful for [Array], [Map], [Set], [Object], [Tags], [Dictionary] and [TupleNumberArray] _(where it represents the empty collection)_. For [Book], [Tuple], [TWKB], [Binary], and [UTF16 String], the `ss=00` form is invalid; decoders **MUST** reject it as malformed, and encoders **MUST NOT** emit it. +When `ss=0` _(empty structure)_, the `byte_size` field is omitted; the total size of the structure is exactly 1 byte _(the **lead-in** only)_. All other fields shown in the structure tables below are likewise absent in the empty form. The `ss=00` _(empty form)_ is only meaningful for [Array], [Map], [TagList], [Object], [TagMap], [Dictionary] and [TupleNumberArray] _(where it represents the empty collection)_. For [Book], [Tuple], [TWKB], [Binary], and [UTF16 String], the `ss=00` form is invalid; decoders **MUST** reject it as malformed, and encoders **MUST NOT** emit it. All other values are variable encoded, for example `int` means any integer, `float` means any floating-point number. Therefore, an `int` with value `0` can be encoded as `int4`, the float `1.0` as `float4`. A question mark _(`?`)_ behind a type means that the value is nullable, so `null` can be stored instead of the actual value. If that is not the case, the value must not be `null`, nor a [reference] to `null` is allowed. Beware that all _**units**_ can always be replaced with a [reference] to relocate the _**unit**_ into a [book]. @@ -588,34 +588,34 @@ For the keys, the [primitive-stringification] is used, if needed. --- -### Set (2) -A set is a special [map] that does not store values, therefore it is a key-only map. The **lead-in** byte is `11ss_0010`; with `ss` encoding the size of the size, as usual. +### TagList (2) +A TagList is a list of unique non-null _**primitives**_; the order of the elements is significant, and the list must not have duplicates, null or undefined. The **lead-in** byte is `11ss_0010`; with `ss` encoding the size of the size, as usual. | Name | Type | Description | |-----------|------------------|-----------------------------------------------------------------------| | lead_in | `byte` | The **lead-in** byte, `11ss_0010`. | | byte_size | `int32` | The total size of the structure, including the **lead-in**, in bytes. | | | | | -| entries | ([primitive])... | The entries of the set. | +| entries | ([primitive])... | The entries of the TagList. | -If `ss=00` _(**lead-in** is `1100_0010`)_, this implies an empty set _(`{"@type":"naksha:set"}`)_. +If `ss=00` _(**lead-in** is `1100_0010`)_, this implies an empty TagList _(`{"@type":"naksha:taglist"}`)_. -The entries in a set are not sorted, the order is significant. The entries must not be `null`, `undefined` or duplicates. +The entries in a TagList are not sorted, the order is significant. The entries must not be `null`, `undefined` or duplicates. #### Logical Bytes -The [logical bytes] of the set are calculated by adding the **lead-in** `1111_0010`, followed by the byte-size as 32-bit BE integer, followed by all `entries` [sorted] in ascending order. The same rules apply while generating the [logical bytes] that apply generally when encoding [logical bytes]. So, `entries` being [references] have to be treated as if they were embedded, so they need to be added to the [logical bytes] the same way that real embedded values are. +The [logical bytes] of the TagList are calculated by adding the **lead-in** `1111_0010`, followed by the byte-size as 32-bit BE integer, followed by all `entries` in their given order. The same rules apply while generating the [logical bytes] that apply generally when encoding [logical bytes]. So, `entries` being [references] have to be treated as if they were embedded, so they need to be added to the [logical bytes] the same way that real embedded values are. #### JSON -The [JSON] serialization is done as object with values being `null`, and with a special type property: +The [JSON] serialization is done as a normal array: ```javascript -var set = { - "@type": "naksha:set", - "entries": [] -} +var tagList = [ + "foo", + "bar" +] ``` -In [JSON] we have no better alternative to encode a set. For the entries, the [primitive-stringification] is used, if needed. +For the entries, the [primitive-stringification] is used, if needed. ### Object (3) An object is a special [map] that only allows strings as keys. The **lead-in** byte is `11ss_0011`; with `ss` encoding the size of the size, as usual. All keys must be [strings]. @@ -645,8 +645,8 @@ However, in conflict case the explicit type name is `naksha:object`, needed only --- -### Tags (4) -The tags are a special [map] that allow only strings as keys and [primitives] as values. The **lead-in** byte is `11ss_0100`; with `ss` encoding the size of the size, as usual. +### TagMap (4) +A TagMap is a map with unique non-null _**string**_ keys and _**primitive**_ values. The **lead-in** byte is `11ss_0100`; with `ss` encoding the size of the size, as usual. | Name | Type | Description | |-----------|-----------------------------|-----------------------------------------------------------------------| @@ -655,20 +655,20 @@ The tags are a special [map] that allow only strings as keys and [primitives] as | | | | | entries | ([string], [primitive]?)... | The key-value pairs. | -If `ss=00` _(**lead-in** is `1100_0100`)_, this implies empty tags _(`{"@type":"naksha:tags"}`)_. +If `ss=00` _(**lead-in** is `1100_0100`)_, this implies an empty TagMap _(`{"@type":"naksha:tagmap"}`)_. -The entries in the maps are always encoded [sorted] ascending by the key. +The entries in the TagMap are always encoded [sorted] ascending by the key. #### Logical Bytes -The [logical bytes] of the tags are created by adding the **lead-in** `1111_0100`, followed by the byte-size as 32-bit BE integer, then all entries in order _(therefore, [sorted] by key, ascending)_. +The [logical bytes] of the TagMap are created by adding the **lead-in** `1111_0100`, followed by the byte-size as 32-bit BE integer, then all entries in order _(therefore, [sorted] by key, ascending)_. #### JSON The [JSON] serialization is as object with special type property: ```javascript -var tags = { - "@type": "naksha:tags", - "tag_name": tag_value +var tagMap = { + "@type": "naksha:tagmap", + "key_name": key_value } ``` @@ -1285,10 +1285,10 @@ public enum JbonUnitType { BOOK, TUPLE_NUMBER, TUPLE, - SET, + TAG_LIST, MAP, DICTIONARY, - TAGS, + TAG_MAP, TWKB, BINARY } @@ -1374,7 +1374,7 @@ public class Jbon { public void leave(int return_address) { /* ... */ } } -// A thread-local JBON decoder that can support IArray, IObject, ISet, IMap, ITuple and ITupleNumber. +// A thread-local JBON decoder that can support IArray, IObject, ITagList, IMap, ITuple and ITupleNumber. public class JbonDecoder { public JbonDecoder(@NotNull Jbon jbon) { this.jbon = jbon; } @@ -1513,7 +1513,7 @@ public final class JbonArray extends JbonStruct implements IArray { public JbonArray(@NotNull JbonUnit unit) { super(unit); } } -public final class JbonSet extends JbonStruct implements ISet { +public final class JbonTagList extends JbonStruct implements ITagList { public JbonStruct(@NotNull JbonUnit unit) { super(unit); } } @@ -1776,13 +1776,13 @@ The following changes are introduced in version 2 of this specification, compare [Map]: #map-1 [map]: #map-1 [maps]: #map-1 -[Set]: #set-2 +[TagList]: #set-2 [set]: #set-2 [sets]: #set-2 [Object]: #object-3 [object]: #object-3 [objects]: #object-3 -[Tags]: #tags-4 +[TagMap]: #tags-4 [tags]: #tags-4 [Dictionary]: #dictionary-5 [dictionary]: #dictionary-5 diff --git a/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/JbDecoder2.kt b/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/JbDecoder2.kt index 0aacf8e79..08fbf3aa0 100644 --- a/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/JbDecoder2.kt +++ b/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/JbDecoder2.kt @@ -11,8 +11,9 @@ import kotlin.jvm.JvmStatic * * This reader is intentionally focused on the subset that [JbEncoder2] produces: the `@JB\x02` * file header, primitives, strings (with string-references resolved against a book), and the - * structures [Array], [Object], [Map], [Dictionary], [Book] and [Tuple]. It is enough to keep the - * read path and round-trip tests working; the full JBON2 type set can be added incrementally. + * structures [Array], [Object], [Map], [TagList], [TagMap], [Dictionary], [Book] and [Tuple]. + * It is enough to keep the read path and round-trip tests working; the full JBON2 type set + * can be added incrementally. * * The decoder maps a single top-level unit. To decode a stored tuple, call [mapBytes] which will * skip the file header (if present), descend into the [Tuple], and expose the feature [Object]. @@ -347,7 +348,7 @@ open class JbDecoder2(var globalDict: IBook? = null, var membersDict: IBook? = n val contentStart = at + hs val contentEnd = contentStart + contentSize return when (type) { - JB2_STRUCT_ARRAY -> { + JB2_STRUCT_ARRAY, JB2_STRUCT_TAG_LIST -> { val list = AnyList() var p = contentStart while (p < contentEnd) { @@ -356,7 +357,7 @@ open class JbDecoder2(var globalDict: IBook? = null, var membersDict: IBook? = n } list } - JB2_STRUCT_OBJECT, JB2_STRUCT_MAP -> { + JB2_STRUCT_OBJECT, JB2_STRUCT_MAP, JB2_STRUCT_TAG_MAP -> { val obj = AnyObject() var p = contentStart while (p < contentEnd) { diff --git a/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/JbEncoder2.kt b/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/JbEncoder2.kt index 4a5ed9b03..c7f21d1c0 100644 --- a/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/JbEncoder2.kt +++ b/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/JbEncoder2.kt @@ -24,6 +24,11 @@ import kotlin.math.floor * when a [global] book is provided; the JBON2 string-reference format (`11_aaa_bbs`, 7 append * characters) is used. * + * Special structures: + * - [TagList][encodeTagList] (`JB2_STRUCT_TAG_LIST`): a list of unique primitive values where order + * is significant, but must not have duplicates, null or undefined. + * - [TagMap][encodeTagMap] (`JB2_STRUCT_TAG_MAP`): a string-keyed map with primitive values. + * * @property global The global book/dictionary to use when encoding; if any. */ @Suppress("DuplicatedCode", "MemberVisibilityCanBePrivate", "OPT_IN_USAGE") @@ -748,6 +753,47 @@ open class JbEncoder2(var global: IBook? = null) : Binary() { return start } + /** + * Write a TagList recursively. A TagList is a list of unique primitive values where order + * is significant, but must not have duplicates, null or undefined. + */ + fun encodeTagList(list: ListProxy<*>): Int { + val start = startStruct() + var i = 0 + while (i < list.size) { + pushPath(i) + try { + encodeValue(list[i]) + } finally { + popPath() + } + i++ + } + endStruct(JB2_STRUCT_TAG_LIST, start) + return start + } + + /** + * Write a TagMap recursively. A TagMap is a string-keyed map with primitive values. + * Uses dictionary compression for keys when a global book is provided. + */ + fun encodeTagMap(map: MapProxy): Int { + val start = startStruct() + for (entry in map) { + val key = entry.key + val value = entry.value + writeKey(key) + pushPath(key) + try { + encodeValue(value) + } finally { + popPath() + } + } + endStruct(JB2_STRUCT_TAG_MAP, start) + return start + } + /** * Write a [JB2_STRUCT_TWKB] structure from raw TWKB bytes. * @@ -825,8 +871,8 @@ open class JbEncoder2(var global: IBook? = null) : Binary() { is Double -> if (Platform.canBeFloat32(value)) encodeFloat32(value.toFloat()) else encodeFloat64(value) is SpGeometry -> encodeGeometry(value) is ByteArray -> if (value.isNotEmpty()) encodeByteArray(value) else encodeNull() - is MapProxy<*, *> -> encodeObject(value as MapProxy) - is ListProxy<*> -> encodeList(value) + is MapProxy<*, *> -> if (value::class.simpleName == "TagMap") encodeTagMap(value as MapProxy) else encodeObject(value as MapProxy) + is ListProxy<*> -> if (value::class.simpleName == "TagList") encodeTagList(value) else encodeList(value) is Array<*> -> encodeArray(value as Array) null -> encodeNull() else -> throw IllegalArgumentException("Could not encode value for type: ${value::class}") diff --git a/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/LibJbon2.kt b/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/LibJbon2.kt index 7188e7ab9..378798329 100644 --- a/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/LibJbon2.kt +++ b/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/LibJbon2.kt @@ -179,9 +179,14 @@ internal const val JB2_STRUCT_SIZE32 = 0b0011_0000 internal const val JB2_STRUCT_TYPE_MASK = 0b0000_1111 internal const val JB2_STRUCT_ARRAY = 0 // 0000 internal const val JB2_STRUCT_MAP = 1 // 0001 -internal const val JB2_STRUCT_SET = 2 // 0010 +/** + * TagList: a list of unique primitive values (booleans, numbers, strings). Value order is + * significant, but must not have duplicates, null or undefined. + */ +internal const val JB2_STRUCT_TAG_LIST = 2 // 0010 internal const val JB2_STRUCT_OBJECT = 3 // 0011 -internal const val JB2_STRUCT_TAGS = 4 // 0100 +/** TagMap: a string-keyed map with primitive values. */ +internal const val JB2_STRUCT_TAG_MAP = 4 // 0100 internal const val JB2_STRUCT_DICTIONARY = 5 // 0101 internal const val JB2_STRUCT_BOOK = 6 // 0110 internal const val JB2_STRUCT_TUPLE_NUMBER_ARRAY = 7 // 0111 diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt index 56acdc961..303139983 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt @@ -554,7 +554,7 @@ class Naksha private constructor() { } /** - * Encodes the given tag-list into the [set][naksha.model.objects.MemberType.SET] + * Encodes the given tag-list into the [tag_list][naksha.model.objects.MemberType.TAG_LIST] * representation: a JSON array, with the element order preserved. * @param tags the tags to encode. * @return the JSON array text representation, or _null_ if [tags] is _null_ / empty. @@ -571,7 +571,7 @@ class Naksha private constructor() { * Decodes Naksha tags from their JSON text representation into a [TagList]. * * Supports both persisted forms: - * - a JSON array ([set][naksha.model.objects.MemberType.SET], the default) is returned + * - a JSON array ([tag_list][naksha.model.objects.MemberType.TAG_LIST], the default) is returned * unmodified, preserving the element order; * - a JSON object ([naksha.model.objects.MemberType.TAGS_FROM_ARRAY]) is re-flattened via * [TagMap.toTagList], in which case the original order is not guaranteed. diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TagList.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TagList.kt index 5a3117751..5d9533687 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TagList.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TagList.kt @@ -15,7 +15,7 @@ import kotlin.jvm.JvmStatic * A list of tags. */ @JsExport -open class TagList() : ListProxy(String::class) { +class TagList() : ListProxy(String::class) { /** * Create a tag list from the given arguments; the tags are normalized. diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TagMap.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TagMap.kt index fb0278836..58f9e793a 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TagMap.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TagMap.kt @@ -19,7 +19,7 @@ import kotlin.js.JsName * with use of [TagNormalizer] (that is used for example by [TagList]) */ @JsExport -open class TagMap() : MapProxy(String::class, Any::class) { +class TagMap() : MapProxy(String::class, Any::class) { @Suppress("LeakingThis") @JsName("of") diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Index.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Index.kt index dc3fac781..398f25a98 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Index.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Index.kt @@ -20,7 +20,7 @@ import kotlin.js.JsName * - [IndexType.BTREE]: ordered index for equality and range queries on scalar and text columns. * - [IndexType.SPATIAL]: spatial index covering a 2D-encoded geometry column. [on] must contain exactly one geometry member. * - [IndexType.TAGS]: inverted index over a tags member ([MemberType.TAGS] or [MemberType.TAGS_FROM_ARRAY]) supporting key/value containment queries. [on] must contain exactly one member. - * - [IndexType.SET]: inverted index over a set member ([MemberType.SET]) supporting element containment queries. [on] must contain exactly one member. + * - [IndexType.TAG_LIST]: inverted index over a tag-list member ([MemberType.TAG_LIST]) supporting element containment queries. [on] must contain exactly one member. * * When [internal] is `true` the index is storage-managed (e.g. primary key, id_unique). The storage * injects these into the [NakshaCollection.indices] list; clients must not declare them manually. diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/IndexType.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/IndexType.kt index 01cf6f1a5..9bee0b136 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/IndexType.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/IndexType.kt @@ -14,7 +14,7 @@ import kotlin.reflect.KClass * - [SPATIAL] — spatial index over a geometry column ([MemberType.SPATIAL]) (e.g. the built-in `geo`). * - [TAGS] — inverted index over a tags column ([MemberType.TAGS] or [MemberType.TAGS_FROM_ARRAY]); * supports key/value containment lookups. - * - [SET] — inverted index over a set column ([MemberType.SET]); supports element containment lookups. + * - [TAG_LIST] — inverted index over a tag-list column ([MemberType.TAG_LIST]); supports element containment lookups. * @since 3.0 */ @JsExport @@ -49,12 +49,12 @@ class IndexType : JsEnum() { val TAGS = defIgnoreCase(IndexType::class, "tags") /** - * Inverted index over a [MemberType.SET] column. Supports element containment lookups, - * e.g. find all features whose tags set contains the element `"foo"`. This is the default + * Inverted index over a [MemberType.TAG_LIST] column. Supports element containment lookups, + * e.g. find all features whose tags list contains the element `"foo"`. This is the default * index for the standard `tags` member. * @since 3.0 */ @JvmField - val SET = defIgnoreCase(IndexType::class, "set") + val TAG_LIST = defIgnoreCase(IndexType::class, "tag_list") } } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt index 4ea0fe936..f9c8721e5 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt @@ -29,8 +29,8 @@ import naksha.model.objects.Int16Member import naksha.model.objects.Int32Member import naksha.model.objects.Int64Member import naksha.model.objects.Int8Member -import naksha.model.objects.SetMember import naksha.model.objects.SpatialMember +import naksha.model.objects.TagListMember import naksha.model.objects.StringMember import naksha.model.objects.TagsMember import naksha.model.objects.TupleNumberMember @@ -400,7 +400,7 @@ open class Member() : AnyObject(), Comparator { MemberType.TUPLE_NUMBER -> proxy(TupleNumberMember::class) MemberType.SPATIAL -> proxy(SpatialMember::class) MemberType.TAGS, MemberType.TAGS_FROM_ARRAY -> proxy(TagsMember::class) - MemberType.SET -> proxy(SetMember::class) + MemberType.TAG_LIST -> proxy(TagListMember::class) } return this } @@ -417,5 +417,5 @@ open class Member() : AnyObject(), Comparator { fun asTupleNumber(): TupleNumberMember = proxy(TupleNumberMember::class) fun asSpatial(): SpatialMember = proxy(SpatialMember::class) fun asTags(): TagsMember = proxy(TagsMember::class) - fun asSet(): SetMember = proxy(SetMember::class) + fun asTagList(): TagListMember = proxy(TagListMember::class) } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberType.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberType.kt index 1483f811d..3cd2bd7fd 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberType.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberType.kt @@ -17,8 +17,8 @@ import naksha.model.objects.Int16Member import naksha.model.objects.Int32Member import naksha.model.objects.Int64Member import naksha.model.objects.Int8Member -import naksha.model.objects.SetMember import naksha.model.objects.SpatialMember +import naksha.model.objects.TagListMember import naksha.model.objects.StringMember import naksha.model.objects.TagsMember import naksha.model.objects.TupleNumberMember @@ -41,11 +41,12 @@ import kotlin.reflect.KClass * order is **not** preserved. * Only valid as a [Member] type; not a valid [IndexType]. * To index a [TAGS_FROM_ARRAY] column use [IndexType.TAGS]. - * - [SET]: a JSON array of unique primitive values (booleans, numbers, strings). The array is stored - * unmodified, so the element order is preserved when reading the feature back. Supports - * element-containment queries via [IndexType.SET]. This is the default type of the standard - * `tags` member, which keeps 100% downward compatibility with the classic XYZ tags array at - * `properties -> @ns:com:here:xyz -> tags`. + * - [TAG_LIST]: a list of unique primitive values (booleans, numbers, strings). Value order is + * significant, but must not have duplicates, null or undefined. The storage persists the list + * unmodified, so the element order is guaranteed to be preserved when reading the feature back. + * Supports element-containment queries via [IndexType.TAG_LIST]. This is the default type of the + * standard `tags` member, which keeps 100% downward compatibility with the classic XYZ tags array + * at `properties -> @ns:com:here:xyz -> tags`. * @since 3.0 */ @JsExport @@ -165,21 +166,21 @@ class MemberType : JsEnum() { val TAGS_FROM_ARRAY = defIgnoreCase(MemberType::class, "tags_from_array") { self -> self.sortOrder = 9; self.subtype = TagsMember::class } /** - * A JSON array of unique primitive values (booleans, numbers, strings), following the JBON2 - * set specification: entries must not be `null` or duplicates, and the order is significant. - * The storage persists the array unmodified (as a JSON array in `jsonb`), so the element - * order is guaranteed to be preserved when reading the feature back. + * A list of unique primitive values (booleans, numbers, strings), following the JBON2 + * TagList specification: entries must not be `null` or duplicates, and the order is + * significant. The storage persists the list unmodified (as a JSON array in `jsonb`), so + * the element order is guaranteed to be preserved when reading the feature back. * * This is the default type of the standard `tags` member (the classic XYZ tags array at * `properties -> @ns:com:here:xyz -> tags`, e.g. `["foo", "bar"]`). In contrast to * [TAGS_FROM_ARRAY] the values are not split into key/value pairs, therefore only full * elements can be searched, not keys or values. * - * Indexed via [IndexType.SET]. + * Indexed via [IndexType.TAG_LIST]. * @since 3.0 */ @JvmField - val SET = defIgnoreCase(MemberType::class, "set") { self -> self.sortOrder = 10; self.subtype = SetMember::class } + val TAG_LIST = defIgnoreCase(MemberType::class, "tag_list") { self -> self.sortOrder = 10; self.subtype = TagListMember::class } } /** @@ -209,7 +210,7 @@ class MemberType : JsEnum() { TUPLE_NUMBER -> value is TupleNumber SPATIAL -> value is SpGeometry TAGS, TAGS_FROM_ARRAY -> value is TagMap - SET -> value is List<*> + TAG_LIST -> value is List<*> else -> false } } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt index 5057fa1ae..5477af283 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt @@ -426,7 +426,7 @@ open class NakshaCollection() : NakshaFeature() { /** * The indices to maintain on this collection. * - * Each [Index] declares a name, an [IndexType] ([IndexType.BTREE] / [IndexType.SPATIAL] / [IndexType.TAGS] / [IndexType.SET]), the column(s) to index, an optional include-list (for [IndexType.BTREE]), and a `unique` flag. + * Each [Index] declares a name, an [IndexType] ([IndexType.BTREE] / [IndexType.SPATIAL] / [IndexType.TAGS] / [IndexType.TAG_LIST]), the column(s) to index, an optional include-list (for [IndexType.BTREE]), and a `unique` flag. * * Indices are applied to every variant of the collection (head, history, deleted, meta) by the storage. Mandatory and default indices are injected by the storage; clients only need to declare additional custom indices here. * @since 3.0 diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/SetMember.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TagListMember.kt similarity index 60% rename from here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/SetMember.kt rename to here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TagListMember.kt index c2c45ce5f..b90a6b15e 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/SetMember.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TagListMember.kt @@ -3,13 +3,13 @@ package naksha.model.objects import naksha.base.AnyList import naksha.model.illegalArg import naksha.model.illegalState -import naksha.model.objects.MemberType.MemberType_C.SET +import naksha.model.objects.MemberType.MemberType_C.TAG_LIST import kotlin.js.JsName -class SetMember() : TypedMember() { - override fun verify(): SetMember { - if (dataType != SET) { - throw illegalState("The member was illegally cast, expected subtype: $SET, found: $dataType") +class TagListMember() : TypedMember() { + override fun verify(): TagListMember { + if (dataType != TAG_LIST) { + throw illegalState("The member was illegally cast, expected subtype: $TAG_LIST, found: $dataType") } return this } @@ -17,19 +17,19 @@ class SetMember() : TypedMember() { @JsName("of") constructor(name: String, path: JsonPath? = null) : this() { this.name = name - this.dataType = SET + this.dataType = TAG_LIST this.path = path ?: JsonPath(listOf("properties", name)) this.path.validate() } @JsName("from") constructor(member: Member, path: JsonPath? = null) : this() { - if (member.dataType != SET) throw illegalArg("The given member is not of set type") + if (member.dataType != TAG_LIST) throw illegalArg("The given member is not of tag_list type") this.name = member.name - this.dataType = SET + this.dataType = TAG_LIST this.path = path?.validate() ?: member.path } - fun get(feature: NakshaFeature): AnyList? = getSet(feature) + fun get(feature: NakshaFeature): AnyList? = getTagList(feature) fun set(feature: NakshaFeature, value: AnyList): Any? = setPath(feature, path, value) } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzIndices.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzIndices.kt index 0eee33658..fd4858af7 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzIndices.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzIndices.kt @@ -49,12 +49,12 @@ class XyzIndices private constructor() { val XyzAuthor = Index("author", IndexType.BTREE, "author", "author_ts", "fn", "version") /** - * `tags` — inverted ([IndexType.SET]) index over the `tags` member, supporting element + * `tags` — inverted ([IndexType.TAG_LIST]) index over the `tags` member, supporting element * containment queries. See [XyzMembers.XyzTags]. * @since 3.0 */ @JvmField @JsStatic - val XyzTags = Index("tags", IndexType.SET, "tags") + val XyzTags = Index("tags", IndexType.TAG_LIST, "tags") /** * `feature_type` — index on `ft`, `fn`, `version` (WHERE `ft IS NOT NULL`). diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzMembers.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzMembers.kt index 9b7673e6f..fad518798 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzMembers.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzMembers.kt @@ -218,13 +218,13 @@ class XyzMembers private constructor() { /** * `tags` — feature tags, the classic XYZ tags array located at * `properties -> @ns:com:here:xyz -> tags` (e.g. `["foo", "bar"]`), stored as a - * [set][MemberType.SET] of unique strings. The array is persisted unmodified, so the + * [tag_list][MemberType.TAG_LIST] of unique strings. The list is persisted unmodified, so the * element order is preserved when reading the feature back. `null` if the feature has no - * tags. Supports element containment queries via [IndexType.SET]. Default member. + * tags. Supports element containment queries via [IndexType.TAG_LIST]. Default member. * @since 3.0 */ @JvmField @JsStatic - val XyzTags = Member("tags", MemberType.SET, JsonPath("properties", "@ns:com:here:xyz", "tags")) + val XyzTags = Member("tags", MemberType.TAG_LIST, JsonPath("properties", "@ns:com:here:xyz", "tags")) /** * `ref_point` — geometry reference point (always a single point), stored as TWKB. Used to compute the [XyzHereTile] value. `null` if the feature has no explicit reference point. diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/TagExists.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/TagExists.kt index 947076a93..ed1fdba4f 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/TagExists.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/TagExists.kt @@ -9,7 +9,7 @@ import kotlin.js.JsName * Tests if the tag with given name exists, ignoring the value. * * For map-form tags ([naksha.model.objects.MemberType.TAGS] / [naksha.model.objects.MemberType.TAGS_FROM_ARRAY]) - * this tests if the key exists. For set-form tags ([naksha.model.objects.MemberType.SET], the default) + * this tests if the key exists. For tag-list-form tags ([naksha.model.objects.MemberType.TAG_LIST], the default) * this tests if the full string element exists, e.g. `TagExists("foo")` matches a feature tagged * `["foo", "bar"]`. * @since 3.0.0 diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/TagQuery.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/TagQuery.kt index 19bb60163..b774a4725 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/TagQuery.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/TagQuery.kt @@ -12,7 +12,7 @@ import kotlin.js.JsExport * Note: the `TagValueIs*` and [TagValueMatches] queries operate on keys/values and therefore only * match map-form tags ([naksha.model.objects.MemberType.TAGS] / * [naksha.model.objects.MemberType.TAGS_FROM_ARRAY]). Against the default set-form tags - * ([naksha.model.objects.MemberType.SET]) the values are never split into key/value pairs; use + * ([naksha.model.objects.MemberType.TAG_LIST]) the values are never split into key/value pairs; use * [TagExists] or [TagSetContains] to match full elements instead. * @since 3.0.0 * @see TagExists diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/TagSetContains.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/TagSetContains.kt index 98b0427c0..2298814c0 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/TagSetContains.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/TagSetContains.kt @@ -8,7 +8,7 @@ import kotlin.js.JsExport import kotlin.js.JsName /** - * Tests if a set-form tags member ([naksha.model.objects.MemberType.SET], the default for the + * Tests if a tag-list-form tags member ([naksha.model.objects.MemberType.TAG_LIST], the default for the * standard `tags` member) contains the given element. * * The element is matched in its type: strings, booleans, and numbers are all supported. For string diff --git a/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/MemberTest.kt b/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/MemberTest.kt index 77f1768f7..9ebd6b54f 100644 --- a/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/MemberTest.kt +++ b/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/MemberTest.kt @@ -83,7 +83,7 @@ class MemberTest { assertNotNull(IndexType.BTREE) assertNotNull(IndexType.SPATIAL) assertNotNull(IndexType.TAGS) - assertNotNull(IndexType.SET) + assertNotNull(IndexType.TAG_LIST) } @Test @@ -101,13 +101,13 @@ class MemberTest { // Virtual / jsonb. assertNotNull(MemberType.TAGS) assertNotNull(MemberType.TAGS_FROM_ARRAY) - assertNotNull(MemberType.SET) + assertNotNull(MemberType.TAG_LIST) } @Test fun standardTagsMemberDefaultsToSet() { - assertEquals(MemberType.SET, naksha.model.objects.StandardMembers.XyzTags.dataType) - assertEquals(IndexType.SET, naksha.model.objects.XyzIndices.XyzTags.type) + assertEquals(MemberType.TAG_LIST, naksha.model.objects.StandardMembers.XyzTags.dataType) + assertEquals(IndexType.TAG_LIST, naksha.model.objects.XyzIndices.XyzTags.type) } @Test diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgIndex.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgIndex.kt index 32bbe9364..b97ce441c 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgIndex.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgIndex.kt @@ -54,7 +54,7 @@ data class PgIndex( val using = when (type) { BTREE -> "btree" SPATIAL -> "gist" - TAGS, SET -> "gin" + TAGS, TAG_LIST -> "gin" else -> throw NakshaException(INTERNAL_ERROR, "Invalid index type for index $name on table $tableName") } val indexName = quoteIdent(tableName, "\$i_", tableName) diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMemberHelper.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMemberHelper.kt index 5888ff35e..2f168be00 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMemberHelper.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMemberHelper.kt @@ -48,7 +48,7 @@ class PgMemberHelper private constructor() { MemberType.SPATIAL -> PgType.BYTE_ARRAY MemberType.TAGS -> PgType.JSONB MemberType.TAGS_FROM_ARRAY -> PgType.JSONB - MemberType.SET -> PgType.JSONB + MemberType.TAG_LIST -> PgType.JSONB else -> PgType.STRING } @@ -56,7 +56,7 @@ class PgMemberHelper private constructor() { * Returns the PostgreSQL DDL type string for the given member type, used inside `CREATE TABLE` / `ALTER TABLE ADD COLUMN`. * Note: there is no 1-byte signed integer type in PostgreSQL, so [MemberType.INT8] is materialized as `smallint`; * the storage enforces the 8-bit range on coercion. - * [MemberType.TAGS], [MemberType.TAGS_FROM_ARRAY], and [MemberType.SET] all use `jsonb STORAGE MAIN` — + * [MemberType.TAGS], [MemberType.TAGS_FROM_ARRAY], and [MemberType.TAG_LIST] all use `jsonb STORAGE MAIN` — * compressed inline, only TOASTed as a last resort. The only difference is the JSON shape: * TAGS and TAGS_FROM_ARRAY persist a JSON object, SET persists a JSON array. */ @@ -73,7 +73,7 @@ class PgMemberHelper private constructor() { MemberType.SPATIAL -> "bytea STORAGE EXTERNAL" MemberType.TAGS -> "jsonb STORAGE MAIN" MemberType.TAGS_FROM_ARRAY -> "jsonb STORAGE MAIN" - MemberType.SET -> "jsonb STORAGE MAIN" + MemberType.TAG_LIST -> "jsonb STORAGE MAIN" else -> "text" } @@ -110,7 +110,7 @@ class PgMemberHelper private constructor() { MemberType.SPATIAL -> coerceByteArray(value, featureId, memberName) MemberType.TAGS -> coerceTags(value, featureId, memberName) MemberType.TAGS_FROM_ARRAY -> coerceTagsFromArray(value, featureId, memberName) - MemberType.SET -> coerceSet(value, featureId, memberName) + MemberType.TAG_LIST -> coerceTagList(value, featureId, memberName) else -> { warnMismatch(featureId, memberName, type.toString(), value) null @@ -240,11 +240,11 @@ class PgMemberHelper private constructor() { } /** - * Coerces a [MemberType.SET] value: a JSON array of unique primitives (booleans, numbers, strings). + * Coerces a [MemberType.TAG_LIST] value: a JSON array of unique primitives (booleans, numbers, strings). * The array is persisted unmodified (element order preserved). Entries that are `null`, non-primitive, * or duplicates violate the set contract; the value is then not materialized (warning + NULL column). */ - private fun coerceSet(value: Any, featureId: String, memberName: String): String? { + private fun coerceTagList(value: Any, featureId: String, memberName: String): String? { val list: ListProxy<*> = when (value) { is ListProxy<*> -> value is PlatformList -> value.proxy(AnyList::class) @@ -312,7 +312,7 @@ class PgMemberHelper private constructor() { MemberType.SPATIAL -> 8 MemberType.TAGS -> 9 MemberType.TAGS_FROM_ARRAY -> 10 - MemberType.SET -> 11 + MemberType.TAG_LIST -> 11 else -> 12 } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgType.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgType.kt index ec48e2606..13ca84c62 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgType.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgType.kt @@ -128,7 +128,7 @@ class PgType : JsEnum() { /** * JSONB column type, bound as text (JSON). * - * Used by storages to materialize [naksha.model.objects.MemberType.TAGS], [naksha.model.objects.MemberType.TAGS_FROM_ARRAY] (JSON object), and [naksha.model.objects.MemberType.SET] (JSON array) members. + * Used by storages to materialize [naksha.model.objects.MemberType.TAGS], [naksha.model.objects.MemberType.TAGS_FROM_ARRAY] (JSON object), and [naksha.model.objects.MemberType.TAG_LIST] (JSON array) members. * @since 3.0 */ @JvmField @@ -188,7 +188,7 @@ class PgType : JsEnum() { // MemberType.SPATIAL -> BYTE_ARRAY MemberType.TAGS -> JSONB MemberType.TAGS_FROM_ARRAY -> JSONB - MemberType.SET -> JSONB + MemberType.TAG_LIST -> JSONB else -> BYTE_ARRAY } } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt index 24e5d63d5..4f452f78c 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt @@ -515,7 +515,7 @@ open class PgWriter internal constructor( "but '$firstColName' has type $firstColType." ) } - IndexType.SET -> if (firstColType != MemberType.SET) { + IndexType.TAG_LIST -> if (firstColType != MemberType.TAG_LIST) { throw illegalArg( "SET index '${idx.name}' must target a member of type SET, " + "but '$firstColName' has type $firstColType." diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/CollectionTests.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/CollectionTests.kt index b2f0b5a6f..84a0468f4 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/CollectionTests.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/CollectionTests.kt @@ -547,14 +547,14 @@ class CollectionTests : PgTestBase(collection = null, mapId = "") { } /** - * When a custom [MemberType.SET] member is declared with a [IndexType.SET] index, the collection + * When a custom [MemberType.TAG_LIST] member is declared with a [IndexType.TAG_LIST] index, the collection * must materialize the member as a `jsonb` column and create a GIN index over it. */ @Test fun membersSet_shouldCreateJsonbColumnAndGinIndex() { val collection = NakshaCollection("members_set_test", map.id).apply { - addMember(Member("labels", MemberType.SET)) - addIndex(Index("idx_labels", IndexType.SET, "labels")) + addMember(Member("labels", MemberType.TAG_LIST)) + addIndex(Index("idx_labels", IndexType.TAG_LIST, "labels")) } executeWrite(WriteRequest().add(Write().createCollection(collection))) @@ -581,13 +581,13 @@ class CollectionTests : PgTestBase(collection = null, mapId = "") { } /** - * A [IndexType.SET] index must be rejected when it targets a member that is not a [MemberType.SET]. + * A [IndexType.TAG_LIST] index must be rejected when it targets a member that is not a [MemberType.TAG_LIST]. */ @Test - fun membersSet_indexOnNonSetMemberShouldFail() { + fun membersSet_indexOnNonTagListMemberShouldFail() { val collection = NakshaCollection("members_set_invalid_test", map.id).apply { addMember(Member("score", MemberType.INT64)) - addIndex(Index("idx_set_score", IndexType.SET, "score")) + addIndex(Index("idx_tag_list_score", IndexType.TAG_LIST, "score")) } executeWriteErrorResponse(WriteRequest().add(Write().createCollection(collection))) } @@ -615,7 +615,7 @@ class CollectionTests : PgTestBase(collection = null, mapId = "") { addMember(Member("g_i16", MemberType.INT16)) addMember(Member("h_f32", MemberType.FLOAT32)) addMember(Member("i_json", MemberType.TAGS)) - addMember(Member("j_set", MemberType.SET)) + addMember(Member("j_tag_list", MemberType.TAG_LIST)) } executeWrite(WriteRequest().add(Write().createCollection(collection))) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByTagsTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByTagsTest.kt index 1ee0da5df..2219855e6 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByTagsTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByTagsTest.kt @@ -48,7 +48,7 @@ class ReadFeaturesByTagsTest : PgTestBase() { } /** - * The default tags member is a [naksha.model.objects.MemberType.SET]: the tags array is stored + * The default tags member is a [naksha.model.objects.MemberType.TAG_LIST]: the tags list is stored * unmodified, the values are never split into key/value pairs. Therefore only full elements can * be matched — `TagExists("fullelem")` does not find a feature tagged `["fullelem=bar"]`. */ From 808f63756a76ee9b8e0da461d0fe946bd1d38efd Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Mon, 22 Jun 2026 11:24:54 +0200 Subject: [PATCH 30/57] Add missing operations. Signed-off-by: Alexander Lowey-Weber --- .../naksha/model/request/ReadFeatures.kt | 23 ++++---- .../kotlin/naksha/model/request/ops/Equals.kt | 32 +++++++++++ .../kotlin/naksha/model/request/ops/Gt.kt | 29 ++++++++++ .../kotlin/naksha/model/request/ops/Gte.kt | 29 ++++++++++ .../naksha/model/request/ops/Intersects.kt | 42 ++++++++++++++ .../naksha/model/request/ops/IsAnyOf.kt | 30 ++++++++++ .../naksha/model/request/ops/IsFalse.kt | 20 +++++++ .../kotlin/naksha/model/request/ops/IsNull.kt | 20 +++++++ .../kotlin/naksha/model/request/ops/IsTrue.kt | 20 +++++++ .../kotlin/naksha/model/request/ops/Lt.kt | 29 ++++++++++ .../kotlin/naksha/model/request/ops/Lte.kt | 29 ++++++++++ .../kotlin/naksha/model/request/ops/Op.kt | 46 ++++++++++++++++ .../naksha/model/request/ops/SpBuffer.kt | 55 +++++++++++++++++++ .../naksha/model/request/ops/SpEndCap.kt | 20 +++++++ .../naksha/model/request/ops/SpJoinStyle.kt | 24 ++++++++ .../kotlin/naksha/model/request/ops/SpSide.kt | 21 +++++++ .../model/request/ops/SpTransformation.kt | 14 +++++ .../model/request/ops/SpTransformationList.kt | 10 ++++ .../naksha/model/request/ops/StartsWith.kt | 28 ++++++++++ .../naksha/model/request/ops/TagEquals.kt | 32 +++++++++++ .../naksha/model/request/ops/TagExists.kt | 28 ++++++++++ .../kotlin/naksha/model/request/ops/TagGt.kt | 32 +++++++++++ .../kotlin/naksha/model/request/ops/TagGte.kt | 32 +++++++++++ .../naksha/model/request/ops/TagHasAllOf.kt | 30 ++++++++++ .../naksha/model/request/ops/TagHasAnyOf.kt | 30 ++++++++++ .../naksha/model/request/ops/TagIsNull.kt | 28 ++++++++++ .../model/request/ops/TagListHasAllOf.kt | 30 ++++++++++ .../model/request/ops/TagListHasAnyOf.kt | 30 ++++++++++ .../kotlin/naksha/model/request/ops/TagLt.kt | 32 +++++++++++ .../kotlin/naksha/model/request/ops/TagLte.kt | 32 +++++++++++ .../naksha/model/request/ops/TagStartsWith.kt | 31 +++++++++++ 31 files changed, 876 insertions(+), 12 deletions(-) create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Equals.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Gt.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Gte.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Intersects.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/IsAnyOf.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/IsFalse.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/IsNull.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/IsTrue.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Lt.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Lte.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/SpBuffer.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/SpEndCap.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/SpJoinStyle.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/SpSide.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/SpTransformation.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/SpTransformationList.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/StartsWith.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagEquals.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagExists.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagGt.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagGte.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagHasAllOf.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagHasAnyOf.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagIsNull.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagListHasAllOf.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagListHasAnyOf.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagLt.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagLte.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagStartsWith.kt diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadFeatures.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadFeatures.kt index 35b9598ca..9fb3701c5 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadFeatures.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadFeatures.kt @@ -7,6 +7,7 @@ import naksha.base.NullableProperty import naksha.base.StringList import naksha.model.GuidList import naksha.model.Version +import naksha.model.request.ops.Op import naksha.model.request.query.IMemberQuery import naksha.model.request.query.IPropertyQuery import naksha.model.request.query.ITagQuery @@ -31,6 +32,7 @@ open class ReadFeatures : ReadRequest() { private val ORDER_BY_OR_NULL = NullableProperty(OrderBy::class) private val GUID_LIST = NotNullProperty(GuidList::class) private val QUERY = NotNullProperty(RequestQuery::class) + private val OP_OR_NULL = NullableProperty(Op::class) } /** @@ -57,7 +59,7 @@ open class ReadFeatures : ReadRequest() { * * @since 3.0 */ - @Deprecated("Replaced with memberQuery", replaceWith = ReplaceWith("memberQuery")) + @Deprecated("Replaced with op", replaceWith = ReplaceWith("op")) open fun withPropertyQuery(pQuery: IPropertyQuery?): ReadFeatures { this.query.properties = pQuery this.resultFilters.removeAll { it is PropertyFilter } @@ -74,7 +76,7 @@ open class ReadFeatures : ReadRequest() { * This method comes handy if [IPropertyQuery] was mutated outside of this class scope, * in such cases we need to populate the filter once again so it will be in sync with the query */ - @Deprecated("Replaced with memberQuery", replaceWith = ReplaceWith("memberQuery")) + @Deprecated("Replaced with op", replaceWith = ReplaceWith("op")) fun refreshPropertyFilter() { this.resultFilters.removeAll { it is PropertyFilter } if(query.properties != null) { @@ -91,7 +93,7 @@ open class ReadFeatures : ReadRequest() { * * @since 3.0 */ - @Deprecated("Replaced with memberQuery", replaceWith = ReplaceWith("memberQuery")) + @Deprecated("Replaced with op", replaceWith = ReplaceWith("op")) open fun withTagQuery(tQuery: ITagQuery?): ReadFeatures { this.query.tags = tQuery return this @@ -181,7 +183,7 @@ open class ReadFeatures : ReadRequest() { * Add all features that match the given IDs into the result-set. * @since 3.0.0 */ - @Deprecated("Replaced with memberQuery", replaceWith = ReplaceWith("memberQuery")) + @Deprecated("Replaced with op", replaceWith = ReplaceWith("op")) var featureIds: StringList by STRING_LIST /** @@ -192,26 +194,23 @@ open class ReadFeatures : ReadRequest() { */ // TODO: We should replace this with `tupleNumbers`, because that is what we will encode into `uuid` and that is what the clients need. // Is there any use-case for the GUID any longer? + @Deprecated("Replaced with op", replaceWith = ReplaceWith("op")) var guids: GuidList by GUID_LIST /** * Add all features that match the given query into the result-set. * @since 3.0.0 */ - @Deprecated("Replaced with memberQuery", replaceWith = ReplaceWith("memberQuery")) + @Deprecated("Replaced with op", replaceWith = ReplaceWith("op")) var query: RequestQuery by QUERY /** - * Search for [members][naksha.model.objects.Member]s. + * The [operation][Op] to execute. * - * This method now supports to search for all custom defined members. It allows arbitrary combination. + * This replaces [query] and must not be used together with [query]. It actually allows to query for any member value. * @since 3.0 */ - var memberQuery: IMemberQuery? - get() = query.members - set(value) { - query.members = value - } + var op: Op? by OP_OR_NULL /** * Tests whether this request is effectively a query for all features in their current **HEAD** state, diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Equals.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Equals.kt new file mode 100644 index 000000000..98458ef25 --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Equals.kt @@ -0,0 +1,32 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model.request.ops + +import naksha.base.NullableProperty +import naksha.model.objects.Member +import kotlin.js.JsExport + +/** + * Tests if the member at [at] equals the given [value]. + * @since 3.0 + */ +@JsExport +class Equals() : Op() { + companion object Equals_C { + private val VALUE = NullableProperty(Any::class) + } + + constructor(at: String, value: Any?) : this() { + this.op = EQ + this.at = at + this.value = value + } + + constructor(at: Member, value: Any?) : this(at.name, value) + + /** + * The value to compare against. + * @since 3.0 + */ + var value: Any? by VALUE +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Gt.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Gt.kt new file mode 100644 index 000000000..eb4f47b4e --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Gt.kt @@ -0,0 +1,29 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model.request.ops + +import naksha.base.NotNullProperty +import naksha.base.NullableProperty +import naksha.model.objects.Member +import kotlin.js.JsExport + +/** + * Tests if the member at [at] is greater than the given [value] (GreaterThan). + * @since 3.0 + */ +@JsExport +class Gt() : Op() { + companion object Gt_C { + private val VALUE = NotNullProperty(Any::class) + } + + constructor(at: String, value: Any) : this() { + this.op = GT + this.at = at + this.value = value + } + + constructor(at: Member, value: Any) : this(at.name, value) + + var value: Any by VALUE +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Gte.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Gte.kt new file mode 100644 index 000000000..6117a61cf --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Gte.kt @@ -0,0 +1,29 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model.request.ops + +import naksha.base.NotNullProperty +import naksha.base.NullableProperty +import naksha.model.objects.Member +import kotlin.js.JsExport + +/** + * Tests if the member at [at] is greater than or equal to the given [value] (GreaterThanOrEqual). + * @since 3.0 + */ +@JsExport +class Gte() : Op() { + companion object Gte_C { + private val VALUE = NotNullProperty(Any::class) + } + + constructor(at: String, value: Any) : this() { + this.op = GTE + this.at = at + this.value = value + } + + constructor(at: Member, value: Any) : this(at.name, value) + + var value: Any by VALUE +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Intersects.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Intersects.kt new file mode 100644 index 000000000..6e75b013b --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Intersects.kt @@ -0,0 +1,42 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model.request.ops + +import naksha.base.NotNullProperty +import naksha.geo.SpGeometry +import naksha.model.objects.Member +import kotlin.js.JsExport + +/** + * Tests if the geometry member at [at] intersects with the given [value] geometry. + * @since 3.0 + */ +@JsExport +class Intersects() : Op() { + companion object Intersects_C { + private val VALUE = NotNullProperty(SpGeometry::class) { _,_ -> SpGeometry() } + private val TRANSFORMERS = NotNullProperty(SpTransformationList::class) { _,_ -> SpTransformationList() } + } + + constructor(at: String, geometry: SpGeometry, vararg transformers: SpTransformation) : this() { + this.op = INTERSECTS + this.at = at + this.value = geometry + val _transformers = this.transformers + for (t in transformers) _transformers.add(t) + } + + constructor(at: Member, geometry: SpGeometry, vararg transformers: SpTransformation) : this(at.name, geometry, *transformers) + + /** + * The geometry to test for intersection. + * @since 3.0 + */ + var value: SpGeometry by VALUE + + /** + * Optional transformations to apply to the geometry. + * @since 3.0 + */ + var transformers: SpTransformationList by TRANSFORMERS +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/IsAnyOf.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/IsAnyOf.kt new file mode 100644 index 000000000..9df78143b --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/IsAnyOf.kt @@ -0,0 +1,30 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model.request.ops + +import naksha.base.AnyList +import naksha.base.NotNullProperty +import naksha.model.objects.Member +import kotlin.js.JsExport + +/** + * Tests if the member at [at] is any of the given [items]. + * @since 3.0 + */ +@JsExport +class IsAnyOf() : Op() { + companion object IsAnyOf_C { + private val ITEMS = NotNullProperty(AnyList::class) { _,_ -> AnyList() } + } + + constructor(at: String, vararg items: Any) : this() { + this.op = ANY_OF + this.at = at + val _items = this.items + for (item in items) _items.add(item) + } + + constructor(at: Member, vararg items: Any) : this(at.name, *items) + + var items: AnyList by ITEMS +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/IsFalse.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/IsFalse.kt new file mode 100644 index 000000000..0fcd3862d --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/IsFalse.kt @@ -0,0 +1,20 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model.request.ops + +import naksha.model.objects.Member +import kotlin.js.JsExport + +/** + * Tests if the member at [at] is false. + * @since 3.0 + */ +@JsExport +class IsFalse() : Op() { + constructor(at: String) : this() { + this.op = IS_FALSE + this.at = at + } + + constructor(at: Member) : this(at.name) +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/IsNull.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/IsNull.kt new file mode 100644 index 000000000..d9773ad47 --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/IsNull.kt @@ -0,0 +1,20 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model.request.ops + +import naksha.model.objects.Member +import kotlin.js.JsExport + +/** + * Tests if the member at [at] is null. + * @since 3.0 + */ +@JsExport +class IsNull() : Op() { + constructor(at: String) : this() { + this.op = IS_NULL + this.at = at + } + + constructor(at: Member) : this(at.name) +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/IsTrue.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/IsTrue.kt new file mode 100644 index 000000000..45f0befab --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/IsTrue.kt @@ -0,0 +1,20 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model.request.ops + +import naksha.model.objects.Member +import kotlin.js.JsExport + +/** + * Tests if the member at [at] is true. + * @since 3.0 + */ +@JsExport +class IsTrue() : Op() { + constructor(at: String) : this() { + this.op = IS_TRUE + this.at = at + } + + constructor(at: Member) : this(at.name) +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Lt.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Lt.kt new file mode 100644 index 000000000..d810eb90a --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Lt.kt @@ -0,0 +1,29 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model.request.ops + +import naksha.base.NotNullProperty +import naksha.base.NullableProperty +import naksha.model.objects.Member +import kotlin.js.JsExport + +/** + * Tests if the member at [at] is less than the given [value] (LessThan). + * @since 3.0 + */ +@JsExport +class Lt() : Op() { + companion object Lt_C { + private val VALUE = NotNullProperty(Any::class) + } + + constructor(at: String, value: Any) : this() { + this.op = LT + this.at = at + this.value = value + } + + constructor(at: Member, value: Any) : this(at.name, value) + + var value: Any by VALUE +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Lte.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Lte.kt new file mode 100644 index 000000000..6bd89cc9e --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Lte.kt @@ -0,0 +1,29 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model.request.ops + +import naksha.base.NotNullProperty +import naksha.base.NullableProperty +import naksha.model.objects.Member +import kotlin.js.JsExport + +/** + * Tests if the member at [at] is less than or equal to the given [value] (LessThanOrEqual). + * @since 3.0 + */ +@JsExport +class Lte() : Op() { + companion object Lte_C { + private val VALUE = NotNullProperty(Any::class) + } + + constructor(at: String, value: Any) : this() { + this.op = LTE + this.at = at + this.value = value + } + + constructor(at: Member, value: Any) : this(at.name, value) + + var value: Any by VALUE +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Op.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Op.kt index c0b303aea..9ff27f243 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Op.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Op.kt @@ -25,6 +25,29 @@ open class Op : AnyObject() { const val AND = "and" const val OR = "or" const val NOT = "not" + const val IS_NULL = "is_null" + const val IS_TRUE = "is_true" + const val IS_FALSE = "is_false" + const val EQ = "eq" + const val GT = "gt" + const val GTE = "gte" + const val LT = "lt" + const val LTE = "lte" + const val STARTS_WITH = "starts_with" + const val ANY_OF = "any_of" + const val INTERSECTS = "intersects" + const val TAG_EXISTS = "tag_exists" + const val TAG_HAS_ANY_OF = "tag_has_any_of" + const val TAG_HAS_ALL_OF = "tag_has_all_of" + const val TAG_IS_NULL = "tag_is_null" + const val TAG_EQ = "tag_eq" + const val TAG_STARTS_WITH = "tag_starts_with" + const val TAG_GT = "tag_gt" + const val TAG_GTE = "tag_gte" + const val TAG_LT = "tag_lt" + const val TAG_LTE = "tag_lte" + const val TAGLIST_HAS_ANY_OF = "taglist_has_any_of" + const val TAGLIST_HAS_ALL_OF = "taglist_has_all_of" /** * Auto-detect the concrete type of member operation and return the cast real type. @@ -37,6 +60,29 @@ open class Op : AnyObject() { AND -> op.proxy(And::class) OR -> op.proxy(Or::class) NOT -> op.proxy(Not::class) + IS_NULL -> op.proxy(IsNull::class) + IS_TRUE -> op.proxy(IsTrue::class) + IS_FALSE -> op.proxy(IsFalse::class) + EQ -> op.proxy(Equals::class) + GT -> op.proxy(Gt::class) + GTE -> op.proxy(Gte::class) + LT -> op.proxy(Lt::class) + LTE -> op.proxy(Lte::class) + STARTS_WITH -> op.proxy(StartsWith::class) + ANY_OF -> op.proxy(IsAnyOf::class) + INTERSECTS -> op.proxy(Intersects::class) + TAG_EXISTS -> op.proxy(TagExists::class) + TAG_HAS_ANY_OF -> op.proxy(TagHasAnyOf::class) + TAG_HAS_ALL_OF -> op.proxy(TagHasAllOf::class) + TAG_IS_NULL -> op.proxy(TagIsNull::class) + TAG_EQ -> op.proxy(TagEquals::class) + TAG_STARTS_WITH -> op.proxy(TagStartsWith::class) + TAG_GT -> op.proxy(TagGt::class) + TAG_GTE -> op.proxy(TagGte::class) + TAG_LT -> op.proxy(TagLt::class) + TAG_LTE -> op.proxy(TagLte::class) + TAGLIST_HAS_ANY_OF -> op.proxy(TagListHasAnyOf::class) + TAGLIST_HAS_ALL_OF -> op.proxy(TagListHasAllOf::class) else -> null } } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/SpBuffer.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/SpBuffer.kt new file mode 100644 index 000000000..1467781d6 --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/SpBuffer.kt @@ -0,0 +1,55 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model.request.ops + +import naksha.base.NotNullProperty +import naksha.base.NullableEnum +import naksha.base.NullableProperty +import kotlin.js.JsExport +import kotlin.js.JsName +import kotlin.jvm.JvmOverloads + +/** + * Computes a POLYGON or MULTIPOLYGON that represents all points whose distance from a geometry/geography + * is less than or equal to a given distance. + */ +@JsExport +open class SpBuffer() : SpTransformation() { + @JsName("of") + @JvmOverloads + constructor( + distance: Double, + geography: Boolean = true, + quadSegments: Int? = null, + joinStyle: SpJoinStyle? = null, + joinLimit: Double? = null, + endCap: SpEndCap? = null, + side: SpSide? = null + ) : this() { + this.distance = distance + this.geography = geography + this.quadSegments = quadSegments + this.joinStyle = joinStyle + this.joinLimit = joinLimit + this.endCap = endCap + this.side = side + } + + companion object SpBuffer_C { + private val DOUBLE = NotNullProperty(Double::class) { _,_ -> 0.0 } + private val DOUBLE_NULL = NullableProperty(Double::class) + private val BOOLEAN = NotNullProperty(Boolean::class) { _,_ -> true } + private val INT_NULL = NullableProperty(Int::class) + private val JOIN_STYLE = NullableEnum(SpJoinStyle::class) + private val ENDCAP_NULL = NullableEnum(SpEndCap::class) + private val SIDE_NULL = NullableEnum(SpSide::class) + } + + var distance by DOUBLE + var geography by BOOLEAN + var quadSegments by INT_NULL + var joinStyle by JOIN_STYLE + var joinLimit by DOUBLE_NULL + var endCap by ENDCAP_NULL + var side by SIDE_NULL +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/SpEndCap.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/SpEndCap.kt new file mode 100644 index 000000000..49e3f31e3 --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/SpEndCap.kt @@ -0,0 +1,20 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model.request.ops + +import naksha.base.JsEnum +import kotlin.js.JsExport +import kotlin.reflect.KClass + +@JsExport +class SpEndCap : JsEnum() { + companion object SpEndCap_C { + val ROUND = def(SpEndCap::class, "round") + val BUTT = def(SpEndCap::class, "butt").alias("flat") + } + + @Suppress("NON_EXPORTABLE_TYPE") + override fun namespace(): KClass = SpEndCap::class + + override fun initClass() {} +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/SpJoinStyle.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/SpJoinStyle.kt new file mode 100644 index 000000000..7a6147728 --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/SpJoinStyle.kt @@ -0,0 +1,24 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model.request.ops + +import naksha.base.JsEnum +import kotlin.js.JsExport +import kotlin.reflect.KClass + +/** + * The join style. + */ +@JsExport +class SpJoinStyle : JsEnum() { + companion object SpJoinStyle_C { + val ROUND = def(SpJoinStyle::class, "round") + val MITRE = def(SpJoinStyle::class, "mitre") + val BEVEL = def(SpJoinStyle::class, "bevel") + } + + @Suppress("NON_EXPORTABLE_TYPE") + override fun namespace(): KClass = SpJoinStyle::class + + override fun initClass() {} +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/SpSide.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/SpSide.kt new file mode 100644 index 000000000..42f4cc167 --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/SpSide.kt @@ -0,0 +1,21 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model.request.ops + +import naksha.base.JsEnum +import kotlin.js.JsExport +import kotlin.reflect.KClass + +@JsExport +class SpSide : JsEnum() { + companion object SpSide_C { + val BOTH = def(SpSide::class, "both") + val LEFT = def(SpSide::class, "left") + val RIGHT = def(SpSide::class, "right") + } + + @Suppress("NON_EXPORTABLE_TYPE") + override fun namespace(): KClass = SpSide::class + + override fun initClass() {} +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/SpTransformation.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/SpTransformation.kt new file mode 100644 index 000000000..c34e38a85 --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/SpTransformation.kt @@ -0,0 +1,14 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model.request.ops + +import naksha.base.AnyObject +import naksha.base.NullableProperty +import kotlin.js.JsExport +import kotlin.js.JsName + +/** + * Base class for all transformations to be applied to client geometries. + */ +@JsExport +open class SpTransformation() : AnyObject() \ No newline at end of file diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/SpTransformationList.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/SpTransformationList.kt new file mode 100644 index 000000000..dd7997d6c --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/SpTransformationList.kt @@ -0,0 +1,10 @@ +@file:OptIn(ExperimentalJsExport::class) + +package naksha.model.request.ops + +import naksha.base.ListProxy +import kotlin.js.ExperimentalJsExport +import kotlin.js.JsExport + +@JsExport +class SpTransformationList : ListProxy(SpTransformation::class) diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/StartsWith.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/StartsWith.kt new file mode 100644 index 000000000..7b3eea6bc --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/StartsWith.kt @@ -0,0 +1,28 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model.request.ops + +import naksha.base.NotNullProperty +import naksha.model.objects.Member +import kotlin.js.JsExport + +/** + * Tests if the member at [at] starts with the given [value]. + * @since 3.0 + */ +@JsExport +class StartsWith() : Op() { + companion object StartsWith_C { + private val VALUE = NotNullProperty(String::class) { _,_ -> "" } + } + + constructor(at: String, value: String) : this() { + this.op = STARTS_WITH + this.at = at + this.value = value + } + + constructor(at: Member, value: String) : this(at.name, value) + + var value: String by VALUE +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagEquals.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagEquals.kt new file mode 100644 index 000000000..3a4da3714 --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagEquals.kt @@ -0,0 +1,32 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model.request.ops + +import naksha.base.NotNullProperty +import naksha.base.NullableProperty +import naksha.model.objects.Member +import kotlin.js.JsExport + +/** + * Tests if the tag [key] on the member at [at] equals the given [value]. + * @since 3.0 + */ +@JsExport +class TagEquals() : Op() { + companion object TagEquals_C { + private val KEY = NotNullProperty(String::class) { _,_ -> "" } + private val VALUE = NullableProperty(Any::class) + } + + constructor(at: String, key: String, value: Any?) : this() { + this.op = TAG_EQ + this.at = at + this.key = key + this.value = value + } + + constructor(at: Member, key: String, value: Any?) : this(at.name, key, value) + + var key: String by KEY + var value: Any? by VALUE +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagExists.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagExists.kt new file mode 100644 index 000000000..c1bfd691f --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagExists.kt @@ -0,0 +1,28 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model.request.ops + +import naksha.base.NotNullProperty +import naksha.model.objects.Member +import kotlin.js.JsExport + +/** + * Tests if the tag [key] exists on the member at [at]. + * @since 3.0 + */ +@JsExport +class TagExists() : Op() { + companion object TagExists_C { + private val KEY = NotNullProperty(String::class) { _,_ -> "" } + } + + constructor(at: String, key: String) : this() { + this.op = TAG_EXISTS + this.at = at + this.key = key + } + + constructor(at: Member, key: String) : this(at.name, key) + + var key: String by KEY +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagGt.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagGt.kt new file mode 100644 index 000000000..c131f5bf2 --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagGt.kt @@ -0,0 +1,32 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model.request.ops + +import naksha.base.NotNullProperty +import naksha.base.NullableProperty +import naksha.model.objects.Member +import kotlin.js.JsExport + +/** + * Tests if the tag [key] on the member at [at] is greater than the given [value]. + * @since 3.0 + */ +@JsExport +class TagGt() : Op() { + companion object TagGt_C { + private val KEY = NotNullProperty(String::class) { _,_ -> "" } + private val VALUE = NotNullProperty(Any::class) + } + + constructor(at: String, key: String, value: Any) : this() { + this.op = TAG_GT + this.at = at + this.key = key + this.value = value + } + + constructor(at: Member, key: String, value: Any) : this(at.name, key, value) + + var key: String by KEY + var value: Any by VALUE +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagGte.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagGte.kt new file mode 100644 index 000000000..d565d1f3a --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagGte.kt @@ -0,0 +1,32 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model.request.ops + +import naksha.base.NotNullProperty +import naksha.base.NullableProperty +import naksha.model.objects.Member +import kotlin.js.JsExport + +/** + * Tests if the tag [key] on the member at [at] is greater than or equal to the given [value]. + * @since 3.0 + */ +@JsExport +class TagGte() : Op() { + companion object TagGte_C { + private val KEY = NotNullProperty(String::class) { _,_ -> "" } + private val VALUE = NotNullProperty(Any::class) + } + + constructor(at: String, key: String, value: Any) : this() { + this.op = TAG_GTE + this.at = at + this.key = key + this.value = value + } + + constructor(at: Member, key: String, value: Any) : this(at.name, key, value) + + var key: String by KEY + var value: Any by VALUE +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagHasAllOf.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagHasAllOf.kt new file mode 100644 index 000000000..37824028a --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagHasAllOf.kt @@ -0,0 +1,30 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model.request.ops + +import naksha.base.NotNullProperty +import naksha.base.StringList +import naksha.model.objects.Member +import kotlin.js.JsExport + +/** + * Tests if all of the given [keys] exist on the member at [at]. + * @since 3.0 + */ +@JsExport +class TagHasAllOf() : Op() { + companion object TagHasAllOf_C { + private val KEYS = NotNullProperty(StringList::class) { _,_ -> StringList() } + } + + constructor(at: String, vararg keys: String) : this() { + this.op = TAG_HAS_ALL_OF + this.at = at + val _tagKeys = this.tagKeys + for (key in keys) _tagKeys.add(key) + } + + constructor(at: Member, vararg keys: String) : this(at.name, *keys) + + var tagKeys: StringList by KEYS +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagHasAnyOf.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagHasAnyOf.kt new file mode 100644 index 000000000..9e72a04ab --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagHasAnyOf.kt @@ -0,0 +1,30 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model.request.ops + +import naksha.base.NotNullProperty +import naksha.base.StringList +import naksha.model.objects.Member +import kotlin.js.JsExport + +/** + * Tests if any of the given [keys] exist on the member at [at]. + * @since 3.0 + */ +@JsExport +class TagHasAnyOf() : Op() { + companion object TagHasAnyOf_C { + private val KEYS = NotNullProperty(StringList::class) { _,_ -> StringList() } + } + + constructor(at: String, vararg keys: String) : this() { + this.op = TAG_HAS_ANY_OF + this.at = at + val _tagKeys = this.tagKeys + for (key in keys) _tagKeys.add(key) + } + + constructor(at: Member, vararg keys: String) : this(at.name, *keys) + + var tagKeys: StringList by KEYS +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagIsNull.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagIsNull.kt new file mode 100644 index 000000000..5ed3fd60b --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagIsNull.kt @@ -0,0 +1,28 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model.request.ops + +import naksha.base.NotNullProperty +import naksha.model.objects.Member +import kotlin.js.JsExport + +/** + * Tests if the tag [key] on the member at [at] is null. + * @since 3.0 + */ +@JsExport +class TagIsNull() : Op() { + companion object TagIsNull_C { + private val KEY = NotNullProperty(String::class) { _,_ -> "" } + } + + constructor(at: String, key: String) : this() { + this.op = TAG_IS_NULL + this.at = at + this.key = key + } + + constructor(at: Member, key: String) : this(at.name, key) + + var key: String by KEY +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagListHasAllOf.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagListHasAllOf.kt new file mode 100644 index 000000000..6d11ee972 --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagListHasAllOf.kt @@ -0,0 +1,30 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model.request.ops + +import naksha.base.AnyList +import naksha.base.NotNullProperty +import naksha.model.objects.Member +import kotlin.js.JsExport + +/** + * Tests if all of the given [items] exist in the member at [at]. + * @since 3.0 + */ +@JsExport +class TagListHasAllOf() : Op() { + companion object TagListHasAllOf_C { + private val ITEMS = NotNullProperty(AnyList::class) { _,_ -> AnyList() } + } + + constructor(at: String, vararg items: Any) : this() { + this.op = TAGLIST_HAS_ALL_OF + this.at = at + val _items = this.items + for (item in items) _items.add(item) + } + + constructor(at: Member, vararg items: Any) : this(at.name, *items) + + var items: AnyList by ITEMS +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagListHasAnyOf.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagListHasAnyOf.kt new file mode 100644 index 000000000..31cf26f51 --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagListHasAnyOf.kt @@ -0,0 +1,30 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model.request.ops + +import naksha.base.AnyList +import naksha.base.NotNullProperty +import naksha.model.objects.Member +import kotlin.js.JsExport + +/** + * Tests if any of the given [items] exist in the member at [at]. + * @since 3.0 + */ +@JsExport +class TagListHasAnyOf() : Op() { + companion object TagListHasAnyOf_C { + private val ITEMS = NotNullProperty(AnyList::class) { _,_ -> AnyList() } + } + + constructor(at: String, vararg items: Any) : this() { + this.op = TAGLIST_HAS_ANY_OF + this.at = at + val _items = this.items + for (item in items) _items.add(item) + } + + constructor(at: Member, vararg items: Any) : this(at.name, *items) + + var items: AnyList by ITEMS +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagLt.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagLt.kt new file mode 100644 index 000000000..204840b89 --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagLt.kt @@ -0,0 +1,32 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model.request.ops + +import naksha.base.NotNullProperty +import naksha.base.NullableProperty +import naksha.model.objects.Member +import kotlin.js.JsExport + +/** + * Tests if the tag [key] on the member at [at] is less than the given [value]. + * @since 3.0 + */ +@JsExport +class TagLt() : Op() { + companion object TagLt_C { + private val KEY = NotNullProperty(String::class) { _,_ -> "" } + private val VALUE = NotNullProperty(Any::class) + } + + constructor(at: String, key: String, value: Any) : this() { + this.op = TAG_LT + this.at = at + this.key = key + this.value = value + } + + constructor(at: Member, key: String, value: Any) : this(at.name, key, value) + + var key: String by KEY + var value: Any by VALUE +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagLte.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagLte.kt new file mode 100644 index 000000000..de2d65fdc --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagLte.kt @@ -0,0 +1,32 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model.request.ops + +import naksha.base.NotNullProperty +import naksha.base.NullableProperty +import naksha.model.objects.Member +import kotlin.js.JsExport + +/** + * Tests if the tag [key] on the member at [at] is less than or equal to the given [value]. + * @since 3.0 + */ +@JsExport +class TagLte() : Op() { + companion object TagLte_C { + private val KEY = NotNullProperty(String::class) { _,_ -> "" } + private val VALUE = NotNullProperty(Any::class) + } + + constructor(at: String, key: String, value: Any) : this() { + this.op = TAG_LTE + this.at = at + this.key = key + this.value = value + } + + constructor(at: Member, key: String, value: Any) : this(at.name, key, value) + + var key: String by KEY + var value: Any by VALUE +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagStartsWith.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagStartsWith.kt new file mode 100644 index 000000000..9e358dc75 --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagStartsWith.kt @@ -0,0 +1,31 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model.request.ops + +import naksha.base.NotNullProperty +import naksha.model.objects.Member +import kotlin.js.JsExport + +/** + * Tests if the tag [key] on the member at [at] starts with the given [value]. + * @since 3.0 + */ +@JsExport +class TagStartsWith() : Op() { + companion object TagStartsWith_C { + private val KEY = NotNullProperty(String::class) { _,_ -> "" } + private val VALUE = NotNullProperty(String::class) { _,_ -> "" } + } + + constructor(at: String, key: String, value: String) : this() { + this.op = TAG_STARTS_WITH + this.at = at + this.key = key + this.value = value + } + + constructor(at: Member, key: String, value: String) : this(at.name, key, value) + + var key: String by KEY + var value: String by VALUE +} From be9e9e10cf51198b54f232e4e962418ead42613f Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Tue, 23 Jun 2026 11:33:26 +0200 Subject: [PATCH 31/57] Update JBON2 examples. Signed-off-by: Alexander Lowey-Weber --- docs/latest/JBON2.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/latest/JBON2.md b/docs/latest/JBON2.md index e2ad2192c..e34e7e1ed 100644 --- a/docs/latest/JBON2.md +++ b/docs/latest/JBON2.md @@ -1093,14 +1093,14 @@ object lead-in (1 byte) <-- root {…} string "highway" (8 byte) string "residential" (12 byte) string "maxSpeed" (9 byte) - tiny-int 50 (2 byte) <-- int8 lead-in + 1 byte value + int8 50 (2 byte) <-- int8 lead-in + 1 byte value string "oneway" (7 byte) boolean false (1 byte) = 1+1+3+3+5+8+11+1+1+8+12+9+2+7+1 = 73 byte ``` -So 73 byte per feature, two features ≈ **146 byte**, plus an array wrapper (lead-in + size = 2 byte) ≈ **148 byte**. Compared to 195 byte [JSON], a saving of around **24%**, which is what you would also expect from [CBOR]. Not impressive — the binary is just more compact than text. +So 73 byte per feature. Two features (146 byte) plus the array wrapper (lead-in + size = 2 byte) = **148 byte** of payload, wrapped in an array structure (lead-in + byte_size = 2 byte) = **150 byte** total. Compared to 195 byte [JSON], a saving of around **23%**, which is what you would also expect from [CBOR]. Not impressive — the binary is just more compact than text. ### Medium compression: shared `local` book A **JBON** encoder can detect that both features share the same keys and values, and lift them into a `local` [book]. The book is embedded next to the features: @@ -1137,29 +1137,29 @@ object lead-in (1 byte) <-- root {…} ref "highway" (1 byte) <-- mref4 ref "residential" (1 byte) <-- mref4 ref "maxSpeed" (1 byte) <-- mref4 - tiny-int 50 (2 byte) + int8 50 (2 byte) ref "oneway" (1 byte) <-- mref4 boolean false (1 byte) = 1+1+1+3+1+1+1+1+1+1+1+1+2+1+1 = 18 byte ``` -So per feature 18 byte. Two features (36 byte) plus the array wrapper (2 byte) plus the `local` [book] (67 byte) ≈ **105 byte**. That is **46% smaller** than [JSON] (195 byte), and crucially the saving grows with the number of features: at 100 features the book amortises over 100 × 18 + 67 ≈ 1867 byte, vs. ~9700 byte of [JSON] — an **81% reduction**. +So per feature 18 byte. Two features (36 byte) plus the `local` [book] (67 byte) plus the array wrapper (2 byte) = **105 byte** of payload, wrapped in an array structure (2 byte) = **107 byte** total. That is **45% smaller** than [JSON] (195 byte), and crucially the saving grows with the number of features: at 100 features the book amortises over 100 × 18 + 67 + 2 ≈ 1869 byte, vs. ~9700 byte of [JSON] — an **81% reduction**. ### Large compression: shared `global` book with template -If a `global` [book] is provided that defines a template _feature_ — i.e. an [Object] with `type:"Feature"`, `properties.highway:"residential"`, `properties.maxSpeed:50`, `properties.oneway:false` as default values — then a feature that matches the template only needs to encode its own `id`: +If a `global` [book] is provided that defines a template _feature_ — i.e. an [Object] with `type:"Feature"`, `properties.highway:"residential"`, `properties.maxSpeed:50`, `properties.oneway:false` as default values — then a feature that matches the template only needs to encode a reference to that template plus its own `id`. The decoder merges the template defaults with the explicitly encoded key-value pairs, so the resulting object appears as if all properties were present in the binary: ``` object lead-in (1 byte) <-- root {…} byte_size (1 byte) - ref to template (2 byte) <-- ref8 into global book + ref to properties (2 byte) <-- ref8 into global book of shared properties ref "id" (1 byte) <-- mref4 string "f1" (3 byte) = 1+1+2+1+3 = 8 byte ``` -So per feature 8 byte. Two features (16 byte) plus array wrapper (2 byte) ≈ **18 byte**, while the `global` [book] is **not** part of the file (it is shared across features, possibly across whole collections). This is a **91% reduction** vs. [JSON] (195 byte) — and the ratio improves further as more features share the same template. +So per feature 8 byte. Two features (16 byte) plus array wrapper (2 byte) = **18 byte** of payload, wrapped in an array structure (2 byte) = **20 byte** total, while the `global` [book] is **not** part of the file (it is shared across features, possibly across whole collections). This is a **90% reduction** vs. [JSON] (195 byte) — and the ratio improves further as more features share the same template. At 1,000,000 such features, [JSON] would be roughly 96 MB, while **JBON** with a shared `global` [book] would be roughly **8 MB**, regardless of where the [book] is stored. The `global` [book] itself is small _(a few hundred bytes for this example)_ and is loaded once into the decoder. @@ -1168,9 +1168,9 @@ At 1,000,000 such features, [JSON] would be roughly 96 MB, while **JBON** with a | Encoding | Per feature | 2 features total | 1,000,000 features | Reduction (2 features) | Reduction (1M features) | |----------------------------|------------:|-----------------:|-------------------:|-----------------------:|------------------------:| | [JSON] | 96 byte | 195 byte | ~96 MB | 0% | 0% | -| **JBON** _(no book)_ | 73 byte | 148 byte | ~73 MB | 24% | 24% | -| **JBON** _(`local` book)_ | 18 byte | 105 byte | ~18 MB | 46% | 81% | -| **JBON** _(`global` book)_ | 8 byte | 18 byte | ~8 MB | 91% | 92% | +| **JBON** _(no book)_ | 73 byte | 150 byte | ~73 MB | 23% | 24% | +| **JBON** _(`local` book)_ | 18 byte | 107 byte | ~18 MB | 45% | 81% | +| **JBON** _(`global` book)_ | 8 byte | 20 byte | ~8 MB | 90% | 92% | The decisive observation is that none of these levels require a different decoder or a different format version; they all use the same **JBON** binary format. The encoder picks the level it wants, the decoder is oblivious. From c5e3559b9794cc33fa066f994ae0f2a0352ca42a Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Tue, 23 Jun 2026 11:34:55 +0200 Subject: [PATCH 32/57] Add queryMembers to ReadFeatures as new member query, add converter from old query to new. Signed-off-by: Alexander Lowey-Weber --- .../naksha/model/request/ReadFeatures.kt | 11 +- .../naksha/model/request/ReadRequest.kt | 6 +- .../naksha/model/request/RequestQuery.kt | 21 +- .../kotlin/naksha/model/request/ops/Equals.kt | 2 +- .../kotlin/naksha/model/request/ops/IOp.kt | 12 + .../naksha/model/request/ops/IsAnyOf.kt | 2 +- .../kotlin/naksha/model/request/ops/Op.kt | 79 +- .../model/request/ops/PgQueryConverter.kt | 522 +++++++++++++ .../model/request/ops/TagListContains.kt | 25 + ...istHasAllOf.kt => TagListContainsAllOf.kt} | 6 +- ...istHasAnyOf.kt => TagListContainsAnyOf.kt} | 6 +- .../ops/{TagHasAllOf.kt => TagMapHasAllOf.kt} | 6 +- .../ops/{TagHasAnyOf.kt => TagMapHasAnyOf.kt} | 6 +- .../ops/{TagExists.kt => TagMapHasKey.kt} | 6 +- .../kotlin/naksha/psql/PgQueryWhereBuilder.kt | 691 +++++------------- .../commonMain/kotlin/naksha/psql/PgType.kt | 25 + 16 files changed, 856 insertions(+), 570 deletions(-) create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/IOp.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/PgQueryConverter.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagListContains.kt rename here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/{TagListHasAllOf.kt => TagListContainsAllOf.kt} (75%) rename here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/{TagListHasAnyOf.kt => TagListContainsAnyOf.kt} (75%) rename here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/{TagHasAllOf.kt => TagMapHasAllOf.kt} (77%) rename here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/{TagHasAnyOf.kt => TagMapHasAnyOf.kt} (77%) rename here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/{TagExists.kt => TagMapHasKey.kt} (75%) diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadFeatures.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadFeatures.kt index 9fb3701c5..81ac8a4c8 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadFeatures.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadFeatures.kt @@ -8,7 +8,6 @@ import naksha.base.StringList import naksha.model.GuidList import naksha.model.Version import naksha.model.request.ops.Op -import naksha.model.request.query.IMemberQuery import naksha.model.request.query.IPropertyQuery import naksha.model.request.query.ITagQuery import kotlin.js.JsExport @@ -59,7 +58,7 @@ open class ReadFeatures : ReadRequest() { * * @since 3.0 */ - @Deprecated("Replaced with op", replaceWith = ReplaceWith("op")) + @Deprecated("Remove, need always to be done on the client using post-filtering", replaceWith = ReplaceWith("op")) open fun withPropertyQuery(pQuery: IPropertyQuery?): ReadFeatures { this.query.properties = pQuery this.resultFilters.removeAll { it is PropertyFilter } @@ -194,7 +193,7 @@ open class ReadFeatures : ReadRequest() { */ // TODO: We should replace this with `tupleNumbers`, because that is what we will encode into `uuid` and that is what the clients need. // Is there any use-case for the GUID any longer? - @Deprecated("Replaced with op", replaceWith = ReplaceWith("op")) + @Deprecated("Replace with load by tuple-number, should not be part of the query!", replaceWith = ReplaceWith("op")) var guids: GuidList by GUID_LIST /** @@ -205,12 +204,12 @@ open class ReadFeatures : ReadRequest() { var query: RequestQuery by QUERY /** - * The [operation][Op] to execute. + * The [operations][Op] to execute to query members. * - * This replaces [query] and must not be used together with [query]. It actually allows to query for any member value. + * This replaces [query] and must not be used together with [query]. It actually allows to query for any member value. In doubt, [queryMembers] always wins. * @since 3.0 */ - var op: Op? by OP_OR_NULL + var queryMembers: Op? by OP_OR_NULL /** * Tests whether this request is effectively a query for all features in their current **HEAD** state, diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadRequest.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadRequest.kt index 5beecf1bd..d93aa1ccf 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadRequest.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadRequest.kt @@ -17,10 +17,8 @@ import kotlin.js.JsExport open class ReadRequest : Request() { companion object ReadRequestCompanion { private val INT_NULL = NullableProperty(Int::class) - private val BOOLEAN = - NullableProperty(Boolean::class) { _, _ -> false } - private val FETCH_MODE = - NotNullProperty(FetchMode::class) { _, _ -> FETCH_ALL } + private val BOOLEAN = NullableProperty(Boolean::class) { _, _ -> false } + private val FETCH_MODE = NotNullProperty(FetchMode::class) { _, _ -> FETCH_ALL } } override fun defaultRowOptions(): ReturnColumns = ReturnColumns.all() diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/RequestQuery.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/RequestQuery.kt index 18f4594ae..5c838938c 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/RequestQuery.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/RequestQuery.kt @@ -43,9 +43,7 @@ open class RequestQuery : AnyObject() { * @since 3.0.0 * @see ISpatialQuery */ - // TODO: We need to replace this with MemberQueries. - // Actually, in the members we can store multiple geometries, all of them can be searched. - @Deprecated("Use member queries, there can be multiple spatial members that can be searched and combined.") + @Deprecated("Use op queries, there can be multiple spatial members that can be searched and combined.") var spatial by SPATIAL_QUERY_OR_NULL /** @@ -53,9 +51,7 @@ open class RequestQuery : AnyObject() { * @since 3.0.0 * @see ITagQuery */ - // TODO: We need to replace this with MemberQueries. - // Actually, in the members we can store multiple tag-like members, all of them can be searched. - @Deprecated("Use member queries, there can be multiple tag-like members that can be searched and combined.") + @Deprecated("Use op queries, there can be multiple tag-like members that can be searched and combined.") var tags by TAG_QUERY_OR_NULL /** @@ -63,9 +59,6 @@ open class RequestQuery : AnyObject() { * @since 3.0.0 * @see IPropertyQuery */ - // TODO: Remove this completely, we should only allow to actually search for members. - // Not members must be post-filtered by the client, we can offer the helper we have for this case. - // This makes it as well very clear to the client and user, what can found fast, and what will be slow. @Deprecated("Remove this completely, we only allow to actually search for members.") var properties by PROPERTIES_QUERY_OR_NULL @@ -74,8 +67,7 @@ open class RequestQuery : AnyObject() { * @since 3.0.0 * @see IMemberQuery */ - // TODO: Because actually everything boils down to member-queries only, we should move this into the ReadFeatures directly. - // We only need this property in ReadFeaturs, so that clients can defined how indices they have created are used. + @Deprecated("Use op queries, meta has been removed.") var members by MEMBER_QUERY_OR_NULL /** @@ -84,8 +76,7 @@ open class RequestQuery : AnyObject() { * If the list is empty, no limit is applied. * @since 3.0.0 */ - // TODO: Remove this completely, clients that need spatial queries should use spatial members. - @Deprecated("Please use spatial members instead") + @Deprecated("Use op queries, there can be multiple refTiles-like members that can be searched and combined.") var refTiles by INT_LIST /** @@ -94,7 +85,7 @@ open class RequestQuery : AnyObject() { * @return this. * @since 3.0.0 */ - @Deprecated("Please use spatial members instead") + @Deprecated("Please use op queries instead") fun addRefTile(tile: HereTile): RequestQuery { refTiles.add(tile.intKey) return this @@ -106,7 +97,7 @@ open class RequestQuery : AnyObject() { * @return this. * @since 3.0.0 */ - @Deprecated("Please use spatial members instead") + @Deprecated("Please use op queries instead") fun removeRefTile(tile: HereTile): RequestQuery { refTiles.remove(tile.intKey) return this diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Equals.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Equals.kt index 98458ef25..c16f88c61 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Equals.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Equals.kt @@ -11,7 +11,7 @@ import kotlin.js.JsExport * @since 3.0 */ @JsExport -class Equals() : Op() { +class Equals() : Op(), ICompare { companion object Equals_C { private val VALUE = NullableProperty(Any::class) } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/IOp.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/IOp.kt new file mode 100644 index 000000000..74b546eb8 --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/IOp.kt @@ -0,0 +1,12 @@ +@file:OptIn(ExperimentalJsExport::class) + +package naksha.model.request.ops + +import kotlin.js.ExperimentalJsExport +import kotlin.js.JsExport + +/** + * Marker interface added to all real operation. + */ +@JsExport +interface IOp diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/IsAnyOf.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/IsAnyOf.kt index 9df78143b..43af0ecc9 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/IsAnyOf.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/IsAnyOf.kt @@ -18,7 +18,7 @@ class IsAnyOf() : Op() { } constructor(at: String, vararg items: Any) : this() { - this.op = ANY_OF + this.op = IS_ANY_OF this.at = at val _items = this.items for (item in items) _items.add(item) diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Op.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Op.kt index 9ff27f243..c47fcb9ed 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Op.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Op.kt @@ -34,20 +34,24 @@ open class Op : AnyObject() { const val LT = "lt" const val LTE = "lte" const val STARTS_WITH = "starts_with" - const val ANY_OF = "any_of" + const val IS_ANY_OF = "any_of" const val INTERSECTS = "intersects" - const val TAG_EXISTS = "tag_exists" - const val TAG_HAS_ANY_OF = "tag_has_any_of" - const val TAG_HAS_ALL_OF = "tag_has_all_of" + const val TAGMAP_HAS_KEY = "tagmap_has_key" + const val TAGMAP_HAS_ANY_OF = "tagmap_has_any_of" + const val TAGMAP_HAS_ALL_OF = "tagmap_has_all_of" const val TAG_IS_NULL = "tag_is_null" const val TAG_EQ = "tag_eq" - const val TAG_STARTS_WITH = "tag_starts_with" const val TAG_GT = "tag_gt" const val TAG_GTE = "tag_gte" const val TAG_LT = "tag_lt" const val TAG_LTE = "tag_lte" - const val TAGLIST_HAS_ANY_OF = "taglist_has_any_of" - const val TAGLIST_HAS_ALL_OF = "taglist_has_all_of" + const val TAG_STARTS_WITH = "tag_starts_with" + @Suppress("SpellCheckingInspection") + const val TAGLIST_CONTAINS = "taglist_contains" + @Suppress("SpellCheckingInspection") + const val TAGLIST_CONTAINS_ANY_OF = "taglist_contains_any_of" + @Suppress("SpellCheckingInspection") + const val TAGLIST_CONTAINS_ALL_OF = "taglist_contains_all_of" /** * Auto-detect the concrete type of member operation and return the cast real type. @@ -56,36 +60,39 @@ open class Op : AnyObject() { */ @JvmStatic @JsStatic - fun detect(op: MapProxy<*,*>): Op? = when(op.getRaw("op") as String?) { - AND -> op.proxy(And::class) - OR -> op.proxy(Or::class) - NOT -> op.proxy(Not::class) - IS_NULL -> op.proxy(IsNull::class) - IS_TRUE -> op.proxy(IsTrue::class) - IS_FALSE -> op.proxy(IsFalse::class) - EQ -> op.proxy(Equals::class) - GT -> op.proxy(Gt::class) - GTE -> op.proxy(Gte::class) - LT -> op.proxy(Lt::class) - LTE -> op.proxy(Lte::class) - STARTS_WITH -> op.proxy(StartsWith::class) - ANY_OF -> op.proxy(IsAnyOf::class) - INTERSECTS -> op.proxy(Intersects::class) - TAG_EXISTS -> op.proxy(TagExists::class) - TAG_HAS_ANY_OF -> op.proxy(TagHasAnyOf::class) - TAG_HAS_ALL_OF -> op.proxy(TagHasAllOf::class) - TAG_IS_NULL -> op.proxy(TagIsNull::class) - TAG_EQ -> op.proxy(TagEquals::class) - TAG_STARTS_WITH -> op.proxy(TagStartsWith::class) - TAG_GT -> op.proxy(TagGt::class) - TAG_GTE -> op.proxy(TagGte::class) - TAG_LT -> op.proxy(TagLt::class) - TAG_LTE -> op.proxy(TagLte::class) - TAGLIST_HAS_ANY_OF -> op.proxy(TagListHasAnyOf::class) - TAGLIST_HAS_ALL_OF -> op.proxy(TagListHasAllOf::class) - else -> null + fun detect(op: MapProxy<*,*>): Op? { + if (op is Op && op::class != Op::class) return op + return when(op.getRaw("op") as String?) { + AND -> op.proxy(And::class) + OR -> op.proxy(Or::class) + NOT -> op.proxy(Not::class) + IS_NULL -> op.proxy(IsNull::class) + IS_TRUE -> op.proxy(IsTrue::class) + IS_FALSE -> op.proxy(IsFalse::class) + EQ -> op.proxy(Equals::class) + GT -> op.proxy(Gt::class) + GTE -> op.proxy(Gte::class) + LT -> op.proxy(Lt::class) + LTE -> op.proxy(Lte::class) + STARTS_WITH -> op.proxy(StartsWith::class) + IS_ANY_OF -> op.proxy(IsAnyOf::class) + INTERSECTS -> op.proxy(Intersects::class) + TAGMAP_HAS_KEY -> op.proxy(TagMapHasKey::class) + TAGMAP_HAS_ANY_OF -> op.proxy(TagMapHasAnyOf::class) + TAGMAP_HAS_ALL_OF -> op.proxy(TagMapHasAllOf::class) + TAG_IS_NULL -> op.proxy(TagIsNull::class) + TAG_EQ -> op.proxy(TagEquals::class) + TAG_STARTS_WITH -> op.proxy(TagStartsWith::class) + TAG_GT -> op.proxy(TagGt::class) + TAG_GTE -> op.proxy(TagGte::class) + TAG_LT -> op.proxy(TagLt::class) + TAG_LTE -> op.proxy(TagLte::class) + TAGLIST_CONTAINS_ANY_OF -> op.proxy(TagListContainsAnyOf::class) + TAGLIST_CONTAINS_ALL_OF -> op.proxy(TagListContainsAllOf::class) + else -> null + } } - } + } /** * The operation identifier. diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/PgQueryConverter.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/PgQueryConverter.kt new file mode 100644 index 000000000..3e0ac707c --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/PgQueryConverter.kt @@ -0,0 +1,522 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model.request.ops + +import naksha.base.AnyList +import naksha.base.Int64 +import naksha.base.ListProxy +import naksha.base.Platform.PlatformCompanion.toJSON +import naksha.base.StringList +import naksha.geo.HereTile +import naksha.geo.SpGeometry +import naksha.model.Naksha +import naksha.model.NakshaError +import naksha.model.NakshaException +import naksha.model.illegalArg +import naksha.model.objects.StandardMembers +import naksha.model.request.RequestQuery +import kotlin.js.JsExport + +@JsExport +class PgQueryConverter private constructor() { + companion object PgQueryConverter_C { + fun convert(query: RequestQuery): Op? { + // TODO: Implement a convertion. + return null + } + } + + + private fun whereFeatureId() { + // Partition into numeric IDs (fn >= 0, id stored as NULL in DB) and named IDs (fn < 0, id NOT NULL). + val reqIds: StringList = request.featureIds + val featureNumbers: MutableList = mutableListOf() + val featureIds: MutableList = mutableListOf() + for (id in reqIds) { + if (id == null) continue + val fn = Naksha.featureNumber(id) + if (fn >= Int64(0)) { + featureNumbers.add(fn) + } else { + featureIds.add(id) + } + } + if (featureNumbers.isEmpty() && featureIds.isEmpty()) return + + // For each collection: + if (where.isNotEmpty()) where.append(" AND ") + + where.append("( ") + if (featureIds.isNotEmpty()) { + val placeholder: String = placeholderForArg(featureIds.toTypedArray(), PgType.STRING_ARRAY) + val ID = collection.column(StandardMembers.Id) ?: throw illegalArg("Collection does not defined `id` column") + where.append(ID.ident).append(" = ANY(").append(placeholder).append(")") + } + if (featureNumbers.isNotEmpty()) { + if (featureIds.isNotEmpty()) where.append(" OR ") + + val placeholder: String = placeholderForArg(featureNumbers.toTypedArray(), PgType.INT64_ARRAY) + if (where.isNotEmpty()) where.append(" AND ") + where.append(FN.ident).append(" = ANY(").append(placeholder).append(")") + } + where.append(")") + } + + private fun whereGuids() { + val tupleNumbers = request.guids.mapNotNull { it?.tupleNumber } + if (tupleNumbers.isNotEmpty()) { + if (where.isNotEmpty()) where.append(" AND ") + + val fns = arrayOfNulls(tupleNumbers.size) + val versions = arrayOfNulls(tupleNumbers.size) + for (i in tupleNumbers.indices) { + fns[i] = tupleNumbers[i].featureNumber + versions[i] = tupleNumbers[i].version + } + val featureNumbersArg = placeholderForArg(fns, PgType.INT64_ARRAY) + val versionsArg = placeholderForArg(versions, PgType.INT64_ARRAY) + where.append("($FN, $VERSION) IN (SELECT * FROM unnest($featureNumbersArg::int8[], $versionsArg::int8[]))") + } + } + + private fun whereVersion() { + val version = request.version + if (version != null) { + if (where.isNotEmpty()) where.append(" AND ") + where.append("$VERSION <= ${version.number}") + } + val minVersion = request.minVersion + if (minVersion != null) { + if (where.isNotEmpty()) where.append(" AND ") + where.append("$VERSION >= ${minVersion.number}") + } + } + + private fun whereSpatial() { + val spatialQuery = request.query.spatial + if (spatialQuery != null) { + if (where.isNotEmpty()) { + where.append(" AND (") + } else { + where.append(" (") + } + whereNestedSpatial(spatialQuery) + where.append(")") + } + } + + private fun whereNestedSpatial(spatial: ISpatialQuery) { + when (spatial) { + is SpNot -> not( + subClause = spatial.query, + subClauseResolver = this::whereNestedSpatial + ) + + is SpAnd -> and( + subClauses = spatial.filterNotNull(), + subClauseResolver = this::whereNestedSpatial + ) + + is SpOr -> or( + subClauses = spatial.filterNotNull(), + subClauseResolver = this::whereNestedSpatial + ) + + is SpIntersects -> { + val queryGeometry = nakshaGeometry(spatial.geometry) + val geometryToCompare = when (val transformation = spatial.transformation) { + null -> queryGeometry + else -> resolveTransformation(transformation, queryGeometry) + } + where.append("ST_Intersects(naksha_2d(${PgColumn.geo}), $geometryToCompare)") + } + + is SpRefInHereTile -> { + where.append(refPointInTile(spatial.getHereTile())) + } + + else -> throw NakshaException( + NakshaError.ILLEGAL_ARGUMENT, + "Invalid spatial query found: $spatial" + ) + } + } + + private fun nakshaGeometry(geometry: SpGeometry): String { + val geoBytes = Naksha.encodeGeometry(geometry) + val geoBytesPlaceholder = placeholderForArg(geoBytes, PgType.BYTE_ARRAY) + return "naksha_2d($geoBytesPlaceholder)" + } + + private fun resolveTransformation( + transformation: SpTransformation, + basicGeometry: String + ): String { + return when (transformation) { + is SpBuffer -> resolveBuffer(transformation, basicGeometry) + else -> throw NakshaException( + NakshaError.UNSUPPORTED_OPERATION, + "This transformation is not yet supported: ${transformation::class.simpleName}" + ) + } + } + + private fun resolveBuffer(buffer: SpBuffer, basicGeometry: String): String { + val geo = if (buffer.geography) { + "$basicGeometry::geography" + } else { + basicGeometry + } + val distancePlaceholder = placeholderForArg(buffer.distance, PgType.DOUBLE) + val bufferStyleParams = bufferStyleParams(buffer) + return if (bufferStyleParams != null) { + "ST_Buffer($geo, $distancePlaceholder, $bufferStyleParams)" + } else { + "ST_Buffer($geo, $distancePlaceholder)" + } + } + + private fun bufferStyleParams(buffer: SpBuffer): String? { + val bufferStyleParams = StringBuilder() + if (buffer.quadSegments != null) { + val quadSegPlaceholder = placeholderForArg(buffer.quadSegments, PgType.INT) + bufferStyleParams.append("quad_segs=$quadSegPlaceholder") + } + if (buffer.joinStyle != null) { + val joinStylePlaceholder = placeholderForArg(buffer.joinStyle!!.value, PgType.STRING) + if (bufferStyleParams.isNotEmpty()) bufferStyleParams.append(" ") + bufferStyleParams.append("join=$joinStylePlaceholder") + } + if (buffer.joinLimit != null) { + val joinLimitPlaceholder = placeholderForArg(buffer.joinLimit, PgType.DOUBLE) + if (bufferStyleParams.isNotEmpty()) bufferStyleParams.append(" ") + bufferStyleParams.append("mitre_limit=$joinLimitPlaceholder") + } + if (buffer.endCap != null) { + val endCapPlaceholder = placeholderForArg(buffer.endCap!!.value, PgType.STRING) + if (bufferStyleParams.isNotEmpty()) bufferStyleParams.append(" ") + bufferStyleParams.append("endcap=$endCapPlaceholder") + } + if (buffer.side != null) { + val sidePlaceholder = placeholderForArg(buffer.side!!.value, PgType.STRING) + if (bufferStyleParams.isNotEmpty()) bufferStyleParams.append(" ") + bufferStyleParams.append("side=$sidePlaceholder") + } + return if (bufferStyleParams.isNotEmpty()) { + bufferStyleParams.toString() + } else { + null + } + } + + private fun whereRefTiles() { + val hereTiles = request.query.refTiles + .filterNotNull() + .map { HereTile(it) } + if (hereTiles.isNotEmpty()) { + if (where.isNotEmpty()) { + where.append(" AND (") + } else { + where.append(" (") + } + where.append(refPointInAnyOfTiles(hereTiles)) + where.append(")") + } + } + + private fun refPointInAnyOfTiles(hereTiles: List): String { + return hereTiles.joinToString(separator = " OR ") { hereTile -> + refPointInTile(hereTile) + } + } + + private fun refPointInTile(hereTile: HereTile): String { + val lowerBoundPlaceholder = placeholderForArg( + hereTile.maxLevelLowerBound().intKey, + PgType.INT + ) + val upperBoundPlaceholder = placeholderForArg( + hereTile.maxLevelUpperBound().intKey, + PgType.INT + ) + return "(${PgColumn.here_tile} >= $lowerBoundPlaceholder AND ${PgColumn.here_tile} <= $upperBoundPlaceholder)" + } + + private fun whereMetadata() { + val metaQuery = request.query.members + if (metaQuery != null) { + if (where.isNotEmpty()) { + where.append(" AND (") + } else { + where.append(" (") + } + whereNestedMetadata(metaQuery) + where.append(")") + } + } + + private fun whereNestedMetadata(metaQuery: IMemberQuery) { + when (metaQuery) { + is MemberNot -> not( + subClause = metaQuery.query, + subClauseResolver = this::whereNestedMetadata + ) + + is MemberAnd -> and( + subClauses = metaQuery.filterNotNull(), + subClauseResolver = this::whereNestedMetadata + ) + + is MemberOr -> or( + subClauses = metaQuery.filterNotNull(), + subClauseResolver = this::whereNestedMetadata + ) + + is MemberQuery -> { + val isActionQuery = metaQuery.member == MetaColumn.action() + val pgColumn = + if (isActionQuery) { + PgColumn.version + } else { + PgColumn.ofRowColumn(metaQuery.member) ?: throw NakshaException( + NakshaError.ILLEGAL_STATE, + "Couldn't find PgColumn for TupleColumn: ${metaQuery.member.name}" + ) + } + val leftOperand = if (isActionQuery) { + "(${PgColumn.version.name} & 3)::int4" + } else if (pgColumn == PgColumn.created_at || pgColumn == PgColumn.author_ts) { + "COALESCE(${pgColumn.name}, ${PgColumn.updated_at.name})" + } else { + pgColumn.name + } + // Action lives in the lower 2 bits of `version`; the comparison value is a small int. + val placeholderType = if (isActionQuery) PgType.INT else pgColumn.type + val resolvedQuery = when (val op = metaQuery.op) { + is StringOp -> { + val placeholder = placeholderForArg(metaQuery.value, placeholderType) + resolveStringOp(op, leftOperand, placeholder) + } + is DoubleOp -> { + val placeholder = placeholderForArg(metaQuery.value, placeholderType) + resolveDoubleOp(op, leftOperand, placeholder) + } + AnyOp.IS_ANY_OF -> { + val placeholder = placeholderForArg(metaQuery.value, arrayTypeFor(placeholderType)) + "$leftOperand = ANY($placeholder)" + } + else -> throw illegalArg("Unknown op type: ${op::class.simpleName}") + } + where.append(resolvedQuery) + } + + else -> throw NakshaException( + NakshaError.ILLEGAL_ARGUMENT, + "Unknown metadata query type: ${metaQuery::class.simpleName}" + ) + } + } + + private fun arrayTypeFor(pgType: PgType): PgType { + return when (pgType) { + PgType.BOOLEAN -> PgType.BOOLEAN_ARRAY + PgType.SHORT -> PgType.SHORT_ARRAY + PgType.INT -> PgType.INT_ARRAY + PgType.INT64 -> PgType.INT64_ARRAY + PgType.FLOAT -> PgType.FLOAT_ARRAY + PgType.DOUBLE -> PgType.DOUBLE_ARRAY + PgType.STRING -> PgType.STRING_ARRAY + PgType.BYTE_ARRAY -> PgType.BYTE_ARRAY_ARRAY + else -> throw illegalArg("Unknown array type for PgType: ${pgType::class.simpleName}") + } + } + + private fun whereTags() { + val tagQuery = request.query.tags + if (tagQuery != null) { + if (where.isNotEmpty()) { + where.append(" AND (") + } else { + where.append(" (") + } + whereNestedTags(tagQuery) + where.append(")") + } + } + + private fun whereNestedTags(tagQuery: ITagQuery) { + when (tagQuery) { + is TagSetContains -> resolveTagSetContains(tagQuery) + is TagNot -> not(tagQuery.query, this::whereNestedTags) + is TagOr -> { + if(containsOnlyTagExists(tagQuery)){ + // for tags without values we can utilize top-level-key based '?|' operand + // https://www.postgresql.org/docs/current/functions-json.html#FUNCTIONS-JSONB-OP-TABLE + val tagNames = tagQuery.filterIsInstance().map { it.name } + resolveTagNamesArrayOperation( + jsonbOperator = "?|", // 'jsonb_exists_any' is equivalent but will not hit the GIN index + tagNames = tagNames + ) + } else { + or(tagQuery.filterNotNull(), this::whereNestedTags) + } + } + is TagAnd -> { + if(containsOnlyTagExists(tagQuery)){ + // for tags without values we can utilize top-level-key based '?&' operand + // https://www.postgresql.org/docs/current/functions-json.html#FUNCTIONS-JSONB-OP-TABLE + val tagNames = tagQuery.filterIsInstance().map { it.name } + resolveTagNamesArrayOperation( + jsonbOperator = "?&", // 'jsonb_exists_all' is equivalent but MIGHT not hit the GIN index + tagNames = tagNames + ) + } else { + and(tagQuery.filterNotNull(), this::whereNestedTags) + } + } + is TagQuery -> resolveSingleTagQuery(tagQuery) + } + } + + private fun containsOnlyTagExists(container: ListProxy): Boolean = + container.all { it == null || it is TagMapHasKey } + + /** + * Element containment on a set-form tags column (jsonb array): `tags @> '[]'::jsonb`. + * The `@>` operator matches the element in its type (string, boolean, number) and is supported + * by the GIN index over the column. + */ + private fun resolveTagSetContains(tagQuery: TagSetContains) { + val element = AnyList() + element.add(tagQuery.element) + val placeholder = placeholderForArg(toJSON(element), PgType.STRING) + where.append("$tagsAsJsonb @> $placeholder::jsonb") + } + + private fun resolveTagNamesArrayOperation(jsonbOperator: String, tagNames: List) { + val tagKeysArray = tagNames.toTypedArray() + val tagKeysPlaceholder = placeholderForArg(tagKeysArray, PgType.STRING_ARRAY) + where.append("$tagsAsJsonb ?$jsonbOperator $tagKeysPlaceholder") + } + + private fun resolveSingleTagQuery(tagQuery: TagQuery) { + when (tagQuery) { + is TagMapHasKey -> { + val tagNamePlaceholder = placeholderForArg(tagQuery.name, PgType.STRING) + where.append("$tagsAsJsonb ?? $tagNamePlaceholder") + } + + is TagValueIsNull -> { + val tagValuePlaceholder = placeholderForArg(selectTagValue(tagQuery), PgType.STRING) + where.append("$tagValuePlaceholder IS NULL") + } + + is TagValueIsBool -> { + if (tagQuery.value) { + where.append(selectTagValue(tagQuery, PgType.BOOLEAN)) + } else { + where.append("not(${selectTagValue(tagQuery, PgType.BOOLEAN)})") + } + } + + is TagValueIsDouble -> { + val queryValuePlaceholder = placeholderForArg(tagQuery.value, PgType.DOUBLE) + val doubleOp = resolveDoubleOp( + tagQuery.op, + selectTagValue(tagQuery, PgType.DOUBLE), + queryValuePlaceholder + ) + where.append(doubleOp) + } + + is TagValueIsString -> { + val queryValuePlaceholder = placeholderForArg(tagQuery.value, PgType.STRING) + val stringEquals = resolveStringOp( + StringOp.EQUALS, + selectTagValue(tagQuery, PgType.STRING), + queryValuePlaceholder + ) + where.append(stringEquals) + } + + is TagValueMatches -> { + val jsonPathPlaceholder = placeholderForArg( + "\$.${tagQuery.name} ? (@ like_regex \"${tagQuery.regex}\")", + PgType.STRING + ) + where.append("$tagsAsJsonb @?? $jsonPathPlaceholder::jsonpath") + } + } + } + + private fun selectTagValue(tagQuery: TagQuery, castTo: PgType? = null): String { + val tagKeyPlaceholder = placeholderForArg(tagQuery.name, PgType.STRING) + return when (castTo) { + null -> "$tagsAsJsonb->$tagKeyPlaceholder" + PgType.STRING -> "$tagsAsJsonb->>$tagKeyPlaceholder" + else -> "($tagsAsJsonb->$tagKeyPlaceholder)::${castTo.value}" + } + } + + private fun not(subClause: T, subClauseResolver: (T) -> Unit) { + where.append(" NOT (") + subClauseResolver(subClause) + where.append(") ") + } + + private fun and(subClauses: List, subClauseResolver: (T) -> Unit) = + multiClause("AND", subClauses, subClauseResolver) + + private fun or(subClauses: List, subClauseResolver: (T) -> Unit) = + multiClause("OR", subClauses, subClauseResolver) + + private fun multiClause( + operand: String, + subClauses: List, + subClauseResolver: (T) -> Unit + ) { + where.append(" (") + subClauses.forEachIndexed { index, subClause -> + if (index > 0) { + where.append(" $operand ") + } + subClauseResolver(subClause) + } + where.append(") ") + } + + + private fun resolveStringOp( + stringOp: StringOp, + leftOperand: String, + rightOperand: String + ): String { + return when (stringOp) { + StringOp.EQUALS -> "$leftOperand = $rightOperand" + StringOp.NOT_EQUALS -> "$leftOperand != $rightOperand" + StringOp.STARTS_WITH -> "starts_with($leftOperand, $rightOperand)" + else -> throw NakshaException( + NakshaError.ILLEGAL_ARGUMENT, + "Unknown StringOp: $stringOp" + ) + } + } + + private fun resolveDoubleOp( + doubleOp: DoubleOp, + leftOperand: String, + rightOperand: String + ): String { + return when (doubleOp) { + DoubleOp.EQ -> "$leftOperand = $rightOperand" + DoubleOp.NE -> "$leftOperand != $rightOperand" + DoubleOp.GT -> "$leftOperand > $rightOperand" + DoubleOp.GTE -> "$leftOperand >= $rightOperand" + DoubleOp.LT -> "$leftOperand < $rightOperand" + DoubleOp.LTE -> "$leftOperand <= $rightOperand" + else -> throw NakshaException( + NakshaError.ILLEGAL_ARGUMENT, + "Unknown DoubleOp: $doubleOp" + ) + } + }} \ No newline at end of file diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagListContains.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagListContains.kt new file mode 100644 index 000000000..adf29d768 --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagListContains.kt @@ -0,0 +1,25 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model.request.ops + +import naksha.base.NotNullProperty +import kotlin.js.JsExport + +/** + * Tests if the given [item] exist in the member at [at], expects the member to be a tag-list. + * @since 3.0 + */ +@JsExport +class TagListContains() : Op() { + companion object TagListHasAllOf_C { + private val ITEM = NotNullProperty(Any::class) + } + + constructor(at: String, item: Any) : this() { + this.op = TAGLIST_CONTAINS + this.at = at + this.item = item + } + + var item: Any by ITEM +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagListHasAllOf.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagListContainsAllOf.kt similarity index 75% rename from here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagListHasAllOf.kt rename to here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagListContainsAllOf.kt index 6d11ee972..d4cb73122 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagListHasAllOf.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagListContainsAllOf.kt @@ -12,13 +12,13 @@ import kotlin.js.JsExport * @since 3.0 */ @JsExport -class TagListHasAllOf() : Op() { +class TagListContainsAllOf() : Op() { companion object TagListHasAllOf_C { - private val ITEMS = NotNullProperty(AnyList::class) { _,_ -> AnyList() } + private val ITEMS = NotNullProperty(AnyList::class) { _, _ -> AnyList() } } constructor(at: String, vararg items: Any) : this() { - this.op = TAGLIST_HAS_ALL_OF + this.op = TAGLIST_CONTAINS_ALL_OF this.at = at val _items = this.items for (item in items) _items.add(item) diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagListHasAnyOf.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagListContainsAnyOf.kt similarity index 75% rename from here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagListHasAnyOf.kt rename to here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagListContainsAnyOf.kt index 31cf26f51..b84f63e47 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagListHasAnyOf.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagListContainsAnyOf.kt @@ -12,13 +12,13 @@ import kotlin.js.JsExport * @since 3.0 */ @JsExport -class TagListHasAnyOf() : Op() { +class TagListContainsAnyOf() : Op() { companion object TagListHasAnyOf_C { - private val ITEMS = NotNullProperty(AnyList::class) { _,_ -> AnyList() } + private val ITEMS = NotNullProperty(AnyList::class) { _, _ -> AnyList() } } constructor(at: String, vararg items: Any) : this() { - this.op = TAGLIST_HAS_ANY_OF + this.op = TAGLIST_CONTAINS_ANY_OF this.at = at val _items = this.items for (item in items) _items.add(item) diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagHasAllOf.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagMapHasAllOf.kt similarity index 77% rename from here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagHasAllOf.kt rename to here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagMapHasAllOf.kt index 37824028a..80f8cc413 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagHasAllOf.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagMapHasAllOf.kt @@ -12,13 +12,13 @@ import kotlin.js.JsExport * @since 3.0 */ @JsExport -class TagHasAllOf() : Op() { +class TagMapHasAllOf() : Op() { companion object TagHasAllOf_C { - private val KEYS = NotNullProperty(StringList::class) { _,_ -> StringList() } + private val KEYS = NotNullProperty(StringList::class) { _, _ -> StringList() } } constructor(at: String, vararg keys: String) : this() { - this.op = TAG_HAS_ALL_OF + this.op = TAGMAP_HAS_ALL_OF this.at = at val _tagKeys = this.tagKeys for (key in keys) _tagKeys.add(key) diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagHasAnyOf.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagMapHasAnyOf.kt similarity index 77% rename from here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagHasAnyOf.kt rename to here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagMapHasAnyOf.kt index 9e72a04ab..31fbbe1de 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagHasAnyOf.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagMapHasAnyOf.kt @@ -12,13 +12,13 @@ import kotlin.js.JsExport * @since 3.0 */ @JsExport -class TagHasAnyOf() : Op() { +class TagMapHasAnyOf() : Op() { companion object TagHasAnyOf_C { - private val KEYS = NotNullProperty(StringList::class) { _,_ -> StringList() } + private val KEYS = NotNullProperty(StringList::class) { _, _ -> StringList() } } constructor(at: String, vararg keys: String) : this() { - this.op = TAG_HAS_ANY_OF + this.op = TAGMAP_HAS_ANY_OF this.at = at val _tagKeys = this.tagKeys for (key in keys) _tagKeys.add(key) diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagExists.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagMapHasKey.kt similarity index 75% rename from here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagExists.kt rename to here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagMapHasKey.kt index c1bfd691f..ecbc5c323 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagExists.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagMapHasKey.kt @@ -11,13 +11,13 @@ import kotlin.js.JsExport * @since 3.0 */ @JsExport -class TagExists() : Op() { +class TagMapHasKey() : Op() { companion object TagExists_C { - private val KEY = NotNullProperty(String::class) { _,_ -> "" } + private val KEY = NotNullProperty(String::class) { _, _ -> "" } } constructor(at: String, key: String) : this() { - this.op = TAG_EXISTS + this.op = TAGMAP_HAS_KEY this.at = at this.key = key } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt index 3cd5ffbb7..3e1ec25cf 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt @@ -1,19 +1,8 @@ package naksha.psql -import naksha.base.AnyList -import naksha.base.Int64 -import naksha.base.ListProxy -import naksha.base.Platform.PlatformCompanion.toJSON -import naksha.base.StringList -import naksha.geo.HereTile -import naksha.geo.SpGeometry import naksha.model.* -import naksha.model.objects.Member -import naksha.model.objects.StandardMembers import naksha.model.request.ReadFeatures -import naksha.model.request.query.* -import naksha.psql.PgColumn.PgColumn_C.FN -import naksha.psql.PgColumn.PgColumn_C.VERSION +import naksha.model.request.ops.* /** * Helper to convert a [ReadFeatures] request into a sql `WHERE` query. @@ -32,520 +21,238 @@ internal class PgQueryWhereBuilder(private val request: ReadFeatures, private va /** * Convert the request into `WHERE` queries. - * @return the [PgQueryWhereClause] for each collection given. + * @return the [PgQueryWhereClause]; `null` if basically everything should be read. * @since 3.0 */ - fun build(): PgQueryWhereClause { - whereFeatureId() - whereGuids() - whereVersion() - whereMetadata() - whereSpatial() - whereRefTiles() - whereTags() + fun build(): PgQueryWhereClause? { + var op: Op? = request.queryMembers + if (op == null) op = PgQueryConverter.convert(request.query) + // TODO: Convert `featureIds` + // TODO: Convert `guids` + if (op == null) return null + applyOp(op) return PgQueryWhereClause(collection, where.toString(), argValues, argTypes) } - private fun whereFeatureId() { - // Partition into numeric IDs (fn >= 0, id stored as NULL in DB) and named IDs (fn < 0, id NOT NULL). - val reqIds: StringList = request.featureIds - val featureNumbers: MutableList = mutableListOf() - val featureIds: MutableList = mutableListOf() - for (id in reqIds) { - if (id == null) continue - val fn = Naksha.featureNumber(id) - if (fn >= Int64(0)) { - featureNumbers.add(fn) - } else { - featureIds.add(id) - } - } - if (featureNumbers.isEmpty() && featureIds.isEmpty()) return - - // For each collection: - if (where.isNotEmpty()) where.append(" AND ") - - where.append("( ") - if (featureIds.isNotEmpty()) { - val placeholder: String = placeholderForArg(featureIds.toTypedArray(), PgType.STRING_ARRAY) - val ID = collection.column(StandardMembers.Id) ?: throw illegalArg("Collection does not defined `id` column") - where.append(ID.ident).append(" = ANY(").append(placeholder).append(")") - } - if (featureNumbers.isNotEmpty()) { - if (featureIds.isNotEmpty()) where.append(" OR ") - - val placeholder: String = placeholderForArg(featureNumbers.toTypedArray(), PgType.INT64_ARRAY) - if (where.isNotEmpty()) where.append(" AND ") - where.append(FN.ident).append(" = ANY(").append(placeholder).append(")") - } - where.append(")") - } - - private fun whereGuids() { - val tupleNumbers = request.guids.mapNotNull { it?.tupleNumber } - if (tupleNumbers.isNotEmpty()) { - if (where.isNotEmpty()) where.append(" AND ") - - val fns = arrayOfNulls(tupleNumbers.size) - val versions = arrayOfNulls(tupleNumbers.size) - for (i in tupleNumbers.indices) { - fns[i] = tupleNumbers[i].featureNumber - versions[i] = tupleNumbers[i].version - } - val featureNumbersArg = placeholderForArg(fns, PgType.INT64_ARRAY) - val versionsArg = placeholderForArg(versions, PgType.INT64_ARRAY) - where.append("($FN, $VERSION) IN (SELECT * FROM unnest($featureNumbersArg::int8[], $versionsArg::int8[]))") - } - } - - private fun whereVersion() { - val version = request.version - if (version != null) { - if (where.isNotEmpty()) where.append(" AND ") - where.append("$VERSION <= ${version.number}") - } - val minVersion = request.minVersion - if (minVersion != null) { - if (where.isNotEmpty()) where.append(" AND ") - where.append("$VERSION >= ${minVersion.number}") - } - } - - private fun whereSpatial() { - val spatialQuery = request.query.spatial - if (spatialQuery != null) { - if (where.isNotEmpty()) { - where.append(" AND (") - } else { - where.append(" (") + private fun applyOp(rawOp: Op, negate: Boolean = false) { + val opName = rawOp.op + val op = Op.detect(rawOp) ?: throw illegalArg("Unknown operation: '$opName'") + when (op) { + is And -> { + if (negate) where.append(" NOT ") + val children = op.children + if (children.size > 1) where.append('(') + var first = true + for (child in children) { + if (child == null) continue + if (first) first = false else where.append(" AND ") + applyOp(child) + } + if (children.size > 1) where.append(") ") else where.append(" ") + return } - whereNestedSpatial(spatialQuery) - where.append(")") - } - } - - private fun whereNestedSpatial(spatial: ISpatialQuery) { - when (spatial) { - is SpNot -> not( - subClause = spatial.query, - subClauseResolver = this::whereNestedSpatial - ) - - is SpAnd -> and( - subClauses = spatial.filterNotNull(), - subClauseResolver = this::whereNestedSpatial - ) - - is SpOr -> or( - subClauses = spatial.filterNotNull(), - subClauseResolver = this::whereNestedSpatial - ) - - is SpIntersects -> { - val queryGeometry = nakshaGeometry(spatial.geometry) - val geometryToCompare = when (val transformation = spatial.transformation) { - null -> queryGeometry - else -> resolveTransformation(transformation, queryGeometry) + is Or -> { + if (negate) where.append(" NOT ") + val children = op.children + if (children.size > 1) where.append('(') + var first = true + for (child in children) { + if (child == null) continue + if (first) first = false else where.append(" OR ") + applyOp(child) } - where.append("ST_Intersects(naksha_2d(${PgColumn.geo}), $geometryToCompare)") + if (children.size > 1) where.append(") ") else where.append(" ") + return } - - is SpRefInHereTile -> { - where.append(refPointInTile(spatial.getHereTile())) + is Not -> { + applyOp(op.child, !negate) + return } - - else -> throw NakshaException( - NakshaError.ILLEGAL_ARGUMENT, - "Invalid spatial query found: $spatial" - ) - } - } - - private fun nakshaGeometry(geometry: SpGeometry): String { - val geoBytes = Naksha.encodeGeometry(geometry) - val geoBytesPlaceholder = placeholderForArg(geoBytes, PgType.BYTE_ARRAY) - return "naksha_2d($geoBytesPlaceholder)" - } - - private fun resolveTransformation( - transformation: SpTransformation, - basicGeometry: String - ): String { - return when (transformation) { - is SpBuffer -> resolveBuffer(transformation, basicGeometry) - else -> throw NakshaException( - NakshaError.UNSUPPORTED_OPERATION, - "This transformation is not yet supported: ${transformation::class.simpleName}" - ) - } - } - - private fun resolveBuffer(buffer: SpBuffer, basicGeometry: String): String { - val geo = if (buffer.geography) { - "$basicGeometry::geography" - } else { - basicGeometry - } - val distancePlaceholder = placeholderForArg(buffer.distance, PgType.DOUBLE) - val bufferStyleParams = bufferStyleParams(buffer) - return if (bufferStyleParams != null) { - "ST_Buffer($geo, $distancePlaceholder, $bufferStyleParams)" - } else { - "ST_Buffer($geo, $distancePlaceholder)" - } - } - - private fun bufferStyleParams(buffer: SpBuffer): String? { - val bufferStyleParams = StringBuilder() - if (buffer.quadSegments != null) { - val quadSegPlaceholder = placeholderForArg(buffer.quadSegments, PgType.INT) - bufferStyleParams.append("quad_segs=$quadSegPlaceholder") - } - if (buffer.joinStyle != null) { - val joinStylePlaceholder = placeholderForArg(buffer.joinStyle!!.value, PgType.STRING) - if (bufferStyleParams.isNotEmpty()) bufferStyleParams.append(" ") - bufferStyleParams.append("join=$joinStylePlaceholder") - } - if (buffer.joinLimit != null) { - val joinLimitPlaceholder = placeholderForArg(buffer.joinLimit, PgType.DOUBLE) - if (bufferStyleParams.isNotEmpty()) bufferStyleParams.append(" ") - bufferStyleParams.append("mitre_limit=$joinLimitPlaceholder") - } - if (buffer.endCap != null) { - val endCapPlaceholder = placeholderForArg(buffer.endCap!!.value, PgType.STRING) - if (bufferStyleParams.isNotEmpty()) bufferStyleParams.append(" ") - bufferStyleParams.append("endcap=$endCapPlaceholder") } - if (buffer.side != null) { - val sidePlaceholder = placeholderForArg(buffer.side!!.value, PgType.STRING) - if (bufferStyleParams.isNotEmpty()) bufferStyleParams.append(" ") - bufferStyleParams.append("side=$sidePlaceholder") - } - return if (bufferStyleParams.isNotEmpty()) { - bufferStyleParams.toString() - } else { - null - } - } - - private fun whereRefTiles() { - val hereTiles = request.query.refTiles - .filterNotNull() - .map { HereTile(it) } - if (hereTiles.isNotEmpty()) { - if (where.isNotEmpty()) { - where.append(" AND (") - } else { - where.append(" (") + val at: String = op.at ?: throw illegalArg("Missing member name for operation $opName") + when (op) { + is IsNull -> { + if (negate) + where.append(at).append(" IS NOT NULL").append(' ') + else + where.append(at).append(" IS NULL").append(' ') } - where.append(refPointInAnyOfTiles(hereTiles)) - where.append(")") - } - } - - private fun refPointInAnyOfTiles(hereTiles: List): String { - return hereTiles.joinToString(separator = " OR ") { hereTile -> - refPointInTile(hereTile) - } - } - - private fun refPointInTile(hereTile: HereTile): String { - val lowerBoundPlaceholder = placeholderForArg( - hereTile.maxLevelLowerBound().intKey, - PgType.INT - ) - val upperBoundPlaceholder = placeholderForArg( - hereTile.maxLevelUpperBound().intKey, - PgType.INT - ) - return "(${PgColumn.here_tile} >= $lowerBoundPlaceholder AND ${PgColumn.here_tile} <= $upperBoundPlaceholder)" - } - - private fun whereMetadata() { - val metaQuery = request.query.members - if (metaQuery != null) { - if (where.isNotEmpty()) { - where.append(" AND (") - } else { - where.append(" (") + is IsTrue -> { + if (negate) + where.append(at).append('=').append(placeholderForArg(false, PgType.BOOLEAN)).append(' ') + else + where.append(at).append('=').append(placeholderForArg(true, PgType.BOOLEAN)).append(' ') } - whereNestedMetadata(metaQuery) - where.append(")") - } - } - - private fun whereNestedMetadata(metaQuery: IMemberQuery) { - when (metaQuery) { - is MemberNot -> not( - subClause = metaQuery.query, - subClauseResolver = this::whereNestedMetadata - ) - - is MemberAnd -> and( - subClauses = metaQuery.filterNotNull(), - subClauseResolver = this::whereNestedMetadata - ) - - is MemberOr -> or( - subClauses = metaQuery.filterNotNull(), - subClauseResolver = this::whereNestedMetadata - ) - - is MemberQuery -> { - val isActionQuery = metaQuery.member == MetaColumn.action() - val pgColumn = - if (isActionQuery) { - PgColumn.version - } else { - PgColumn.ofRowColumn(metaQuery.member) ?: throw NakshaException( - NakshaError.ILLEGAL_STATE, - "Couldn't find PgColumn for TupleColumn: ${metaQuery.member.name}" - ) - } - val leftOperand = if (isActionQuery) { - "(${PgColumn.version.name} & 3)::int4" - } else if (pgColumn == PgColumn.created_at || pgColumn == PgColumn.author_ts) { - "COALESCE(${pgColumn.name}, ${PgColumn.updated_at.name})" + is IsFalse -> { + if (negate) + where.append(at).append('=').append(placeholderForArg(true, PgType.BOOLEAN)).append(' ') + else + where.append(at).append('=').append(placeholderForArg(false, PgType.BOOLEAN)).append(' ') + } + is Equals -> { + val value: Any? = op.value + if (value == null) { + if (negate) where.append(at).append(" IS NOT NULL ") else where.append(at).append(" IS NULL ") } else { - pgColumn.name - } - // Action lives in the lower 2 bits of `version`; the comparison value is a small int. - val placeholderType = if (isActionQuery) PgType.INT else pgColumn.type - val resolvedQuery = when (val op = metaQuery.op) { - is StringOp -> { - val placeholder = placeholderForArg(metaQuery.value, placeholderType) - resolveStringOp(op, leftOperand, placeholder) - } - is DoubleOp -> { - val placeholder = placeholderForArg(metaQuery.value, placeholderType) - resolveDoubleOp(op, leftOperand, placeholder) - } - AnyOp.IS_ANY_OF -> { - val placeholder = placeholderForArg(metaQuery.value, arrayTypeFor(placeholderType)) - "$leftOperand = ANY($placeholder)" - } - else -> throw illegalArg("Unknown op type: ${op::class.simpleName}") + if (negate) + where.append(at).append("!=").append(placeholderForArg(value)).append(' ') + else + where.append(at).append('=').append(placeholderForArg(value)).append(' ') } - where.append(resolvedQuery) } - - else -> throw NakshaException( - NakshaError.ILLEGAL_ARGUMENT, - "Unknown metadata query type: ${metaQuery::class.simpleName}" - ) - } - } - - private fun arrayTypeFor(pgType: PgType): PgType { - return when (pgType) { - PgType.BOOLEAN -> PgType.BOOLEAN_ARRAY - PgType.SHORT -> PgType.SHORT_ARRAY - PgType.INT -> PgType.INT_ARRAY - PgType.INT64 -> PgType.INT64_ARRAY - PgType.FLOAT -> PgType.FLOAT_ARRAY - PgType.DOUBLE -> PgType.DOUBLE_ARRAY - PgType.STRING -> PgType.STRING_ARRAY - PgType.BYTE_ARRAY -> PgType.BYTE_ARRAY_ARRAY - else -> throw illegalArg("Unknown array type for PgType: ${pgType::class.simpleName}") - } - } - - private fun whereTags() { - val tagQuery = request.query.tags - if (tagQuery != null) { - if (where.isNotEmpty()) { - where.append(" AND (") - } else { - where.append(" (") + is Lt -> { + if (negate) // NOT Greater Than + where.append(at).append("<=").append(placeholderForArg(op.value)).append(' ') + else // Greater Than + where.append(at).append(">").append(placeholderForArg(op.value)).append(' ') } - whereNestedTags(tagQuery) - where.append(")") - } - } - - private fun whereNestedTags(tagQuery: ITagQuery) { - when (tagQuery) { - is TagSetContains -> resolveTagSetContains(tagQuery) - is TagNot -> not(tagQuery.query, this::whereNestedTags) - is TagOr -> { - if(containsOnlyTagExists(tagQuery)){ - // for tags without values we can utilize top-level-key based '?|' operand - // https://www.postgresql.org/docs/current/functions-json.html#FUNCTIONS-JSONB-OP-TABLE - val tagNames = tagQuery.filterIsInstance().map { it.name } - resolveTagNamesArrayOperation( - jsonbOperator = "?|", // 'jsonb_exists_any' is equivalent but will not hit the GIN index - tagNames = tagNames - ) - } else { - or(tagQuery.filterNotNull(), this::whereNestedTags) - } + is Gte -> { + if (negate) // NOT Greater Than or Equal to + where.append(at).append("<").append(placeholderForArg(op.value)).append(' ') + else // Greater Than or Equal to + where.append(at).append(">=").append(placeholderForArg(op.value)).append(' ') } - is TagAnd -> { - if(containsOnlyTagExists(tagQuery)){ - // for tags without values we can utilize top-level-key based '?&' operand - // https://www.postgresql.org/docs/current/functions-json.html#FUNCTIONS-JSONB-OP-TABLE - val tagNames = tagQuery.filterIsInstance().map { it.name } - resolveTagNamesArrayOperation( - jsonbOperator = "?&", // 'jsonb_exists_all' is equivalent but MIGHT not hit the GIN index - tagNames = tagNames - ) - } else { - and(tagQuery.filterNotNull(), this::whereNestedTags) - } + is Lt -> { + if (negate) // NOT Less Than + where.append(at).append(">=").append(placeholderForArg(op.value)).append(' ') + else // Less Than + where.append(at).append("<").append(placeholderForArg(op.value)).append(' ') } - is TagQuery -> resolveSingleTagQuery(tagQuery) - } - } - - private fun containsOnlyTagExists(container: ListProxy): Boolean = - container.all { it == null || it is TagExists } - - /** - * Element containment on a set-form tags column (jsonb array): `tags @> '[]'::jsonb`. - * The `@>` operator matches the element in its type (string, boolean, number) and is supported - * by the GIN index over the column. - */ - private fun resolveTagSetContains(tagQuery: TagSetContains) { - val element = AnyList() - element.add(tagQuery.element) - val placeholder = placeholderForArg(toJSON(element), PgType.STRING) - where.append("$tagsAsJsonb @> $placeholder::jsonb") - } - - private fun resolveTagNamesArrayOperation(jsonbOperator: String, tagNames: List) { - val tagKeysArray = tagNames.toTypedArray() - val tagKeysPlaceholder = placeholderForArg(tagKeysArray, PgType.STRING_ARRAY) - where.append("$tagsAsJsonb ?$jsonbOperator $tagKeysPlaceholder") - } - - private fun resolveSingleTagQuery(tagQuery: TagQuery) { - when (tagQuery) { - is TagExists -> { - val tagNamePlaceholder = placeholderForArg(tagQuery.name, PgType.STRING) - where.append("$tagsAsJsonb ?? $tagNamePlaceholder") + is Lte -> { + if (negate) // NOT Less Than or Equal to + where.append(at).append(">").append(placeholderForArg(op.value)).append(' ') + else // Less Than or Equal to + where.append(at).append("<=").append(placeholderForArg(op.value)).append(' ') } - - is TagValueIsNull -> { - val tagValuePlaceholder = placeholderForArg(selectTagValue(tagQuery), PgType.STRING) - where.append("$tagValuePlaceholder IS NULL") + is StartsWith -> { + if (negate) where.append("NOT ") + where.append("starts_with(").append(at).append(", ").append(placeholderForArg(op.value)).append(") ") } - - is TagValueIsBool -> { - if (tagQuery.value) { - where.append(selectTagValue(tagQuery, PgType.BOOLEAN)) - } else { - where.append("not(${selectTagValue(tagQuery, PgType.BOOLEAN)})") - } + is IsAnyOf -> { + if (negate) where.append("NOT ") + where.append(at).append("= ANY(").append(placeholderForArg(op.items)).append(") ") } - - is TagValueIsDouble -> { - val queryValuePlaceholder = placeholderForArg(tagQuery.value, PgType.DOUBLE) - val doubleOp = resolveDoubleOp( - tagQuery.op, - selectTagValue(tagQuery, PgType.DOUBLE), - queryValuePlaceholder - ) - where.append(doubleOp) + is TagMapHasKey -> { + if (negate) where.append("NOT ") + where.append(at).append("::jsonb").append(" ? ").append(placeholderForArg(op.key)).append(" ") } - - is TagValueIsString -> { - val queryValuePlaceholder = placeholderForArg(tagQuery.value, PgType.STRING) - val stringEquals = resolveStringOp( - StringOp.EQUALS, - selectTagValue(tagQuery, PgType.STRING), - queryValuePlaceholder - ) - where.append(stringEquals) + is TagMapHasAnyOf -> { + if (negate) where.append("NOT ") + where.append(at).append("::jsonb").append(" ?| ").append(placeholderForArg(op.keys)).append(" ") } - - is TagValueMatches -> { - val jsonPathPlaceholder = placeholderForArg( - "\$.${tagQuery.name} ? (@ like_regex \"${tagQuery.regex}\")", - PgType.STRING - ) - where.append("$tagsAsJsonb @?? $jsonPathPlaceholder::jsonpath") + is TagMapHasAllOf -> { + if (negate) where.append("NOT ") + where.append(at).append("::jsonb").append(" ?& ").append(placeholderForArg(op.keys)).append(" ") } - } - } - - private fun selectTagValue(tagQuery: TagQuery, castTo: PgType? = null): String { - val tagKeyPlaceholder = placeholderForArg(tagQuery.name, PgType.STRING) - return when (castTo) { - null -> "$tagsAsJsonb->$tagKeyPlaceholder" - PgType.STRING -> "$tagsAsJsonb->>$tagKeyPlaceholder" - else -> "($tagsAsJsonb->$tagKeyPlaceholder)::${castTo.value}" - } - } - - private fun not(subClause: T, subClauseResolver: (T) -> Unit) { - where.append(" NOT (") - subClauseResolver(subClause) - where.append(") ") - } - - private fun and(subClauses: List, subClauseResolver: (T) -> Unit) = - multiClause("AND", subClauses, subClauseResolver) - - private fun or(subClauses: List, subClauseResolver: (T) -> Unit) = - multiClause("OR", subClauses, subClauseResolver) - - private fun multiClause( - operand: String, - subClauses: List, - subClauseResolver: (T) -> Unit - ) { - where.append(" (") - subClauses.forEachIndexed { index, subClause -> - if (index > 0) { - where.append(" $operand ") + is TagIsNull -> { + // ( foo::jsonb ? $1 AND ((foo::jsonb)->>$1) IS [NOT ]NULL) + where.append("( ") + .append(at).append("::jsonb").append(" ? ").append(placeholderForArg(op.key)).append(" AND ") + .append("((").append(at).append("::jsonb)").append("->>").append(placeholderForArg(op.key)).append(")") + if (negate) where.append("IS NOT NULL) ") else where.append("IS NULL) ") + } + is TagEquals -> { + val pgType = PgType.ofValue(op.value) ?: throw illegalArg("The given value is no valid argument for ${op.op}}: ${op.value}") + val value = pgType.convertValue(op.value) + if (negate) where.append("NOT ") + // [NOT ]((foo::jsonb)->>$1)::int8 = $2 + where.append("((").append(at).append("::jsonb)") + .append("->>").append(placeholderForArg(op.key)).append("::").append(pgType).append(")") + .append("=").append(placeholderForArg(value, pgType)) + } + is TagGt -> { + val pgType = PgType.ofValue(op.value) ?: throw illegalArg("The given value is no valid argument for ${op.op}}: ${op.value}") + val value = pgType.convertValue(op.value) + if (negate) where.append("NOT ") + // [NOT ]((foo::jsonb)->>$1)::int8 > $2 + where.append("((").append(at).append("::jsonb)") + .append("->>").append(placeholderForArg(op.key)).append("::").append(pgType).append(")") + .append(">").append(placeholderForArg(value, pgType)) + } + is TagGte -> { + val pgType = PgType.ofValue(op.value) ?: throw illegalArg("The given value is no valid argument for ${op.op}}: ${op.value}") + val value = pgType.convertValue(op.value) + if (negate) where.append("NOT ") + // [NOT ]((foo::jsonb)->>$1)::int8 >= $2 + where.append("((").append(at).append("::jsonb)") + .append("->>").append(placeholderForArg(op.key)).append("::").append(pgType).append(")") + .append(">=").append(placeholderForArg(value, pgType)) + } + is TagLt -> { + val pgType = PgType.ofValue(op.value) ?: throw illegalArg("The given value is no valid argument for ${op.op}}: ${op.value}") + val value = pgType.convertValue(op.value) + if (negate) where.append("NOT ") + // [NOT ]((foo::jsonb)->>$1)::int8 < $2 + where.append("((").append(at).append("::jsonb)") + .append("->>").append(placeholderForArg(op.key)).append("::").append(pgType).append(")") + .append("<").append(placeholderForArg(value, pgType)) } - subClauseResolver(subClause) + is TagLte -> { + val pgType = PgType.ofValue(op.value) ?: throw illegalArg("The given value is no valid argument for ${op.op}}: ${op.value}") + val value = pgType.convertValue(op.value) + if (negate) where.append("NOT ") + // [NOT ]((foo::jsonb)->>$1)::int8 <= $2 + where.append("((").append(at).append("::jsonb)") + .append("->>").append(placeholderForArg(op.key)).append("::").append(pgType).append(")") + .append("<=").append(placeholderForArg(value, pgType)) + } + is TagStartsWith -> { + val pgType = PgType.ofValue(op.value) ?: throw illegalArg("The given value is no valid argument for ${op.op}}: ${op.value}") + val value = pgType.convertValue(op.value) + // [NOT ]starts_with(((foo::jsonb)->>$1), $2) + if (negate) where.append("NOT ") + where.append("starts_with(((").append(at).append("::jsonb)") + .append("->>").append(placeholderForArg(op.key)).append("::").append(pgType).append(")") + .append(", ").append(placeholderForArg(value, pgType)).append(") ") + } + is TagListContains -> { + // TODO: Implement me + } + is TagListContainsAllOf -> { + // TODO: Implement me + } + is TagListContainsAnyOf -> { + // TODO: Implement me + } + is Intersects -> { + // TODO: Implement me + } + else -> throw illegalArg("Unknown operation: '$op'") } - where.append(") ") } - private fun placeholderForArg(value: Any?, type: PgType): String { + /** + * Returns a placeholder string like `$1` for the given value, and add the values and its type into the value and type arrays. + */ + private fun placeholderForArg(value: Any?): String { + // TODO: Detect the type, if not possible, throw illegalArg() + // AnyList for + ANY() + val type: PgType = PgType.STRING; argValues.add(value) argTypes.add(type) return "\$${argTypes.size}" } - private fun resolveStringOp( - stringOp: StringOp, - leftOperand: String, - rightOperand: String - ): String { - return when (stringOp) { - StringOp.EQUALS -> "$leftOperand = $rightOperand" - StringOp.NOT_EQUALS -> "$leftOperand != $rightOperand" - StringOp.STARTS_WITH -> "starts_with($leftOperand, $rightOperand)" - else -> throw NakshaException( - NakshaError.ILLEGAL_ARGUMENT, - "Unknown StringOp: $stringOp" - ) - } - } - - private fun resolveDoubleOp( - doubleOp: DoubleOp, - leftOperand: String, - rightOperand: String - ): String { - return when (doubleOp) { - DoubleOp.EQ -> "$leftOperand = $rightOperand" - DoubleOp.NE -> "$leftOperand != $rightOperand" - DoubleOp.GT -> "$leftOperand > $rightOperand" - DoubleOp.GTE -> "$leftOperand >= $rightOperand" - DoubleOp.LT -> "$leftOperand < $rightOperand" - DoubleOp.LTE -> "$leftOperand <= $rightOperand" - else -> throw NakshaException( - NakshaError.ILLEGAL_ARGUMENT, - "Unknown DoubleOp: $doubleOp" - ) - } + /** + * Returns a placeholder string like `$1` for the given value, and add the values and its type into the value and type arrays. + * @param value any number _(including `Int64`)_ + * @return + */ + private fun placeholderForNumber(value: Any?): String { + // TODO: Detect the type, which must be any number, otherwise throw illegalArg() + val type: PgType = PgType.STRING; + argValues.add(value) + argTypes.add(type) + return "\$${argTypes.size}" } - companion object { - private val tagsAsJsonb = PgColumn.tags.name + /** + * Returns a placeholder string like `$1` for the given value, and add the values and its type into the value and type arrays. + */ + private fun placeholderForArg(value: Any?, type: PgType): String { + // TODO: Ensure that the value matches the type, otherwise throw illegalArg()! + argValues.add(value) + argTypes.add(type) + return "\$${argTypes.size}" } } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgType.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgType.kt index 13ca84c62..e99afa64b 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgType.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgType.kt @@ -156,6 +156,18 @@ class PgType : JsEnum() { @JvmStatic fun of(name: String?): PgType? = getDefined(name, PgType::class) + /** + * Detects the [PgType] of the given value, if it has any. + * @param value the value for which to detect the [PgType] + * @return the matching [PgType] or `null`, if none matches. + */ + @JsStatic + @JvmStatic + fun ofValue(value: Any?): PgType? { + // TODO: Implement detection. + return null + } + /** * Returns the database column type to be used for a specific [Member]. * @param member the [Member] to lookup. @@ -220,4 +232,17 @@ class PgType : JsEnum() { */ var childType: PgType? = null private set + + /** + * Converts the given value in this type, so that it can be given to JDBC. + * + * For example, converts a `List` into a `String[]`. + * @param value the value to convert into this type. + * @return the value so that it can be used with JDBC. + */ + fun convertValue(value: Any?): Any? { + // TODO: We need to convert certain values to postgres valid ones + // For lists, we need to convert them into typed arrays. + return value + } } \ No newline at end of file From 95cc88798b1bfef0fb4fc8e972c30da8e73f496f Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Tue, 23 Jun 2026 11:51:55 +0200 Subject: [PATCH 33/57] Moved code to correct placed. Signed-off-by: Alexander Lowey-Weber --- .../model/request/ops/PgQueryConverter.kt | 522 ------------------ .../model/request/ops/QueryConverter.kt | 16 + .../kotlin/naksha/psql/PgQueryWhereBuilder.kt | 509 ++++++++++++++++- 3 files changed, 524 insertions(+), 523 deletions(-) delete mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/PgQueryConverter.kt create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/QueryConverter.kt diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/PgQueryConverter.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/PgQueryConverter.kt deleted file mode 100644 index 3e0ac707c..000000000 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/PgQueryConverter.kt +++ /dev/null @@ -1,522 +0,0 @@ -@file:Suppress("OPT_IN_USAGE") - -package naksha.model.request.ops - -import naksha.base.AnyList -import naksha.base.Int64 -import naksha.base.ListProxy -import naksha.base.Platform.PlatformCompanion.toJSON -import naksha.base.StringList -import naksha.geo.HereTile -import naksha.geo.SpGeometry -import naksha.model.Naksha -import naksha.model.NakshaError -import naksha.model.NakshaException -import naksha.model.illegalArg -import naksha.model.objects.StandardMembers -import naksha.model.request.RequestQuery -import kotlin.js.JsExport - -@JsExport -class PgQueryConverter private constructor() { - companion object PgQueryConverter_C { - fun convert(query: RequestQuery): Op? { - // TODO: Implement a convertion. - return null - } - } - - - private fun whereFeatureId() { - // Partition into numeric IDs (fn >= 0, id stored as NULL in DB) and named IDs (fn < 0, id NOT NULL). - val reqIds: StringList = request.featureIds - val featureNumbers: MutableList = mutableListOf() - val featureIds: MutableList = mutableListOf() - for (id in reqIds) { - if (id == null) continue - val fn = Naksha.featureNumber(id) - if (fn >= Int64(0)) { - featureNumbers.add(fn) - } else { - featureIds.add(id) - } - } - if (featureNumbers.isEmpty() && featureIds.isEmpty()) return - - // For each collection: - if (where.isNotEmpty()) where.append(" AND ") - - where.append("( ") - if (featureIds.isNotEmpty()) { - val placeholder: String = placeholderForArg(featureIds.toTypedArray(), PgType.STRING_ARRAY) - val ID = collection.column(StandardMembers.Id) ?: throw illegalArg("Collection does not defined `id` column") - where.append(ID.ident).append(" = ANY(").append(placeholder).append(")") - } - if (featureNumbers.isNotEmpty()) { - if (featureIds.isNotEmpty()) where.append(" OR ") - - val placeholder: String = placeholderForArg(featureNumbers.toTypedArray(), PgType.INT64_ARRAY) - if (where.isNotEmpty()) where.append(" AND ") - where.append(FN.ident).append(" = ANY(").append(placeholder).append(")") - } - where.append(")") - } - - private fun whereGuids() { - val tupleNumbers = request.guids.mapNotNull { it?.tupleNumber } - if (tupleNumbers.isNotEmpty()) { - if (where.isNotEmpty()) where.append(" AND ") - - val fns = arrayOfNulls(tupleNumbers.size) - val versions = arrayOfNulls(tupleNumbers.size) - for (i in tupleNumbers.indices) { - fns[i] = tupleNumbers[i].featureNumber - versions[i] = tupleNumbers[i].version - } - val featureNumbersArg = placeholderForArg(fns, PgType.INT64_ARRAY) - val versionsArg = placeholderForArg(versions, PgType.INT64_ARRAY) - where.append("($FN, $VERSION) IN (SELECT * FROM unnest($featureNumbersArg::int8[], $versionsArg::int8[]))") - } - } - - private fun whereVersion() { - val version = request.version - if (version != null) { - if (where.isNotEmpty()) where.append(" AND ") - where.append("$VERSION <= ${version.number}") - } - val minVersion = request.minVersion - if (minVersion != null) { - if (where.isNotEmpty()) where.append(" AND ") - where.append("$VERSION >= ${minVersion.number}") - } - } - - private fun whereSpatial() { - val spatialQuery = request.query.spatial - if (spatialQuery != null) { - if (where.isNotEmpty()) { - where.append(" AND (") - } else { - where.append(" (") - } - whereNestedSpatial(spatialQuery) - where.append(")") - } - } - - private fun whereNestedSpatial(spatial: ISpatialQuery) { - when (spatial) { - is SpNot -> not( - subClause = spatial.query, - subClauseResolver = this::whereNestedSpatial - ) - - is SpAnd -> and( - subClauses = spatial.filterNotNull(), - subClauseResolver = this::whereNestedSpatial - ) - - is SpOr -> or( - subClauses = spatial.filterNotNull(), - subClauseResolver = this::whereNestedSpatial - ) - - is SpIntersects -> { - val queryGeometry = nakshaGeometry(spatial.geometry) - val geometryToCompare = when (val transformation = spatial.transformation) { - null -> queryGeometry - else -> resolveTransformation(transformation, queryGeometry) - } - where.append("ST_Intersects(naksha_2d(${PgColumn.geo}), $geometryToCompare)") - } - - is SpRefInHereTile -> { - where.append(refPointInTile(spatial.getHereTile())) - } - - else -> throw NakshaException( - NakshaError.ILLEGAL_ARGUMENT, - "Invalid spatial query found: $spatial" - ) - } - } - - private fun nakshaGeometry(geometry: SpGeometry): String { - val geoBytes = Naksha.encodeGeometry(geometry) - val geoBytesPlaceholder = placeholderForArg(geoBytes, PgType.BYTE_ARRAY) - return "naksha_2d($geoBytesPlaceholder)" - } - - private fun resolveTransformation( - transformation: SpTransformation, - basicGeometry: String - ): String { - return when (transformation) { - is SpBuffer -> resolveBuffer(transformation, basicGeometry) - else -> throw NakshaException( - NakshaError.UNSUPPORTED_OPERATION, - "This transformation is not yet supported: ${transformation::class.simpleName}" - ) - } - } - - private fun resolveBuffer(buffer: SpBuffer, basicGeometry: String): String { - val geo = if (buffer.geography) { - "$basicGeometry::geography" - } else { - basicGeometry - } - val distancePlaceholder = placeholderForArg(buffer.distance, PgType.DOUBLE) - val bufferStyleParams = bufferStyleParams(buffer) - return if (bufferStyleParams != null) { - "ST_Buffer($geo, $distancePlaceholder, $bufferStyleParams)" - } else { - "ST_Buffer($geo, $distancePlaceholder)" - } - } - - private fun bufferStyleParams(buffer: SpBuffer): String? { - val bufferStyleParams = StringBuilder() - if (buffer.quadSegments != null) { - val quadSegPlaceholder = placeholderForArg(buffer.quadSegments, PgType.INT) - bufferStyleParams.append("quad_segs=$quadSegPlaceholder") - } - if (buffer.joinStyle != null) { - val joinStylePlaceholder = placeholderForArg(buffer.joinStyle!!.value, PgType.STRING) - if (bufferStyleParams.isNotEmpty()) bufferStyleParams.append(" ") - bufferStyleParams.append("join=$joinStylePlaceholder") - } - if (buffer.joinLimit != null) { - val joinLimitPlaceholder = placeholderForArg(buffer.joinLimit, PgType.DOUBLE) - if (bufferStyleParams.isNotEmpty()) bufferStyleParams.append(" ") - bufferStyleParams.append("mitre_limit=$joinLimitPlaceholder") - } - if (buffer.endCap != null) { - val endCapPlaceholder = placeholderForArg(buffer.endCap!!.value, PgType.STRING) - if (bufferStyleParams.isNotEmpty()) bufferStyleParams.append(" ") - bufferStyleParams.append("endcap=$endCapPlaceholder") - } - if (buffer.side != null) { - val sidePlaceholder = placeholderForArg(buffer.side!!.value, PgType.STRING) - if (bufferStyleParams.isNotEmpty()) bufferStyleParams.append(" ") - bufferStyleParams.append("side=$sidePlaceholder") - } - return if (bufferStyleParams.isNotEmpty()) { - bufferStyleParams.toString() - } else { - null - } - } - - private fun whereRefTiles() { - val hereTiles = request.query.refTiles - .filterNotNull() - .map { HereTile(it) } - if (hereTiles.isNotEmpty()) { - if (where.isNotEmpty()) { - where.append(" AND (") - } else { - where.append(" (") - } - where.append(refPointInAnyOfTiles(hereTiles)) - where.append(")") - } - } - - private fun refPointInAnyOfTiles(hereTiles: List): String { - return hereTiles.joinToString(separator = " OR ") { hereTile -> - refPointInTile(hereTile) - } - } - - private fun refPointInTile(hereTile: HereTile): String { - val lowerBoundPlaceholder = placeholderForArg( - hereTile.maxLevelLowerBound().intKey, - PgType.INT - ) - val upperBoundPlaceholder = placeholderForArg( - hereTile.maxLevelUpperBound().intKey, - PgType.INT - ) - return "(${PgColumn.here_tile} >= $lowerBoundPlaceholder AND ${PgColumn.here_tile} <= $upperBoundPlaceholder)" - } - - private fun whereMetadata() { - val metaQuery = request.query.members - if (metaQuery != null) { - if (where.isNotEmpty()) { - where.append(" AND (") - } else { - where.append(" (") - } - whereNestedMetadata(metaQuery) - where.append(")") - } - } - - private fun whereNestedMetadata(metaQuery: IMemberQuery) { - when (metaQuery) { - is MemberNot -> not( - subClause = metaQuery.query, - subClauseResolver = this::whereNestedMetadata - ) - - is MemberAnd -> and( - subClauses = metaQuery.filterNotNull(), - subClauseResolver = this::whereNestedMetadata - ) - - is MemberOr -> or( - subClauses = metaQuery.filterNotNull(), - subClauseResolver = this::whereNestedMetadata - ) - - is MemberQuery -> { - val isActionQuery = metaQuery.member == MetaColumn.action() - val pgColumn = - if (isActionQuery) { - PgColumn.version - } else { - PgColumn.ofRowColumn(metaQuery.member) ?: throw NakshaException( - NakshaError.ILLEGAL_STATE, - "Couldn't find PgColumn for TupleColumn: ${metaQuery.member.name}" - ) - } - val leftOperand = if (isActionQuery) { - "(${PgColumn.version.name} & 3)::int4" - } else if (pgColumn == PgColumn.created_at || pgColumn == PgColumn.author_ts) { - "COALESCE(${pgColumn.name}, ${PgColumn.updated_at.name})" - } else { - pgColumn.name - } - // Action lives in the lower 2 bits of `version`; the comparison value is a small int. - val placeholderType = if (isActionQuery) PgType.INT else pgColumn.type - val resolvedQuery = when (val op = metaQuery.op) { - is StringOp -> { - val placeholder = placeholderForArg(metaQuery.value, placeholderType) - resolveStringOp(op, leftOperand, placeholder) - } - is DoubleOp -> { - val placeholder = placeholderForArg(metaQuery.value, placeholderType) - resolveDoubleOp(op, leftOperand, placeholder) - } - AnyOp.IS_ANY_OF -> { - val placeholder = placeholderForArg(metaQuery.value, arrayTypeFor(placeholderType)) - "$leftOperand = ANY($placeholder)" - } - else -> throw illegalArg("Unknown op type: ${op::class.simpleName}") - } - where.append(resolvedQuery) - } - - else -> throw NakshaException( - NakshaError.ILLEGAL_ARGUMENT, - "Unknown metadata query type: ${metaQuery::class.simpleName}" - ) - } - } - - private fun arrayTypeFor(pgType: PgType): PgType { - return when (pgType) { - PgType.BOOLEAN -> PgType.BOOLEAN_ARRAY - PgType.SHORT -> PgType.SHORT_ARRAY - PgType.INT -> PgType.INT_ARRAY - PgType.INT64 -> PgType.INT64_ARRAY - PgType.FLOAT -> PgType.FLOAT_ARRAY - PgType.DOUBLE -> PgType.DOUBLE_ARRAY - PgType.STRING -> PgType.STRING_ARRAY - PgType.BYTE_ARRAY -> PgType.BYTE_ARRAY_ARRAY - else -> throw illegalArg("Unknown array type for PgType: ${pgType::class.simpleName}") - } - } - - private fun whereTags() { - val tagQuery = request.query.tags - if (tagQuery != null) { - if (where.isNotEmpty()) { - where.append(" AND (") - } else { - where.append(" (") - } - whereNestedTags(tagQuery) - where.append(")") - } - } - - private fun whereNestedTags(tagQuery: ITagQuery) { - when (tagQuery) { - is TagSetContains -> resolveTagSetContains(tagQuery) - is TagNot -> not(tagQuery.query, this::whereNestedTags) - is TagOr -> { - if(containsOnlyTagExists(tagQuery)){ - // for tags without values we can utilize top-level-key based '?|' operand - // https://www.postgresql.org/docs/current/functions-json.html#FUNCTIONS-JSONB-OP-TABLE - val tagNames = tagQuery.filterIsInstance().map { it.name } - resolveTagNamesArrayOperation( - jsonbOperator = "?|", // 'jsonb_exists_any' is equivalent but will not hit the GIN index - tagNames = tagNames - ) - } else { - or(tagQuery.filterNotNull(), this::whereNestedTags) - } - } - is TagAnd -> { - if(containsOnlyTagExists(tagQuery)){ - // for tags without values we can utilize top-level-key based '?&' operand - // https://www.postgresql.org/docs/current/functions-json.html#FUNCTIONS-JSONB-OP-TABLE - val tagNames = tagQuery.filterIsInstance().map { it.name } - resolveTagNamesArrayOperation( - jsonbOperator = "?&", // 'jsonb_exists_all' is equivalent but MIGHT not hit the GIN index - tagNames = tagNames - ) - } else { - and(tagQuery.filterNotNull(), this::whereNestedTags) - } - } - is TagQuery -> resolveSingleTagQuery(tagQuery) - } - } - - private fun containsOnlyTagExists(container: ListProxy): Boolean = - container.all { it == null || it is TagMapHasKey } - - /** - * Element containment on a set-form tags column (jsonb array): `tags @> '[]'::jsonb`. - * The `@>` operator matches the element in its type (string, boolean, number) and is supported - * by the GIN index over the column. - */ - private fun resolveTagSetContains(tagQuery: TagSetContains) { - val element = AnyList() - element.add(tagQuery.element) - val placeholder = placeholderForArg(toJSON(element), PgType.STRING) - where.append("$tagsAsJsonb @> $placeholder::jsonb") - } - - private fun resolveTagNamesArrayOperation(jsonbOperator: String, tagNames: List) { - val tagKeysArray = tagNames.toTypedArray() - val tagKeysPlaceholder = placeholderForArg(tagKeysArray, PgType.STRING_ARRAY) - where.append("$tagsAsJsonb ?$jsonbOperator $tagKeysPlaceholder") - } - - private fun resolveSingleTagQuery(tagQuery: TagQuery) { - when (tagQuery) { - is TagMapHasKey -> { - val tagNamePlaceholder = placeholderForArg(tagQuery.name, PgType.STRING) - where.append("$tagsAsJsonb ?? $tagNamePlaceholder") - } - - is TagValueIsNull -> { - val tagValuePlaceholder = placeholderForArg(selectTagValue(tagQuery), PgType.STRING) - where.append("$tagValuePlaceholder IS NULL") - } - - is TagValueIsBool -> { - if (tagQuery.value) { - where.append(selectTagValue(tagQuery, PgType.BOOLEAN)) - } else { - where.append("not(${selectTagValue(tagQuery, PgType.BOOLEAN)})") - } - } - - is TagValueIsDouble -> { - val queryValuePlaceholder = placeholderForArg(tagQuery.value, PgType.DOUBLE) - val doubleOp = resolveDoubleOp( - tagQuery.op, - selectTagValue(tagQuery, PgType.DOUBLE), - queryValuePlaceholder - ) - where.append(doubleOp) - } - - is TagValueIsString -> { - val queryValuePlaceholder = placeholderForArg(tagQuery.value, PgType.STRING) - val stringEquals = resolveStringOp( - StringOp.EQUALS, - selectTagValue(tagQuery, PgType.STRING), - queryValuePlaceholder - ) - where.append(stringEquals) - } - - is TagValueMatches -> { - val jsonPathPlaceholder = placeholderForArg( - "\$.${tagQuery.name} ? (@ like_regex \"${tagQuery.regex}\")", - PgType.STRING - ) - where.append("$tagsAsJsonb @?? $jsonPathPlaceholder::jsonpath") - } - } - } - - private fun selectTagValue(tagQuery: TagQuery, castTo: PgType? = null): String { - val tagKeyPlaceholder = placeholderForArg(tagQuery.name, PgType.STRING) - return when (castTo) { - null -> "$tagsAsJsonb->$tagKeyPlaceholder" - PgType.STRING -> "$tagsAsJsonb->>$tagKeyPlaceholder" - else -> "($tagsAsJsonb->$tagKeyPlaceholder)::${castTo.value}" - } - } - - private fun not(subClause: T, subClauseResolver: (T) -> Unit) { - where.append(" NOT (") - subClauseResolver(subClause) - where.append(") ") - } - - private fun and(subClauses: List, subClauseResolver: (T) -> Unit) = - multiClause("AND", subClauses, subClauseResolver) - - private fun or(subClauses: List, subClauseResolver: (T) -> Unit) = - multiClause("OR", subClauses, subClauseResolver) - - private fun multiClause( - operand: String, - subClauses: List, - subClauseResolver: (T) -> Unit - ) { - where.append(" (") - subClauses.forEachIndexed { index, subClause -> - if (index > 0) { - where.append(" $operand ") - } - subClauseResolver(subClause) - } - where.append(") ") - } - - - private fun resolveStringOp( - stringOp: StringOp, - leftOperand: String, - rightOperand: String - ): String { - return when (stringOp) { - StringOp.EQUALS -> "$leftOperand = $rightOperand" - StringOp.NOT_EQUALS -> "$leftOperand != $rightOperand" - StringOp.STARTS_WITH -> "starts_with($leftOperand, $rightOperand)" - else -> throw NakshaException( - NakshaError.ILLEGAL_ARGUMENT, - "Unknown StringOp: $stringOp" - ) - } - } - - private fun resolveDoubleOp( - doubleOp: DoubleOp, - leftOperand: String, - rightOperand: String - ): String { - return when (doubleOp) { - DoubleOp.EQ -> "$leftOperand = $rightOperand" - DoubleOp.NE -> "$leftOperand != $rightOperand" - DoubleOp.GT -> "$leftOperand > $rightOperand" - DoubleOp.GTE -> "$leftOperand >= $rightOperand" - DoubleOp.LT -> "$leftOperand < $rightOperand" - DoubleOp.LTE -> "$leftOperand <= $rightOperand" - else -> throw NakshaException( - NakshaError.ILLEGAL_ARGUMENT, - "Unknown DoubleOp: $doubleOp" - ) - } - }} \ No newline at end of file diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/QueryConverter.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/QueryConverter.kt new file mode 100644 index 000000000..ac7055ec4 --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/QueryConverter.kt @@ -0,0 +1,16 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model.request.ops + +import naksha.model.request.RequestQuery +import kotlin.js.JsExport + +@JsExport +class QueryConverter private constructor() { + companion object PgQueryConverter_C { + fun convert(query: RequestQuery): Op? { + // TODO: Implement a convertion. + return null + } + } +} \ No newline at end of file diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt index 3e1ec25cf..2d7c522e5 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt @@ -1,8 +1,19 @@ package naksha.psql +import naksha.base.AnyList +import naksha.base.Int64 +import naksha.base.ListProxy +import naksha.base.Platform.PlatformCompanion.toJSON +import naksha.base.StringList +import naksha.geo.HereTile +import naksha.geo.SpGeometry import naksha.model.* +import naksha.model.objects.StandardMembers import naksha.model.request.ReadFeatures import naksha.model.request.ops.* +import kotlin.collections.get +import kotlin.text.append +import kotlin.text.iterator /** * Helper to convert a [ReadFeatures] request into a sql `WHERE` query. @@ -26,7 +37,7 @@ internal class PgQueryWhereBuilder(private val request: ReadFeatures, private va */ fun build(): PgQueryWhereClause? { var op: Op? = request.queryMembers - if (op == null) op = PgQueryConverter.convert(request.query) + if (op == null) op = QueryConverter.convert(request.query) // TODO: Convert `featureIds` // TODO: Convert `guids` if (op == null) return null @@ -255,4 +266,500 @@ internal class PgQueryWhereBuilder(private val request: ReadFeatures, private va argTypes.add(type) return "\$${argTypes.size}" } + + // --------------------------------------------------------< OLD CODE >------------------------------------------------------------- + + private fun whereFeatureId() { + // Partition into numeric IDs (fn >= 0, id stored as NULL in DB) and named IDs (fn < 0, id NOT NULL). + val reqIds: StringList = request.featureIds + val featureNumbers: MutableList = mutableListOf() + val featureIds: MutableList = mutableListOf() + for (id in reqIds) { + if (id == null) continue + val fn = Naksha.featureNumber(id) + if (fn >= Int64(0)) { + featureNumbers.add(fn) + } else { + featureIds.add(id) + } + } + if (featureNumbers.isEmpty() && featureIds.isEmpty()) return + + // For each collection: + if (where.isNotEmpty()) where.append(" AND ") + + where.append("( ") + if (featureIds.isNotEmpty()) { + val placeholder: String = placeholderForArg(featureIds.toTypedArray(), PgType.STRING_ARRAY) + val ID = collection.column(StandardMembers.Id) ?: throw illegalArg("Collection does not defined `id` column") + where.append(ID.ident).append(" = ANY(").append(placeholder).append(")") + } + if (featureNumbers.isNotEmpty()) { + if (featureIds.isNotEmpty()) where.append(" OR ") + + val placeholder: String = placeholderForArg(featureNumbers.toTypedArray(), PgType.INT64_ARRAY) + if (where.isNotEmpty()) where.append(" AND ") + where.append(FN.ident).append(" = ANY(").append(placeholder).append(")") + } + where.append(")") + } + + private fun whereGuids() { + val tupleNumbers = request.guids.mapNotNull { it?.tupleNumber } + if (tupleNumbers.isNotEmpty()) { + if (where.isNotEmpty()) where.append(" AND ") + + val fns = arrayOfNulls(tupleNumbers.size) + val versions = arrayOfNulls(tupleNumbers.size) + for (i in tupleNumbers.indices) { + fns[i] = tupleNumbers[i].featureNumber + versions[i] = tupleNumbers[i].version + } + val featureNumbersArg = placeholderForArg(fns, PgType.INT64_ARRAY) + val versionsArg = placeholderForArg(versions, PgType.INT64_ARRAY) + where.append("($FN, $VERSION) IN (SELECT * FROM unnest($featureNumbersArg::int8[], $versionsArg::int8[]))") + } + } + + private fun whereVersion() { + val version = request.version + if (version != null) { + if (where.isNotEmpty()) where.append(" AND ") + where.append("$VERSION <= ${version.number}") + } + val minVersion = request.minVersion + if (minVersion != null) { + if (where.isNotEmpty()) where.append(" AND ") + where.append("$VERSION >= ${minVersion.number}") + } + } + + private fun whereSpatial() { + val spatialQuery = request.query.spatial + if (spatialQuery != null) { + if (where.isNotEmpty()) { + where.append(" AND (") + } else { + where.append(" (") + } + whereNestedSpatial(spatialQuery) + where.append(")") + } + } + + private fun whereNestedSpatial(spatial: ISpatialQuery) { + when (spatial) { + is SpNot -> not( + subClause = spatial.query, + subClauseResolver = this::whereNestedSpatial + ) + + is SpAnd -> and( + subClauses = spatial.filterNotNull(), + subClauseResolver = this::whereNestedSpatial + ) + + is SpOr -> or( + subClauses = spatial.filterNotNull(), + subClauseResolver = this::whereNestedSpatial + ) + + is SpIntersects -> { + val queryGeometry = nakshaGeometry(spatial.geometry) + val geometryToCompare = when (val transformation = spatial.transformation) { + null -> queryGeometry + else -> resolveTransformation(transformation, queryGeometry) + } + where.append("ST_Intersects(naksha_2d(${PgColumn.geo}), $geometryToCompare)") + } + + is SpRefInHereTile -> { + where.append(refPointInTile(spatial.getHereTile())) + } + + else -> throw NakshaException( + NakshaError.ILLEGAL_ARGUMENT, + "Invalid spatial query found: $spatial" + ) + } + } + + private fun nakshaGeometry(geometry: SpGeometry): String { + val geoBytes = Naksha.encodeGeometry(geometry) + val geoBytesPlaceholder = placeholderForArg(geoBytes, PgType.BYTE_ARRAY) + return "naksha_2d($geoBytesPlaceholder)" + } + + private fun resolveTransformation( + transformation: SpTransformation, + basicGeometry: String + ): String { + return when (transformation) { + is SpBuffer -> resolveBuffer(transformation, basicGeometry) + else -> throw NakshaException( + NakshaError.UNSUPPORTED_OPERATION, + "This transformation is not yet supported: ${transformation::class.simpleName}" + ) + } + } + + private fun resolveBuffer(buffer: SpBuffer, basicGeometry: String): String { + val geo = if (buffer.geography) { + "$basicGeometry::geography" + } else { + basicGeometry + } + val distancePlaceholder = placeholderForArg(buffer.distance, PgType.DOUBLE) + val bufferStyleParams = bufferStyleParams(buffer) + return if (bufferStyleParams != null) { + "ST_Buffer($geo, $distancePlaceholder, $bufferStyleParams)" + } else { + "ST_Buffer($geo, $distancePlaceholder)" + } + } + + private fun bufferStyleParams(buffer: SpBuffer): String? { + val bufferStyleParams = StringBuilder() + if (buffer.quadSegments != null) { + val quadSegPlaceholder = placeholderForArg(buffer.quadSegments, PgType.INT) + bufferStyleParams.append("quad_segs=$quadSegPlaceholder") + } + if (buffer.joinStyle != null) { + val joinStylePlaceholder = placeholderForArg(buffer.joinStyle!!.value, PgType.STRING) + if (bufferStyleParams.isNotEmpty()) bufferStyleParams.append(" ") + bufferStyleParams.append("join=$joinStylePlaceholder") + } + if (buffer.joinLimit != null) { + val joinLimitPlaceholder = placeholderForArg(buffer.joinLimit, PgType.DOUBLE) + if (bufferStyleParams.isNotEmpty()) bufferStyleParams.append(" ") + bufferStyleParams.append("mitre_limit=$joinLimitPlaceholder") + } + if (buffer.endCap != null) { + val endCapPlaceholder = placeholderForArg(buffer.endCap!!.value, PgType.STRING) + if (bufferStyleParams.isNotEmpty()) bufferStyleParams.append(" ") + bufferStyleParams.append("endcap=$endCapPlaceholder") + } + if (buffer.side != null) { + val sidePlaceholder = placeholderForArg(buffer.side!!.value, PgType.STRING) + if (bufferStyleParams.isNotEmpty()) bufferStyleParams.append(" ") + bufferStyleParams.append("side=$sidePlaceholder") + } + return if (bufferStyleParams.isNotEmpty()) { + bufferStyleParams.toString() + } else { + null + } + } + + private fun whereRefTiles() { + val hereTiles = request.query.refTiles + .filterNotNull() + .map { HereTile(it) } + if (hereTiles.isNotEmpty()) { + if (where.isNotEmpty()) { + where.append(" AND (") + } else { + where.append(" (") + } + where.append(refPointInAnyOfTiles(hereTiles)) + where.append(")") + } + } + + private fun refPointInAnyOfTiles(hereTiles: List): String { + return hereTiles.joinToString(separator = " OR ") { hereTile -> + refPointInTile(hereTile) + } + } + + private fun refPointInTile(hereTile: HereTile): String { + val lowerBoundPlaceholder = placeholderForArg( + hereTile.maxLevelLowerBound().intKey, + PgType.INT + ) + val upperBoundPlaceholder = placeholderForArg( + hereTile.maxLevelUpperBound().intKey, + PgType.INT + ) + return "(${PgColumn.here_tile} >= $lowerBoundPlaceholder AND ${PgColumn.here_tile} <= $upperBoundPlaceholder)" + } + + private fun whereMetadata() { + val metaQuery = request.query.members + if (metaQuery != null) { + if (where.isNotEmpty()) { + where.append(" AND (") + } else { + where.append(" (") + } + whereNestedMetadata(metaQuery) + where.append(")") + } + } + + private fun whereNestedMetadata(metaQuery: IMemberQuery) { + when (metaQuery) { + is MemberNot -> not( + subClause = metaQuery.query, + subClauseResolver = this::whereNestedMetadata + ) + + is MemberAnd -> and( + subClauses = metaQuery.filterNotNull(), + subClauseResolver = this::whereNestedMetadata + ) + + is MemberOr -> or( + subClauses = metaQuery.filterNotNull(), + subClauseResolver = this::whereNestedMetadata + ) + + is MemberQuery -> { + val isActionQuery = metaQuery.member == MetaColumn.action() + val pgColumn = + if (isActionQuery) { + PgColumn.version + } else { + PgColumn.ofRowColumn(metaQuery.member) ?: throw NakshaException( + NakshaError.ILLEGAL_STATE, + "Couldn't find PgColumn for TupleColumn: ${metaQuery.member.name}" + ) + } + val leftOperand = if (isActionQuery) { + "(${PgColumn.version.name} & 3)::int4" + } else if (pgColumn == PgColumn.created_at || pgColumn == PgColumn.author_ts) { + "COALESCE(${pgColumn.name}, ${PgColumn.updated_at.name})" + } else { + pgColumn.name + } + // Action lives in the lower 2 bits of `version`; the comparison value is a small int. + val placeholderType = if (isActionQuery) PgType.INT else pgColumn.type + val resolvedQuery = when (val op = metaQuery.op) { + is StringOp -> { + val placeholder = placeholderForArg(metaQuery.value, placeholderType) + resolveStringOp(op, leftOperand, placeholder) + } + is DoubleOp -> { + val placeholder = placeholderForArg(metaQuery.value, placeholderType) + resolveDoubleOp(op, leftOperand, placeholder) + } + AnyOp.IS_ANY_OF -> { + val placeholder = placeholderForArg(metaQuery.value, arrayTypeFor(placeholderType)) + "$leftOperand = ANY($placeholder)" + } + else -> throw illegalArg("Unknown op type: ${op::class.simpleName}") + } + where.append(resolvedQuery) + } + + else -> throw NakshaException( + NakshaError.ILLEGAL_ARGUMENT, + "Unknown metadata query type: ${metaQuery::class.simpleName}" + ) + } + } + + private fun arrayTypeFor(pgType: PgType): PgType { + return when (pgType) { + PgType.BOOLEAN -> PgType.BOOLEAN_ARRAY + PgType.SHORT -> PgType.SHORT_ARRAY + PgType.INT -> PgType.INT_ARRAY + PgType.INT64 -> PgType.INT64_ARRAY + PgType.FLOAT -> PgType.FLOAT_ARRAY + PgType.DOUBLE -> PgType.DOUBLE_ARRAY + PgType.STRING -> PgType.STRING_ARRAY + PgType.BYTE_ARRAY -> PgType.BYTE_ARRAY_ARRAY + else -> throw illegalArg("Unknown array type for PgType: ${pgType::class.simpleName}") + } + } + + private fun whereTags() { + val tagQuery = request.query.tags + if (tagQuery != null) { + if (where.isNotEmpty()) { + where.append(" AND (") + } else { + where.append(" (") + } + whereNestedTags(tagQuery) + where.append(")") + } + } + + private fun whereNestedTags(tagQuery: ITagQuery) { + when (tagQuery) { + is TagSetContains -> resolveTagSetContains(tagQuery) + is TagNot -> not(tagQuery.query, this::whereNestedTags) + is TagOr -> { + if(containsOnlyTagExists(tagQuery)){ + // for tags without values we can utilize top-level-key based '?|' operand + // https://www.postgresql.org/docs/current/functions-json.html#FUNCTIONS-JSONB-OP-TABLE + val tagNames = tagQuery.filterIsInstance().map { it.name } + resolveTagNamesArrayOperation( + jsonbOperator = "?|", // 'jsonb_exists_any' is equivalent but will not hit the GIN index + tagNames = tagNames + ) + } else { + or(tagQuery.filterNotNull(), this::whereNestedTags) + } + } + is TagAnd -> { + if(containsOnlyTagExists(tagQuery)){ + // for tags without values we can utilize top-level-key based '?&' operand + // https://www.postgresql.org/docs/current/functions-json.html#FUNCTIONS-JSONB-OP-TABLE + val tagNames = tagQuery.filterIsInstance().map { it.name } + resolveTagNamesArrayOperation( + jsonbOperator = "?&", // 'jsonb_exists_all' is equivalent but MIGHT not hit the GIN index + tagNames = tagNames + ) + } else { + and(tagQuery.filterNotNull(), this::whereNestedTags) + } + } + is TagQuery -> resolveSingleTagQuery(tagQuery) + } + } + + private fun containsOnlyTagExists(container: ListProxy): Boolean = + container.all { it == null || it is TagMapHasKey } + + /** + * Element containment on a set-form tags column (jsonb array): `tags @> '[]'::jsonb`. + * The `@>` operator matches the element in its type (string, boolean, number) and is supported + * by the GIN index over the column. + */ + private fun resolveTagSetContains(tagQuery: TagSetContains) { + val element = AnyList() + element.add(tagQuery.element) + val placeholder = placeholderForArg(toJSON(element), PgType.STRING) + where.append("$tagsAsJsonb @> $placeholder::jsonb") + } + + private fun resolveTagNamesArrayOperation(jsonbOperator: String, tagNames: List) { + val tagKeysArray = tagNames.toTypedArray() + val tagKeysPlaceholder = placeholderForArg(tagKeysArray, PgType.STRING_ARRAY) + where.append("$tagsAsJsonb ?$jsonbOperator $tagKeysPlaceholder") + } + + private fun resolveSingleTagQuery(tagQuery: TagQuery) { + when (tagQuery) { + is TagMapHasKey -> { + val tagNamePlaceholder = placeholderForArg(tagQuery.name, PgType.STRING) + where.append("$tagsAsJsonb ?? $tagNamePlaceholder") + } + + is TagValueIsNull -> { + val tagValuePlaceholder = placeholderForArg(selectTagValue(tagQuery), PgType.STRING) + where.append("$tagValuePlaceholder IS NULL") + } + + is TagValueIsBool -> { + if (tagQuery.value) { + where.append(selectTagValue(tagQuery, PgType.BOOLEAN)) + } else { + where.append("not(${selectTagValue(tagQuery, PgType.BOOLEAN)})") + } + } + + is TagValueIsDouble -> { + val queryValuePlaceholder = placeholderForArg(tagQuery.value, PgType.DOUBLE) + val doubleOp = resolveDoubleOp( + tagQuery.op, + selectTagValue(tagQuery, PgType.DOUBLE), + queryValuePlaceholder + ) + where.append(doubleOp) + } + + is TagValueIsString -> { + val queryValuePlaceholder = placeholderForArg(tagQuery.value, PgType.STRING) + val stringEquals = resolveStringOp( + StringOp.EQUALS, + selectTagValue(tagQuery, PgType.STRING), + queryValuePlaceholder + ) + where.append(stringEquals) + } + + is TagValueMatches -> { + val jsonPathPlaceholder = placeholderForArg( + "\$.${tagQuery.name} ? (@ like_regex \"${tagQuery.regex}\")", + PgType.STRING + ) + where.append("$tagsAsJsonb @?? $jsonPathPlaceholder::jsonpath") + } + } + } + + private fun selectTagValue(tagQuery: TagQuery, castTo: PgType? = null): String { + val tagKeyPlaceholder = placeholderForArg(tagQuery.name, PgType.STRING) + return when (castTo) { + null -> "$tagsAsJsonb->$tagKeyPlaceholder" + PgType.STRING -> "$tagsAsJsonb->>$tagKeyPlaceholder" + else -> "($tagsAsJsonb->$tagKeyPlaceholder)::${castTo.value}" + } + } + + private fun not(subClause: T, subClauseResolver: (T) -> Unit) { + where.append(" NOT (") + subClauseResolver(subClause) + where.append(") ") + } + + private fun and(subClauses: List, subClauseResolver: (T) -> Unit) = + multiClause("AND", subClauses, subClauseResolver) + + private fun or(subClauses: List, subClauseResolver: (T) -> Unit) = + multiClause("OR", subClauses, subClauseResolver) + + private fun multiClause( + operand: String, + subClauses: List, + subClauseResolver: (T) -> Unit + ) { + where.append(" (") + subClauses.forEachIndexed { index, subClause -> + if (index > 0) { + where.append(" $operand ") + } + subClauseResolver(subClause) + } + where.append(") ") + } + + + private fun resolveStringOp( + stringOp: StringOp, + leftOperand: String, + rightOperand: String + ): String { + return when (stringOp) { + StringOp.EQUALS -> "$leftOperand = $rightOperand" + StringOp.NOT_EQUALS -> "$leftOperand != $rightOperand" + StringOp.STARTS_WITH -> "starts_with($leftOperand, $rightOperand)" + else -> throw NakshaException( + NakshaError.ILLEGAL_ARGUMENT, + "Unknown StringOp: $stringOp" + ) + } + } + + private fun resolveDoubleOp( + doubleOp: DoubleOp, + leftOperand: String, + rightOperand: String + ): String { + return when (doubleOp) { + DoubleOp.EQ -> "$leftOperand = $rightOperand" + DoubleOp.NE -> "$leftOperand != $rightOperand" + DoubleOp.GT -> "$leftOperand > $rightOperand" + DoubleOp.GTE -> "$leftOperand >= $rightOperand" + DoubleOp.LT -> "$leftOperand < $rightOperand" + DoubleOp.LTE -> "$leftOperand <= $rightOperand" + else -> throw NakshaException( + NakshaError.ILLEGAL_ARGUMENT, + "Unknown DoubleOp: $doubleOp" + ) + } + } } From 71a9d4b60d810f93b1532a1ba4794c008922b594 Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Tue, 23 Jun 2026 16:30:40 +0200 Subject: [PATCH 34/57] Fixed PgQueryBuilder Signed-off-by: Alexander Lowey-Weber --- .../commonMain/kotlin/naksha/model/Version.kt | 87 +++++-- .../commonMain/kotlin/naksha/model/XyzNs.kt | 4 +- .../kotlin/naksha/model/request/OrderBy.kt | 50 ++-- .../naksha/model/request/ReadFeatures.kt | 104 ++++++--- .../commonMain/kotlin/naksha/psql/PgColumn.kt | 18 +- .../kotlin/naksha/psql/PgQueryBuilder.kt | 213 ++++++------------ 6 files changed, 271 insertions(+), 205 deletions(-) diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Version.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Version.kt index d456b9009..d1480ac58 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Version.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Version.kt @@ -88,7 +88,7 @@ open class Version(@JvmField val number: Int64) : Comparable { private val SEQ_30_MASK = Int64(0x3FFF_FFFF) /** Mask for the 41-bit manual-version seq field (upper 21 bits of the 64-bit value must be 0). */ - private val MANUAL_SEQ_MASK = Int64(0x1FF_FFFF_FFFF) // 41 bits + private val MANUAL_SEQ_MASK = Int64(0x1FF_FFFF_FFFF) // 41 bits; 2,199,023,255,551 /** * Create a version from a double (JavaScript number). @@ -130,13 +130,12 @@ open class Version(@JvmField val number: Int64) : Comparable { * @param month month of the year; must be in 1..12. * @param day day of the month; must be in 1..31. * @param seq 30-bit sequence number within the day; must be in 0..1073741823 (0x3FFF_FFFF). - * @param action the [Action] to encode in the lower 2 bits; defaults to [Action.CREATE]. + * @param action the [Action] to encode in the lower 2 bits. * @since 3.0 */ @JvmStatic @JsStatic - @JvmOverloads - fun auto(year: Int, month: Int, day: Int, seq: Int64, action: Action = Action.CREATE): Version { + fun auto(year: Int, month: Int, day: Int, seq: Int64, action: Action): Version { require(year in YEAR_MIN..YEAR_MAX) { "year must be in $YEAR_MIN..$YEAR_MAX, got $year" } @@ -156,20 +155,19 @@ open class Version(@JvmField val number: Int64) : Comparable { /** * Constructs a **manual** version. * - * The resulting [number] must have its upper 21 bits (63–43) all zero, which means the effective + * The resulting [seq] must have its upper 21 bits (63–43) all zero, which means the effective * value fits in 43 bits. The [seq] therefore must be in 0..0x1FF_FFFF_FFFF (41 bits), since * the lower 2 bits are reserved for [action]. * * Throws [IllegalArgumentException] if [seq] is out of range. * * @param seq 41-bit sequence value; must be in 0..0x1FF_FFFF_FFFF. - * @param action the [Action] to encode in the lower 2 bits; defaults to [Action.CREATE]. + * @param action the [Action] to encode in the lower 2 bits. * @since 3.0 */ @JvmStatic @JsStatic - @JvmOverloads - fun manual(seq: Int64, action: Action = Action.CREATE): Version { + fun manual(seq: Int64, action: Action): Version { require(seq >= Int64(0) && seq <= MANUAL_SEQ_MASK) { "seq for a manual version must be in 0..${MANUAL_SEQ_MASK.toLong()} (41-bit), got $seq" } @@ -180,17 +178,34 @@ open class Version(@JvmField val number: Int64) : Comparable { * Creates a dated version for the current wall-clock time. * * @param seq 30-bit sequence number within the current day; must be in 0..1073741823. - * @param action the [Action] to encode; defaults to [Action.CREATE]. + * @param action the [Action] to encode. * @since 3.0 */ @JvmStatic @JsStatic - @JvmOverloads - fun now(seq: Int64, action: Action = Action.CREATE): Version { + fun now(seq: Int64, action: Action): Version { val now = Timestamp.now() return auto(now.year, now.month, now.day, seq, action) } + /** + * Turns the given version into a real version, so setting the lower two bit to two, and ensure that the value is a valid version number. + * + * @param version the version to turn into a version. + * @return the given version with the lowest two bit set. + * @throws NakshaException with error [ILLEGAL_ARGUMENT], if the given version is no valid version. + * @since 3.0 + */ + @JvmStatic + @JsStatic + @JvmOverloads + fun asVersion(version: Int64): Int64 { + val v = version or Int64(3) + if (v > HEAD.number) return HEAD.number + if (v < 0) throw illegalArg("Versions must not be negative") + return v + } + /** * The _HEAD_ sentinel version _(`9_007_199_254_740_991` aka `2^53-1`)_. Can be used as well to mask version to ensure valid version number, like `version & Version.HEAD`. * @@ -210,7 +225,7 @@ open class Version(@JvmField val number: Int64) : Comparable { */ @JvmField @JsStatic - val MIN = auto(16, 1, 1, Int64(0), Action.CREATE) + val MIN_AUTO = auto(16, 1, 1, Int64(0), Action.CREATE) // 0n + (0n << 2n) + (1n << 32n) + (1n << (32n+5n)) + (16n << (32n+5n+4n)) = 35326106009600n // bitwise: 0x0000_2021_0000_0000 @@ -220,10 +235,42 @@ open class Version(@JvmField val number: Int64) : Comparable { */ @JvmField @JsStatic - val MAX = auto(4095, 12, 31, Int64(1_073_741_823), Action.VERSION) + val MAX_AUTO = auto(4095, 12, 31, Int64(1_073_741_823), Action.VERSION) // 3n + (1073741823n << 2n) + (31n << 32n) + (12n << (32n+5n)) + (4095n << (32n+5n+4n)) = 9006786937880575n // bitwise: 0x001f_ff9f_ffff_ffff + /** + * The minimum manual version (year=0, month=0, day=0, seq=1, action=CREATED). + * @since 3.0 + */ + @JvmField + @JsStatic + val MIN_MANUAL = manual(Int64(1), Action.CREATE) + + /** + * The maximum valid manual version (seq=2,199,023,255,551, action=VERSION). + * @since 3.0 + */ + @JvmField + @JsStatic + val MAX_MANUAL = manual(MANUAL_SEQ_MASK, Action.CREATE) + + /** + * The absolute minimum version number _(3)_. + * @since 3.0 + */ + @JvmField + @JsStatic + val MIN = MIN_MANUAL + + /** + * The absolute maximal valid version number _(9,007,199,254,740,988)_. This is three less than [HEAD]. + * @since 3.0 + */ + @JvmField + @JsStatic + val MAX = Version(9_007_199_254_740_988L) + /** * The minimum value of the 30-bit sequence field (zero). * @since 3.0 @@ -291,18 +338,22 @@ open class Version(@JvmField val number: Int64) : Comparable { private var _seq: Int64? = null /** - * The 30-bit sequence number (`(txn ushr 2) and 0x3FFF_FFFF`). + * The 30-bit or 41-bit sequence number. * - * For dated versions this is the sequence within the day (0–1073741823). - * For manual versions this is the upper 30 bits of the 41-bit seq value passed to [manual]. + * - For dated versions this is the sequence within the day _(0..1,073,741,823)_. + * - For manual versions this is the version-number shifted right by 2 _(1..2,199,023,255,551)_. * @since 3.0 */ val seq: Int64 get() { var s = _seq if (s == null) { - s = (number ushr 2) and SEQ_MAX - _seq = s + if (isDated()) { + s = (number ushr 2) and SEQ_MAX + _seq = s + } else { + s = (number ushr 2) and MANUAL_SEQ_MASK + } } return s } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/XyzNs.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/XyzNs.kt index 6d7050a6a..c64fe7721 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/XyzNs.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/XyzNs.kt @@ -3,8 +3,6 @@ package naksha.model import naksha.base.* -import naksha.model.objects.StandardMembers -import naksha.model.objects.XyzMembers import naksha.model.objects.XyzMembers.XyzMembers_C.XyzAppId import naksha.model.objects.XyzMembers.XyzMembers_C.XyzAuthor import naksha.model.objects.XyzMembers.XyzMembers_C.XyzAuthorTimestamp @@ -517,7 +515,7 @@ class XyzNs : AnyObject() { get() { // Downward compatibility hack. val raw = getRaw("version") - if (raw is Int64 && raw >= Version.MIN) return Version(raw) + if (raw is Int64 && raw >= Version.MIN_AUTO) return Version(raw) val version = guid?.tupleNumber?.version return if (version != null) Version(version) else null } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/OrderBy.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/OrderBy.kt index ebe1d5bad..d776caf11 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/OrderBy.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/OrderBy.kt @@ -31,10 +31,26 @@ class OrderBy() : AnyObject() { * @param order the sort order, if [ANY][SortOrder.ANY] is given, then the storage can pick whatever is faster. * @param next if a second-level order is requested; i.e. order by `id`, then by `version`. */ - @JsName("of") + @JsName("ofMember") @JvmOverloads constructor(member: Member?, order: SortOrder = ANY, next: OrderBy? = null) : this() { - this.column = member + this.member = member?.name + this.sortOrder = order + this.next = next + } + + /** + * Create an order. + * + * If [member] is `null`, [next] must be `null` as well. + * @param memberName the name fo the member by which to order by, if _null_, any member is accepted, only a deterministic order is requested. + * @param order the sort order, if [ANY][SortOrder.ANY] is given, then the storage can pick whatever is faster. + * @param next if a second-level order is requested; i.e. order by `id`, then by `version`. + */ + @JsName("of") + @JvmOverloads + constructor(memberName: String?, order: SortOrder = ANY, next: OrderBy? = null) : this() { + this.member = memberName this.sortOrder = order this.next = next } @@ -68,22 +84,30 @@ class OrderBy() : AnyObject() { @JvmStatic fun id(): OrderBy = OrderBy(StandardMembers.Id, next = version()) - private val MEMBER_OR_NULL = NullableProperty(Member::class) + private val STRING_OR_NULL = NullableProperty(String::class) private val SORT_ORDER = NotNullEnum(SortOrder::class) { _, _ -> ANY } private val NEXT_OR_NULL = NullableProperty(OrderBy::class) } /** - * The [Member] by which to order, if `null`, then deterministic ordering is requested. + * The name of the [Member] by which to order, if `null`, then deterministic ordering is requested. * @since 3.0 */ - var column: Member? by MEMBER_OR_NULL + var member: String? by STRING_OR_NULL + + /** + * @see [member] + */ + fun withMember(member: Member?): OrderBy { + this.member = member?.name + return this + } /** - * @see [column] + * @see [member] */ - fun withMember(value: Member?): OrderBy { - column = value + fun withMember(name: String?): OrderBy { + this.member = name return this } @@ -119,20 +143,16 @@ class OrderBy() : AnyObject() { * Tests if this represents deterministic ordering, which means that no specific column is selected (`null`), the order is [Any], and no other conditions are given ([next] = `null`). * @return `true` if this represents the deterministic order; `false` otherwise. */ - fun isDeterministic(): Boolean = column == null && sortOrder == ANY && next == null + fun isDeterministic(): Boolean = member == null && sortOrder == ANY && next == null override fun equals(other: Any?): Boolean { if (other !is OrderBy) return false - return column == other.column + return member == other.member && sortOrder == other.sortOrder && next == other.next } override fun hashCode(): Int = super.hashCode() - override fun toString(): String { - val col = column ?: return "" - val next = this.next - return "${col.name} $sortOrder${if (next != null) ", $next" else ""}" - } + override fun toString(): String = "OrderBy(member=$member, sortOrder=$sortOrder, next=$next)" } \ No newline at end of file diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadFeatures.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadFeatures.kt index 81ac8a4c8..6412d65c3 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadFeatures.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadFeatures.kt @@ -2,15 +2,18 @@ package naksha.model.request +import naksha.base.Int64 import naksha.base.NotNullProperty import naksha.base.NullableProperty import naksha.base.StringList import naksha.model.GuidList import naksha.model.Version +import naksha.model.illegalArg import naksha.model.request.ops.Op import naksha.model.request.query.IPropertyQuery import naksha.model.request.query.ITagQuery import kotlin.js.JsExport +import kotlin.math.max /** * Read features from a collection of a map of a storage. @@ -27,7 +30,6 @@ open class ReadFeatures : ReadRequest() { private val STRING_LIST = NotNullProperty(StringList::class) { _, _ -> StringList() } private val BOOLEAN_OR_FALSE = NotNullProperty(Boolean::class) { _, _ -> false } private val INT_OR_1 = NotNullProperty(Int::class) { _, _ -> 1 } - private val VERSION_OR_NULL = NullableProperty(Version::class) private val ORDER_BY_OR_NULL = NullableProperty(OrderBy::class) private val GUID_LIST = NotNullProperty(GuidList::class) private val QUERY = NotNullProperty(RequestQuery::class) @@ -123,53 +125,103 @@ open class ReadFeatures : ReadRequest() { /** * Extend the request to search through historic states of features _(defaults to `false`)_. * - * Setting this to `true` adds past states from the **HISTORY** section to the result set. When - * [versions] is greater than `1`, results are ordered automatically by the storage in reverse - * version order, so the most recent state is returned first. + * Setting this to `true` adds past states from the **HISTORY** section to the result set. When [versions] is greater than `1`, results are ordered automatically by the storage in reverse version order, so the most recent state is returned first. */ var queryHistory: Boolean by BOOLEAN_OR_FALSE /** * Defines how many states (versions) of each matching feature should be returned _(defaults to `1`)_. * - * A value of `1` means only the single latest state closest to the given maximal [version] is - * returned; if no [version] is given, the current HEAD state is meant. - * - * This parameter is ignored for queries by [Guid][naksha.model.Guid], because a - * [Guid][naksha.model.Guid] already identifies an exact state. The parameter requires - * [queryHistory] to be `true`. - * - * If set to anything other than `1` _(the default)_ while [queryHistory] is `false`, the request - * will be rejected with [ILLEGAL_ARGUMENT][naksha.model.NakshaError.ILLEGAL_ARGUMENT]. + * - A value of `1` _(the default)_ means only the single latest state closest to the given maximal [version] is returned. + * - If the underlying JSON map contains a values that is not a number or invalid, the default value `1` will be used. * * Requesting multiple versions can have a significant performance impact and should be used with care. * @since 3.0.0 */ - var versions: Int by INT_OR_1 + var versions: Int + get() { + val raw = getRaw("versions") + if (raw is Int64) return max(1, raw.toInt()) + if (raw is Number) return max(1, raw.toInt()) + return 1 + } + set(value) { + if (value < 1) throw illegalArg("versions must not be a value less than 1") + set("versions", value) + } /** * Limit the read to all states at or after the given minimum version, `null` if no limit. * - * If set to anything other than `null` _(the default)_ while [queryHistory] is `false`, the request - * will be rejected with [ILLEGAL_ARGUMENT][naksha.model.NakshaError.ILLEGAL_ARGUMENT]. + * If the underlying JSON map contains a values that is not a number or invalid, the default value `null` will be used. * @since 3.0.0 */ - // TODO: Change to Int64 aka Long! - var minVersion: Version? by VERSION_OR_NULL + var minVersion: Int64? + get() { + val raw = getRaw("minVersion") + if (raw is Int64) return if (raw < Version.MIN.number || raw > Version.HEAD.number) null else raw + if (raw is Number) { + val value = Int64(raw.toLong()) + return if (value < Version.MIN.number || value > Version.HEAD.number) null else value + } + return null + } + set(value) { + if (value != null && (value < Version.MIN.number || value > Version.HEAD.number)) { + throw illegalArg("minVersion must be a value between ${Version.MIN} and ${Version.HEAD}, but was $value") + } + set("minVersion", value) + } + + fun withMinVersion(minVersion: Int64?): ReadFeatures { + this.minVersion = minVersion + return this + } + + fun withMinVersion(minVersion: Version?): ReadFeatures { + this.minVersion = minVersion?.number + return this + } + + fun withMinVersion(minVersion: Long?): ReadFeatures { + this.minVersion = if (minVersion != null) Int64(minVersion) else null + return this + } /** - * Limit the read to states at or before the given maximum version, `null` if no limit - * _(returns the current HEAD state)_. + * Limit the read to states at or before the given maximum version, `null` if no limit _([HEAD][naksha.model.Version.VersionCompanion.HEAD])_. * - * This effectively requests a specific historical snapshot when no [minVersion] is set and - * [versions] is `1` (the default). + * This effectively requests a specific historical snapshot, when no [minVersion] is set and [versions] is `1`, which is the default for both parameters. * - * If set to anything other than `null` _(the default)_ while [queryHistory] is `false`, the request - * will be rejected with [ILLEGAL_ARGUMENT][naksha.model.NakshaError.ILLEGAL_ARGUMENT]. + * If the underlying JSON map contains a values that is not a number or invalid, the default value `null` will be used. * @since 3.0.0 */ - // TODO: Change to Int64 aka Long! - var version: Version? by VERSION_OR_NULL + var version: Int64? + get() { + val raw = getRaw("version") + if (raw is Int64) return raw + if (raw is Number) return Int64(raw.toLong()) + return null + } + set(value) { + set("version", value) + } + + fun withVersion(version: Int64?): ReadFeatures { + this.version = version + return this + } + + fun withVersion(version: Version?): ReadFeatures { + this.version = version?.number + return this + } + + fun withVersion(version: Long?): ReadFeatures { + this.version = if (version != null) Int64(version) else null + return this + } + /** * Order the result-set like given; this is an expensive operation and should be avoided. diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumn.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumn.kt index 45be17569..23bc7e5e9 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumn.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumn.kt @@ -119,6 +119,9 @@ data class PgColumn( */ const val MAIN = "MAIN" + /** Constant for the name of the feature-number column. */ + const val FN_NAME = "_fn" + /** * The feature-number. * @@ -127,7 +130,10 @@ data class PgColumn( */ @JvmField @JsStatic - val FN = PgColumn(0, "_fn", INT64, "STORAGE $PLAIN NOT NULL") + val FN = PgColumn(0, FN_NAME, INT64, "STORAGE $PLAIN NOT NULL") + + /** Constant for the name of the version column. */ + const val VERSION_NAME = "_version" /** * The version (with action in the lower 2 bits) of this tuple. @@ -137,7 +143,13 @@ data class PgColumn( */ @JvmField @JsStatic - val VERSION = PgColumn(1, "_version", INT64, "STORAGE $PLAIN NOT NULL") + val VERSION = PgColumn(1, VERSION_NAME, INT64, "STORAGE $PLAIN NOT NULL") + + /** + * Constant for the name of the next-version column. + * @see [naksha.model.objects.StandardMembers.NextVersion] + */ + const val NEXT_VERSION_NAME = "_nv" /** * The next-version (with action in the lower 2 bits) of this tuple, only available in the history. @@ -145,7 +157,7 @@ data class PgColumn( */ @JvmField @JsStatic - val NEXT_VERSION = PgColumn(2, NextVersion.name, INT64, "STORAGE $PLAIN NOT NULL") + val NEXT_VERSION = PgColumn(2, NEXT_VERSION_NAME, INT64, "STORAGE $PLAIN NOT NULL") } /** Returns the [ident] of the column, so the quoted [name]. */ diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryBuilder.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryBuilder.kt index ea104ebe9..b49433b4e 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryBuilder.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryBuilder.kt @@ -1,9 +1,14 @@ package naksha.psql +import naksha.base.Int64 import naksha.model.* import naksha.model.Naksha.NakshaCompanion.HARD_TUPLE_LIMIT import naksha.model.request.* -import naksha.model.request.query.SortOrder.SortOrderCompanion.ASCENDING +import naksha.model.request.query.SortOrder +import naksha.psql.PgColumn.PgColumn_C.FN +import naksha.psql.PgColumn.PgColumn_C.NEXT_VERSION_NAME +import naksha.psql.PgColumn.PgColumn_C.VERSION +import naksha.psql.PgColumn.PgColumn_C.VERSION_NAME import kotlin.math.max import kotlin.math.min @@ -39,168 +44,96 @@ class PgQueryBuilder(val session: PgSession, val readRequest: ReadRequest) { val collectionId: String = req.collectionId ?: throw illegalArg("Request has no 'collectionId'") val pgCollection = session.getPgCollectionById(pgCatalog, collectionId) ?: throw collectionNotFound("Collection with id '$collectionId' not found in catalog '$catalogId'") - val version = req.version - val minVersion = req.minVersion - val versions = req.versions - if (versions < 1) throw illegalArg("It is not possible to request less than one version of each feature") - val whereClause = PgQueryWhereBuilder(req).build() + val whereClause = PgQueryWhereBuilder(req, pgCollection).build() val whereQuery = whereClause?.where ?: "" - val thePgCollection = if (pgCollections.size == 1) pgCollections[0] else null - // Columns to select, so `col_num`, `fn`, `version`, and whatever is needed for ordering. - val select_cols = mutableListOf("col_num", "fn", "version") // Column name and ordering (ASC | DESC) for customer order. - val order_by = mutableListOf>() - req.orderBy?.also { orderBy -> - // The default order is descending by tuple-number, therefore we do not need - // any special order, when the client asks for deterministic, or it asks - // for tuple-number ordering (except he asks for ascending ordering) - if (!orderBy.isDeterministic() - && !(orderBy.column == MetaColumn.tupleNumber() && orderBy.sortOrder != ASCENDING && orderBy.next == null) - ) { - val selected_names = mutableListOf() - var o: OrderBy? = orderBy - while (o != null) { - val col = o.column - if (col != null) { - // TODO: Support all columns - val col_name = col.name - val pgColumn = when (col_name) { - MetaColumn.ATTACHMENT -> PgColumn.attachment - MetaColumn.ID -> PgColumn.id - MetaColumn.VERSION -> PgColumn.version - // No single "tuple-number" column exists post-refactor; sort by `fn` - // as a pragmatic approximation of the natural primary order. - MetaColumn.TUPLE_NUMBER -> PgColumn.fn - MetaColumn.HASH -> PgColumn.hash - MetaColumn.HERE_TILE -> PgColumn.here_tile - MetaColumn.AUTHOR -> PgColumn.author - MetaColumn.AUTHOR_TS -> PgColumn.author_ts - MetaColumn.APP_ID -> PgColumn.app_id - MetaColumn.CHANGE_COUNT -> PgColumn.cc - MetaColumn.CV0 -> PgColumn.cv0 - MetaColumn.CV1 -> PgColumn.cv1 - MetaColumn.CV2 -> PgColumn.cv2 - MetaColumn.CV3 -> PgColumn.cv3 - MetaColumn.CS0 -> PgColumn.cs0 - MetaColumn.CS1 -> PgColumn.cs1 - MetaColumn.CS2 -> PgColumn.cs2 - MetaColumn.CS3 -> PgColumn.cs3 - else -> throw illegalArg("Invalid column for ordering: '$col_name'") - } - if (!selected_names.contains(col_name)) { - selected_names.add(col_name) - if (!select_cols.contains(pgColumn.name)) { - select_cols.add(pgColumn.name) - } - val SORT = if (o.sortOrder == ASCENDING) "ASC" else "DESC" - order_by.add(when (pgColumn) { - PgColumn.author_ts -> Pair("COALESCE(${PgColumn.author_ts}, ${PgColumn.updated_at})", SORT) - else -> Pair(pgColumn.name, SORT) - }) - } + var order_by: MutableList>? = null + do { // scope + var orderBy = req.orderBy + if (orderBy != null) { + order_by = mutableListOf() + if (!orderBy.isDeterministic()) { + var exit = 10 + while (orderBy != null && exit >= 0) { + val memberName = orderBy.member ?: throw illegalArg("Missing member in orderBy") + val sortOrder = orderBy.sortOrder.text + order_by.add(Pair(memberName, sortOrder)) + orderBy = orderBy.next + exit-- } - o = o.next + if (exit < 0) throw illegalArg("Too many orders in orderBy") + } else { // deterministic ordering is by `feature-number ASC, version DESC` + order_by.add(Pair(FN.toString(), SortOrder.ASCENDING.toString())) + order_by.add(Pair(VERSION.toString(), SortOrder.DESCENDING.toString())) } } - } - - // Generate query. - val selects = StringBuilder() - for (entry in pgCollections.withIndex()) { - val pgCollection = entry.value - val map = pgCollection.catalog - val read = PgRead(pgCatalog, pgCollection) - - // Note: To simplify queries, we actually always embed the collection-number internally, - // eventually, before returning the result, we decide if we put it into the header - // of the tuple-number-binary or individually into each row-identifier. - select_cols[0] = "${pgCollection.collectionNumber} AS col_num" - val select_cols_string = select_cols.joinToString(", ") - - val where = if (whereQuery.isEmpty()) "" else "WHERE $whereQuery" - // HEAD has no `next_version` column — substitute with NULL so predicates referencing - // it evaluate to NULL (no HEAD rows match), matching the "no successor yet" semantics. - val whereForHeadBase = whereQuery.replace(Regex("\\b${next_version.name}\\b"), "NULL::int8") - // Filter tombstones from HEAD by default: only live rows ((version & 3) < 2). - // queryDeleted=true is the sole gate that lifts this filter and exposes tombstones. - // queryHistory=true is fully orthogonal — it adds past states from the history table - // but does NOT change tombstone visibility in HEAD. - val deletedFilter = if (!req.queryDeleted) "(version & 3) < 2" else null - val whereForHead = when { - whereForHeadBase.isEmpty() && deletedFilter != null -> "WHERE $deletedFilter" - whereForHeadBase.isEmpty() -> "" - deletedFilter != null -> "WHERE $whereForHeadBase AND $deletedFilter" - else -> "WHERE $whereForHeadBase" - } - for (head in read.headTables) { - if (selects.isNotEmpty()) selects.append(" UNION ALL\n") - selects.append("\t(SELECT $select_cols_string FROM ${map.quotedId}.${head.quotedName} $whereForHead)\n") - } - - // The shadow/deleted table has been removed. Deleted rows now live in HEAD with (version & 3) == 2. - // queryDeleted=true is handled above by omitting the tombstone filter on HEAD. - - val historyTables = read.historyTables - if (req.queryHistory && historyTables != null) { - // TODO: We need to improve, because we only want $versions variants! - // If only one version is requested, we can improve the query to only return this version! - val better_where = if (version != null && versions == 1) - (if (where.isEmpty()) "WHERE " else "$where AND ") + "$next_version > $version" - else - where - for (history in historyTables) { - if (selects.isNotEmpty()) selects.append(" UNION ALL\n") - selects.append("\t(SELECT $select_cols_string FROM ${map.quotedId}.${history.quotedName} $better_where)\n") - } + } while(false) + + // TODO: We want to use placeholders! + + val WHERE = StringBuilder() + if (!req.queryDeleted) WHERE.append("($VERSION_NAME & 3) < 2 ") + var version = req.version + if (version != null) { + version = version or Int64(3) + if (version == Version.HEAD.number) { + version = null // HEAD + } else { + if (WHERE.isNotEmpty()) WHERE.append(" AND ") + WHERE.append("($VERSION_NAME <= ").append(version).append(")") } } - // Restore original value for `col_num` selection. - select_cols[0] = "col_num" - - // The columns we need until the last final result building. - val select_cols_string = select_cols.joinToString(", ") - - // If history is queried, and only a certain amount of versions should be returned - // we need to partition the result, so that we can select have the requested amount of versions. - val part = if (req.queryHistory && versions > 1) """, query_with_v AS ( - SELECT - $select_cols_string, - ROW_NUMBER() OVER (PARTITION BY col_num, fn ORDER BY version DESC) AS v - FROM query -), part AS ( - SELECT $select_cols_string - FROM query_with_v - WHERE v <= $versions -)""" else "" + var minVersion = req.minVersion + if (minVersion != null) { + minVersion = minVersion or Int64(3) + if (WHERE.isNotEmpty()) WHERE.append(" AND ") + WHERE.append("($VERSION_NAME >= ").append(minVersion).append(")") + } + val readHistory = req.queryHistory && pgCollection.storeHistory + val versions = req.versions + if (readHistory && versions == 1 && version != null) { // Return latest version only, but involve history. + if (WHERE.isNotEmpty()) WHERE.append(" AND ") + WHERE.append("($NEXT_VERSION_NAME > ").append(version).append(" OR $NEXT_VERSION_NAME IS NULL) ") + } + val where = if (whereQuery.isEmpty()) "" else "WHERE $WHERE " + + // The query starts with select all tuple-numbers of matching tuple. + val query = if (readHistory) """query AS +( SELECT $FN, $VERSION FROM ${pgCollection.headTable.quotedName}$where + UNION ALL + SELECT $FN, $VERSION FROM ${pgCollection.historyTable.quotedName}$where )""" else """query AS +( SELECT $FN, $VERSION FROM ${pgCollection.headTable.quotedName}$where )""" + + // If history is queried, and only a certain amount of versions should be returned, + // or we want only the latest version, but no specific version target is given, then + // we need to partition the result, so that we can select the requested amount of latest versions. + val all = if (readHistory && (versions > 1 || version == null)) """, query_partitioned AS +(SELECT $FN, $VERSION, ROW_NUMBER() OVER (PARTITION BY fn ORDER BY version DESC) AS v FROM query) +, all AS +(SELECT $FN, $VERSION FROM query_partitioned WHERE v <= $versions)""" else "" // `order_by` may be empty, if no custom ordering was requested. - val order_by_string = order_by.joinToString(", ") { "${it.first} ${it.second}" } + val order_by_string = order_by?.joinToString(", ") { "${it.first} ${it.second}" } ?: "" // apply limit and order, if given - val limited = """, limited AS ( - SELECT $select_cols_string - FROM ${if (part.isNotEmpty()) "part" else "query"} - ${if (order_by_string.isNotEmpty()) "ORDER BY $order_by_string " // Explicit ordering. - else if (part.isNotEmpty()) "ORDER BY col_num DESC, fn DESC, version DESC " // If multiple versions requested, order by version. - else "" // No explicit ordering, no multiple versions, use random oder - }LIMIT $REQ_LIMIT -)""" + val limited = """, limited AS +(SELECT $FN, $VERSION FROM ${if (all.isNotEmpty()) "all" else "query"} ${if (order_by_string.isNotEmpty()) "ORDER BY $order_by_string " +else if (all.isNotEmpty()) "ORDER BY $FN ASC, $VERSION DESC " else ""}LIMIT $REQ_LIMIT)""" // The final SQL query. // We only need `col_num`, `fn`, and `version`; the additional columns in limit were only for sorting. // If we only select from a single collection (thePgCollection != null), `col_num` is implicit. - val SQL = """WITH query AS ( -$selects)$part$limited -SELECT ${if (thePgCollection == null) "col_num, fn, version" else "fn, version"} FROM limited""" + val SQL = """WITH $query$all$limited +SELECT $FN, $VERSION FROM limited""" return PgQuery( sql = SQL, argValues = whereClause?.argValues?.toTypedArray() ?: emptyArray(), argTypes = whereClause?.argTypeNames ?: emptyArray(), pgStorage.number, pgCatalog.catalogNumber, - thePgCollection?.collectionNumber + pgCollection.collectionNumber ) } } \ No newline at end of file From e9ca517c480a731a711f22d0cb5c7ad803717e4b Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Tue, 23 Jun 2026 16:59:34 +0200 Subject: [PATCH 35/57] Fix PgQueryBuilder and lots of small issues, like JS annotations. Signed-off-by: Alexander Lowey-Weber --- .../naksha/model/FeatureMemberValues.kt | 4 +- .../commonMain/kotlin/naksha/model/Naksha.kt | 2 +- .../kotlin/naksha/model/objects/Index.kt | 2 +- .../kotlin/naksha/model/objects/IndexType.kt | 6 +-- .../kotlin/naksha/model/objects/Member.kt | 15 +------ .../kotlin/naksha/model/objects/MemberType.kt | 37 ++++++----------- .../naksha/model/objects/NakshaCollection.kt | 4 +- .../kotlin/naksha/model/objects/NakshaTx.kt | 2 +- .../naksha/model/objects/TagListMember.kt | 7 ++-- .../kotlin/naksha/model/objects/TagsMember.kt | 13 +++--- .../naksha/model/objects/TupleNumberMember.kt | 1 + .../kotlin/naksha/model/request/OrderBy.kt | 2 + .../naksha/model/request/ReadCollections.kt | 2 +- .../naksha/model/request/ReadFeatures.kt | 7 ++++ .../kotlin/naksha/model/request/ReadMaps.kt | 2 +- .../naksha/model/request/ReadTransactions.kt | 2 +- .../kotlin/naksha/model/request/ops/And.kt | 2 + .../kotlin/naksha/model/request/ops/Equals.kt | 5 ++- .../kotlin/naksha/model/request/ops/Gt.kt | 3 ++ .../kotlin/naksha/model/request/ops/Gte.kt | 3 ++ .../naksha/model/request/ops/Intersects.kt | 3 ++ .../naksha/model/request/ops/IsAnyOf.kt | 3 ++ .../naksha/model/request/ops/IsFalse.kt | 3 ++ .../kotlin/naksha/model/request/ops/IsNull.kt | 3 ++ .../kotlin/naksha/model/request/ops/IsTrue.kt | 3 ++ .../kotlin/naksha/model/request/ops/Lt.kt | 3 ++ .../kotlin/naksha/model/request/ops/Lte.kt | 3 ++ .../kotlin/naksha/model/request/ops/Not.kt | 2 + .../kotlin/naksha/model/request/ops/Or.kt | 2 + .../naksha/model/request/ops/StartsWith.kt | 3 ++ .../naksha/model/request/ops/TagEquals.kt | 3 ++ .../kotlin/naksha/model/request/ops/TagGt.kt | 3 ++ .../kotlin/naksha/model/request/ops/TagGte.kt | 3 ++ .../naksha/model/request/ops/TagIsNull.kt | 3 ++ .../model/request/ops/TagListContains.kt | 10 +++++ .../model/request/ops/TagListContainsAllOf.kt | 3 ++ .../model/request/ops/TagListContainsAnyOf.kt | 3 ++ .../kotlin/naksha/model/request/ops/TagLt.kt | 3 ++ .../kotlin/naksha/model/request/ops/TagLte.kt | 3 ++ .../model/request/ops/TagMapHasAllOf.kt | 3 ++ .../model/request/ops/TagMapHasAnyOf.kt | 3 ++ .../naksha/model/request/ops/TagMapHasKey.kt | 3 ++ .../naksha/model/request/ops/TagStartsWith.kt | 3 ++ .../naksha/model/request/query/TagExists.kt | 2 +- .../naksha/model/request/query/TagQuery.kt | 4 +- .../model/request/query/TagSetContains.kt | 4 +- .../kotlin/naksha/model/MemberTest.kt | 6 +-- .../commonMain/kotlin/naksha/psql/LibPsql.kt | 2 +- .../kotlin/naksha/psql/PgAdminCatalog.kt | 41 +++++++++---------- .../commonMain/kotlin/naksha/psql/PgIndex.kt | 6 +-- .../kotlin/naksha/psql/PgMemberHelper.kt | 18 ++++---- .../commonMain/kotlin/naksha/psql/PgType.kt | 6 +-- .../commonMain/kotlin/naksha/psql/PgWriter.kt | 2 +- .../kotlin/naksha/psql/CollectionTests.kt | 2 +- .../kotlin/naksha/psql/PsqlAdminCatalog.kt | 5 +-- 55 files changed, 180 insertions(+), 113 deletions(-) diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/FeatureMemberValues.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/FeatureMemberValues.kt index 83f9efc2e..cb5e3f99e 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/FeatureMemberValues.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/FeatureMemberValues.kt @@ -65,8 +65,8 @@ object FeatureMemberValues { MemberType.STRING -> coerceString(value, featureId, memberName) MemberType.BYTE_ARRAY -> coerceByteArray(value, featureId, memberName) MemberType.SPATIAL -> coerceSpatial(value, featureId, memberName) - MemberType.TAGS -> coerceTags(value, featureId, memberName) - MemberType.TAGS_FROM_ARRAY -> coerceTagsFromArray(value, featureId, memberName) + MemberType.TAG_MAP -> coerceTags(value, featureId, memberName) + MemberType.TAG_MAP_FROM_ARRAY -> coerceTagsFromArray(value, featureId, memberName) else -> { warnMismatch(featureId, memberName, type.toString(), value) null diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt index 303139983..1ec0f2851 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt @@ -573,7 +573,7 @@ class Naksha private constructor() { * Supports both persisted forms: * - a JSON array ([tag_list][naksha.model.objects.MemberType.TAG_LIST], the default) is returned * unmodified, preserving the element order; - * - a JSON object ([naksha.model.objects.MemberType.TAGS_FROM_ARRAY]) is re-flattened via + * - a JSON object ([naksha.model.objects.MemberType.TAG_MAP_FROM_ARRAY]) is re-flattened via * [TagMap.toTagList], in which case the original order is not guaranteed. * @param json the JSON text to decode (value of the `tags` member). * @return the decoded tag-list, or _null_ if [json] is _null_, blank, or neither an array nor an object. diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Index.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Index.kt index 398f25a98..e49d6cde0 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Index.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Index.kt @@ -19,7 +19,7 @@ import kotlin.js.JsName * collection. The [type] decides the storage method: * - [IndexType.BTREE]: ordered index for equality and range queries on scalar and text columns. * - [IndexType.SPATIAL]: spatial index covering a 2D-encoded geometry column. [on] must contain exactly one geometry member. - * - [IndexType.TAGS]: inverted index over a tags member ([MemberType.TAGS] or [MemberType.TAGS_FROM_ARRAY]) supporting key/value containment queries. [on] must contain exactly one member. + * - [IndexType.TAG_MAP]: inverted index over a tags member ([MemberType.TAG_MAP] or [MemberType.TAG_MAP_FROM_ARRAY]) supporting key/value containment queries. [on] must contain exactly one member. * - [IndexType.TAG_LIST]: inverted index over a tag-list member ([MemberType.TAG_LIST]) supporting element containment queries. [on] must contain exactly one member. * * When [internal] is `true` the index is storage-managed (e.g. primary key, id_unique). The storage diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/IndexType.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/IndexType.kt index 9bee0b136..3415b6054 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/IndexType.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/IndexType.kt @@ -12,7 +12,7 @@ import kotlin.reflect.KClass * * - [BTREE] — ordered index for equality and range queries on primitive columns (numbers, booleans, strings, byte-arrays). * - [SPATIAL] — spatial index over a geometry column ([MemberType.SPATIAL]) (e.g. the built-in `geo`). - * - [TAGS] — inverted index over a tags column ([MemberType.TAGS] or [MemberType.TAGS_FROM_ARRAY]); + * - [TAG_MAP] — inverted index over a tags column ([MemberType.TAG_MAP] or [MemberType.TAG_MAP_FROM_ARRAY]); * supports key/value containment lookups. * - [TAG_LIST] — inverted index over a tag-list column ([MemberType.TAG_LIST]); supports element containment lookups. * @since 3.0 @@ -41,12 +41,12 @@ class IndexType : JsEnum() { val SPATIAL = defIgnoreCase(IndexType::class, "spatial") /** - * Inverted index over a [MemberType.TAGS] or [MemberType.TAGS_FROM_ARRAY] column. + * Inverted index over a [MemberType.TAG_MAP] or [MemberType.TAG_MAP_FROM_ARRAY] column. * Supports key/value containment lookups. * @since 3.0 */ @JvmField - val TAGS = defIgnoreCase(IndexType::class, "tags") + val TAG_MAP = defIgnoreCase(IndexType::class, "tag_map") /** * Inverted index over a [MemberType.TAG_LIST] column. Supports element containment lookups, diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt index f9c8721e5..9cf6f09a1 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt @@ -13,7 +13,6 @@ import naksha.base.NullableProperty import naksha.base.PlatformList import naksha.base.PlatformMap import naksha.base.Proxy -import naksha.base.proxy import naksha.geo.SpGeometry import naksha.model.Naksha import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_ARGUMENT @@ -22,18 +21,6 @@ import naksha.model.NakshaException import naksha.model.TagList import naksha.model.TagMap import naksha.model.TupleNumber -import naksha.model.objects.ByteArrayMember -import naksha.model.objects.Float32Member -import naksha.model.objects.Float64Member -import naksha.model.objects.Int16Member -import naksha.model.objects.Int32Member -import naksha.model.objects.Int64Member -import naksha.model.objects.Int8Member -import naksha.model.objects.SpatialMember -import naksha.model.objects.TagListMember -import naksha.model.objects.StringMember -import naksha.model.objects.TagsMember -import naksha.model.objects.TupleNumberMember import kotlin.js.JsExport import kotlin.js.JsName @@ -399,7 +386,7 @@ open class Member() : AnyObject(), Comparator { MemberType.BYTE_ARRAY -> proxy(ByteArrayMember::class) MemberType.TUPLE_NUMBER -> proxy(TupleNumberMember::class) MemberType.SPATIAL -> proxy(SpatialMember::class) - MemberType.TAGS, MemberType.TAGS_FROM_ARRAY -> proxy(TagsMember::class) + MemberType.TAG_MAP, MemberType.TAG_MAP_FROM_ARRAY -> proxy(TagsMember::class) MemberType.TAG_LIST -> proxy(TagListMember::class) } return this diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberType.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberType.kt index 3cd2bd7fd..22c77a1fe 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberType.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberType.kt @@ -9,19 +9,6 @@ import naksha.model.NakshaError.NakshaErrorCompanion.INITIALIZATION_FAILED import naksha.model.NakshaException import naksha.model.TagMap import naksha.model.TupleNumber -import naksha.model.objects.BoolMember -import naksha.model.objects.ByteArrayMember -import naksha.model.objects.Float32Member -import naksha.model.objects.Float64Member -import naksha.model.objects.Int16Member -import naksha.model.objects.Int32Member -import naksha.model.objects.Int64Member -import naksha.model.objects.Int8Member -import naksha.model.objects.SpatialMember -import naksha.model.objects.TagListMember -import naksha.model.objects.StringMember -import naksha.model.objects.TagsMember -import naksha.model.objects.TupleNumberMember import kotlin.js.JsExport import kotlin.jvm.JvmField import kotlin.reflect.KClass @@ -32,15 +19,15 @@ import kotlin.reflect.KClass * - Primitives: [BOOLEAN], [INT8], [INT16], [INT32], [INT64], [FLOAT32], [FLOAT64], [STRING], [BYTE_ARRAY]. * - [SPATIAL]: a geometry stored as raw TWKB bytes. The storage persists this as a binary column and * supports spatial queries. Only a [IndexType.SPATIAL] index may be placed on a [SPATIAL] member. - * - [TAGS]: a map whose keys are strings and values are primitives (matches JBON2 tag-map specification). + * - [TAG_MAP]: a map whose keys are strings and values are primitives (matches JBON2 tag-map specification). * The storage persists this as a flat key/value map that supports containment queries. - * - [TAGS_FROM_ARRAY]: like [TAGS] but the input is a `TagList` (Naksha tag-array syntax, e.g. + * - [TAG_MAP_FROM_ARRAY]: like [TAG_MAP] but the input is a `TagList` (Naksha tag-array syntax, e.g. * `["key=value", "name:=42"]`). The list is converted to a tag-map at write time and stored in the - * same flat key/value representation as [TAGS]. Beware that the conversion has a side effect: + * same flat key/value representation as [TAG_MAP]. Beware that the conversion has a side effect: * reading the feature back returns the tags re-flattened from the map, so the original array * order is **not** preserved. * Only valid as a [Member] type; not a valid [IndexType]. - * To index a [TAGS_FROM_ARRAY] column use [IndexType.TAGS]. + * To index a [TAG_MAP_FROM_ARRAY] column use [IndexType.TAG_MAP]. * - [TAG_LIST]: a list of unique primitive values (booleans, numbers, strings). Value order is * significant, but must not have duplicates, null or undefined. The storage persists the list * unmodified, so the element order is guaranteed to be preserved when reading the feature back. @@ -145,25 +132,25 @@ class MemberType : JsEnum() { * specification. The storage persists this as a flat key/value map that supports * containment queries. * - * Indexed via [IndexType.TAGS]. + * Indexed via [IndexType.TAG_MAP]. * @since 3.0 */ @JvmField - val TAGS = defIgnoreCase(MemberType::class, "tags") { self -> self.sortOrder = 8; self.subtype = TagsMember::class } + val TAG_MAP = defIgnoreCase(MemberType::class, "tag_map") { self -> self.sortOrder = 8; self.subtype = TagsMember::class } /** - * A string-array using Naksha tag syntax that is expanded into a [TAGS] map at write time. + * A string-array using Naksha tag syntax that is expanded into a [TAG_MAP] map at write time. * Provided for downward compatibility with XYZ Hub and previous Naksha v2 clients that send * tags as arrays rather than maps. * * Input is `["key=value", "name:=42"]`; materialized form is `{"key":"value","name":42}`. - * Stored in the same flat key/value representation as [TAGS]. + * Stored in the same flat key/value representation as [TAG_MAP]. * - * **Not valid as an [IndexType].** To index this column use [IndexType.TAGS]. + * **Not valid as an [IndexType].** To index this column use [IndexType.TAG_MAP]. * @since 3.0 */ @JvmField - val TAGS_FROM_ARRAY = defIgnoreCase(MemberType::class, "tags_from_array") { self -> self.sortOrder = 9; self.subtype = TagsMember::class } + val TAG_MAP_FROM_ARRAY = defIgnoreCase(MemberType::class, "tag_map_from_array") { self -> self.sortOrder = 9; self.subtype = TagsMember::class } /** * A list of unique primitive values (booleans, numbers, strings), following the JBON2 @@ -173,7 +160,7 @@ class MemberType : JsEnum() { * * This is the default type of the standard `tags` member (the classic XYZ tags array at * `properties -> @ns:com:here:xyz -> tags`, e.g. `["foo", "bar"]`). In contrast to - * [TAGS_FROM_ARRAY] the values are not split into key/value pairs, therefore only full + * [TAG_MAP_FROM_ARRAY] the values are not split into key/value pairs, therefore only full * elements can be searched, not keys or values. * * Indexed via [IndexType.TAG_LIST]. @@ -209,7 +196,7 @@ class MemberType : JsEnum() { BYTE_ARRAY -> value is ByteArray TUPLE_NUMBER -> value is TupleNumber SPATIAL -> value is SpGeometry - TAGS, TAGS_FROM_ARRAY -> value is TagMap + TAG_MAP, TAG_MAP_FROM_ARRAY -> value is TagMap TAG_LIST -> value is List<*> else -> false } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt index 5477af283..e390dd7c6 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt @@ -6,12 +6,10 @@ import naksha.base.* import naksha.geo.SpBoundingBox import naksha.geo.SpGeometry import naksha.geo.SpPoint -import naksha.model.DataEncoding import naksha.model.Naksha import naksha.model.NakshaError import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_ARGUMENT import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_STATE -import naksha.model.NakshaError.NakshaErrorCompanion.NOT_FOUND import naksha.model.NakshaException import naksha.model.TupleNumber import kotlin.js.JsExport @@ -426,7 +424,7 @@ open class NakshaCollection() : NakshaFeature() { /** * The indices to maintain on this collection. * - * Each [Index] declares a name, an [IndexType] ([IndexType.BTREE] / [IndexType.SPATIAL] / [IndexType.TAGS] / [IndexType.TAG_LIST]), the column(s) to index, an optional include-list (for [IndexType.BTREE]), and a `unique` flag. + * Each [Index] declares a name, an [IndexType] ([IndexType.BTREE] / [IndexType.SPATIAL] / [IndexType.TAG_MAP] / [IndexType.TAG_LIST]), the column(s) to index, an optional include-list (for [IndexType.BTREE]), and a `unique` flag. * * Indices are applied to every variant of the collection (head, history, deleted, meta) by the storage. Mandatory and default indices are injected by the storage; clients only need to declare additional custom indices here. * @since 3.0 diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaTx.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaTx.kt index dc2cd9066..0a1e3e8c2 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaTx.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaTx.kt @@ -45,7 +45,7 @@ open class NakshaTx : NakshaFeature() { */ @JvmOverloads fun setEpoch(epoch: Timestamp, seq: Int64 = Int64(0)): NakshaTx { - val version = Version.auto(epoch.year, epoch.month, epoch.day, seq) + val version = Version.auto(epoch.year, epoch.month, epoch.day, seq, Action.VERSION) setRaw("id", version.toString()) setRaw("time", epoch.ts) return this diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TagListMember.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TagListMember.kt index b90a6b15e..4cb05b478 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TagListMember.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TagListMember.kt @@ -1,6 +1,7 @@ package naksha.model.objects -import naksha.base.AnyList +import naksha.base.ListProxy +import naksha.model.TagList import naksha.model.illegalArg import naksha.model.illegalState import naksha.model.objects.MemberType.MemberType_C.TAG_LIST @@ -30,6 +31,6 @@ class TagListMember() : TypedMember() { this.path = path?.validate() ?: member.path } - fun get(feature: NakshaFeature): AnyList? = getTagList(feature) - fun set(feature: NakshaFeature, value: AnyList): Any? = setPath(feature, path, value) + fun get(feature: NakshaFeature): TagList? = getTagList(feature) + fun set(feature: NakshaFeature, value: ListProxy<*>): Any? = setPath(feature, path, value) } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TagsMember.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TagsMember.kt index 22d51a632..4521c4d16 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TagsMember.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TagsMember.kt @@ -1,14 +1,15 @@ package naksha.model.objects +import naksha.model.TagMap import naksha.model.illegalArg import naksha.model.illegalState -import naksha.model.objects.MemberType.MemberType_C.TAGS +import naksha.model.objects.MemberType.MemberType_C.TAG_MAP import kotlin.js.JsName class TagsMember() : TypedMember() { override fun verify(): TagsMember { - if (dataType != TAGS) { - throw illegalState("The member was illegally cast, expected subtype: $TAGS, found: $dataType") + if (dataType != TAG_MAP) { + throw illegalState("The member was illegally cast, expected subtype: $TAG_MAP, found: $dataType") } return this } @@ -16,16 +17,16 @@ class TagsMember() : TypedMember() { @JsName("of") constructor(name: String, path: JsonPath? = null) : this() { this.name = name - this.dataType = TAGS + this.dataType = TAG_MAP this.path = path ?: JsonPath(listOf("properties", name)) this.path.validate() } @JsName("from") constructor(member: Member, path: JsonPath? = null) : this() { - if (member.dataType != TAGS) throw illegalArg("The given member is not of tags type") + if (member.dataType != TAG_MAP) throw illegalArg("The given member is not of tags type") this.name = member.name - this.dataType = TAGS + this.dataType = TAG_MAP this.path = path?.validate() ?: member.path } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TupleNumberMember.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TupleNumberMember.kt index b5fd30a1e..f07c4f7be 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TupleNumberMember.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TupleNumberMember.kt @@ -1,5 +1,6 @@ package naksha.model.objects +import naksha.model.TupleNumber import naksha.model.illegalArg import naksha.model.illegalState import naksha.model.objects.MemberType.MemberType_C.TUPLE_NUMBER diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/OrderBy.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/OrderBy.kt index d776caf11..f6beaf9de 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/OrderBy.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/OrderBy.kt @@ -98,6 +98,7 @@ class OrderBy() : AnyObject() { /** * @see [member] */ + @JsName("withMember") fun withMember(member: Member?): OrderBy { this.member = member?.name return this @@ -106,6 +107,7 @@ class OrderBy() : AnyObject() { /** * @see [member] */ + @JsName("withMemberName") fun withMember(name: String?): OrderBy { this.member = name return this diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadCollections.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadCollections.kt index 5dea0963d..57e04bbc9 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadCollections.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadCollections.kt @@ -81,7 +81,7 @@ open class ReadCollections : ReadRequest() { fun toReadFeatures(): ReadFeatures { val req = ReadFeatures() req.catalogId = mapId - req.collectionId.add(Naksha.COLLECTIONS_COL_ID) + req.collectionId = Naksha.COLLECTIONS_COL_ID req.featureIds.addAll(collectionIds) return req } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadFeatures.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadFeatures.kt index 6412d65c3..fcad04ab9 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadFeatures.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadFeatures.kt @@ -13,6 +13,7 @@ import naksha.model.request.ops.Op import naksha.model.request.query.IPropertyQuery import naksha.model.request.query.ITagQuery import kotlin.js.JsExport +import kotlin.js.JsName import kotlin.math.max /** @@ -173,16 +174,19 @@ open class ReadFeatures : ReadRequest() { set("minVersion", value) } + @JsName("withMinVersionInt64") fun withMinVersion(minVersion: Int64?): ReadFeatures { this.minVersion = minVersion return this } + @JsName("withMinVersion") fun withMinVersion(minVersion: Version?): ReadFeatures { this.minVersion = minVersion?.number return this } + @JsName("withMinVersionLong") fun withMinVersion(minVersion: Long?): ReadFeatures { this.minVersion = if (minVersion != null) Int64(minVersion) else null return this @@ -207,16 +211,19 @@ open class ReadFeatures : ReadRequest() { set("version", value) } + @JsName("withVersionInt64") fun withVersion(version: Int64?): ReadFeatures { this.version = version return this } + @JsName("withVersion") fun withVersion(version: Version?): ReadFeatures { this.version = version?.number return this } + @JsName("withVersionLong") fun withVersion(version: Long?): ReadFeatures { this.version = if (version != null) Int64(version) else null return this diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadMaps.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadMaps.kt index 91f0721d3..e41411429 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadMaps.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadMaps.kt @@ -72,7 +72,7 @@ open class ReadMaps() : ReadRequest() { fun toReadFeatures(): ReadFeatures { val req = ReadFeatures() req.catalogId = Naksha.ADMIN_CATALOG_ID - req.collectionId.add(Naksha.CATALOGS_COL_ID) + req.collectionId = Naksha.CATALOGS_COL_ID req.featureIds.addAll(mapIds) return req } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadTransactions.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadTransactions.kt index e1b2cf81c..864b1c042 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadTransactions.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ReadTransactions.kt @@ -14,7 +14,7 @@ import kotlin.js.JsExport open class ReadTransactions : ReadFeatures() { init { catalogId = Naksha.ADMIN_CATALOG_ID - collectionId.add(Naksha.TRANSACTIONS_COL_ID) + collectionId = Naksha.TRANSACTIONS_COL_ID } /** diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/And.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/And.kt index 9898412b5..625474b36 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/And.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/And.kt @@ -4,6 +4,7 @@ package naksha.model.request.ops import naksha.base.NotNullProperty import kotlin.js.JsExport +import kotlin.js.JsName /** * Logical AND. @@ -15,6 +16,7 @@ class And() : Op() { private val VALUES = NotNullProperty(OpList::class) { _, _ -> OpList() } } + @JsName("of") constructor(vararg children: Op) : this() { this.op = AND val _children = this.children diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Equals.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Equals.kt index c16f88c61..ddf3854ce 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Equals.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Equals.kt @@ -5,23 +5,26 @@ package naksha.model.request.ops import naksha.base.NullableProperty import naksha.model.objects.Member import kotlin.js.JsExport +import kotlin.js.JsName /** * Tests if the member at [at] equals the given [value]. * @since 3.0 */ @JsExport -class Equals() : Op(), ICompare { +class Equals() : Op() { companion object Equals_C { private val VALUE = NullableProperty(Any::class) } + @JsName("forName") constructor(at: String, value: Any?) : this() { this.op = EQ this.at = at this.value = value } + @JsName("forMember") constructor(at: Member, value: Any?) : this(at.name, value) /** diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Gt.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Gt.kt index eb4f47b4e..b64ecd8ca 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Gt.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Gt.kt @@ -6,6 +6,7 @@ import naksha.base.NotNullProperty import naksha.base.NullableProperty import naksha.model.objects.Member import kotlin.js.JsExport +import kotlin.js.JsName /** * Tests if the member at [at] is greater than the given [value] (GreaterThan). @@ -17,12 +18,14 @@ class Gt() : Op() { private val VALUE = NotNullProperty(Any::class) } + @JsName("forName") constructor(at: String, value: Any) : this() { this.op = GT this.at = at this.value = value } + @JsName("forMember") constructor(at: Member, value: Any) : this(at.name, value) var value: Any by VALUE diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Gte.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Gte.kt index 6117a61cf..e58452914 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Gte.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Gte.kt @@ -6,6 +6,7 @@ import naksha.base.NotNullProperty import naksha.base.NullableProperty import naksha.model.objects.Member import kotlin.js.JsExport +import kotlin.js.JsName /** * Tests if the member at [at] is greater than or equal to the given [value] (GreaterThanOrEqual). @@ -17,12 +18,14 @@ class Gte() : Op() { private val VALUE = NotNullProperty(Any::class) } + @JsName("forName") constructor(at: String, value: Any) : this() { this.op = GTE this.at = at this.value = value } + @JsName("forMember") constructor(at: Member, value: Any) : this(at.name, value) var value: Any by VALUE diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Intersects.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Intersects.kt index 6e75b013b..385c577c9 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Intersects.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Intersects.kt @@ -6,6 +6,7 @@ import naksha.base.NotNullProperty import naksha.geo.SpGeometry import naksha.model.objects.Member import kotlin.js.JsExport +import kotlin.js.JsName /** * Tests if the geometry member at [at] intersects with the given [value] geometry. @@ -18,6 +19,7 @@ class Intersects() : Op() { private val TRANSFORMERS = NotNullProperty(SpTransformationList::class) { _,_ -> SpTransformationList() } } + @JsName("forName") constructor(at: String, geometry: SpGeometry, vararg transformers: SpTransformation) : this() { this.op = INTERSECTS this.at = at @@ -26,6 +28,7 @@ class Intersects() : Op() { for (t in transformers) _transformers.add(t) } + @JsName("forMember") constructor(at: Member, geometry: SpGeometry, vararg transformers: SpTransformation) : this(at.name, geometry, *transformers) /** diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/IsAnyOf.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/IsAnyOf.kt index 43af0ecc9..bbd086e5c 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/IsAnyOf.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/IsAnyOf.kt @@ -6,6 +6,7 @@ import naksha.base.AnyList import naksha.base.NotNullProperty import naksha.model.objects.Member import kotlin.js.JsExport +import kotlin.js.JsName /** * Tests if the member at [at] is any of the given [items]. @@ -17,6 +18,7 @@ class IsAnyOf() : Op() { private val ITEMS = NotNullProperty(AnyList::class) { _,_ -> AnyList() } } + @JsName("forName") constructor(at: String, vararg items: Any) : this() { this.op = IS_ANY_OF this.at = at @@ -24,6 +26,7 @@ class IsAnyOf() : Op() { for (item in items) _items.add(item) } + @JsName("forMember") constructor(at: Member, vararg items: Any) : this(at.name, *items) var items: AnyList by ITEMS diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/IsFalse.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/IsFalse.kt index 0fcd3862d..567605d70 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/IsFalse.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/IsFalse.kt @@ -4,6 +4,7 @@ package naksha.model.request.ops import naksha.model.objects.Member import kotlin.js.JsExport +import kotlin.js.JsName /** * Tests if the member at [at] is false. @@ -11,10 +12,12 @@ import kotlin.js.JsExport */ @JsExport class IsFalse() : Op() { + @JsName("forName") constructor(at: String) : this() { this.op = IS_FALSE this.at = at } + @JsName("forMember") constructor(at: Member) : this(at.name) } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/IsNull.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/IsNull.kt index d9773ad47..6d99ba2c5 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/IsNull.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/IsNull.kt @@ -4,6 +4,7 @@ package naksha.model.request.ops import naksha.model.objects.Member import kotlin.js.JsExport +import kotlin.js.JsName /** * Tests if the member at [at] is null. @@ -11,10 +12,12 @@ import kotlin.js.JsExport */ @JsExport class IsNull() : Op() { + @JsName("forName") constructor(at: String) : this() { this.op = IS_NULL this.at = at } + @JsName("forMember") constructor(at: Member) : this(at.name) } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/IsTrue.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/IsTrue.kt index 45f0befab..93dedc396 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/IsTrue.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/IsTrue.kt @@ -4,6 +4,7 @@ package naksha.model.request.ops import naksha.model.objects.Member import kotlin.js.JsExport +import kotlin.js.JsName /** * Tests if the member at [at] is true. @@ -11,10 +12,12 @@ import kotlin.js.JsExport */ @JsExport class IsTrue() : Op() { + @JsName("forName") constructor(at: String) : this() { this.op = IS_TRUE this.at = at } + @JsName("forMember") constructor(at: Member) : this(at.name) } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Lt.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Lt.kt index d810eb90a..84f896f8d 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Lt.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Lt.kt @@ -6,6 +6,7 @@ import naksha.base.NotNullProperty import naksha.base.NullableProperty import naksha.model.objects.Member import kotlin.js.JsExport +import kotlin.js.JsName /** * Tests if the member at [at] is less than the given [value] (LessThan). @@ -17,12 +18,14 @@ class Lt() : Op() { private val VALUE = NotNullProperty(Any::class) } + @JsName("forName") constructor(at: String, value: Any) : this() { this.op = LT this.at = at this.value = value } + @JsName("forMember") constructor(at: Member, value: Any) : this(at.name, value) var value: Any by VALUE diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Lte.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Lte.kt index 6bd89cc9e..61b7e470b 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Lte.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Lte.kt @@ -6,6 +6,7 @@ import naksha.base.NotNullProperty import naksha.base.NullableProperty import naksha.model.objects.Member import kotlin.js.JsExport +import kotlin.js.JsName /** * Tests if the member at [at] is less than or equal to the given [value] (LessThanOrEqual). @@ -17,12 +18,14 @@ class Lte() : Op() { private val VALUE = NotNullProperty(Any::class) } + @JsName("forName") constructor(at: String, value: Any) : this() { this.op = LTE this.at = at this.value = value } + @JsName("forMember") constructor(at: Member, value: Any) : this(at.name, value) var value: Any by VALUE diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Not.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Not.kt index cf2042ee2..6936c6d0f 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Not.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Not.kt @@ -5,6 +5,7 @@ package naksha.model.request.ops import naksha.base.NotNullProperty import naksha.base.NullableProperty import kotlin.js.JsExport +import kotlin.js.JsName /** * Logical NOT. @@ -16,6 +17,7 @@ class Not() : Op() { private val CHILD = NotNullProperty(Op::class) } + @JsName("of") constructor(child: Op) : this() { this.op = NOT this.child = child diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Or.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Or.kt index 65b81e9c5..927560518 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Or.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Or.kt @@ -4,6 +4,7 @@ package naksha.model.request.ops import naksha.base.NotNullProperty import kotlin.js.JsExport +import kotlin.js.JsName /** * Logical OR. @@ -15,6 +16,7 @@ class Or() : Op() { private val VALUES = NotNullProperty(OpList::class) { _, _ -> OpList() } } + @JsName("of") constructor(vararg children: Op) : this() { this.op = OR val _children = this.children diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/StartsWith.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/StartsWith.kt index 7b3eea6bc..5244d58e8 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/StartsWith.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/StartsWith.kt @@ -5,6 +5,7 @@ package naksha.model.request.ops import naksha.base.NotNullProperty import naksha.model.objects.Member import kotlin.js.JsExport +import kotlin.js.JsName /** * Tests if the member at [at] starts with the given [value]. @@ -16,12 +17,14 @@ class StartsWith() : Op() { private val VALUE = NotNullProperty(String::class) { _,_ -> "" } } + @JsName("forName") constructor(at: String, value: String) : this() { this.op = STARTS_WITH this.at = at this.value = value } + @JsName("forMember") constructor(at: Member, value: String) : this(at.name, value) var value: String by VALUE diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagEquals.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagEquals.kt index 3a4da3714..46134e0db 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagEquals.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagEquals.kt @@ -6,6 +6,7 @@ import naksha.base.NotNullProperty import naksha.base.NullableProperty import naksha.model.objects.Member import kotlin.js.JsExport +import kotlin.js.JsName /** * Tests if the tag [key] on the member at [at] equals the given [value]. @@ -18,6 +19,7 @@ class TagEquals() : Op() { private val VALUE = NullableProperty(Any::class) } + @JsName("forName") constructor(at: String, key: String, value: Any?) : this() { this.op = TAG_EQ this.at = at @@ -25,6 +27,7 @@ class TagEquals() : Op() { this.value = value } + @JsName("forMember") constructor(at: Member, key: String, value: Any?) : this(at.name, key, value) var key: String by KEY diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagGt.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagGt.kt index c131f5bf2..d16f232ce 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagGt.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagGt.kt @@ -6,6 +6,7 @@ import naksha.base.NotNullProperty import naksha.base.NullableProperty import naksha.model.objects.Member import kotlin.js.JsExport +import kotlin.js.JsName /** * Tests if the tag [key] on the member at [at] is greater than the given [value]. @@ -18,6 +19,7 @@ class TagGt() : Op() { private val VALUE = NotNullProperty(Any::class) } + @JsName("forName") constructor(at: String, key: String, value: Any) : this() { this.op = TAG_GT this.at = at @@ -25,6 +27,7 @@ class TagGt() : Op() { this.value = value } + @JsName("forMember") constructor(at: Member, key: String, value: Any) : this(at.name, key, value) var key: String by KEY diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagGte.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagGte.kt index d565d1f3a..d65c9fb74 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagGte.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagGte.kt @@ -6,6 +6,7 @@ import naksha.base.NotNullProperty import naksha.base.NullableProperty import naksha.model.objects.Member import kotlin.js.JsExport +import kotlin.js.JsName /** * Tests if the tag [key] on the member at [at] is greater than or equal to the given [value]. @@ -18,6 +19,7 @@ class TagGte() : Op() { private val VALUE = NotNullProperty(Any::class) } + @JsName("forName") constructor(at: String, key: String, value: Any) : this() { this.op = TAG_GTE this.at = at @@ -25,6 +27,7 @@ class TagGte() : Op() { this.value = value } + @JsName("forMember") constructor(at: Member, key: String, value: Any) : this(at.name, key, value) var key: String by KEY diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagIsNull.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagIsNull.kt index 5ed3fd60b..f25b660c7 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagIsNull.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagIsNull.kt @@ -5,6 +5,7 @@ package naksha.model.request.ops import naksha.base.NotNullProperty import naksha.model.objects.Member import kotlin.js.JsExport +import kotlin.js.JsName /** * Tests if the tag [key] on the member at [at] is null. @@ -16,12 +17,14 @@ class TagIsNull() : Op() { private val KEY = NotNullProperty(String::class) { _,_ -> "" } } + @JsName("forName") constructor(at: String, key: String) : this() { this.op = TAG_IS_NULL this.at = at this.key = key } + @JsName("forMember") constructor(at: Member, key: String) : this(at.name, key) var key: String by KEY diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagListContains.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagListContains.kt index adf29d768..1a89488ee 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagListContains.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagListContains.kt @@ -3,7 +3,9 @@ package naksha.model.request.ops import naksha.base.NotNullProperty +import naksha.model.objects.Member import kotlin.js.JsExport +import kotlin.js.JsName /** * Tests if the given [item] exist in the member at [at], expects the member to be a tag-list. @@ -15,11 +17,19 @@ class TagListContains() : Op() { private val ITEM = NotNullProperty(Any::class) } + @JsName("forName") constructor(at: String, item: Any) : this() { this.op = TAGLIST_CONTAINS this.at = at this.item = item } + @JsName("forMember") + constructor(at: Member, item: Any) : this() { + this.op = TAGLIST_CONTAINS + this.at = at.name + this.item = item + } + var item: Any by ITEM } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagListContainsAllOf.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagListContainsAllOf.kt index d4cb73122..6443f2a29 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagListContainsAllOf.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagListContainsAllOf.kt @@ -6,6 +6,7 @@ import naksha.base.AnyList import naksha.base.NotNullProperty import naksha.model.objects.Member import kotlin.js.JsExport +import kotlin.js.JsName /** * Tests if all of the given [items] exist in the member at [at]. @@ -17,6 +18,7 @@ class TagListContainsAllOf() : Op() { private val ITEMS = NotNullProperty(AnyList::class) { _, _ -> AnyList() } } + @JsName("forName") constructor(at: String, vararg items: Any) : this() { this.op = TAGLIST_CONTAINS_ALL_OF this.at = at @@ -24,6 +26,7 @@ class TagListContainsAllOf() : Op() { for (item in items) _items.add(item) } + @JsName("forMember") constructor(at: Member, vararg items: Any) : this(at.name, *items) var items: AnyList by ITEMS diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagListContainsAnyOf.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagListContainsAnyOf.kt index b84f63e47..2664c1d7b 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagListContainsAnyOf.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagListContainsAnyOf.kt @@ -6,6 +6,7 @@ import naksha.base.AnyList import naksha.base.NotNullProperty import naksha.model.objects.Member import kotlin.js.JsExport +import kotlin.js.JsName /** * Tests if any of the given [items] exist in the member at [at]. @@ -17,6 +18,7 @@ class TagListContainsAnyOf() : Op() { private val ITEMS = NotNullProperty(AnyList::class) { _, _ -> AnyList() } } + @JsName("forName") constructor(at: String, vararg items: Any) : this() { this.op = TAGLIST_CONTAINS_ANY_OF this.at = at @@ -24,6 +26,7 @@ class TagListContainsAnyOf() : Op() { for (item in items) _items.add(item) } + @JsName("forMember") constructor(at: Member, vararg items: Any) : this(at.name, *items) var items: AnyList by ITEMS diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagLt.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagLt.kt index 204840b89..890772433 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagLt.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagLt.kt @@ -6,6 +6,7 @@ import naksha.base.NotNullProperty import naksha.base.NullableProperty import naksha.model.objects.Member import kotlin.js.JsExport +import kotlin.js.JsName /** * Tests if the tag [key] on the member at [at] is less than the given [value]. @@ -18,6 +19,7 @@ class TagLt() : Op() { private val VALUE = NotNullProperty(Any::class) } + @JsName("forName") constructor(at: String, key: String, value: Any) : this() { this.op = TAG_LT this.at = at @@ -25,6 +27,7 @@ class TagLt() : Op() { this.value = value } + @JsName("forMember") constructor(at: Member, key: String, value: Any) : this(at.name, key, value) var key: String by KEY diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagLte.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagLte.kt index de2d65fdc..ad4ece3e8 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagLte.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagLte.kt @@ -6,6 +6,7 @@ import naksha.base.NotNullProperty import naksha.base.NullableProperty import naksha.model.objects.Member import kotlin.js.JsExport +import kotlin.js.JsName /** * Tests if the tag [key] on the member at [at] is less than or equal to the given [value]. @@ -18,6 +19,7 @@ class TagLte() : Op() { private val VALUE = NotNullProperty(Any::class) } + @JsName("forName") constructor(at: String, key: String, value: Any) : this() { this.op = TAG_LTE this.at = at @@ -25,6 +27,7 @@ class TagLte() : Op() { this.value = value } + @JsName("forMember") constructor(at: Member, key: String, value: Any) : this(at.name, key, value) var key: String by KEY diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagMapHasAllOf.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagMapHasAllOf.kt index 80f8cc413..f52405d28 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagMapHasAllOf.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagMapHasAllOf.kt @@ -6,6 +6,7 @@ import naksha.base.NotNullProperty import naksha.base.StringList import naksha.model.objects.Member import kotlin.js.JsExport +import kotlin.js.JsName /** * Tests if all of the given [keys] exist on the member at [at]. @@ -17,6 +18,7 @@ class TagMapHasAllOf() : Op() { private val KEYS = NotNullProperty(StringList::class) { _, _ -> StringList() } } + @JsName("forName") constructor(at: String, vararg keys: String) : this() { this.op = TAGMAP_HAS_ALL_OF this.at = at @@ -24,6 +26,7 @@ class TagMapHasAllOf() : Op() { for (key in keys) _tagKeys.add(key) } + @JsName("forMember") constructor(at: Member, vararg keys: String) : this(at.name, *keys) var tagKeys: StringList by KEYS diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagMapHasAnyOf.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagMapHasAnyOf.kt index 31fbbe1de..4444dc5be 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagMapHasAnyOf.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagMapHasAnyOf.kt @@ -6,6 +6,7 @@ import naksha.base.NotNullProperty import naksha.base.StringList import naksha.model.objects.Member import kotlin.js.JsExport +import kotlin.js.JsName /** * Tests if any of the given [keys] exist on the member at [at]. @@ -17,6 +18,7 @@ class TagMapHasAnyOf() : Op() { private val KEYS = NotNullProperty(StringList::class) { _, _ -> StringList() } } + @JsName("forName") constructor(at: String, vararg keys: String) : this() { this.op = TAGMAP_HAS_ANY_OF this.at = at @@ -24,6 +26,7 @@ class TagMapHasAnyOf() : Op() { for (key in keys) _tagKeys.add(key) } + @JsName("forMember") constructor(at: Member, vararg keys: String) : this(at.name, *keys) var tagKeys: StringList by KEYS diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagMapHasKey.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagMapHasKey.kt index ecbc5c323..b6febccf1 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagMapHasKey.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagMapHasKey.kt @@ -5,6 +5,7 @@ package naksha.model.request.ops import naksha.base.NotNullProperty import naksha.model.objects.Member import kotlin.js.JsExport +import kotlin.js.JsName /** * Tests if the tag [key] exists on the member at [at]. @@ -16,12 +17,14 @@ class TagMapHasKey() : Op() { private val KEY = NotNullProperty(String::class) { _, _ -> "" } } + @JsName("forName") constructor(at: String, key: String) : this() { this.op = TAGMAP_HAS_KEY this.at = at this.key = key } + @JsName("forMember") constructor(at: Member, key: String) : this(at.name, key) var key: String by KEY diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagStartsWith.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagStartsWith.kt index 9e358dc75..8a61471fb 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagStartsWith.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagStartsWith.kt @@ -5,6 +5,7 @@ package naksha.model.request.ops import naksha.base.NotNullProperty import naksha.model.objects.Member import kotlin.js.JsExport +import kotlin.js.JsName /** * Tests if the tag [key] on the member at [at] starts with the given [value]. @@ -17,6 +18,7 @@ class TagStartsWith() : Op() { private val VALUE = NotNullProperty(String::class) { _,_ -> "" } } + @JsName("forName") constructor(at: String, key: String, value: String) : this() { this.op = TAG_STARTS_WITH this.at = at @@ -24,6 +26,7 @@ class TagStartsWith() : Op() { this.value = value } + @JsName("forMember") constructor(at: Member, key: String, value: String) : this(at.name, key, value) var key: String by KEY diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/TagExists.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/TagExists.kt index ed1fdba4f..35332c441 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/TagExists.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/TagExists.kt @@ -8,7 +8,7 @@ import kotlin.js.JsName /** * Tests if the tag with given name exists, ignoring the value. * - * For map-form tags ([naksha.model.objects.MemberType.TAGS] / [naksha.model.objects.MemberType.TAGS_FROM_ARRAY]) + * For map-form tags ([naksha.model.objects.MemberType.TAG_MAP] / [naksha.model.objects.MemberType.TAG_MAP_FROM_ARRAY]) * this tests if the key exists. For tag-list-form tags ([naksha.model.objects.MemberType.TAG_LIST], the default) * this tests if the full string element exists, e.g. `TagExists("foo")` matches a feature tagged * `["foo", "bar"]`. diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/TagQuery.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/TagQuery.kt index b774a4725..21030eb5a 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/TagQuery.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/TagQuery.kt @@ -10,8 +10,8 @@ import kotlin.js.JsExport * A general form of a tag query without any operation. * * Note: the `TagValueIs*` and [TagValueMatches] queries operate on keys/values and therefore only - * match map-form tags ([naksha.model.objects.MemberType.TAGS] / - * [naksha.model.objects.MemberType.TAGS_FROM_ARRAY]). Against the default set-form tags + * match map-form tags ([naksha.model.objects.MemberType.TAG_MAP] / + * [naksha.model.objects.MemberType.TAG_MAP_FROM_ARRAY]). Against the default set-form tags * ([naksha.model.objects.MemberType.TAG_LIST]) the values are never split into key/value pairs; use * [TagExists] or [TagSetContains] to match full elements instead. * @since 3.0.0 diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/TagSetContains.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/TagSetContains.kt index 2298814c0..53c55d302 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/TagSetContains.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/query/TagSetContains.kt @@ -19,8 +19,8 @@ import kotlin.js.JsName * complete elements can be matched: a feature tagged `["foo=bar"]` is found by * `TagSetContains("foo=bar")`, not by `TagSetContains("foo")`. * - * This query does **not** match map-form tags ([naksha.model.objects.MemberType.TAGS] or - * [naksha.model.objects.MemberType.TAGS_FROM_ARRAY]); use [TagExists] and the `TagValueIs*` queries + * This query does **not** match map-form tags ([naksha.model.objects.MemberType.TAG_MAP] or + * [naksha.model.objects.MemberType.TAG_MAP_FROM_ARRAY]); use [TagExists] and the `TagValueIs*` queries * for those. * @since 3.0 */ diff --git a/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/MemberTest.kt b/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/MemberTest.kt index 9ebd6b54f..19146d20a 100644 --- a/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/MemberTest.kt +++ b/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/MemberTest.kt @@ -82,7 +82,7 @@ class MemberTest { fun indexTypesExist() { assertNotNull(IndexType.BTREE) assertNotNull(IndexType.SPATIAL) - assertNotNull(IndexType.TAGS) + assertNotNull(IndexType.TAG_MAP) assertNotNull(IndexType.TAG_LIST) } @@ -99,8 +99,8 @@ class MemberTest { assertNotNull(MemberType.STRING) assertNotNull(MemberType.BYTE_ARRAY) // Virtual / jsonb. - assertNotNull(MemberType.TAGS) - assertNotNull(MemberType.TAGS_FROM_ARRAY) + assertNotNull(MemberType.TAG_MAP) + assertNotNull(MemberType.TAG_MAP_FROM_ARRAY) assertNotNull(MemberType.TAG_LIST) } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/LibPsql.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/LibPsql.kt index c6bdc428b..63104ffb0 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/LibPsql.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/LibPsql.kt @@ -84,7 +84,7 @@ internal const val PG_DIST_PARTITION = "${PG_S}p" */ internal const val PG_INTERNAL_PREFIX = Naksha.INTERNAL_PREFIX -internal const val NAKSHA_TXN_SEQ = "naksha_txn_seq" +internal const val NAKSHA_VERSION_SEQ = "naksha_version_seq" //internal const val NAKSHA_MAP_SEQ = "naksha_map_seq" //internal const val NAKSHA_COL_SEQ = "naksha_col_seq" diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgAdminCatalog.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgAdminCatalog.kt index 2d11e9fd7..b6286c2be 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgAdminCatalog.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgAdminCatalog.kt @@ -116,10 +116,10 @@ abstract class PgAdminCatalog internal constructor( val postgresVersion: NakshaVersion /** - * The OID of the transaction sequence. + * The OID of the current HEAD version. * @since 3.0.0 */ - val txnSequenceOid: Int + val versionSequenceOid: Int /** * The OID of the map-number sequence. @@ -312,14 +312,14 @@ SELECT basics.*, procs.* FROM basics, procs; } } setSearchPath(conn) - logger.info("Load OID of '$NAKSHA_TXN_SEQ' from admin schema (schema-oid=$schemaOid)") - val SQL = "SELECT oid FROM pg_class WHERE relnamespace = $schemaOid AND relname = '$NAKSHA_TXN_SEQ'" + logger.info("Load OID of '$NAKSHA_VERSION_SEQ' from admin schema (schema-oid=$schemaOid)") + val SQL = "SELECT oid FROM pg_class WHERE relnamespace = $schemaOid AND relname = '$NAKSHA_VERSION_SEQ'" conn.execute(SQL).fetch().use { cursor -> - txnSequenceOid = cursor["oid"] + versionSequenceOid = cursor["oid"] //mapNumberSequenceOid = cursor["map_oid"] //colNumberSequenceOid = cursor["col_oid"] } - logger.info("Storage ${config.id} / ${config.databaseNumber} initialized, txn-seq-oid=$txnSequenceOid, commit") + logger.info("Storage ${config.id} / ${config.databaseNumber} initialized, txn-seq-oid=$versionSequenceOid, commit") conn.commit() } } @@ -377,7 +377,7 @@ SELECT basics.*, procs.* FROM basics, procs; */ fun getTxn(conn: PgConnection): Int64 { val QUERY = "SELECT currval($1) as txn" - val cursor = conn.execute(QUERY, arrayOf(txnSequenceOid)).fetch() + val cursor = conn.execute(QUERY, arrayOf(versionSequenceOid)).fetch() cursor.use { val txn: Int64 = cursor["txn"] return txn @@ -391,30 +391,29 @@ SELECT basics.*, procs.* FROM basics, procs; * @since 3.0.0 */ fun newTxn(conn: PgConnection): PgTxn { - val QUERY = "SELECT nextval($1) as txn, (extract(epoch from transaction_timestamp())*1000)::int8 as time" - val cursor = conn.execute(QUERY, arrayOf(txnSequenceOid)).fetch() + val QUERY = "SELECT nextval($1) as version, (extract(epoch from transaction_timestamp())*1000)::int8 as time" + val cursor = conn.execute(QUERY, arrayOf(versionSequenceOid)).fetch() cursor.use { - var txn: Int64 = cursor["txn"] - val txts: Int64 = cursor["time"] - var version = Version(txn) - val txInstant = Instant.fromEpochMilliseconds(txts.toLong()) + var number: Int64 = cursor["version"] + val time: Int64 = cursor["time"] + var version = Version(number) + val txInstant = Instant.fromEpochMilliseconds(time.toLong()) val txDate = txInstant.toLocalDateTime(TimeZone.UTC) if (version.year != txDate.year || version.month != txDate.monthNumber || version.day != txDate.dayOfMonth) { logger.info("Transaction counter is in wrong day") logger.info("Acquire advisory lock") conn.execute("SELECT pg_advisory_lock($1)", arrayOf(PgUtil.TXN_LOCK_ID)).close() try { - val c2 = conn.execute("SELECT nextval($1) as txn", arrayOf(txnSequenceOid)).fetch() + val c2 = conn.execute("SELECT nextval($1) as version", arrayOf(versionSequenceOid)).fetch() c2.use { - txn = c2["txn"] - version = Version(txn) + number = c2["version"] + version = Version(number) } if (version.year != txDate.year || version.month != txDate.monthNumber || version.day != txDate.dayOfMonth) { logger.info("Transaction counter is still at wrong day, rollover to next day") - // Rollover, we update sequence of the day. Start at seq=1 (auto() encodes action in bits 1-0). - version = Version.auto(txDate.year, txDate.monthNumber, txDate.dayOfMonth, Int64(1)) - txn = version.number - conn.execute("SELECT setval($1, $2)", arrayOf(txnSequenceOid, txn + 4)).close() + version = Version.auto(txDate.year, txDate.monthNumber, txDate.dayOfMonth, Int64(0), Action.VERSION) + number = version.number + conn.execute("SELECT setval($1, $2)", arrayOf(versionSequenceOid, number + 4)).close() } logger.info("Release advisory lock") conn.execute("SELECT pg_advisory_unlock($1)", arrayOf(PgUtil.TXN_LOCK_ID)).close() @@ -432,7 +431,7 @@ SELECT basics.*, procs.* FROM basics, procs; // Doing a commit here is necessary to avoid that we get a lock to the txn sequence! // Even while sequences are normally not locked, it can happen under circumstances. conn.commit() - return PgTxn(txn, txts, version) + return PgTxn(number, time, version) } } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgIndex.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgIndex.kt index b97ce441c..b45902383 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgIndex.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgIndex.kt @@ -5,9 +5,9 @@ import naksha.model.NakshaException import naksha.model.objects.Index import naksha.model.objects.IndexType import naksha.model.objects.IndexType.IndexType_C.BTREE -import naksha.model.objects.IndexType.IndexType_C.SET import naksha.model.objects.IndexType.IndexType_C.SPATIAL -import naksha.model.objects.IndexType.IndexType_C.TAGS +import naksha.model.objects.IndexType.IndexType_C.TAG_MAP +import naksha.model.objects.IndexType.IndexType_C.TAG_LIST import naksha.psql.PgUtil.PgUtilCompanion.quoteIdent import kotlin.js.JsExport import kotlin.jvm.JvmField @@ -54,7 +54,7 @@ data class PgIndex( val using = when (type) { BTREE -> "btree" SPATIAL -> "gist" - TAGS, TAG_LIST -> "gin" + TAG_MAP, TAG_LIST -> "gin" else -> throw NakshaException(INTERNAL_ERROR, "Invalid index type for index $name on table $tableName") } val indexName = quoteIdent(tableName, "\$i_", tableName) diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMemberHelper.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMemberHelper.kt index 2f168be00..c7138f74f 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMemberHelper.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgMemberHelper.kt @@ -46,8 +46,8 @@ class PgMemberHelper private constructor() { MemberType.STRING -> PgType.STRING MemberType.BYTE_ARRAY -> PgType.BYTE_ARRAY MemberType.SPATIAL -> PgType.BYTE_ARRAY - MemberType.TAGS -> PgType.JSONB - MemberType.TAGS_FROM_ARRAY -> PgType.JSONB + MemberType.TAG_MAP -> PgType.JSONB + MemberType.TAG_MAP_FROM_ARRAY -> PgType.JSONB MemberType.TAG_LIST -> PgType.JSONB else -> PgType.STRING } @@ -56,7 +56,7 @@ class PgMemberHelper private constructor() { * Returns the PostgreSQL DDL type string for the given member type, used inside `CREATE TABLE` / `ALTER TABLE ADD COLUMN`. * Note: there is no 1-byte signed integer type in PostgreSQL, so [MemberType.INT8] is materialized as `smallint`; * the storage enforces the 8-bit range on coercion. - * [MemberType.TAGS], [MemberType.TAGS_FROM_ARRAY], and [MemberType.TAG_LIST] all use `jsonb STORAGE MAIN` — + * [MemberType.TAG_MAP], [MemberType.TAG_MAP_FROM_ARRAY], and [MemberType.TAG_LIST] all use `jsonb STORAGE MAIN` — * compressed inline, only TOASTed as a last resort. The only difference is the JSON shape: * TAGS and TAGS_FROM_ARRAY persist a JSON object, SET persists a JSON array. */ @@ -71,8 +71,8 @@ class PgMemberHelper private constructor() { MemberType.STRING -> "text COLLATE \"C\"" MemberType.BYTE_ARRAY -> "bytea" MemberType.SPATIAL -> "bytea STORAGE EXTERNAL" - MemberType.TAGS -> "jsonb STORAGE MAIN" - MemberType.TAGS_FROM_ARRAY -> "jsonb STORAGE MAIN" + MemberType.TAG_MAP -> "jsonb STORAGE MAIN" + MemberType.TAG_MAP_FROM_ARRAY -> "jsonb STORAGE MAIN" MemberType.TAG_LIST -> "jsonb STORAGE MAIN" else -> "text" } @@ -108,8 +108,8 @@ class PgMemberHelper private constructor() { MemberType.STRING -> coerceString(value, featureId, memberName) MemberType.BYTE_ARRAY -> coerceByteArray(value, featureId, memberName) MemberType.SPATIAL -> coerceByteArray(value, featureId, memberName) - MemberType.TAGS -> coerceTags(value, featureId, memberName) - MemberType.TAGS_FROM_ARRAY -> coerceTagsFromArray(value, featureId, memberName) + MemberType.TAG_MAP -> coerceTags(value, featureId, memberName) + MemberType.TAG_MAP_FROM_ARRAY -> coerceTagsFromArray(value, featureId, memberName) MemberType.TAG_LIST -> coerceTagList(value, featureId, memberName) else -> { warnMismatch(featureId, memberName, type.toString(), value) @@ -310,8 +310,8 @@ class PgMemberHelper private constructor() { MemberType.STRING -> 7 MemberType.BYTE_ARRAY -> 8 MemberType.SPATIAL -> 8 - MemberType.TAGS -> 9 - MemberType.TAGS_FROM_ARRAY -> 10 + MemberType.TAG_MAP -> 9 + MemberType.TAG_MAP_FROM_ARRAY -> 10 MemberType.TAG_LIST -> 11 else -> 12 } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgType.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgType.kt index e99afa64b..ff8f0aba0 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgType.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgType.kt @@ -128,7 +128,7 @@ class PgType : JsEnum() { /** * JSONB column type, bound as text (JSON). * - * Used by storages to materialize [naksha.model.objects.MemberType.TAGS], [naksha.model.objects.MemberType.TAGS_FROM_ARRAY] (JSON object), and [naksha.model.objects.MemberType.TAG_LIST] (JSON array) members. + * Used by storages to materialize [naksha.model.objects.MemberType.TAG_MAP], [naksha.model.objects.MemberType.TAG_MAP_FROM_ARRAY] (JSON object), and [naksha.model.objects.MemberType.TAG_LIST] (JSON array) members. * @since 3.0 */ @JvmField @@ -198,8 +198,8 @@ class PgType : JsEnum() { // MemberType.BYTE_ARRAY -> BYTE_ARRAY // MemberType.TUPLE_NUMBER -> BYTE_ARRAY // MemberType.SPATIAL -> BYTE_ARRAY - MemberType.TAGS -> JSONB - MemberType.TAGS_FROM_ARRAY -> JSONB + MemberType.TAG_MAP -> JSONB + MemberType.TAG_MAP_FROM_ARRAY -> JSONB MemberType.TAG_LIST -> JSONB else -> BYTE_ARRAY } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt index 4f452f78c..6d519b1d7 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt @@ -509,7 +509,7 @@ open class PgWriter internal constructor( "but '$firstColName' has type $firstColType." ) } - IndexType.TAGS -> if (firstColType != MemberType.TAGS && firstColType != MemberType.TAGS_FROM_ARRAY) { + IndexType.TAG_MAP -> if (firstColType != MemberType.TAG_MAP && firstColType != MemberType.TAG_MAP_FROM_ARRAY) { throw illegalArg( "TAGS index '${idx.name}' must target a member of type TAGS or TAGS_FROM_ARRAY, " + "but '$firstColName' has type $firstColType." diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/CollectionTests.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/CollectionTests.kt index 84a0468f4..07c0890b2 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/CollectionTests.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/CollectionTests.kt @@ -614,7 +614,7 @@ class CollectionTests : PgTestBase(collection = null, mapId = "") { addMember(Member("f_i8", MemberType.INT8)) addMember(Member("g_i16", MemberType.INT16)) addMember(Member("h_f32", MemberType.FLOAT32)) - addMember(Member("i_json", MemberType.TAGS)) + addMember(Member("i_json", MemberType.TAG_MAP)) addMember(Member("j_tag_list", MemberType.TAG_LIST)) } executeWrite(WriteRequest().add(Write().createCollection(collection))) diff --git a/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlAdminCatalog.kt b/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlAdminCatalog.kt index 4014b6c2b..d5973ceba 100644 --- a/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlAdminCatalog.kt +++ b/here-naksha-lib-psql/src/jvmMain/kotlin/naksha/psql/PsqlAdminCatalog.kt @@ -4,8 +4,6 @@ import naksha.base.* import naksha.base.Platform.PlatformCompanion.logger import naksha.jbon.JbDictionary import naksha.model.* -import naksha.model.objects.NakshaCollection -import naksha.model.objects.NakshaCatalog import naksha.psql.PgUtil.PgUtilCompanion.quoteLiteral /** @@ -158,7 +156,8 @@ class PsqlAdminCatalog internal constructor( ) ) logger.info("Create transaction-seq, map-sequence, and collection-sequence ...") - conn.execute("CREATE SEQUENCE IF NOT EXISTS $NAKSHA_TXN_SEQ AS ${PgType.INT64} START 4 INCREMENT BY 4 CACHE 40;").close() + // For a version number, the lower two bit must be always set. + conn.execute("CREATE SEQUENCE IF NOT EXISTS $NAKSHA_VERSION_SEQ AS ${PgType.INT64} START 3 INCREMENT BY 4 CACHE 40;").close() logger.info("Create internal collections: transactions, collections, and dictionaries") createPgCollection(conn, collections) // 0 From f079cb3ad34cebf503d8391dda22ad6bc7d55db8 Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Tue, 23 Jun 2026 18:28:11 +0200 Subject: [PATCH 36/57] Fix more issues. Signed-off-by: Alexander Lowey-Weber --- .../kotlin/naksha/psql/PgCollection.kt | 23 ++- .../commonMain/kotlin/naksha/psql/PgRead.kt | 175 ------------------ .../kotlin/naksha/psql/PgRelation.kt | 155 ++++------------ .../commonMain/kotlin/naksha/psql/PgWriter.kt | 122 +----------- .../kotlin/naksha/psql/PgWriterBase.kt | 10 +- .../kotlin/naksha/psql/PgWriterDelete.kt | 4 +- .../kotlin/naksha/psql/PgWriterUpdate.kt | 2 +- .../kotlin/naksha/psql/PgWriterUpsert.kt | 6 +- .../kotlin/naksha/psql/PgRelationTest.kt | 8 +- 9 files changed, 75 insertions(+), 430 deletions(-) delete mode 100644 here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRead.kt diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt index 58f130125..b4aab025e 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt @@ -311,11 +311,22 @@ open class PgCollection internal constructor( * Internal collections have some limitation, for example it is not possible to add or drop indices, nor can they be created through normal methods. They are basically immutable by design, but the content can be read and modified to some degree. */ @JvmField - var internal: Boolean = id.startsWith("naksha~") + val internal: Boolean = id.startsWith("naksha~") - // TODO: We need information from the database which history partitions exist. - // Reading from history must be done using the root table, not individual partitions. - // Only writing is done through individual partitions, and only for writing we need to know what exists! - // We should add a method like this: - // internal fun update(conn: PgConnection) {} + /** + * Verify the given new _HEAD_ state, ensure that none of the following values is modified: + * - [NakshaCollection.members] - Ensure that they result in the same [columns]. + * - [NakshaCollection.indices] - Ensure that they result in the same [headIndices] and [historyIndices]. + * - [NakshaCollection.shift] - The shift must not change, because it impacts partitioning. + * - [NakshaCollection.id] - Must match [id] and the resulting calculated [collectionNumber] must match as well. + * - [NakshaCollection.storageClass] - Must match [storageClass], changing the storage class is not allowed. + * - [NakshaCollection.partitions] - Must patch [partitions], changing the number of partitions is not allowed. + * + * Actually all other values can be changes, with only some having an impact to this object. + * @param newHead the new _HEAD_ state to be verified. + * @throws NakshaException with error [ILLEGAL_STATE] if the columns or indices in the given `newHead` have been changed. + */ + fun verifyNewHeadState(newHead: NakshaCollection) { + // TODO: Implement me! + } } \ No newline at end of file diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRead.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRead.kt deleted file mode 100644 index 6efe96ee8..000000000 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRead.kt +++ /dev/null @@ -1,175 +0,0 @@ -package naksha.psql - -import naksha.base.Epoch -import naksha.model.* -import naksha.model.request.FeatureTuple - -/** - * Helper to simplify reading from partitioned tables. - * @since 3.0 - */ -internal data class PgRead( - /** - * The map to read from. - */ - val catalog: PgCatalog, - - /** - * The collection to read from. - */ - val collection: PgCollection, - - /** - * If a tuple by tuple-number should be read. - */ - val tupleNumber: TupleNumber?, - - /** - * If a tuple by id should be read. - */ - val featureId: String?, - - /** - * The amount of performance-partitions, `-1` if no performance partitions are there, otherwise a value between `2` and `1000`. - */ - val partitionCount: Int = if (collection.partitions > 1) collection.partitions else -1, - - /** - * If we need to read performance partitions. - */ - val readPartition: Boolean = partitionCount >= 0, - - /** - * If a tuple-number or id was given, the partition to read. Will be `-1` if either the collection does not have performance collections, or if we need to read all partitions. - */ - val partition: Int = if (!readPartition) -1 - else if (tupleNumber != null) tupleNumber.partitionNumber % collection.partitions - else if (featureId != null) Naksha.partitionNumber(featureId) % collection.partitions - else -1, - - /** - * A grouping identifier. - * - * Actually, allows grouping so that the same map/collection is merged, and that all reads that are from all partitions of a collection are grouped together, and all that read from the same partition. Therefore, eventually the first read from a group with the same group-id, will always read from the same map, collection, partition(s). - */ - val groupId: String = if (!readPartition) "${catalog.id}:${collection.id}" else "${catalog.id}:${collection.id}:${partition}" -) { - companion object PgRead_C { - private val UNDEFINED = ArrayList(0) - } - - /** - * Optional attachment that helps loading. - */ - var featureTuple: FeatureTuple? = null - - /** - * Read multiple features by arbitrary query from this collection. - * @param map the map to read. - * @param collection the collection to read. - */ - constructor(map: PgCatalog, collection: PgCollection): this(map, collection, null, null) - - /** - * Read a tuple by tuple-number. - * @param conn the database connection. - * @param adminMap the admin-map. - * @param tupleNumber the tuple-number of the tuple to read. - */ - constructor(conn: PgConnection, adminMap: PgAdminCatalog, tupleNumber: TupleNumber) : this( - adminMap.getPgCatalogByNumber(conn, tupleNumber.catalogNumber) - ?: throw mapNotFound("The map for map-number ${tupleNumber.catalogNumber} not found"), - adminMap.getPgCatalogByNumber(conn, tupleNumber.catalogNumber)?.getPgCollectionByNumber(conn, tupleNumber.collectionNumber) - ?: throw collectionNotFound("The collection for collection-number ${tupleNumber.collectionNumber} not found"), - tupleNumber, - null - ) - - /** - * Read a tuple by feature-id. - * @param map the map to read. - * @param collection the collection to read. - * @param id the feature-id of the tuple to read. - */ - constructor(map: PgCatalog, collection: PgCollection, id: String) : this(map, collection, null, id) - - private fun initHeadTables(): List { - val headTable = collection.headTable - if (readPartition) { - val headPartitions = headTable.partitions - // If it is enough to read a single partition, because we know where the feature is - if (partition >= 0) return listOf(headPartitions[partition]) - // Otherwise read all partitions - val tables = ArrayList(headPartitions.size) - for (i in headPartitions.indices) { - tables.add(headPartitions[i]) - } - return tables - } - return listOf(headTable) - } - - private var _headTables: List? = UNDEFINED - /** - * All HEAD tables to read. - */ - val headTables: List - get() { - var tables = _headTables - if (tables !== UNDEFINED && tables != null) return tables - tables = initHeadTables() - _headTables = tables - return tables - } - - fun initHistoryTables(): List? { - val history = collection.historyTable ?: return null - // TODO: hack to be be fixed as part of CASL-1095 - // it was observed that if collection is used for the first time (it is not cached) they `years` are empty - // even if the year partitions actually exist on DB side - // this results in returned history tables being empty (even though they can be there) - // this behavior was observedd during CASL-1057 development - if(history.years.isEmpty()){ - // see: PgMap.createPgCollection - val year = Epoch().year - history.addPartition(year) - history.addPartition(year + 1) - } - val tables = ArrayList(history.years.size) - for (entry in history.years) { - val year = entry.key - val historyTable = history.years[year] - ?: throw illegalState("Internal error, failed to add history year $year") - if (readPartition) { - val hstPartitions = historyTable.partitions - // If it is enough to read a single partition, because we know where the feature is - if (partition >= 0) { - tables.add(hstPartitions[partition]) - } else { - // Otherwise read all partitions - for (i in 0 ..< partitionCount) { - tables.add(hstPartitions[i]) - } - } - } else { - // When we do not performance-partition history, only table per year. - tables.add(historyTable) - } - } - return tables - } - - private var _historyTables: List? = UNDEFINED - - /** - * All history tables to read. - */ - val historyTables: List? - get() { - var tables = _historyTables - if (tables !== UNDEFINED) return tables - tables = initHistoryTables() - _historyTables = tables - return tables - } -} \ No newline at end of file diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRelation.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRelation.kt index 37c9de61c..22b182d32 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRelation.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRelation.kt @@ -11,23 +11,23 @@ import kotlin.jvm.JvmStatic /** * Information extracted from the [pg_class](https://www.postgresql.org/docs/current/catalog-pg-class.html) table, about relations of a collection. - * @property oid the OID of the relation. - * @property name the name of the relation. - * @property schemaOid the OID of the schema in which the relation is located. - * @property schema_name the name of the schema in which the relation is located. - * @property kind the kind of relation. + * @property oid the OID of the relation (`oid`). + * @property rel_name the name of the relation (`relname`). + * @property schema_oid the OID of the schema in which the relation is located (`relnamespace`). + * @property schema_name the name of the schema in which the relation is located, subselected from [pg_namespace](https://www.postgresql.org/docs/current/catalog-pg-namespace.html) using [schema_oid]. + * @property kind the kind of relation (`relkind`). * @property storageClass the storage class of the relation. - * @property tablespaceOid the OID of the tablespace in which the relation is stored. + * @property tablespace_oid the OID of the tablespace in which the relation is stored (`reltablespace`). */ @JsExport data class PgRelation( @JvmField val oid: Int, - @JvmField val name: String, - @JvmField val schemaOid: Int, + @JvmField val rel_name: String, + @JvmField val schema_oid: Int, @JvmField val schema_name: String, @JvmField val kind: PgKind, @JvmField val storageClass: PgStorageClass, - @JvmField val tablespaceOid: Int + @JvmField val tablespace_oid: Int ) { /** * Create an information row from the given cursor, that need to be a cursor as returned by [select]. @@ -36,12 +36,12 @@ data class PgRelation( @JsName("fromCursor") constructor(cursor: PgCursor) : this( oid = cursor["oid"], - name = cursor["relname"], - schemaOid = cursor["schema_oid"], + rel_name = cursor["relname"], + schema_oid = cursor["schema_oid"], schema_name = cursor["schema_name"], kind = PgKind.of(cursor.column("kind") as String), storageClass = PgStorageClass.of(cursor["sc"]), - tablespaceOid = cursor["ts_oid"] + tablespace_oid = cursor["ts_oid"] ) /** @@ -62,114 +62,40 @@ data class PgRelation( */ fun isPartition() = kind === PgKind.PartitionedTable - private var _partNumber: Int? = null + /** The parts of the name. */ + private val parts = rel_name.split('$') /** - * Returns the partition number. - * @return the partition number or -1, when this is no partition. + * Returns the distribution partition of this relation. + * @return the distribution partition of this relation or -1, when this is not distribution partitioned. */ - fun partitionNumber(): Int { - var n = _partNumber - if (n != null) return n - n = -1 - var i = name.indexOf(PG_DIST_PARTITION) - if (i > 0) { - i += PG_DIST_PARTITION.length - if (i + 3 <= name.length) try { - n = name.substring(i, i + 3).toInt(10) - } catch (_: Exception) {} - } - this._partNumber = n - // TODO: KotlinCompilerBug - It should know that "n" is never null - // Either name.substring().toInt() returns or not, either way, n is a number! - return n!! + fun distributionPartition(): Int { + // 0 1 2 3 = 4 + // HISTORY: {name}$hst${shifted}${distribution} + if (parts.size == 4 && parts[1] == "hst") return parts[3].toInt() + + // 0 1 = 2 + // HEAD: {name}${distribution} + if (parts.size == 2 && parts[2] != "hst") return parts[1].toInt() + return -1 } - private var _year: Int? = null - - /** - * Returns the history partition key (`next_version >> shift`) when this is a history year-partition, - * or `-1` when this is not a year-partition. - * - * For the default [shift][naksha.model.objects.NakshaCollection.shift] of 41 this equals the calendar year. - * - * New naming: `{collection}$hst$` (e.g. `mycol$hst$2026`). - * Legacy naming (TRANSACTIONS table): `{table}$y` (e.g. `naksha~transactions$y2026`). - */ - fun year(): Int { - var n = _year - if (n != null) return n - n = -1 - // New history partition naming: $hst$ - val hstIdx = name.indexOf(PG_HST) - if (hstIdx > 0) { - val after = hstIdx + PG_HST.length // points at '$' before the key - if (after < name.length && name[after] == '$') { - val keyStart = after + 1 - val keyEnd = name.indexOf('$', keyStart).let { if (it < 0) name.length else it } - if (keyEnd > keyStart) try { - n = name.substring(keyStart, keyEnd).toInt(10) - } catch (_: Exception) {} - } - } - // Legacy TRANSACTIONS-table naming: $y<4-digit-year> - if (n < 0) { - var i = name.indexOf(PG_YEAR) - if (i > 0) { - i += PG_YEAR.length - if (i + 4 <= name.length) try { - n = name.substring(i, i + 4).toInt(10) - } catch (_: Exception) {} - } - } - this._year = n - return n!! - } - - fun isAnyHeadRelation() = name.indexOf(PG_DEL) < 0 && name.indexOf(PG_HST) < 0 && name.indexOf(PG_META) < 0 - fun isHeadRootRelation() = isAnyHeadRelation() && (isTable() || isPartition()) && name.indexOf(PG_DIST_PARTITION) < 0 - fun isTxnYearRelation() = isAnyHeadRelation() && isTable() && name.indexOf(PG_YEAR) > 0 - - // --- - - fun isAnyDeleteRelation() = name.indexOf(PG_DEL) > 0 - fun isDeleteRootRelation() = isAnyDeleteRelation() && (isTable() || isPartition()) && name.indexOf(PG_DIST_PARTITION) < 0 - - // --- + // 0 1 = 2 + // HEAD: {name}${distribution} + @JvmField + val isHeadTable = parts.size == 1 + @JvmField + val isHeadDistributionPartition = parts.size == 2 && parts[1] != "hst" + @JvmField + val isHead = isHeadTable || isHeadDistributionPartition - fun isAnyHistoryRelation() = name.indexOf(PG_HST) > 0 - /** History root: ends with `$hst` and has no further `$` segments after it. */ - fun isHistoryRootRelation(): Boolean { - if (!isAnyHistoryRelation() || (!isTable() && !isPartition())) return false - val hstIdx = name.indexOf(PG_HST) - return hstIdx + PG_HST.length == name.length // nothing after $hst - } - /** History year-partition: `$hst$` at end, no `$p` suffix. */ - fun isHistoryYearRelation(): Boolean { - if (!isAnyHistoryRelation() || (!isTable() && !isPartition())) return false - return year() > 0 && name.indexOf(PG_DIST_PARTITION, name.indexOf(PG_HST)) < 0 - } - /** History perf-partition: `$hst$$p`. */ - fun isHistoryPartition(): Boolean { - if (!isAnyHistoryRelation() || !isTable()) return false - return year() > 0 && name.indexOf(PG_DIST_PARTITION, name.indexOf(PG_HST)) >= 0 - } - - // --- - - /** - * Test if this is a HEAD relation (any table, partition or index that belongs to HEAD). - * @return _true_ if this is a HEAD relation. - */ - fun isAnyMetaRelation() = name.indexOf(PG_META) > 0 - - /** - * Tests if this the HEAD table, which means, that HEAD is not partitioned. - * @return _true_ if this is the HEAD table. - */ - fun isMetaRootRelation() = isAnyMetaRelation() && isTable() + // 0 1 2 3 = 4 + // HISTORY: {name}$hst${shifted}${distribution} - // --- + val isHistoryTable: Boolean = parts.size == 2 && parts[1] == "hst" + val isHistoryPartition: Boolean = parts.size == 3 && parts[1] == "hst" + val isHistoryDistributionPartition: Boolean = parts.size == 4 && parts[1] == "hst" + val isHistory = isHistoryTable || isHistoryPartition || isHistoryDistributionPartition companion object PgRelationCompanion { /** @@ -193,8 +119,7 @@ SELECT c.oid AS oid, c.reltablespace as ts_oid FROM pg_class c, i WHERE c.relnamespace = i.oid AND (c.relname=${quoteLiteral(collectionId)} OR c.relname LIKE ${quoteLiteral(collectionId, "${PG_S}%")}) -ORDER BY relname; -""" +ORDER BY relname;""" return conn.execute(SQL) } } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt index 6d519b1d7..28146899b 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt @@ -331,8 +331,6 @@ open class PgWriter internal constructor( "The UPDATE (write #${write.i}) failed, because the collection '$featureId' does not exist in map '$mapId'" ) } - // Normalize members and indices (inject defaults, validate, sort) — CREATE only. - normalizeCollection(nakshaCollection) pgCollection = PgCollection(map, nakshaCollection) createPgCollection(pgCollection) } else if (op == WriteOp.CREATE) { @@ -340,8 +338,8 @@ open class PgWriter internal constructor( "The write #${write.i} failed, because the collection '$featureId' does exist already in map '$mapId'" ) } else { - // UPSERT or UPDATE on an existing collection: diff schema (members + custom indexes) and apply. - pgCollection.applyMembersAndIndexes(conn, pgCollection.head, nakshaCollection, write.original.force) + // UPSERT or UPDATE on an existing collection: Ensure that no invalid changes are asked for. + pgCollection.verifyNewHeadState(nakshaCollection) } } else if (op == WriteOp.DELETE || op == WriteOp.PURGE) { if (pgCollection != null) { @@ -421,122 +419,6 @@ open class PgWriter internal constructor( storage.adminCatalog.deletePgCatalog(conn, map) } - /** - * Normalizes a [NakshaCollection] before it is physically created: - * - * **Members** — when the client provides an explicit (non-null) `members` list: - * - Mandatory members (`fn`, `version`, `id`, `feature`) are silently deduplicated if declared - * with the exact same type, or rejected with [NakshaError.ILLEGAL_ARGUMENT] on type mismatch. - * - Non-mandatory names that conflict with any reserved [PgColumn] name are rejected. - * - The surviving client-declared members are sorted for optimal PostgreSQL column layout. - * When `members` is **null** (backward-compatible) it is left as-is; the DDL layer will use the - * full built-in column schema. - * - * **Indices** — when the client provides an explicit (non-null) `indices` list: - * - Any entry whose [naksha.model.objects.Index.internal] flag is `true` is silently dropped - * (clients must not declare storage-managed indices). - * When `indices` is **null** (backward-compatible) it is left as-is; the DDL layer will create - * all default optional indices. - * - * This method modifies [collection] in place and returns it. - */ - protected fun normalizeCollection(collection: NakshaCollection): NakshaCollection { - // ── Members ───────────────────────────────────────────────────────────────── - val clientMembers = collection.members - if (clientMembers != null) { - val mandatoryByName = StandardMembers.MANDATORY.associateBy { it.name } - val normalizedMembers = MemberList() - for (m in clientMembers) { - if (m == null) continue - val mandatory = mandatoryByName[m.name] - if (mandatory != null) { - // Mandatory column declared by client: exact type → silently drop; type conflict → reject. - if (m.dataType != mandatory.dataType) { - throw illegalArg( - "Member '${m.name}' is a mandatory column with type ${mandatory.dataType}; " + - "client declared it with type ${m.dataType}" - ) - } - } else { - normalizedMembers.add(m) - } - } - PgMemberHelper.validateMemberNames(normalizedMembers) - PgMemberHelper.sortMembersForStorage(normalizedMembers) - collection.members = normalizedMembers - } - - // ── Indices ────────────────────────────────────────────────────────────────── - val clientIndices = collection.indices - if (clientIndices != null) { - val normalizedIndices = IndexList() - for (idx in clientIndices) { - if (idx == null) continue - if (idx.internal) continue // clients must not declare internal indices - normalizedIndices.add(idx) - } - // When members are explicitly set, validate that every index column name - // refers to a known member (standard built-in or custom declared). - if (clientMembers != null) { - val knownNames = buildSet { - addAll(StandardMembers.ALL_NAMES) - for (m in collection.members ?: emptyList()) if (m != null) add(m.name) - } - // Build a lookup: member name → dataType (standard + custom) - val memberTypeByName: Map = buildMap { - for (sm in StandardMembers.ALL) put(sm.name, sm.dataType) - for (m in collection.members ?: emptyList()) if (m != null) put(m.name, m.dataType) - } - for (idx in normalizedIndices) { - if (idx == null) continue - val firstColName = idx.on.firstOrNull { it != null } - for (colName in idx.on) { - if (colName != null && colName !in knownNames) { - throw illegalArg( - "Index '${idx.name}' references unknown member '$colName'. " + - "Declare the member in the collection's members list, or use a standard member name." - ) - } - } - // Type-compatibility: SPATIAL index requires a SPATIAL member as its first column; - // BTREE/other index must not target a SPATIAL member (no ordering defined for TWKB). - if (firstColName != null) { - val firstColType = memberTypeByName[firstColName] - when (idx.type) { - IndexType.SPATIAL -> if (firstColType != MemberType.SPATIAL) { - throw illegalArg( - "SPATIAL index '${idx.name}' must target a member of type SPATIAL, " + - "but '$firstColName' has type $firstColType." - ) - } - IndexType.TAG_MAP -> if (firstColType != MemberType.TAG_MAP && firstColType != MemberType.TAG_MAP_FROM_ARRAY) { - throw illegalArg( - "TAGS index '${idx.name}' must target a member of type TAGS or TAGS_FROM_ARRAY, " + - "but '$firstColName' has type $firstColType." - ) - } - IndexType.TAG_LIST -> if (firstColType != MemberType.TAG_LIST) { - throw illegalArg( - "SET index '${idx.name}' must target a member of type SET, " + - "but '$firstColName' has type $firstColType." - ) - } - else -> if (firstColType == MemberType.SPATIAL) { - throw illegalArg( - "Index '${idx.name}' of type ${idx.type} cannot target SPATIAL member '$firstColName'. " + - "Use IndexType.SPATIAL for geometry columns." - ) - } - } - } - } - } - collection.indices = normalizedIndices - } - - return collection - } - /** * Invoked when a [NakshaCollection][naksha.model.objects.NakshaCollection] should be physically created. * @param collection the collection that should be physically created. diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterBase.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterBase.kt index 0ca5e382e..ed3098c65 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterBase.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterBase.kt @@ -46,11 +46,11 @@ internal abstract class PgWriterBase protected constructor( val storageNumber: Int64 get() = collection.storage.number - val mapNumber: Int - get() = collection.catalog.number + val catalogNumber: Int + get() = collection.catalog.catalogNumber val collectionNumber: Int - get() = collection.number + get() = collection.collectionNumber /** * The transaction to operate upon. @@ -74,7 +74,7 @@ internal abstract class PgWriterBase protected constructor( */ val inRows = PgRows() .withDatabaseNumber(storageNumber) - .withCatalogNumber(mapNumber) + .withCatalogNumber(catalogNumber) .withCollectionNumber(collectionNumber) .withMinRows(writes.size) @@ -140,6 +140,8 @@ internal abstract class PgWriterBase protected constructor( val headTable: PgTable = initHeadTable() private fun initHistoryTable(): PgTable? { + // TODO: + tx.version val hst = collection.historyTable ?: return null var yearTable: PgHistoryYear? = hst.years[year] if (yearTable == null) { diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterDelete.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterDelete.kt index 7f0204e87..2bb1ed76a 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterDelete.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterDelete.kt @@ -146,7 +146,7 @@ ${if (purge) "LEFT JOIN head_deleted ON head_deleted.id = query.id" else ""} if (writes.isEmpty()) return val outRows = PgRows() .withDatabaseNumber(storageNumber) - .withCatalogNumber(mapNumber) + .withCatalogNumber(catalogNumber) .withCollectionNumber(collectionNumber) .withDefaultDataEncoding(collection.head.dataEncoding ?: Naksha.DEFAULT_DATA_ENCODING) .addColumns(collection.effectiveHistoryColumns) @@ -190,7 +190,7 @@ ${if (purge) "LEFT JOIN head_deleted ON head_deleted.id = query.id" else ""} val tombstone_fn = outRows.getInt64(row, PgColumn.fn) val tombstone_version = outRows.getInt64(row, PgColumn.version) val tn = if (tombstone_fn != null && tombstone_version != null) { - TupleNumber(storageNumber, mapNumber, collectionNumber, tombstone_fn, Version(tombstone_version)) + TupleNumber(storageNumber, catalogNumber, collectionNumber, tombstone_fn, Version(tombstone_version)) } else null write.tupleNumber = tn diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt index dfae13d03..9214e0cb6 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt @@ -135,7 +135,7 @@ LEFT JOIN inserted ON inserted.id = new_row.id val keepableByteCols = collection.effectiveHeadColumns.filter { it.type == PgType.BYTE_ARRAY && it !== PgColumn.feature } val rows = PgRows() .withDatabaseNumber(storageNumber) - .withCatalogNumber(mapNumber) + .withCatalogNumber(catalogNumber) .withCollectionNumber(collectionNumber) .addColumn("id", PgType.STRING) .addColumn("existing_id", PgType.STRING) diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt index f981afb51..84c254c8b 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt @@ -148,7 +148,7 @@ ${if (head_to_history.isNotEmpty()) "LEFT JOIN head_to_history ON head_to_histor val keepableByteCols = collection.effectiveHeadColumns.filter { it.type == PgType.BYTE_ARRAY && it !== PgColumn.feature } val outRows = PgRows() .withDatabaseNumber(storageNumber) - .withCatalogNumber(mapNumber) + .withCatalogNumber(catalogNumber) .withCollectionNumber(collectionNumber) .addColumn(PgColumn.id) .addColumn(PgColumn.fn) @@ -189,14 +189,14 @@ ${if (head_to_history.isNotEmpty()) "LEFT JOIN head_to_history ON head_to_histor val fn = outRows.getInt64(row, "fn") ?: throw generalException("Missing 'fn' in SQL result") val id = outRows.getString(row, "id") ?: fn.toString() val versionTxn = outRows.getInt64(row, "version") ?: throw generalException("Missing 'version' in SQL result") - val tn = TupleNumber(storageNumber, mapNumber, collectionNumber, fn, Version(versionTxn)) + val tn = TupleNumber(storageNumber, catalogNumber, collectionNumber, fn, Version(versionTxn)) // We need to patch the tuple of all inserts, that were replaced with updates! // The content is the same, but the action, operation, and change-count change. val updatedFn = outRows.getInt64(row, "updated_fn") val updatedVersionTxn = outRows.getInt64(row, "updated_version") if (updatedFn != null && updatedVersionTxn != null) { - val updated_tn = TupleNumber(storageNumber, mapNumber, collectionNumber, updatedFn, Version(updatedVersionTxn)) + val updated_tn = TupleNumber(storageNumber, catalogNumber, collectionNumber, updatedFn, Version(updatedVersionTxn)) // If an update was done, we need the following values to be available: val hasCc = PgColumn.cc in collection.effectiveHeadColumns val changeCount: Int = if (hasCc) { diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/PgRelationTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/PgRelationTest.kt index ed34c26de..685d2a0ba 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/PgRelationTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/PgRelationTest.kt @@ -19,10 +19,10 @@ class PgRelationTest { ) } // expect - assertEquals(1, pgRelation("topology\$p001").partitionNumber()) - assertEquals(0, pgRelation("topology\$p000").partitionNumber()) - assertEquals(256, pgRelation("topology\$del\$p256").partitionNumber()) - assertEquals(7, pgRelation("topology\$hst\$2024\$p007").partitionNumber()) + assertEquals(1, pgRelation("topology\$p001").distributionPartition()) + assertEquals(0, pgRelation("topology\$p000").distributionPartition()) + assertEquals(256, pgRelation("topology\$del\$p256").distributionPartition()) + assertEquals(7, pgRelation("topology\$hst\$2024\$p007").distributionPartition()) assertEquals(2024, pgRelation("topology\$hst\$2024\$p001").year()) assertEquals(2024, pgRelation("topology\$hst\$2024").year()) } From bab6ac26d4ca8ed080575334ce5a118ec588a435 Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Wed, 24 Jun 2026 16:44:49 +0200 Subject: [PATCH 37/57] Fix PgWriteDelete. Signed-off-by: Alexander Lowey-Weber --- .../cli/copy/service/CopyServiceTest.java | 4 +- .../copy/service/CopyServiceTestUtils.java | 2 +- .../handlers/DefaultStorageHandlerTest.java | 14 +- .../kotlin/naksha/model/TupleNumber.kt | 8 +- .../naksha/model/objects/StandardMembers.kt | 7 + .../kotlin/naksha/model/objects/XyzMembers.kt | 2 +- .../kotlin/naksha/model/request/Write.kt | 70 +- .../kotlin/naksha/model/request/WriteOp.kt | 26 +- .../kotlin/naksha/psql/PgCollection.kt | 30 + .../commonMain/kotlin/naksha/psql/PgColumn.kt | 8 + .../kotlin/naksha/psql/PgQueryWhereBuilder.kt | 988 +++++++++--------- .../commonMain/kotlin/naksha/psql/PgRows.kt | 20 +- .../commonMain/kotlin/naksha/psql/PgWrite.kt | 4 +- .../commonMain/kotlin/naksha/psql/PgWriter.kt | 396 ++++--- .../kotlin/naksha/psql/PgWriterBase.kt | 82 +- .../kotlin/naksha/psql/PgWriterDelete.kt | 178 ++-- .../kotlin/naksha/psql/PgWriterInsert.kt | 6 +- .../kotlin/naksha/psql/PgWriterUpdate.kt | 8 +- .../kotlin/naksha/psql/PgWriterUpsert.kt | 10 +- 19 files changed, 945 insertions(+), 918 deletions(-) diff --git a/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/copy/service/CopyServiceTest.java b/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/copy/service/CopyServiceTest.java index 8854e73b5..d67154778 100644 --- a/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/copy/service/CopyServiceTest.java +++ b/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/copy/service/CopyServiceTest.java @@ -839,7 +839,7 @@ private StorageProvider createStorageProvider(IStorage srcStorage, IStorage targ } private void assertCreateMapWrite(Write write) { - assertEquals(Naksha.ADMIN_CATALOG_ID, write.getMapId()); + assertEquals(Naksha.ADMIN_CATALOG_ID, write.getCatalogId()); assertEquals(Naksha.CATALOGS_COL_ID, write.getCollectionId()); assertEquals(WriteOp.CREATE, write.getOp()); assertNotNull(write.getFeature()); @@ -847,7 +847,7 @@ private void assertCreateMapWrite(Write write) { } private void assertCreateCollectionWrite(Write write) { - assertEquals(targetCopyElement.getMapId(), write.getMapId()); + assertEquals(targetCopyElement.getMapId(), write.getCatalogId()); assertEquals(Naksha.COLLECTIONS_COL_ID, write.getCollectionId()); assertEquals(WriteOp.CREATE, write.getOp()); assertNotNull(write.getFeature()); diff --git a/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/copy/service/CopyServiceTestUtils.java b/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/copy/service/CopyServiceTestUtils.java index 4f41ba761..d03057d98 100644 --- a/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/copy/service/CopyServiceTestUtils.java +++ b/here-naksha-cli/src/jvmTest/java/com/here/naksha/cli/copy/service/CopyServiceTestUtils.java @@ -108,7 +108,7 @@ private static void assertCreateWrite(Write w, CopyElement target) { assertEquals(target.getCollectionId(), w.getCollectionId(), "Every write Collection ID should match target Collection ID" ); - assertEquals(target.getMapId(), w.getMapId(), + assertEquals(target.getMapId(), w.getCatalogId(), "Every write Map ID should match target Map ID" ); } diff --git a/here-naksha-lib-handlers/src/jvmTest/java/com/here/naksha/lib/handlers/DefaultStorageHandlerTest.java b/here-naksha-lib-handlers/src/jvmTest/java/com/here/naksha/lib/handlers/DefaultStorageHandlerTest.java index dce4c6127..3e9df52f9 100644 --- a/here-naksha-lib-handlers/src/jvmTest/java/com/here/naksha/lib/handlers/DefaultStorageHandlerTest.java +++ b/here-naksha-lib-handlers/src/jvmTest/java/com/here/naksha/lib/handlers/DefaultStorageHandlerTest.java @@ -321,7 +321,7 @@ void shouldCreateMapIfMissing() { assertEquals(1, secondRequestWrites.size()); Write mapWrite = secondRequestWrites.get(0); assertEquals(WriteOp.CREATE, mapWrite.getOp()); - assertEquals(Naksha.ADMIN_CATALOG_ID, mapWrite.getMapId()); + assertEquals(Naksha.ADMIN_CATALOG_ID, mapWrite.getCatalogId()); assertEquals(Naksha.CATALOGS_COL_ID, mapWrite.getCollectionId()); assertEquals(mapId, mapWrite.getId()); } @@ -354,7 +354,7 @@ void shouldUseMapFromStorageProps() { assertEquals(1, subsmittedWrites.size()); Write submittedWrite = subsmittedWrites.get(0); assertEquals(WriteOp.CREATE, submittedWrite.getOp()); - assertEquals(mapIdFromStorageProps, submittedWrite.getMapId()); + assertEquals(mapIdFromStorageProps, submittedWrite.getCatalogId()); assertEquals(handler.properties.getCollection().getId(), submittedWrite.getCollectionId()); } @@ -394,16 +394,16 @@ void shouldReattemptWriteCollectionsAfterMissingMapByCreatingMap() { assertTrue(RequestTypesUtil.isOnlyWriteCollections(calls.get(0))); assertTrue(RequestTypesUtil.isOnlyWriteCollections(calls.get(2))); assertEquals(Naksha.COLLECTIONS_COL_ID, calls.get(0).getWrites().get(0).getCollectionId()); - assertEquals(mapIdFromStorageProps, calls.get(0).getWrites().get(0).getMapId()); + assertEquals(mapIdFromStorageProps, calls.get(0).getWrites().get(0).getCatalogId()); assertEquals("target_collection", calls.get(0).getWrites().get(0).getFeature().getId()); assertEquals(Naksha.COLLECTIONS_COL_ID, calls.get(2).getWrites().get(0).getCollectionId()); - assertEquals(mapIdFromStorageProps, calls.get(2).getWrites().get(0).getMapId()); + assertEquals(mapIdFromStorageProps, calls.get(2).getWrites().get(0).getCatalogId()); assertEquals("target_collection", calls.get(2).getWrites().get(0).getFeature().getId()); // 2nd call is map creation in admin map / maps collection Write mapCreate = calls.get(1).getWrites().get(0); assertEquals(WriteOp.CREATE, mapCreate.getOp()); - assertEquals(Naksha.ADMIN_CATALOG_ID, mapCreate.getMapId()); + assertEquals(Naksha.ADMIN_CATALOG_ID, mapCreate.getCatalogId()); assertEquals(Naksha.CATALOGS_COL_ID, mapCreate.getCollectionId()); assertEquals(mapIdFromStorageProps, mapCreate.getId()); } @@ -462,7 +462,7 @@ void shouldApplyMapIdAndCollectionsColForWriteCollections() { Write write = captor.getValue().getWrites().get(0); assertTrue(RequestTypesUtil.isOnlyWriteCollections(captor.getValue())); assertEquals(Naksha.COLLECTIONS_COL_ID, write.getCollectionId(), "WriteCollections must target naksa~collections collection"); - assertEquals(mapIdFromStorageProps, write.getMapId(), "MapId must be taken from storage props"); + assertEquals(mapIdFromStorageProps, write.getCatalogId(), "MapId must be taken from storage props"); assertEquals("apply_col", write.getFeature().getId()); } @@ -753,7 +753,7 @@ private static ArgumentMatcher matchesCreateMapRequest(String mapI } Write w = writes.get(0); return WriteOp.CREATE.equals(w.getOp()) - && Naksha.ADMIN_CATALOG_ID.equals(w.getMapId()) + && Naksha.ADMIN_CATALOG_ID.equals(w.getCatalogId()) && Naksha.CATALOGS_COL_ID.equals(w.getCollectionId()) && mapId.equals(w.getId()); }; diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumber.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumber.kt index 25312c047..9ab3bc856 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumber.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumber.kt @@ -85,14 +85,14 @@ data class TupleNumber( get() = Action.fromValue(version.toInt() and 3) /** - * Calculates the partition-index where this [Tuple] will be located. + * Calculates the distribution partition-index where this [Tuple] will be located. * - * If the given partitions are less than `2`, the method always returns `0`, if the number is bigger than `65536` the result will be mapped back into the range between `0` and `65536` _(exclusive)_. + * If the given partitions are less than `2`, the method always returns `-1`. If the number is bigger than `65536` the result will be mapped back into the range between `0` and `65536` _(exclusive)_. * @param partitions the number of partitions - * @return the partition-index, a value between `0` and `partitions - 1`, maximal `65535` + * @return the partition-index, a value between `0` and `partitions - 1` _(maximal 65535)_; or `-1` if there are no distribution partitions. * @since 3.0 */ - fun partitionIndex(partitions: Int): Int = if (partitions <= 1) 0 else (partitionNumber % partitions) and 0xffff + fun partitionIndex(partitions: Int): Int = if (partitions < 2) -1 else (partitionNumber % partitions) and 0xffff override fun hashCode(): Int = version.hashCode() diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt index 4baa27614..44db5d383 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardMembers.kt @@ -139,5 +139,12 @@ class StandardMembers private constructor() { */ @JvmField @JsStatic val Geometry = Member("geo", MemberType.SPATIAL, JsonPath("geometry")) + + /** + * `cc` — change-count: how many times this feature has been modified. There is some special low-level treatment for this member, if it is added to a collection. It is always initialized with `1` and incremented for every new state. + * @since 3.0 + */ + @JvmField @JsStatic + val ChangeCount = Member("cc", MemberType.INT32, JsonPath("properties", "changeCount")) } } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzMembers.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzMembers.kt index fad518798..0bb18d495 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzMembers.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzMembers.kt @@ -110,7 +110,7 @@ class XyzMembers private constructor() { * @since 3.0 */ @JvmField @JsStatic - val XyzChangeCount = Member("cc", MemberType.INT32, JsonPath("properties", "@ns:com:here:xyz", "changeCount")) + val XyzChangeCount = Member(StandardMembers.ChangeCount, JsonPath("properties", "@ns:com:here:xyz", "changeCount")) /** * `base_tn` — base tuple-number (`BYTE_ARRAY`), set when a three-way merge was performed. diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/Write.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/Write.kt index 2acb28f2b..5ec613f7e 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/Write.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/Write.kt @@ -108,8 +108,8 @@ open class Write : AnyObject() { // Sorts by map-id, collection-id, feature-id. // Sorts admin-map, map's collection, and collection's collection first. - val a_mapId = a.mapId ?: throw illegalState("Write for feature '${a.id}' does not have 'mapId'") - val b_mapId = b.mapId ?: throw illegalState("Write for feature '${b.id}' does not have 'mapId'") + val a_mapId = a.catalogId ?: throw illegalState("Write for feature '${a.id}' does not have 'mapId'") + val b_mapId = b.catalogId ?: throw illegalState("Write for feature '${b.id}' does not have 'mapId'") val a_colId = a.collectionId ?: throw illegalState("Write for feature '${a.id}' does not have 'collectionId'") val b_colId = b.collectionId ?: throw illegalState("Write for feature '${b.id}' does not have 'collectionId'") val map_diff = compareMapIds(a_mapId, b_mapId) @@ -154,13 +154,13 @@ open class Write : AnyObject() { * If a [map][NakshaCatalog] or [dictionary][NakshaDictionary] should be modified, then use [Naksha.ADMIN_CATALOG_ID]. * @since 3.0 */ - var mapId by MAP_ID + var catalogId by MAP_ID /** - * @see [mapId] + * @see [catalogId] */ fun withMapId(value: String?): Write { - mapId = value + catalogId = value return this } @@ -446,7 +446,7 @@ open class Write : AnyObject() { * @since 3.0 */ fun createDictionary(dict: NakshaDictionary): Write { - this.mapId = ADMIN_CATALOG_ID + this.catalogId = ADMIN_CATALOG_ID this.collectionId = BOOKS_COL_ID this.op = WriteOp.CREATE this.feature = dict @@ -461,7 +461,7 @@ open class Write : AnyObject() { * @since 3.0 */ fun updateDictionary(dict: NakshaDictionary, atomic: Boolean): Write { - this.mapId = ADMIN_CATALOG_ID + this.catalogId = ADMIN_CATALOG_ID this.collectionId = BOOKS_COL_ID this.op = WriteOp.UPDATE this.feature = dict @@ -476,7 +476,7 @@ open class Write : AnyObject() { * @since 3.0 */ fun upsertDictionary(dict: NakshaDictionary): Write { - this.mapId = ADMIN_CATALOG_ID + this.catalogId = ADMIN_CATALOG_ID this.collectionId = BOOKS_COL_ID this.op = WriteOp.UPSERT this.feature = dict @@ -491,7 +491,7 @@ open class Write : AnyObject() { * @since 3.0 */ fun deleteDictionary(dict: NakshaDictionary, atomic: Boolean): Write { - this.mapId = ADMIN_CATALOG_ID + this.catalogId = ADMIN_CATALOG_ID this.collectionId = BOOKS_COL_ID this.op = WriteOp.DELETE this.feature = dict @@ -508,7 +508,7 @@ open class Write : AnyObject() { */ @JvmOverloads fun deleteDictionaryById(dictId: String, version: Version? = null): Write { - this.mapId = ADMIN_CATALOG_ID + this.catalogId = ADMIN_CATALOG_ID this.collectionId = BOOKS_COL_ID this.op = WriteOp.DELETE this.id = dictId @@ -524,7 +524,7 @@ open class Write : AnyObject() { * @since 3.0 */ fun createMap(map: NakshaCatalog): Write { - this.mapId = ADMIN_CATALOG_ID + this.catalogId = ADMIN_CATALOG_ID this.collectionId = CATALOGS_COL_ID this.op = WriteOp.CREATE this.feature = map @@ -539,7 +539,7 @@ open class Write : AnyObject() { * @since 3.0 */ fun updateMap(map: NakshaCatalog, atomic: Boolean): Write { - this.mapId = ADMIN_CATALOG_ID + this.catalogId = ADMIN_CATALOG_ID this.collectionId = CATALOGS_COL_ID this.op = WriteOp.UPDATE this.feature = map @@ -555,7 +555,7 @@ open class Write : AnyObject() { * @since 3.0 */ fun upsertMap(map: NakshaCatalog, atomic: Boolean): Write { - this.mapId = ADMIN_CATALOG_ID + this.catalogId = ADMIN_CATALOG_ID this.collectionId = CATALOGS_COL_ID this.op = WriteOp.UPSERT this.feature = map @@ -571,7 +571,7 @@ open class Write : AnyObject() { * @since 3.0 */ fun deleteMap(map: NakshaCatalog, atomic: Boolean): Write { - this.mapId = ADMIN_CATALOG_ID + this.catalogId = ADMIN_CATALOG_ID this.collectionId = CATALOGS_COL_ID this.op = WriteOp.DELETE this.feature = map @@ -588,7 +588,7 @@ open class Write : AnyObject() { */ @JvmOverloads fun deleteMapById(id: String, version: Version? = null): Write { - this.mapId = ADMIN_CATALOG_ID + this.catalogId = ADMIN_CATALOG_ID this.collectionId = CATALOGS_COL_ID this.op = WriteOp.DELETE this.id = id @@ -603,7 +603,7 @@ open class Write : AnyObject() { * @since 3.0 */ fun createCollection(collection: NakshaCollection): Write { - this.mapId = collection.catalogId + this.catalogId = collection.catalogId this.collectionId = COLLECTIONS_COL_ID this.op = WriteOp.CREATE this.feature = collection @@ -617,7 +617,7 @@ open class Write : AnyObject() { * @since 3.0 */ fun updateCollection(collection: NakshaCollection, atomic: Boolean): Write { - this.mapId = collection.catalogId + this.catalogId = collection.catalogId this.collectionId = COLLECTIONS_COL_ID this.op = WriteOp.UPDATE this.feature = collection @@ -631,7 +631,7 @@ open class Write : AnyObject() { * @since 3.0 */ fun upsertCollection(collection: NakshaCollection): Write { - this.mapId = collection.catalogId + this.catalogId = collection.catalogId this.collectionId = COLLECTIONS_COL_ID this.op = WriteOp.UPSERT this.feature = collection @@ -645,7 +645,7 @@ open class Write : AnyObject() { * @since 3.0 */ fun deleteCollection(collection: NakshaCollection, atomic: Boolean): Write { - this.mapId = collection.catalogId + this.catalogId = collection.catalogId this.collectionId = COLLECTIONS_COL_ID this.op = WriteOp.DELETE this.feature = collection @@ -662,7 +662,7 @@ open class Write : AnyObject() { */ @JvmOverloads fun deleteCollectionById(mapId: String? = null, collectionId: String, version: Version? = null): Write { - this.mapId = mapId + this.catalogId = mapId this.collectionId = COLLECTIONS_COL_ID this.op = WriteOp.DELETE this.id = collectionId @@ -679,7 +679,7 @@ open class Write : AnyObject() { * @since 3.0 */ fun createFeature(collection: NakshaCollection, feature: NakshaFeature): Write { - this.mapId = collection.catalogId + this.catalogId = collection.catalogId this.collectionId = collection.id this.op = WriteOp.CREATE this.feature = feature @@ -697,7 +697,7 @@ open class Write : AnyObject() { @JsName("createFeatureUsingIds") @JvmOverloads fun createFeature(mapId: String? = null, collectionId: String, feature: NakshaFeature): Write { - this.mapId = mapId + this.catalogId = mapId this.collectionId = collectionId this.op = WriteOp.CREATE this.feature = feature @@ -712,7 +712,7 @@ open class Write : AnyObject() { * @since 3.0 */ fun updateFeature(collection: NakshaCollection, feature: NakshaFeature, atomic: Boolean): Write { - this.mapId = collection.catalogId + this.catalogId = collection.catalogId this.collectionId = collection.id this.op = WriteOp.UPDATE this.feature = feature @@ -731,7 +731,7 @@ open class Write : AnyObject() { @JsName("updateFeatureUsingIds") @JvmOverloads fun updateFeature(mapId: String? = null, collectionId: String, feature: NakshaFeature, atomic: Boolean): Write { - this.mapId = mapId + this.catalogId = mapId this.collectionId = collectionId this.op = WriteOp.UPDATE this.feature = feature @@ -746,7 +746,7 @@ open class Write : AnyObject() { * @since 3.0 */ fun upsertFeature(collection: NakshaCollection, feature: NakshaFeature): Write { - this.mapId = collection.catalogId + this.catalogId = collection.catalogId this.collectionId = collection.id this.op = WriteOp.UPSERT this.feature = feature @@ -763,7 +763,7 @@ open class Write : AnyObject() { @JsName("upsertFeatureUsingIds") @JvmOverloads fun upsertFeature(mapId: String? = null, collectionId: String, feature: NakshaFeature): Write { - this.mapId = mapId + this.catalogId = mapId this.collectionId = collectionId this.op = WriteOp.UPSERT this.feature = feature @@ -778,7 +778,7 @@ open class Write : AnyObject() { * @since 3.0 */ fun deleteFeature(collection: NakshaCollection, feature: NakshaFeature, atomic: Boolean): Write { - this.mapId = collection.catalogId + this.catalogId = collection.catalogId this.collectionId = collection.id this.op = WriteOp.DELETE this.feature = feature @@ -796,7 +796,7 @@ open class Write : AnyObject() { */ @JvmOverloads fun deleteFeatureById(collection: NakshaCollection, id: String, version: Version? = null): Write { - this.mapId = collection.catalogId + this.catalogId = collection.catalogId this.collectionId = collection.id this.op = WriteOp.DELETE this.id = id @@ -817,7 +817,7 @@ open class Write : AnyObject() { @JsName("deleteFeatureByIds") @JvmOverloads fun deleteFeatureById(mapId: String? = null, collectionId: String, id: String, version: Version? = null): Write { - this.mapId = mapId + this.catalogId = mapId this.collectionId = collectionId this.op = WriteOp.DELETE this.id = id @@ -834,7 +834,7 @@ open class Write : AnyObject() { * @since 3.0 */ fun purgeFeature(collection: NakshaCollection, feature: NakshaFeature, atomic: Boolean): Write { - this.mapId = collection.catalogId + this.catalogId = collection.catalogId this.collectionId = collection.id this.op = WriteOp.PURGE this.feature = feature @@ -853,7 +853,7 @@ open class Write : AnyObject() { @JsName("purgeFeatureById") @JvmOverloads fun purgeFeatureById(collection: NakshaCollection, id: String, version: Version? = null): Write { - this.mapId = collection.catalogId + this.catalogId = collection.catalogId this.collectionId = collection.id this.op = WriteOp.PURGE this.id = id @@ -874,7 +874,7 @@ open class Write : AnyObject() { @JsName("purgeFeatureByIds") @JvmOverloads fun purgeFeatureById(mapId: String? = null, collectionId: String, id: String, version: Version? = null): Write { - this.mapId = mapId + this.catalogId = mapId this.collectionId = collectionId this.op = WriteOp.PURGE this.id = id @@ -889,7 +889,7 @@ open class Write : AnyObject() { * @return `true` if this write modifies a dictionary; `false` otherwise. * @since 3.0 */ - fun isDictionaryModification(): Boolean = mapId == ADMIN_CATALOG_ID && collectionId == BOOKS_COL_ID + fun isDictionaryModification(): Boolean = catalogId == ADMIN_CATALOG_ID && collectionId == BOOKS_COL_ID /** * Tests if this write modifies a map. @@ -897,7 +897,7 @@ open class Write : AnyObject() { * @return `true` if this write modifies a map; `false` otherwise. * @since 3.0 */ - fun isMapModification(): Boolean = mapId == ADMIN_CATALOG_ID && collectionId == CATALOGS_COL_ID + fun isMapModification(): Boolean = catalogId == ADMIN_CATALOG_ID && collectionId == CATALOGS_COL_ID /** * Tests if this write modifies a collection. @@ -928,7 +928,7 @@ open class Write : AnyObject() { * @see [WriteOp] */ fun validate(): Write { - if (mapId == ADMIN_CATALOG_ID || collectionId == COLLECTIONS_COL_ID) { + if (catalogId == ADMIN_CATALOG_ID || collectionId == COLLECTIONS_COL_ID) { if (isInternalId(id)) { throw NakshaException(ILLEGAL_STATE, "Modification of internal features forbidden: '$id'") } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/WriteOp.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/WriteOp.kt index b1cdac850..b970220f6 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/WriteOp.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/WriteOp.kt @@ -19,45 +19,45 @@ class WriteOp : JsEnum(), Comparable { val NULL = def(WriteOp::class, null) /** - * Create the feature, fail if the feature exists already. + * Delete the feature, does not fail normally, even when the feature does not exist. */ @JvmField @JsStatic - val CREATE = defIgnoreCase(WriteOp::class, "CREATE") { self -> self.order = 0 } + val DELETE = defIgnoreCase(WriteOp::class, "DELETE") { self -> self.order = 0 } /** - * Update or created the feature, should never fail. + * Delete the feature, and remove remainders from the shadow delete table, so delete fully. + * + * This operation is important for _views_, where a _PURGE_ will restore the underlying version of a feature from lower layers, while a normal _DELETE_ would cause the feature to disappear. It is strongly recommended to only execute a _PURGE_, when being in a view context and the behavior that results from it is explicitly wished. */ @JvmField @JsStatic - val UPSERT = defIgnoreCase(WriteOp::class, "UPSERT") { self -> self.order = 1 } + val PURGE = defIgnoreCase(WriteOp::class, "PURGE") { self -> self.order = 1 } /** - * Update the feature, fail if the feature does not exist. + * Create the feature, fail if the feature exists already. */ @JvmField @JsStatic - val UPDATE = defIgnoreCase(WriteOp::class, "UPDATE") { self -> self.order = 2 } + val CREATE = defIgnoreCase(WriteOp::class, "CREATE") { self -> self.order = 2 } /** - * Delete the feature, does not fail normally, even when the feature does not exist. + * Update or created the feature, should never fail. */ @JvmField @JsStatic - val DELETE = defIgnoreCase(WriteOp::class, "DELETE") { self -> self.order = 3 } + val UPSERT = defIgnoreCase(WriteOp::class, "UPSERT") { self -> self.order = 3 } /** - * Delete the feature, and remove remainders from the shadow delete table, so delete fully. - * - * This operation is important for _views_, where a _PURGE_ will restore the underlying version of a feature from lower layers, while a normal _DELETE_ would cause the feature to disappear. It is strongly recommended to only execute a _PURGE_, when being in a view context and the behavior that results from it is explicitly wished. + * Update the feature, fail if the feature does not exist. */ @JvmField @JsStatic - val PURGE = defIgnoreCase(WriteOp::class, "PURGE") { self -> self.order = 4 } + val UPDATE = defIgnoreCase(WriteOp::class, "UPDATE") { self -> self.order = 4 } } /** - * An ordering number, defaults to 100 (so order at the end). + * An ordering number of the operation. Logically, we first want to `DELETE`, then `PURGE`, then `CREATE`, then `UPSERT`, and finally `UPDATE`. The `PURGE` simply copies the current feature state from _HEAD_ into _HISTORY_, setting the next-version. It can be an explicit operation, but mostly will be an intrinsic operation of `CREATE`, `UPSERT`, or `UPDATE`. */ var order: Int = 100 private set diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt index b4aab025e..0f7e3d8e1 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt @@ -1,6 +1,7 @@ package naksha.psql import naksha.base.* +import naksha.base.fn.Fn1 import naksha.model.* import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_ARGUMENT import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_STATE @@ -149,6 +150,23 @@ open class PgCollection internal constructor( @JvmField val columns: Array = generateColumns(nakshaCollection) + /** + * Join the identities of all [columns], separated by comma, optionally filtered by the given filter. + * @param filter an optional filter method to remove _(or replace)_ certain columns. + * @return a comma separated list of [ident][PgColumn.ident] strings. + * @since 3.0 + */ + fun joinColumns(filter: Fn1? = null): String { + val sb = StringBuilder() + for (column in columns) { + val ident: String? = if (filter != null) filter.call(column) else column.ident + if (ident == null) continue + if (sb.isNotEmpty()) sb.append(", ") + sb.append(ident) + } + return sb.toString() + } + private fun indicesFor(nakshaCollection: NakshaCollection, onHead: Boolean): Array { return Array(nakshaCollection.indices?.size ?: 0) { i -> // We know that when this method is called, indices must not be null! @@ -313,6 +331,18 @@ open class PgCollection internal constructor( @JvmField val internal: Boolean = id.startsWith("naksha~") + /** + * Ensures that the [PgHistoryPartition] for the given version exists. + * + * **Note**: If the current cache does not confirm that the corresponding history table exists, we can simply create it with `IF EXISTS` clause. It will not harm, when it exists already, but prevent yet another roundtrip. As we _(for now)_ have decided that columns and indices are immutable, we do not need to update old partition tables, and we can cache only the latest history table. This may change over time, because ones we start adding the feature that data can be imported, we may need to manage history partitions that are older than the latest _(current)_ table that we normally only write into. However, until that is done, this function can be kept simple . + * @param version the version to be written. + * @since 3.0 + */ + fun prepareWrite(version: Int64) { + val partitionNumber = version shr shift + // TODO: Check if this partition exists, if not, create it, cache it, and initiate the write. + } + /** * Verify the given new _HEAD_ state, ensure that none of the following values is modified: * - [NakshaCollection.members] - Ensure that they result in the same [columns]. diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumn.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumn.kt index 23bc7e5e9..1c4bf2e48 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumn.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgColumn.kt @@ -160,6 +160,14 @@ data class PgColumn( val NEXT_VERSION = PgColumn(2, NEXT_VERSION_NAME, INT64, "STORAGE $PLAIN NOT NULL") } + /** + * Check if this column is the same as the given one, so [name] and [memberType] match. + * @param other the other column to test against. + * @return _true_ if the two columns are the same; _false_ otherwise. + * @since 3.0 + */ + infix fun eq(other: PgColumn?): Boolean = this === other || (other != null && name == other.name && memberType == other.memberType) + /** Returns the [ident] of the column, so the quoted [name]. */ override fun toString(): String = ident } \ No newline at end of file diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt index 2d7c522e5..af3192616 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt @@ -268,498 +268,498 @@ internal class PgQueryWhereBuilder(private val request: ReadFeatures, private va } // --------------------------------------------------------< OLD CODE >------------------------------------------------------------- - - private fun whereFeatureId() { - // Partition into numeric IDs (fn >= 0, id stored as NULL in DB) and named IDs (fn < 0, id NOT NULL). - val reqIds: StringList = request.featureIds - val featureNumbers: MutableList = mutableListOf() - val featureIds: MutableList = mutableListOf() - for (id in reqIds) { - if (id == null) continue - val fn = Naksha.featureNumber(id) - if (fn >= Int64(0)) { - featureNumbers.add(fn) - } else { - featureIds.add(id) - } - } - if (featureNumbers.isEmpty() && featureIds.isEmpty()) return - - // For each collection: - if (where.isNotEmpty()) where.append(" AND ") - - where.append("( ") - if (featureIds.isNotEmpty()) { - val placeholder: String = placeholderForArg(featureIds.toTypedArray(), PgType.STRING_ARRAY) - val ID = collection.column(StandardMembers.Id) ?: throw illegalArg("Collection does not defined `id` column") - where.append(ID.ident).append(" = ANY(").append(placeholder).append(")") - } - if (featureNumbers.isNotEmpty()) { - if (featureIds.isNotEmpty()) where.append(" OR ") - - val placeholder: String = placeholderForArg(featureNumbers.toTypedArray(), PgType.INT64_ARRAY) - if (where.isNotEmpty()) where.append(" AND ") - where.append(FN.ident).append(" = ANY(").append(placeholder).append(")") - } - where.append(")") - } - - private fun whereGuids() { - val tupleNumbers = request.guids.mapNotNull { it?.tupleNumber } - if (tupleNumbers.isNotEmpty()) { - if (where.isNotEmpty()) where.append(" AND ") - - val fns = arrayOfNulls(tupleNumbers.size) - val versions = arrayOfNulls(tupleNumbers.size) - for (i in tupleNumbers.indices) { - fns[i] = tupleNumbers[i].featureNumber - versions[i] = tupleNumbers[i].version - } - val featureNumbersArg = placeholderForArg(fns, PgType.INT64_ARRAY) - val versionsArg = placeholderForArg(versions, PgType.INT64_ARRAY) - where.append("($FN, $VERSION) IN (SELECT * FROM unnest($featureNumbersArg::int8[], $versionsArg::int8[]))") - } - } - - private fun whereVersion() { - val version = request.version - if (version != null) { - if (where.isNotEmpty()) where.append(" AND ") - where.append("$VERSION <= ${version.number}") - } - val minVersion = request.minVersion - if (minVersion != null) { - if (where.isNotEmpty()) where.append(" AND ") - where.append("$VERSION >= ${minVersion.number}") - } - } - - private fun whereSpatial() { - val spatialQuery = request.query.spatial - if (spatialQuery != null) { - if (where.isNotEmpty()) { - where.append(" AND (") - } else { - where.append(" (") - } - whereNestedSpatial(spatialQuery) - where.append(")") - } - } - - private fun whereNestedSpatial(spatial: ISpatialQuery) { - when (spatial) { - is SpNot -> not( - subClause = spatial.query, - subClauseResolver = this::whereNestedSpatial - ) - - is SpAnd -> and( - subClauses = spatial.filterNotNull(), - subClauseResolver = this::whereNestedSpatial - ) - - is SpOr -> or( - subClauses = spatial.filterNotNull(), - subClauseResolver = this::whereNestedSpatial - ) - - is SpIntersects -> { - val queryGeometry = nakshaGeometry(spatial.geometry) - val geometryToCompare = when (val transformation = spatial.transformation) { - null -> queryGeometry - else -> resolveTransformation(transformation, queryGeometry) - } - where.append("ST_Intersects(naksha_2d(${PgColumn.geo}), $geometryToCompare)") - } - - is SpRefInHereTile -> { - where.append(refPointInTile(spatial.getHereTile())) - } - - else -> throw NakshaException( - NakshaError.ILLEGAL_ARGUMENT, - "Invalid spatial query found: $spatial" - ) - } - } - - private fun nakshaGeometry(geometry: SpGeometry): String { - val geoBytes = Naksha.encodeGeometry(geometry) - val geoBytesPlaceholder = placeholderForArg(geoBytes, PgType.BYTE_ARRAY) - return "naksha_2d($geoBytesPlaceholder)" - } - - private fun resolveTransformation( - transformation: SpTransformation, - basicGeometry: String - ): String { - return when (transformation) { - is SpBuffer -> resolveBuffer(transformation, basicGeometry) - else -> throw NakshaException( - NakshaError.UNSUPPORTED_OPERATION, - "This transformation is not yet supported: ${transformation::class.simpleName}" - ) - } - } - - private fun resolveBuffer(buffer: SpBuffer, basicGeometry: String): String { - val geo = if (buffer.geography) { - "$basicGeometry::geography" - } else { - basicGeometry - } - val distancePlaceholder = placeholderForArg(buffer.distance, PgType.DOUBLE) - val bufferStyleParams = bufferStyleParams(buffer) - return if (bufferStyleParams != null) { - "ST_Buffer($geo, $distancePlaceholder, $bufferStyleParams)" - } else { - "ST_Buffer($geo, $distancePlaceholder)" - } - } - - private fun bufferStyleParams(buffer: SpBuffer): String? { - val bufferStyleParams = StringBuilder() - if (buffer.quadSegments != null) { - val quadSegPlaceholder = placeholderForArg(buffer.quadSegments, PgType.INT) - bufferStyleParams.append("quad_segs=$quadSegPlaceholder") - } - if (buffer.joinStyle != null) { - val joinStylePlaceholder = placeholderForArg(buffer.joinStyle!!.value, PgType.STRING) - if (bufferStyleParams.isNotEmpty()) bufferStyleParams.append(" ") - bufferStyleParams.append("join=$joinStylePlaceholder") - } - if (buffer.joinLimit != null) { - val joinLimitPlaceholder = placeholderForArg(buffer.joinLimit, PgType.DOUBLE) - if (bufferStyleParams.isNotEmpty()) bufferStyleParams.append(" ") - bufferStyleParams.append("mitre_limit=$joinLimitPlaceholder") - } - if (buffer.endCap != null) { - val endCapPlaceholder = placeholderForArg(buffer.endCap!!.value, PgType.STRING) - if (bufferStyleParams.isNotEmpty()) bufferStyleParams.append(" ") - bufferStyleParams.append("endcap=$endCapPlaceholder") - } - if (buffer.side != null) { - val sidePlaceholder = placeholderForArg(buffer.side!!.value, PgType.STRING) - if (bufferStyleParams.isNotEmpty()) bufferStyleParams.append(" ") - bufferStyleParams.append("side=$sidePlaceholder") - } - return if (bufferStyleParams.isNotEmpty()) { - bufferStyleParams.toString() - } else { - null - } - } - - private fun whereRefTiles() { - val hereTiles = request.query.refTiles - .filterNotNull() - .map { HereTile(it) } - if (hereTiles.isNotEmpty()) { - if (where.isNotEmpty()) { - where.append(" AND (") - } else { - where.append(" (") - } - where.append(refPointInAnyOfTiles(hereTiles)) - where.append(")") - } - } - - private fun refPointInAnyOfTiles(hereTiles: List): String { - return hereTiles.joinToString(separator = " OR ") { hereTile -> - refPointInTile(hereTile) - } - } - - private fun refPointInTile(hereTile: HereTile): String { - val lowerBoundPlaceholder = placeholderForArg( - hereTile.maxLevelLowerBound().intKey, - PgType.INT - ) - val upperBoundPlaceholder = placeholderForArg( - hereTile.maxLevelUpperBound().intKey, - PgType.INT - ) - return "(${PgColumn.here_tile} >= $lowerBoundPlaceholder AND ${PgColumn.here_tile} <= $upperBoundPlaceholder)" - } - - private fun whereMetadata() { - val metaQuery = request.query.members - if (metaQuery != null) { - if (where.isNotEmpty()) { - where.append(" AND (") - } else { - where.append(" (") - } - whereNestedMetadata(metaQuery) - where.append(")") - } - } - - private fun whereNestedMetadata(metaQuery: IMemberQuery) { - when (metaQuery) { - is MemberNot -> not( - subClause = metaQuery.query, - subClauseResolver = this::whereNestedMetadata - ) - - is MemberAnd -> and( - subClauses = metaQuery.filterNotNull(), - subClauseResolver = this::whereNestedMetadata - ) - - is MemberOr -> or( - subClauses = metaQuery.filterNotNull(), - subClauseResolver = this::whereNestedMetadata - ) - - is MemberQuery -> { - val isActionQuery = metaQuery.member == MetaColumn.action() - val pgColumn = - if (isActionQuery) { - PgColumn.version - } else { - PgColumn.ofRowColumn(metaQuery.member) ?: throw NakshaException( - NakshaError.ILLEGAL_STATE, - "Couldn't find PgColumn for TupleColumn: ${metaQuery.member.name}" - ) - } - val leftOperand = if (isActionQuery) { - "(${PgColumn.version.name} & 3)::int4" - } else if (pgColumn == PgColumn.created_at || pgColumn == PgColumn.author_ts) { - "COALESCE(${pgColumn.name}, ${PgColumn.updated_at.name})" - } else { - pgColumn.name - } - // Action lives in the lower 2 bits of `version`; the comparison value is a small int. - val placeholderType = if (isActionQuery) PgType.INT else pgColumn.type - val resolvedQuery = when (val op = metaQuery.op) { - is StringOp -> { - val placeholder = placeholderForArg(metaQuery.value, placeholderType) - resolveStringOp(op, leftOperand, placeholder) - } - is DoubleOp -> { - val placeholder = placeholderForArg(metaQuery.value, placeholderType) - resolveDoubleOp(op, leftOperand, placeholder) - } - AnyOp.IS_ANY_OF -> { - val placeholder = placeholderForArg(metaQuery.value, arrayTypeFor(placeholderType)) - "$leftOperand = ANY($placeholder)" - } - else -> throw illegalArg("Unknown op type: ${op::class.simpleName}") - } - where.append(resolvedQuery) - } - - else -> throw NakshaException( - NakshaError.ILLEGAL_ARGUMENT, - "Unknown metadata query type: ${metaQuery::class.simpleName}" - ) - } - } - - private fun arrayTypeFor(pgType: PgType): PgType { - return when (pgType) { - PgType.BOOLEAN -> PgType.BOOLEAN_ARRAY - PgType.SHORT -> PgType.SHORT_ARRAY - PgType.INT -> PgType.INT_ARRAY - PgType.INT64 -> PgType.INT64_ARRAY - PgType.FLOAT -> PgType.FLOAT_ARRAY - PgType.DOUBLE -> PgType.DOUBLE_ARRAY - PgType.STRING -> PgType.STRING_ARRAY - PgType.BYTE_ARRAY -> PgType.BYTE_ARRAY_ARRAY - else -> throw illegalArg("Unknown array type for PgType: ${pgType::class.simpleName}") - } - } - - private fun whereTags() { - val tagQuery = request.query.tags - if (tagQuery != null) { - if (where.isNotEmpty()) { - where.append(" AND (") - } else { - where.append(" (") - } - whereNestedTags(tagQuery) - where.append(")") - } - } - - private fun whereNestedTags(tagQuery: ITagQuery) { - when (tagQuery) { - is TagSetContains -> resolveTagSetContains(tagQuery) - is TagNot -> not(tagQuery.query, this::whereNestedTags) - is TagOr -> { - if(containsOnlyTagExists(tagQuery)){ - // for tags without values we can utilize top-level-key based '?|' operand - // https://www.postgresql.org/docs/current/functions-json.html#FUNCTIONS-JSONB-OP-TABLE - val tagNames = tagQuery.filterIsInstance().map { it.name } - resolveTagNamesArrayOperation( - jsonbOperator = "?|", // 'jsonb_exists_any' is equivalent but will not hit the GIN index - tagNames = tagNames - ) - } else { - or(tagQuery.filterNotNull(), this::whereNestedTags) - } - } - is TagAnd -> { - if(containsOnlyTagExists(tagQuery)){ - // for tags without values we can utilize top-level-key based '?&' operand - // https://www.postgresql.org/docs/current/functions-json.html#FUNCTIONS-JSONB-OP-TABLE - val tagNames = tagQuery.filterIsInstance().map { it.name } - resolveTagNamesArrayOperation( - jsonbOperator = "?&", // 'jsonb_exists_all' is equivalent but MIGHT not hit the GIN index - tagNames = tagNames - ) - } else { - and(tagQuery.filterNotNull(), this::whereNestedTags) - } - } - is TagQuery -> resolveSingleTagQuery(tagQuery) - } - } - - private fun containsOnlyTagExists(container: ListProxy): Boolean = - container.all { it == null || it is TagMapHasKey } - - /** - * Element containment on a set-form tags column (jsonb array): `tags @> '[]'::jsonb`. - * The `@>` operator matches the element in its type (string, boolean, number) and is supported - * by the GIN index over the column. - */ - private fun resolveTagSetContains(tagQuery: TagSetContains) { - val element = AnyList() - element.add(tagQuery.element) - val placeholder = placeholderForArg(toJSON(element), PgType.STRING) - where.append("$tagsAsJsonb @> $placeholder::jsonb") - } - - private fun resolveTagNamesArrayOperation(jsonbOperator: String, tagNames: List) { - val tagKeysArray = tagNames.toTypedArray() - val tagKeysPlaceholder = placeholderForArg(tagKeysArray, PgType.STRING_ARRAY) - where.append("$tagsAsJsonb ?$jsonbOperator $tagKeysPlaceholder") - } - - private fun resolveSingleTagQuery(tagQuery: TagQuery) { - when (tagQuery) { - is TagMapHasKey -> { - val tagNamePlaceholder = placeholderForArg(tagQuery.name, PgType.STRING) - where.append("$tagsAsJsonb ?? $tagNamePlaceholder") - } - - is TagValueIsNull -> { - val tagValuePlaceholder = placeholderForArg(selectTagValue(tagQuery), PgType.STRING) - where.append("$tagValuePlaceholder IS NULL") - } - - is TagValueIsBool -> { - if (tagQuery.value) { - where.append(selectTagValue(tagQuery, PgType.BOOLEAN)) - } else { - where.append("not(${selectTagValue(tagQuery, PgType.BOOLEAN)})") - } - } - - is TagValueIsDouble -> { - val queryValuePlaceholder = placeholderForArg(tagQuery.value, PgType.DOUBLE) - val doubleOp = resolveDoubleOp( - tagQuery.op, - selectTagValue(tagQuery, PgType.DOUBLE), - queryValuePlaceholder - ) - where.append(doubleOp) - } - - is TagValueIsString -> { - val queryValuePlaceholder = placeholderForArg(tagQuery.value, PgType.STRING) - val stringEquals = resolveStringOp( - StringOp.EQUALS, - selectTagValue(tagQuery, PgType.STRING), - queryValuePlaceholder - ) - where.append(stringEquals) - } - - is TagValueMatches -> { - val jsonPathPlaceholder = placeholderForArg( - "\$.${tagQuery.name} ? (@ like_regex \"${tagQuery.regex}\")", - PgType.STRING - ) - where.append("$tagsAsJsonb @?? $jsonPathPlaceholder::jsonpath") - } - } - } - - private fun selectTagValue(tagQuery: TagQuery, castTo: PgType? = null): String { - val tagKeyPlaceholder = placeholderForArg(tagQuery.name, PgType.STRING) - return when (castTo) { - null -> "$tagsAsJsonb->$tagKeyPlaceholder" - PgType.STRING -> "$tagsAsJsonb->>$tagKeyPlaceholder" - else -> "($tagsAsJsonb->$tagKeyPlaceholder)::${castTo.value}" - } - } - - private fun not(subClause: T, subClauseResolver: (T) -> Unit) { - where.append(" NOT (") - subClauseResolver(subClause) - where.append(") ") - } - - private fun and(subClauses: List, subClauseResolver: (T) -> Unit) = - multiClause("AND", subClauses, subClauseResolver) - - private fun or(subClauses: List, subClauseResolver: (T) -> Unit) = - multiClause("OR", subClauses, subClauseResolver) - - private fun multiClause( - operand: String, - subClauses: List, - subClauseResolver: (T) -> Unit - ) { - where.append(" (") - subClauses.forEachIndexed { index, subClause -> - if (index > 0) { - where.append(" $operand ") - } - subClauseResolver(subClause) - } - where.append(") ") - } - - - private fun resolveStringOp( - stringOp: StringOp, - leftOperand: String, - rightOperand: String - ): String { - return when (stringOp) { - StringOp.EQUALS -> "$leftOperand = $rightOperand" - StringOp.NOT_EQUALS -> "$leftOperand != $rightOperand" - StringOp.STARTS_WITH -> "starts_with($leftOperand, $rightOperand)" - else -> throw NakshaException( - NakshaError.ILLEGAL_ARGUMENT, - "Unknown StringOp: $stringOp" - ) - } - } - - private fun resolveDoubleOp( - doubleOp: DoubleOp, - leftOperand: String, - rightOperand: String - ): String { - return when (doubleOp) { - DoubleOp.EQ -> "$leftOperand = $rightOperand" - DoubleOp.NE -> "$leftOperand != $rightOperand" - DoubleOp.GT -> "$leftOperand > $rightOperand" - DoubleOp.GTE -> "$leftOperand >= $rightOperand" - DoubleOp.LT -> "$leftOperand < $rightOperand" - DoubleOp.LTE -> "$leftOperand <= $rightOperand" - else -> throw NakshaException( - NakshaError.ILLEGAL_ARGUMENT, - "Unknown DoubleOp: $doubleOp" - ) - } - } +// +// private fun whereFeatureId() { +// // Partition into numeric IDs (fn >= 0, id stored as NULL in DB) and named IDs (fn < 0, id NOT NULL). +// val reqIds: StringList = request.featureIds +// val featureNumbers: MutableList = mutableListOf() +// val featureIds: MutableList = mutableListOf() +// for (id in reqIds) { +// if (id == null) continue +// val fn = Naksha.featureNumber(id) +// if (fn >= Int64(0)) { +// featureNumbers.add(fn) +// } else { +// featureIds.add(id) +// } +// } +// if (featureNumbers.isEmpty() && featureIds.isEmpty()) return +// +// // For each collection: +// if (where.isNotEmpty()) where.append(" AND ") +// +// where.append("( ") +// if (featureIds.isNotEmpty()) { +// val placeholder: String = placeholderForArg(featureIds.toTypedArray(), PgType.STRING_ARRAY) +// val ID = collection.column(StandardMembers.Id) ?: throw illegalArg("Collection does not defined `id` column") +// where.append(ID.ident).append(" = ANY(").append(placeholder).append(")") +// } +// if (featureNumbers.isNotEmpty()) { +// if (featureIds.isNotEmpty()) where.append(" OR ") +// +// val placeholder: String = placeholderForArg(featureNumbers.toTypedArray(), PgType.INT64_ARRAY) +// if (where.isNotEmpty()) where.append(" AND ") +// where.append(FN.ident).append(" = ANY(").append(placeholder).append(")") +// } +// where.append(")") +// } +// +// private fun whereGuids() { +// val tupleNumbers = request.guids.mapNotNull { it?.tupleNumber } +// if (tupleNumbers.isNotEmpty()) { +// if (where.isNotEmpty()) where.append(" AND ") +// +// val fns = arrayOfNulls(tupleNumbers.size) +// val versions = arrayOfNulls(tupleNumbers.size) +// for (i in tupleNumbers.indices) { +// fns[i] = tupleNumbers[i].featureNumber +// versions[i] = tupleNumbers[i].version +// } +// val featureNumbersArg = placeholderForArg(fns, PgType.INT64_ARRAY) +// val versionsArg = placeholderForArg(versions, PgType.INT64_ARRAY) +// where.append("($FN, $VERSION) IN (SELECT * FROM unnest($featureNumbersArg::int8[], $versionsArg::int8[]))") +// } +// } +// +// private fun whereVersion() { +// val version = request.version +// if (version != null) { +// if (where.isNotEmpty()) where.append(" AND ") +// where.append("$VERSION <= ${version.number}") +// } +// val minVersion = request.minVersion +// if (minVersion != null) { +// if (where.isNotEmpty()) where.append(" AND ") +// where.append("$VERSION >= ${minVersion.number}") +// } +// } +// +// private fun whereSpatial() { +// val spatialQuery = request.query.spatial +// if (spatialQuery != null) { +// if (where.isNotEmpty()) { +// where.append(" AND (") +// } else { +// where.append(" (") +// } +// whereNestedSpatial(spatialQuery) +// where.append(")") +// } +// } +// +// private fun whereNestedSpatial(spatial: ISpatialQuery) { +// when (spatial) { +// is SpNot -> not( +// subClause = spatial.query, +// subClauseResolver = this::whereNestedSpatial +// ) +// +// is SpAnd -> and( +// subClauses = spatial.filterNotNull(), +// subClauseResolver = this::whereNestedSpatial +// ) +// +// is SpOr -> or( +// subClauses = spatial.filterNotNull(), +// subClauseResolver = this::whereNestedSpatial +// ) +// +// is SpIntersects -> { +// val queryGeometry = nakshaGeometry(spatial.geometry) +// val geometryToCompare = when (val transformation = spatial.transformation) { +// null -> queryGeometry +// else -> resolveTransformation(transformation, queryGeometry) +// } +// where.append("ST_Intersects(naksha_2d(${PgColumn.geo}), $geometryToCompare)") +// } +// +// is SpRefInHereTile -> { +// where.append(refPointInTile(spatial.getHereTile())) +// } +// +// else -> throw NakshaException( +// NakshaError.ILLEGAL_ARGUMENT, +// "Invalid spatial query found: $spatial" +// ) +// } +// } +// +// private fun nakshaGeometry(geometry: SpGeometry): String { +// val geoBytes = Naksha.encodeGeometry(geometry) +// val geoBytesPlaceholder = placeholderForArg(geoBytes, PgType.BYTE_ARRAY) +// return "naksha_2d($geoBytesPlaceholder)" +// } +// +// private fun resolveTransformation( +// transformation: SpTransformation, +// basicGeometry: String +// ): String { +// return when (transformation) { +// is SpBuffer -> resolveBuffer(transformation, basicGeometry) +// else -> throw NakshaException( +// NakshaError.UNSUPPORTED_OPERATION, +// "This transformation is not yet supported: ${transformation::class.simpleName}" +// ) +// } +// } +// +// private fun resolveBuffer(buffer: SpBuffer, basicGeometry: String): String { +// val geo = if (buffer.geography) { +// "$basicGeometry::geography" +// } else { +// basicGeometry +// } +// val distancePlaceholder = placeholderForArg(buffer.distance, PgType.DOUBLE) +// val bufferStyleParams = bufferStyleParams(buffer) +// return if (bufferStyleParams != null) { +// "ST_Buffer($geo, $distancePlaceholder, $bufferStyleParams)" +// } else { +// "ST_Buffer($geo, $distancePlaceholder)" +// } +// } +// +// private fun bufferStyleParams(buffer: SpBuffer): String? { +// val bufferStyleParams = StringBuilder() +// if (buffer.quadSegments != null) { +// val quadSegPlaceholder = placeholderForArg(buffer.quadSegments, PgType.INT) +// bufferStyleParams.append("quad_segs=$quadSegPlaceholder") +// } +// if (buffer.joinStyle != null) { +// val joinStylePlaceholder = placeholderForArg(buffer.joinStyle!!.value, PgType.STRING) +// if (bufferStyleParams.isNotEmpty()) bufferStyleParams.append(" ") +// bufferStyleParams.append("join=$joinStylePlaceholder") +// } +// if (buffer.joinLimit != null) { +// val joinLimitPlaceholder = placeholderForArg(buffer.joinLimit, PgType.DOUBLE) +// if (bufferStyleParams.isNotEmpty()) bufferStyleParams.append(" ") +// bufferStyleParams.append("mitre_limit=$joinLimitPlaceholder") +// } +// if (buffer.endCap != null) { +// val endCapPlaceholder = placeholderForArg(buffer.endCap!!.value, PgType.STRING) +// if (bufferStyleParams.isNotEmpty()) bufferStyleParams.append(" ") +// bufferStyleParams.append("endcap=$endCapPlaceholder") +// } +// if (buffer.side != null) { +// val sidePlaceholder = placeholderForArg(buffer.side!!.value, PgType.STRING) +// if (bufferStyleParams.isNotEmpty()) bufferStyleParams.append(" ") +// bufferStyleParams.append("side=$sidePlaceholder") +// } +// return if (bufferStyleParams.isNotEmpty()) { +// bufferStyleParams.toString() +// } else { +// null +// } +// } +// +// private fun whereRefTiles() { +// val hereTiles = request.query.refTiles +// .filterNotNull() +// .map { HereTile(it) } +// if (hereTiles.isNotEmpty()) { +// if (where.isNotEmpty()) { +// where.append(" AND (") +// } else { +// where.append(" (") +// } +// where.append(refPointInAnyOfTiles(hereTiles)) +// where.append(")") +// } +// } +// +// private fun refPointInAnyOfTiles(hereTiles: List): String { +// return hereTiles.joinToString(separator = " OR ") { hereTile -> +// refPointInTile(hereTile) +// } +// } +// +// private fun refPointInTile(hereTile: HereTile): String { +// val lowerBoundPlaceholder = placeholderForArg( +// hereTile.maxLevelLowerBound().intKey, +// PgType.INT +// ) +// val upperBoundPlaceholder = placeholderForArg( +// hereTile.maxLevelUpperBound().intKey, +// PgType.INT +// ) +// return "(${PgColumn.here_tile} >= $lowerBoundPlaceholder AND ${PgColumn.here_tile} <= $upperBoundPlaceholder)" +// } +// +// private fun whereMetadata() { +// val metaQuery = request.query.members +// if (metaQuery != null) { +// if (where.isNotEmpty()) { +// where.append(" AND (") +// } else { +// where.append(" (") +// } +// whereNestedMetadata(metaQuery) +// where.append(")") +// } +// } +// +// private fun whereNestedMetadata(metaQuery: IMemberQuery) { +// when (metaQuery) { +// is MemberNot -> not( +// subClause = metaQuery.query, +// subClauseResolver = this::whereNestedMetadata +// ) +// +// is MemberAnd -> and( +// subClauses = metaQuery.filterNotNull(), +// subClauseResolver = this::whereNestedMetadata +// ) +// +// is MemberOr -> or( +// subClauses = metaQuery.filterNotNull(), +// subClauseResolver = this::whereNestedMetadata +// ) +// +// is MemberQuery -> { +// val isActionQuery = metaQuery.member == MetaColumn.action() +// val pgColumn = +// if (isActionQuery) { +// PgColumn.version +// } else { +// PgColumn.ofRowColumn(metaQuery.member) ?: throw NakshaException( +// NakshaError.ILLEGAL_STATE, +// "Couldn't find PgColumn for TupleColumn: ${metaQuery.member.name}" +// ) +// } +// val leftOperand = if (isActionQuery) { +// "(${PgColumn.version.name} & 3)::int4" +// } else if (pgColumn == PgColumn.created_at || pgColumn == PgColumn.author_ts) { +// "COALESCE(${pgColumn.name}, ${PgColumn.updated_at.name})" +// } else { +// pgColumn.name +// } +// // Action lives in the lower 2 bits of `version`; the comparison value is a small int. +// val placeholderType = if (isActionQuery) PgType.INT else pgColumn.type +// val resolvedQuery = when (val op = metaQuery.op) { +// is StringOp -> { +// val placeholder = placeholderForArg(metaQuery.value, placeholderType) +// resolveStringOp(op, leftOperand, placeholder) +// } +// is DoubleOp -> { +// val placeholder = placeholderForArg(metaQuery.value, placeholderType) +// resolveDoubleOp(op, leftOperand, placeholder) +// } +// AnyOp.IS_ANY_OF -> { +// val placeholder = placeholderForArg(metaQuery.value, arrayTypeFor(placeholderType)) +// "$leftOperand = ANY($placeholder)" +// } +// else -> throw illegalArg("Unknown op type: ${op::class.simpleName}") +// } +// where.append(resolvedQuery) +// } +// +// else -> throw NakshaException( +// NakshaError.ILLEGAL_ARGUMENT, +// "Unknown metadata query type: ${metaQuery::class.simpleName}" +// ) +// } +// } +// +// private fun arrayTypeFor(pgType: PgType): PgType { +// return when (pgType) { +// PgType.BOOLEAN -> PgType.BOOLEAN_ARRAY +// PgType.SHORT -> PgType.SHORT_ARRAY +// PgType.INT -> PgType.INT_ARRAY +// PgType.INT64 -> PgType.INT64_ARRAY +// PgType.FLOAT -> PgType.FLOAT_ARRAY +// PgType.DOUBLE -> PgType.DOUBLE_ARRAY +// PgType.STRING -> PgType.STRING_ARRAY +// PgType.BYTE_ARRAY -> PgType.BYTE_ARRAY_ARRAY +// else -> throw illegalArg("Unknown array type for PgType: ${pgType::class.simpleName}") +// } +// } +// +// private fun whereTags() { +// val tagQuery = request.query.tags +// if (tagQuery != null) { +// if (where.isNotEmpty()) { +// where.append(" AND (") +// } else { +// where.append(" (") +// } +// whereNestedTags(tagQuery) +// where.append(")") +// } +// } +// +// private fun whereNestedTags(tagQuery: ITagQuery) { +// when (tagQuery) { +// is TagSetContains -> resolveTagSetContains(tagQuery) +// is TagNot -> not(tagQuery.query, this::whereNestedTags) +// is TagOr -> { +// if(containsOnlyTagExists(tagQuery)){ +// // for tags without values we can utilize top-level-key based '?|' operand +// // https://www.postgresql.org/docs/current/functions-json.html#FUNCTIONS-JSONB-OP-TABLE +// val tagNames = tagQuery.filterIsInstance().map { it.name } +// resolveTagNamesArrayOperation( +// jsonbOperator = "?|", // 'jsonb_exists_any' is equivalent but will not hit the GIN index +// tagNames = tagNames +// ) +// } else { +// or(tagQuery.filterNotNull(), this::whereNestedTags) +// } +// } +// is TagAnd -> { +// if(containsOnlyTagExists(tagQuery)){ +// // for tags without values we can utilize top-level-key based '?&' operand +// // https://www.postgresql.org/docs/current/functions-json.html#FUNCTIONS-JSONB-OP-TABLE +// val tagNames = tagQuery.filterIsInstance().map { it.name } +// resolveTagNamesArrayOperation( +// jsonbOperator = "?&", // 'jsonb_exists_all' is equivalent but MIGHT not hit the GIN index +// tagNames = tagNames +// ) +// } else { +// and(tagQuery.filterNotNull(), this::whereNestedTags) +// } +// } +// is TagQuery -> resolveSingleTagQuery(tagQuery) +// } +// } +// +// private fun containsOnlyTagExists(container: ListProxy): Boolean = +// container.all { it == null || it is TagMapHasKey } +// +// /** +// * Element containment on a set-form tags column (jsonb array): `tags @> '[]'::jsonb`. +// * The `@>` operator matches the element in its type (string, boolean, number) and is supported +// * by the GIN index over the column. +// */ +// private fun resolveTagSetContains(tagQuery: TagSetContains) { +// val element = AnyList() +// element.add(tagQuery.element) +// val placeholder = placeholderForArg(toJSON(element), PgType.STRING) +// where.append("$tagsAsJsonb @> $placeholder::jsonb") +// } +// +// private fun resolveTagNamesArrayOperation(jsonbOperator: String, tagNames: List) { +// val tagKeysArray = tagNames.toTypedArray() +// val tagKeysPlaceholder = placeholderForArg(tagKeysArray, PgType.STRING_ARRAY) +// where.append("$tagsAsJsonb ?$jsonbOperator $tagKeysPlaceholder") +// } +// +// private fun resolveSingleTagQuery(tagQuery: TagQuery) { +// when (tagQuery) { +// is TagMapHasKey -> { +// val tagNamePlaceholder = placeholderForArg(tagQuery.name, PgType.STRING) +// where.append("$tagsAsJsonb ?? $tagNamePlaceholder") +// } +// +// is TagValueIsNull -> { +// val tagValuePlaceholder = placeholderForArg(selectTagValue(tagQuery), PgType.STRING) +// where.append("$tagValuePlaceholder IS NULL") +// } +// +// is TagValueIsBool -> { +// if (tagQuery.value) { +// where.append(selectTagValue(tagQuery, PgType.BOOLEAN)) +// } else { +// where.append("not(${selectTagValue(tagQuery, PgType.BOOLEAN)})") +// } +// } +// +// is TagValueIsDouble -> { +// val queryValuePlaceholder = placeholderForArg(tagQuery.value, PgType.DOUBLE) +// val doubleOp = resolveDoubleOp( +// tagQuery.op, +// selectTagValue(tagQuery, PgType.DOUBLE), +// queryValuePlaceholder +// ) +// where.append(doubleOp) +// } +// +// is TagValueIsString -> { +// val queryValuePlaceholder = placeholderForArg(tagQuery.value, PgType.STRING) +// val stringEquals = resolveStringOp( +// StringOp.EQUALS, +// selectTagValue(tagQuery, PgType.STRING), +// queryValuePlaceholder +// ) +// where.append(stringEquals) +// } +// +// is TagValueMatches -> { +// val jsonPathPlaceholder = placeholderForArg( +// "\$.${tagQuery.name} ? (@ like_regex \"${tagQuery.regex}\")", +// PgType.STRING +// ) +// where.append("$tagsAsJsonb @?? $jsonPathPlaceholder::jsonpath") +// } +// } +// } +// +// private fun selectTagValue(tagQuery: TagQuery, castTo: PgType? = null): String { +// val tagKeyPlaceholder = placeholderForArg(tagQuery.name, PgType.STRING) +// return when (castTo) { +// null -> "$tagsAsJsonb->$tagKeyPlaceholder" +// PgType.STRING -> "$tagsAsJsonb->>$tagKeyPlaceholder" +// else -> "($tagsAsJsonb->$tagKeyPlaceholder)::${castTo.value}" +// } +// } +// +// private fun not(subClause: T, subClauseResolver: (T) -> Unit) { +// where.append(" NOT (") +// subClauseResolver(subClause) +// where.append(") ") +// } +// +// private fun and(subClauses: List, subClauseResolver: (T) -> Unit) = +// multiClause("AND", subClauses, subClauseResolver) +// +// private fun or(subClauses: List, subClauseResolver: (T) -> Unit) = +// multiClause("OR", subClauses, subClauseResolver) +// +// private fun multiClause( +// operand: String, +// subClauses: List, +// subClauseResolver: (T) -> Unit +// ) { +// where.append(" (") +// subClauses.forEachIndexed { index, subClause -> +// if (index > 0) { +// where.append(" $operand ") +// } +// subClauseResolver(subClause) +// } +// where.append(") ") +// } +// +// +// private fun resolveStringOp( +// stringOp: StringOp, +// leftOperand: String, +// rightOperand: String +// ): String { +// return when (stringOp) { +// StringOp.EQUALS -> "$leftOperand = $rightOperand" +// StringOp.NOT_EQUALS -> "$leftOperand != $rightOperand" +// StringOp.STARTS_WITH -> "starts_with($leftOperand, $rightOperand)" +// else -> throw NakshaException( +// NakshaError.ILLEGAL_ARGUMENT, +// "Unknown StringOp: $stringOp" +// ) +// } +// } +// +// private fun resolveDoubleOp( +// doubleOp: DoubleOp, +// leftOperand: String, +// rightOperand: String +// ): String { +// return when (doubleOp) { +// DoubleOp.EQ -> "$leftOperand = $rightOperand" +// DoubleOp.NE -> "$leftOperand != $rightOperand" +// DoubleOp.GT -> "$leftOperand > $rightOperand" +// DoubleOp.GTE -> "$leftOperand >= $rightOperand" +// DoubleOp.LT -> "$leftOperand < $rightOperand" +// DoubleOp.LTE -> "$leftOperand <= $rightOperand" +// else -> throw NakshaException( +// NakshaError.ILLEGAL_ARGUMENT, +// "Unknown DoubleOp: $doubleOp" +// ) +// } +// } } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRows.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRows.kt index 97413ff50..fc510318c 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRows.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRows.kt @@ -46,10 +46,10 @@ internal class PgRows { } /** - * Ensures that in all columns have at least this amount of values, if too short, adds `null` values until the minimal size is reached. + * Ensures that all columns have at least this amount of values, if too short, adds `null` values until the minimal size is reached. * @since 3.0 */ - fun withMinRows(rowsCount: Int): PgRows { + fun setMinRows(rowsCount: Int): PgRows { if (this.size < rowsCount) { this.size = rowsCount for (column in columns) { @@ -150,7 +150,7 @@ internal class PgRows { if (existing == null) { val column = PgColumnWithValues(column, alias).withSize(size) columns.add(column) - withMinRows(size) + setMinRows(size) } return this } @@ -160,7 +160,7 @@ internal class PgRows { if (existing == null) { val column = PgColumnWithValues(PgColumn(-1, alias, type)).withSize(size) columns.add(column) - withMinRows(size) + setMinRows(size) } return this } @@ -175,11 +175,17 @@ internal class PgRows { fun getAny(row: Int, alias: String): Any? = getColumn(alias)?.values?.get(row) + fun getAny(row: Int, column: PgColumn): Any? = getAny(row, column.name) fun getInt(row: Int, alias: String): Int? = getAny(row, alias) as? Int + fun getInt(row: Int, column: PgColumn): Int? = getInt(row, column.name) fun getInt64(row: Int, alias: String): Int64? = getAny(row, alias) as Int64? + fun getInt64(row: Int, column: PgColumn): Int64? = getInt64(row, column.name) fun getDouble(row: Int, alias: String): Double? = getAny(row, alias) as Double? + fun getDouble(row: Int, column: PgColumn): Double? = getDouble(row, column.name) fun getString(row: Int, alias: String): String? = getAny(row, alias) as String? + fun getString(row: Int, column: PgColumn): String? = getString(row, column.name) fun getByteArray(row: Int, alias: String): ByteArray? = getAny(row, alias) as ByteArray? + fun getByteArray(row: Int, column: PgColumn): ByteArray? = getByteArray(row, column.name) fun getSpatial(row: Int, alias: String): SpGeometry? { val raw = getByteArray(row, alias) ?: return null return try { @@ -259,7 +265,7 @@ internal class PgRows { fun set(row: Int, columnName: String, value: Any?): Boolean { val column = getColumn(columnName) if (column != null) { - withMinRows(row) + setMinRows(row) column.values[row] = value return true } @@ -267,7 +273,7 @@ internal class PgRows { } operator fun set(row: Int, tuple: Tuple) { - withMinRows(row) + setMinRows(row) val membersBook = tuple.membersBook val END = membersBook.namesLength() for (i in 0 until END) { @@ -288,7 +294,7 @@ internal class PgRows { */ operator fun set(row: Int, cursor: PgCursor) { if (!cursor.isRow()) return - withMinRows(row) + setMinRows(row) val columnNames = cursor.columnNames() for (columnName in columnNames) { val column = getColumn(columnName) ?: continue diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWrite.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWrite.kt index 21b9b3085..83cabd455 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWrite.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWrite.kt @@ -22,7 +22,7 @@ internal data class PgWrite(val original: Write, val i: Int) { * - If a collection is modified, this is the map in which [Naksha.COLLECTIONS_COL][naksha.model.Naksha.COLLECTIONS_COL_ID] is located, [asPgCollection] and [asNakshaCollection] will be set. * @since 3.0 */ - lateinit var map: PgCatalog + lateinit var catalog: PgCatalog /** * The collection into which to write. @@ -113,7 +113,7 @@ internal data class PgWrite(val original: Write, val i: Int) { val isCollectionModification: Boolean get() = original.isCollectionModification() val isTransactionModification: Boolean - get() = Naksha.ADMIN_CATALOG_ID == map.id && Naksha.TRANSACTIONS_COL_ID == collection.id + get() = Naksha.ADMIN_CATALOG_ID == catalog.id && Naksha.TRANSACTIONS_COL_ID == collection.id // This variant differs from write.isFeatureModification, because it includes dictionaries, which are just features for us! val isFeatureModification: Boolean get() = !isTransactionModification && !isCatalogModification && !isCollectionModification diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt index 28146899b..e0f6e6ffd 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt @@ -4,15 +4,9 @@ package naksha.psql import naksha.base.PlatformUtil import naksha.model.* -import naksha.model.objects.IndexList -import naksha.model.objects.IndexType -import naksha.model.objects.Member -import naksha.model.objects.MemberList -import naksha.model.objects.MemberType import naksha.model.objects.NakshaCollection import naksha.model.objects.NakshaCatalog import naksha.model.objects.NakshaTx -import naksha.model.objects.StandardMembers import naksha.model.request.* import kotlin.js.JsExport import kotlin.jvm.JvmField @@ -75,153 +69,73 @@ open class PgWriter internal constructor( return SuccessResponse().withTupleNumberList(tupleNumberList) } - /** - * Add the given write into an ordered array-list, order by `id` ascending. - * @since 3.0 - */ - private fun addSorted(arrayList: ArrayList, value: PgWrite) { - val index = arrayList.binarySearch { it.id.compareTo(value.id) } - val insertIndex = if (index < 0) -index - 1 else index - arrayList.add(insertIndex, value) - } - - /** - * Groups and orders writes by map, collection, partition, and eventually operation. - * - * @param map the map to write into. - * @param collection the collection to write into. - * @param writeOp the operation to perform, so `INSERT`, `UPSERT`, `UPDATE`, `DELETE` or `PURGE`. - * @param write the write instruction. - * @param byMap the map into which to store the write instruction. - * @param writeListCapacity the capacity of the write array, when a new one need to be allocated. - * @since 3.0 - */ - private fun addPgWrite( - map: PgCatalog, - collection: PgCollection, - writeOp: WriteOp, - write: PgWrite, - byMap: MutableMap>?>>>, - writeListCapacity: Int) { - var byCollection = byMap[map] - if (byCollection == null) { - byCollection = mutableMapOf() - byMap[map] = byCollection - } - var byPartition = byCollection[collection] - if (byPartition == null) { - byPartition = arrayOfNulls(if (collection.partitions > 1) collection.partitions + 1 else 1) - byCollection[collection] = byPartition - } - val partition = write.partition - // Note: byPartition reserved the first entry (index #0) for the case that there is no partitioning, - // therefore when there is no partitioning, the index becomes `0`, otherwise `1` to `n`. - var byWriteOp = byPartition[partition + 1] - if (byWriteOp == null) { - byWriteOp = mutableMapOf() - byPartition[partition + 1] = byWriteOp - } - var writeList = byWriteOp[writeOp] - if (writeList == null) { - writeList = ArrayList(writeListCapacity) - byWriteOp[writeOp] = writeList - } - addSorted(writeList, write) - } - - /** - * Group the writes by map, then by collection, and finally by partition with `-1` as partition number, when the table is not partitioned. - * @param writes the writes that should be done. - * @return a map by map, collection, and partition to write operations executed within. - * @since 3.0 - */ - private fun groupOperations(writes: ArrayList) - : MutableMap>?>>> { - val byMap = mutableMapOf>?>>>() - var writeListCapacity = writes.size - for (i in writes.indices) { - val write = writes[i] - val map = write.map - val collection = write.collection - val op = write.original.op - when (op) { - WriteOp.CREATE -> { - val f = write.feature ?: throw illegalArg("The feature #${write.i} is null") - // In a CREATE case, UNDEFINED means the same as `null` - val tuple = Tuple.encodeFeature(f, collection.head, Action.CREATE, session, null) - write.tuple = tuple - val tupleNumber = tuple.tupleNumber - write.tupleNumber = tupleNumber - } - WriteOp.UPSERT -> { - // Note: We first try an INSERT, then, when that fails, we do an on-conflict UPDATE! - val f = write.feature ?: throw illegalArg("The feature #${write.i} is null") - val tuple = Tuple.encodeFeature(f, collection.head, Action.CREATE, session, null) - write.tuple = tuple - val tupleNumber = tuple.tupleNumber - write.tupleNumber = tupleNumber - } - WriteOp.UPDATE -> { - val f = write.feature ?: throw illegalArg("The feature #${write.i} is null") - val tuple = Tuple.encodeFeature(f, collection.head, Action.UPDATE, session, null) - write.tuple = tuple - val tupleNumber = tuple.tupleNumber - write.tupleNumber = tupleNumber - } - WriteOp.DELETE -> { - if (write.isTransactionModification) throw forbidden("Transactions must not be deleted or purged") - } - WriteOp.PURGE -> { - if (write.isTransactionModification) throw forbidden("Transactions must not be deleted or purged") - // Note: purge and delete are the same operation, except that a purge is not copied into deleted table! - } - else -> { - throw illegalArg("Unknown write operation: $op") - } - } - // In the first run, there is a chance that all writes go into same partition, after that, we can have one less! - addPgWrite(map, collection, op, write, byMap, writeListCapacity--) - } - // Sort all writes within the - return byMap - } - /** * Performs the given writes. * @param writes the writes to perform. * @return the tuple-numbers of the */ private fun executeWrites(writes: MutableList) : TupleNumberList { + if (writes.isEmpty()) return TupleNumberList() // Add the input-index. - val targetWrites = ArrayList(writes.size) - for (i in 0 ..< writes.size) targetWrites.add(PgWrite(writes[i], i)) + val pgWrites = ArrayList(writes.size) + for (i in 0 ..< writes.size) pgWrites.add(PgWrite(writes[i], i)) val savepointId = PlatformUtil.randomString() var conn: PgConnection? = null try { // This can be time-consuming, unless the connection is already open, try not to open it before we do this! - prepareWrite(targetWrites) - val byMap = groupOperations(targetWrites) + prepareWrite(pgWrites) + + // Order by: + // - catalog-number ASC + // - collection-number ASC + // - partition-number ASC + // - PURGE, DELETE, CREATE, UPSERT, UPDATE + // - feature-number ASC. + // + // The ordering is very important, because otherwise there can be deadlocks in the database at row-level locking! + pgWrites.sortWith { a, b -> + val catalogDiff = a.catalog.catalogNumber - b.catalog.catalogNumber + if (catalogDiff != 0) return@sortWith catalogDiff + val collectionDiff = a.collection.collectionNumber - b.collection.collectionNumber + if (collectionDiff != 0) return@sortWith collectionDiff + val partitionDiff = a.partitionNumber - b.partitionNumber + if (partitionDiff != 0) return@sortWith partitionDiff + val opDiff = a.op.order - b.op.order + if (opDiff != 0) return@sortWith opDiff + val featureNumberDiff = a.featureNumber - b.featureNumber + if (featureNumberDiff < 0) return@sortWith -1 + if (featureNumberDiff > 0) return@sortWith 1 + 0 + } // Perform the writes, if any error happens, we will roll back the session to where it was before we started. // Note: We must not close the connection, therefore no `session.useConnection().use {}`! conn = this.conn if (useSavepoint) conn.execute("SAVEPOINT \"$savepointId\"").close() - for (mapEntry in byMap) { - val map = mapEntry.key - val byCol = mapEntry.value - for (colEntry in byCol) { - val collection = colEntry.key - val byPartition = colEntry.value - for (i in byPartition.indices) { - val byWriteOp = byPartition[i] - if (byWriteOp != null) { - val partition = i - 1 // index #0 represents no partitioning - executeWrite(map, collection, partition, byWriteOp) - } - } + var start = 1 + var startTupleNumber: TupleNumber = pgWrites.first().tupleNumber ?: throw illegalState("PgWrite[0] without tuple-number") + val LAST = pgWrites.size - 1 + for (i in 1.. LAST) { + val pgWrite = pgWrites[i] + val tupleNumber = pgWrite.tupleNumber ?: throw illegalState("PgWrite[$i] without tuple-number") + if (start == i) { + startTupleNumber = tupleNumber + continue + } + if (i == LAST || + startTupleNumber.catalogNumber != tupleNumber.catalogNumber || + startTupleNumber.collectionNumber != tupleNumber.collectionNumber) + { + // Either `i` is a different catalog/collection, then write what we have. + // Or we are at the last write, then write as well what we have. + // Note: We need to split by collection, because every collection has its own columns! + executeWrite(pgWrites, start, i) + // Continue from where we ended, if this was LAST. + start = i + startTupleNumber = tupleNumber } } - // If everything worked out as expected, we can drop the savepoint. + // If everything worked out as expected, we can drop the savepoint, if there is any. if (useSavepoint) conn.execute("RELEASE SAVEPOINT \"$savepointId\"").close() } catch (t: Throwable) { if (conn != null && useSavepoint) conn.execute("ROLLBACK TO SAVEPOINT \"$savepointId\"").close() @@ -234,12 +148,12 @@ open class PgWriter internal constructor( val tupleList = ArrayList(writes.size) val transaction = tx.nakshaTx var featuresModified = 0 - for (write in targetWrites) { + for (write in pgWrites) { val tupleNumber = write.tupleNumber tupleNumbers[write.i] = tupleNumber val tuple = write.tuple if (write.isFeatureModification) { - val map = write.map + val map = write.catalog val col = write.collection val txCol = transaction.useCatalog(map.id, map.catalogNumber).useCollection(col.id, col.collectionNumber) if (tupleNumber != null) { @@ -250,7 +164,7 @@ open class PgWriter internal constructor( val map = write.asPgCatalog if (map != null) transaction.useCatalog(map.id, map.catalogNumber, write.action) } else if (write.isCollectionModification) { - val map = write.map + val map = write.catalog val col = write.asPgCollection if (col != null) { transaction.useCatalog(map.id, map.catalogNumber).useCollection(col.id, col.collectionNumber, write.action) @@ -269,88 +183,118 @@ open class PgWriter internal constructor( // Ensure that the needed physical schema and tables are created. // Ensure that the PgTupleWrite data class is ready for action. // After this has run, only the `tuple` is missing! - private fun prepareWrite(writes: ArrayList) { - for (write in writes) { - val featureId = write.original.id + private fun prepareWrite(pgWrites: ArrayList) { + for (pgWrite in pgWrites) { + val featureId = pgWrite.id + val op = pgWrite.op // Detect tbe map into which to write. - val mapId = write.original.mapId ?: throw illegalArg("The given write does not have a map-id") - val map = storage.adminCatalog.getPgCatalogById(conn, mapId) ?: - throw mapNotFound("The write #${write.i} refers to not existing map '$mapId'") - write.map = map + val catalogId = pgWrite.original.catalogId ?: throw illegalArg("The given write does not have a catalog-id") + val pgCatalog = storage.adminCatalog.getPgCatalogById(conn, catalogId) + ?: throw mapNotFound("The write #${pgWrite.i} refers to not existing map '$catalogId'") + pgWrite.catalog = pgCatalog // Detect the collection into which to write. - val colId = write.original.collectionId ?: throw illegalArg("The given write does not have a collection-id") - val collection = map.getPgCollectionById(conn, colId) ?: - throw collectionNotFound("The write #${write.i} refers to not existing collection '$colId'") - write.collection = collection + val collectionId = pgWrite.original.collectionId ?: throw illegalArg("The given write does not have a collection-id") + val pgCollection = pgCatalog.getPgCollectionById(conn, collectionId) + ?: throw collectionNotFound("The write #${pgWrite.i} refers to not existing collection '$collectionId'") + pgWrite.collection = pgCollection // If this operation modifies a catalog. - if (write.isCatalogModification) { - val op = write.op - var pgCatalog = storage.adminCatalog.getPgCatalogById(null, write.id) ?: storage.adminCatalog.getPgCatalogById(conn, write.id) + if (pgWrite.isCatalogModification) { + var targetCatalog = storage.adminCatalog.getPgCatalogById(null, pgWrite.id) + ?: storage.adminCatalog.getPgCatalogById(conn, pgWrite.id) val nakshaMap: NakshaCatalog? if (op == WriteOp.CREATE || op == WriteOp.UPSERT || op == WriteOp.UPDATE) { - val feature = write.feature ?: throw illegalArg("The write #${write.i} is $op, but the feature is null") - nakshaMap = if (feature is NakshaCatalog) feature else feature.proxy(NakshaCatalog::class) + val feature = pgWrite.feature ?: throw illegalArg("The write #${pgWrite.i} is $op, but the feature is null") + nakshaMap = feature as? NakshaCatalog ?: feature.proxy(NakshaCatalog::class) nakshaMap.databaseId = storage.id - if (pgCatalog == null) { + if (targetCatalog == null) { if (op == WriteOp.UPDATE) { - throw mapNotFound("The UPDATE (write #${write.i}) failed, because the map '$featureId' does not exist") + throw mapNotFound("The UPDATE (write #${pgWrite.i}) failed, because the map '$featureId' does not exist") } - pgCatalog = PgCatalog(storage, nakshaMap) - createPgCatalog(pgCatalog) + targetCatalog = PgCatalog(storage, nakshaMap) + createPgCatalog(targetCatalog) } else if (op == WriteOp.CREATE) { - throw mapExists("The write #${write.i} failed, because the map '$featureId' does exist already") + throw mapExists("The write #${pgWrite.i} failed, because the map '$featureId' does exist already") } } else if (op == WriteOp.DELETE || op == WriteOp.PURGE) { - if (pgCatalog != null) { - deletePgMap(pgCatalog) + if (targetCatalog != null) { + deletePgMap(targetCatalog) } nakshaMap = null } else { - throw illegalState("The write #${write.i} refers to an unsupported operation: '$op'") + throw illegalState("The write #${pgWrite.i} refers to an unsupported operation: '$op'") } - write.asPgCatalog = pgCatalog - write.asNakshaMap = nakshaMap + pgWrite.asPgCatalog = targetCatalog + pgWrite.asNakshaMap = nakshaMap } // If this operation modifies a collection. - if (write.isCollectionModification) { - val op = write.op - var pgCollection = map.getPgCollectionById(null, write.id) ?: map.getPgCollectionById(conn, write.id) + if (pgWrite.isCollectionModification) { + var targetCollection = pgCatalog.getPgCollectionById(null, pgWrite.id) ?: pgCatalog.getPgCollectionById(conn, pgWrite.id) val nakshaCollection: NakshaCollection? if (op == WriteOp.CREATE || op == WriteOp.UPSERT || op == WriteOp.UPDATE) { - val feature = write.feature ?: throw illegalArg("The write #${write.i} is $op, but the feature is null") + val feature = pgWrite.feature ?: throw illegalArg("The write #${pgWrite.i} is $op, but the feature is null") nakshaCollection = feature as? NakshaCollection ?: feature.proxy(NakshaCollection::class) - if (pgCollection == null) { + if (targetCollection == null) { if (op == WriteOp.UPDATE) { throw collectionNotFound( - "The UPDATE (write #${write.i}) failed, because the collection '$featureId' does not exist in map '$mapId'" + "The UPDATE (write #${pgWrite.i}) failed, because the collection '$featureId' does not exist in map '$catalogId'" ) } - pgCollection = PgCollection(map, nakshaCollection) - createPgCollection(pgCollection) + targetCollection = PgCollection(pgCatalog, nakshaCollection) + createPgCollection(targetCollection) } else if (op == WriteOp.CREATE) { throw collectionExists( - "The write #${write.i} failed, because the collection '$featureId' does exist already in map '$mapId'" + "The write #${pgWrite.i} failed, because the collection '$featureId' does exist already in map '$catalogId'" ) } else { // UPSERT or UPDATE on an existing collection: Ensure that no invalid changes are asked for. - pgCollection.verifyNewHeadState(nakshaCollection) + targetCollection.verifyNewHeadState(nakshaCollection) } } else if (op == WriteOp.DELETE || op == WriteOp.PURGE) { - if (pgCollection != null) { - deletePgCollection(pgCollection) + if (targetCollection != null) { + deletePgCollection(targetCollection) } nakshaCollection = null } else { - throw illegalState("The write #${write.i} refers to an unsupported operation: '$op'") + throw illegalState("The write #${pgWrite.i} refers to an unsupported operation: '$op'") + } + pgWrite.asPgCollection = targetCollection + pgWrite.asNakshaCollection = nakshaCollection + } + + when (op) { + WriteOp.CREATE -> { + val f = pgWrite.feature ?: throw illegalArg("The feature #${pgWrite.i} is null") + // In a CREATE case, UNDEFINED means the same as `null` + val tuple = Tuple.encodeFeature(f, pgCollection.head, Action.CREATE, session, null) + pgWrite.tuple = tuple + pgWrite.tupleNumber = tuple.tupleNumber + } + WriteOp.UPSERT -> { + // Note: We first try an INSERT, then, when that fails, we do an on-conflict UPDATE! + val f = pgWrite.feature ?: throw illegalArg("The feature #${pgWrite.i} is null") + val tuple = Tuple.encodeFeature(f, pgCollection.head, Action.CREATE, session, null) + pgWrite.tuple = tuple + pgWrite.tupleNumber = tuple.tupleNumber + } + WriteOp.UPDATE -> { + val f = pgWrite.feature ?: throw illegalArg("The feature #${pgWrite.i} is null") + val tuple = Tuple.encodeFeature(f, pgCollection.head, Action.UPDATE, session, null) + pgWrite.tuple = tuple + pgWrite.tupleNumber = tuple.tupleNumber + } + WriteOp.DELETE, WriteOp.PURGE -> { + // TODO: For DELETE we want to support new states, so providing a feature! + if (pgWrite.isTransactionModification) throw forbidden("Transactions must not be deleted or purged") + } + else -> { + throw illegalArg("Unknown write operation: $op") } - write.asPgCollection = pgCollection - write.asNakshaCollection = nakshaCollection } } } @@ -364,41 +308,93 @@ open class PgWriter internal constructor( return list } - private fun executeWrite(map: PgCatalog, collection: PgCollection, partition: Int, byWriteOp: Map>) { - // DELETE - val deletes = byWriteOp[WriteOp.DELETE] - if (deletes != null) { - val tupleWriter = PgWriterDelete(this, collection, partition, deletes) + /** + * Execute all writes between `start` _(inclusive)_ and `end` _(exclusive)_. + * @param pgWrites the list of ordered writes. + * @param start the index in the list of the first element to execute. + * @param end the index in the list of the first element **NOT** to execute _(excluded)_. + */ + private fun executeWrite(pgWrites: ArrayList, start: Int, end: Int) { + if (start == end) return + // We expect that all writes go into the same catalog and collection. + val first = pgWrites[start] + val pgCatalog = first.catalog + val pgCollection = first.collection + var s = start + var e = start + + // -------------------------------- DELETE ------------------------------------------------------------------------ + while (e < end) { + val pgWrite = pgWrites[e] + check(pgWrite.catalog.catalogNumber != pgCatalog.catalogNumber) + check(pgWrite.collection.collectionNumber != pgCollection.collectionNumber) + if (pgWrite.op != WriteOp.DELETE) break + e++ + } + if (e > s) { + val tupleWriter = PgWriterDelete(this, pgCollection, pgWrites, s, e, purge = false) tupleWriter.execute(conn) + s = e } - // PURGE - val purges = byWriteOp[WriteOp.PURGE] - if (purges != null) { - val tupleWriter = PgWriterDelete(this, collection, partition, purges, purge = true) + // -------------------------------- PURGE ------------------------------------------------------------------------ + while (e < end) { + val pgWrite = pgWrites[e] + check(pgWrite.catalog.catalogNumber != pgCatalog.catalogNumber) + check(pgWrite.collection.collectionNumber != pgCollection.collectionNumber) + if (pgWrite.op != WriteOp.PURGE) break + e++ + } + // + if (e > s) { + val tupleWriter = PgWriterDelete(this, pgCollection, partition, purges, purge = true) tupleWriter.execute(conn) + e = s } - // INSERT - val inserts = byWriteOp[WriteOp.CREATE] - if (inserts != null) { - val tupleWriter = PgWriterInsert(this, collection, partition, inserts) + // -------------------------------- CREATE ------------------------------------------------------------------------ + while (e < end) { + val pgWrite = pgWrites[e] + check(pgWrite.catalog.catalogNumber != pgCatalog.catalogNumber) + check(pgWrite.collection.collectionNumber != pgCollection.collectionNumber) + if (pgWrite.op != WriteOp.CREATE) break + e++ + } + if (e > s) { + val tupleWriter = PgWriterInsert(this, pgCollection, partition, inserts) tupleWriter.execute(conn) + e = s } - // UPSERT - val upserts = byWriteOp[WriteOp.UPSERT] - if (upserts != null) { - val tupleWriter = PgWriterUpsert(this, collection, partition, upserts) + // -------------------------------- UPSERT ------------------------------------------------------------------------ + while (e < end) { + val pgWrite = pgWrites[e] + check(pgWrite.catalog.catalogNumber != pgCatalog.catalogNumber) + check(pgWrite.collection.collectionNumber != pgCollection.collectionNumber) + if (pgWrite.op != WriteOp.UPSERT) break + e++ + } + if (e > s) { + val tupleWriter = PgWriterUpsert(this, pgCollection, partition, upserts) tupleWriter.execute(conn) + e = s } - // UPDATE - val updates = byWriteOp[WriteOp.UPDATE] - if (updates != null) { - val tupleWriter = PgWriterUpdate(this, collection, partition, updates) + // -------------------------------- UPDATE ------------------------------------------------------------------------ + while (e < end) { + val pgWrite = pgWrites[e] + check(pgWrite.catalog.catalogNumber != pgCatalog.catalogNumber) + check(pgWrite.collection.collectionNumber != pgCollection.collectionNumber) + if (pgWrite.op != WriteOp.UPDATE) break + e++ + } + if (e > s) { + val tupleWriter = PgWriterUpdate(this, pgCollection, partition, updates) tupleWriter.execute(conn) + e = s } + + if (e != s) throw illegalState("We missed some writes beyond $s in the ordered write-operation list") } /** diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterBase.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterBase.kt index ed3098c65..ec53835bc 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterBase.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterBase.kt @@ -3,7 +3,6 @@ package naksha.psql import naksha.base.Int64 import naksha.base.IntMutable import naksha.model.Version -import naksha.model.illegalState import naksha.model.objects.NakshaTx /** @@ -20,37 +19,41 @@ internal abstract class PgWriterBase protected constructor( * The [writer][PgWriter] to which this write is bound. * @since 3.0 */ - val writer: PgWriter, + val pgWriter: PgWriter, /** * The collection to operate upon. * @since 3.0 */ - val collection: PgCollection, + val pgCollection: PgCollection, /** - * The partition to write into, `-1` if writes should enter base table. + * The list of writes to perform. * @since 3.0 */ - val partition: Int, + val pgWrites: List, /** - * The list of writes to perform. - * @since 3.0 + * The index of first [PgWrite] from the [pgWrites] list to process. + */ + val start: Int, + + /** + * The index of first [PgWrite] from the [pgWrites] list to **NOT** process. */ - val writes: List, + val end: Int, ) { val session: PgSession - get() = writer.session + get() = pgWriter.session val storageNumber: Int64 - get() = collection.storage.number + get() = pgCollection.storage.number val catalogNumber: Int - get() = collection.catalog.catalogNumber + get() = pgCollection.catalog.catalogNumber val collectionNumber: Int - get() = collection.collectionNumber + get() = pgCollection.collectionNumber /** * The transaction to operate upon. @@ -76,7 +79,6 @@ internal abstract class PgWriterBase protected constructor( .withDatabaseNumber(storageNumber) .withCatalogNumber(catalogNumber) .withCollectionNumber(collectionNumber) - .withMinRows(writes.size) /** * Generates a live mapping between the write instructions and the partition-index into which they will write. @@ -87,10 +89,10 @@ internal abstract class PgWriterBase protected constructor( */ val featureCountByPartition: Map get() { - val partitions = collection.head.partitions + val partitions = pgCollection.partitions val partIndices = mutableMapOf() - for (i in writes.indices) { - val write = writes[i] + for (i in 0 ..< pgWrites.size) { + val write = pgWrites[i] val partIndex = write.tupleNumber?.partitionIndex(partitions) ?: -1 val existing = partIndices[partIndex] if (existing != null) existing.plus(1) else partIndices[partIndex] = IntMutable(1) @@ -112,60 +114,18 @@ internal abstract class PgWriterBase protected constructor( */ val featureCountByPartitionJoined: String get() { - val partitions = collection.head.partitions - return if (partitions <= 1) "-1: ${writes.size}" + val partitions = pgCollection.head.partitions + return if (partitions <= 1) "-1: ${pgWrites.size}" else featureCountByPartition.entries.joinToString(", ") { "${it.key}=${it.value.value}" } } - /** - * If this write should be done into a partition. - */ - val writeIntoPartition: Boolean = partition >= 0 - - /** - * The year when the transaction started, for transactions and history writes. - */ - val year: Int = tx.version.year - - private fun initHeadTable(): PgTable { - if (writeIntoPartition) { - return collection.headTable.partitions[partition] - } - return collection.headTable - } - - /** - * The head table to write into. - */ - val headTable: PgTable = initHeadTable() - - private fun initHistoryTable(): PgTable? { - // TODO: - tx.version - val hst = collection.historyTable ?: return null - var yearTable: PgHistoryYear? = hst.years[year] - if (yearTable == null) { - hst.addPartition(year) - yearTable = hst.years[year] - if (yearTable == null) { - throw illegalState("Internal error, failed to add history year $year") - } - } - return if (writeIntoPartition) yearTable.partitions[partition] else yearTable - } - - /** - * The history table to write into, if any. - */ - val historyTable: PgTable? = initHistoryTable() - /** * Execute the operation. * @param conn the connection to be used. * @since 3.0 */ fun execute(conn: PgConnection) { - collection.catalog.setSearchPath(conn) + pgCollection.catalog.setSearchPath(conn) return doExecute(conn) } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterDelete.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterDelete.kt index 2bb1ed76a..0ad132349 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterDelete.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterDelete.kt @@ -5,6 +5,11 @@ import naksha.base.Platform.PlatformCompanion.logger import naksha.base.PlatformUtil import naksha.model.* import naksha.model.objects.StoreMode +import naksha.model.objects.MemberType +import naksha.model.objects.StandardMembers +import naksha.psql.PgColumn.PgColumn_C.FN +import naksha.psql.PgColumn.PgColumn_C.NEXT_VERSION +import naksha.psql.PgColumn.PgColumn_C.VERSION /** * Execute a [DELETE][naksha.model.request.WriteOp.DELETE] or PURGE. @@ -24,118 +29,127 @@ import naksha.model.objects.StoreMode * @since 3.0 * @see [PgWriter] */ -internal class PgWriterDelete(writer: PgWriter, collection: PgCollection, partition: Int, writes: List, val purge: Boolean = false) - : PgWriterBase(writer, collection, partition, writes) -{ +internal class PgWriterDelete( + pgWriter: PgWriter, + pgCollection: PgCollection, + pgWrites: List, + start: Int, + end: Int, + val purge: Boolean = false +) : PgWriterBase(pgWriter, pgCollection, pgWrites, start, end) { init { - inRows.addColumn("id", PgType.STRING) - inRows.addColumn("expected_version", PgType.INT64) - for (e in writes.withIndex()) { - val row = e.index - val write = e.value - inRows.set(row, "id", write.id) - inRows.set(row, "expected_version", write.version?.number) + inRows.addColumn("id", MemberType.STRING) + inRows.addColumn("expected_version", MemberType.INT64) + var row = 0 + for (i in start until end) { + val pgWrite = pgWrites[i] + inRows.set(row, "id", pgWrite.id) + inRows.set(row, "expected_version", pgWrite.version?.number) + row++ } + check(row == (end-start)) } + val headTable = pgCollection.headTable + val historyTable = if (pgCollection.storeHistory) pgCollection.historyTable else null + val ID: PgColumn = pgCollection.column(StandardMembers.Id) ?: throw illegalState("The collection does not have an 'id' column.") + val CC: PgColumn? = pgCollection.column(StandardMembers.ChangeCount) + private fun plan(conn: PgConnection, collection: PgCollection): PgWriterPlan { - // We do not insert into history, if the table does not exist, or is disabled - val insert_into_history = if (historyTable != null && collection.head.storeHistory == StoreMode.ON) historyTable else null // The new version with action bits set to DELETED (2). val deleted_version = "(${tx.version.number}::int8 | 2)" // All input provided by client, `id` and optionally `expected_version` val query = """WITH query AS ( - SELECT * FROM UNNEST($1, $2) AS t(id, expected_version) + SELECT * FROM UNNEST($1, $2) AS t($ID, expected_version) )""" // Select id and version of all rows matching query.id — used for conflict detection. val head_select = """, head_select AS ( - SELECT head.id AS id, head.fn AS fn, head.version AS version + SELECT head.$ID AS $ID, head.$FN AS $FN, head.$VERSION AS $VERSION FROM ${headTable.quotedName} AS head, query - WHERE head.id = query.id + WHERE head.$ID = query.$ID )""" - val effHead = collection.effectiveHeadColumns - val effCopyHistory = collection.effectiveCopyIntoHistoryColumns - val hasCc = PgColumn.cc in effHead - // Select the HEAD rows to act on: // - Optional atomic version check (expected_version). // - Skip rows already deleted ((version & 3) >= 2) — idempotent. val head_row = """, head_row AS ( - SELECT ${effHead.joinToString(", ") { "head.${it.name} AS ${it.name}" }} + SELECT ${pgCollection.columns.joinToString(", ") { column -> "head.$column AS $column" }} FROM ${headTable.quotedName} AS head, query - WHERE head.id = query.id - AND (query.expected_version IS NULL OR (query.expected_version & -4) = (head.version & -4)) - AND (head.version & 3) < 2 + WHERE head.$ID = query.$ID + AND (query.expected_version IS NULL OR (query.expected_version & -4) = (head.$VERSION & -4)) + AND (head.$VERSION & 3) < 2 )""" // Archive the current HEAD row into history (identical to how UPDATE does it). - // next_version = the new deleted version, signalling "succeeded by a deletion". - val head_to_history = if (insert_into_history != null) """, head_to_history AS ( - INSERT INTO ${insert_into_history.quotedName} (${PgColumn.next_version}, ${effCopyHistory.joinToString(",") { it.name }}) - SELECT $deleted_version AS ${PgColumn.next_version}, - ${effCopyHistory.joinToString(", ") { "head_row.${it.name} AS ${it.name}" }} + // next_version = the new deleted version, signaling "succeeded by a deletion". + val head_to_history = if (historyTable != null) """, head_to_history AS ( + INSERT INTO ${historyTable.quotedName} ($NEXT_VERSION, ${pgCollection.joinColumns { if (it.name != NEXT_VERSION.name) it.ident else null }}) + SELECT $deleted_version AS $NEXT_VERSION, + ${pgCollection.joinColumns { column -> if (column eq NEXT_VERSION) null else "head_row.$column AS $column" }} FROM head_row - RETURNING id, fn, version + RETURNING $ID, $FN, $VERSION )""" else "" // For DELETE: UPDATE version (action bits = DELETED) and cc in HEAD. // Only these two control-columns change; all data columns remain identical. val head_updated = if (!purge) """, head_updated AS ( UPDATE ${headTable.quotedName} - SET - ${PgColumn.version.name} = $deleted_version${if (hasCc) ",\n ${PgColumn.cc.name} = ${headTable.quotedName}.${PgColumn.cc.name} + 1" else ""} + SET $VERSION = $deleted_version + ${if (CC!=null) ", $CC = ${headTable.quotedName}.$CC + 1" else ""} FROM head_row - WHERE ${headTable.quotedName}.fn = head_row.fn - RETURNING ${headTable.quotedName}.id, ${headTable.quotedName}.fn, ${headTable.quotedName}.version${if (hasCc) ",\n ${headTable.quotedName}.${PgColumn.cc.name}" else ""} + WHERE ${headTable.quotedName}.$FN = head_row.$FN + RETURNING ${headTable.quotedName}.$ID + , ${headTable.quotedName}.$FN + , ${headTable.quotedName}.$VERSION +${if (CC!=null) " , ${headTable.quotedName}.$CC" else ""} )""" else "" // For PURGE: DELETE the HEAD row entirely. val head_deleted = if (purge) """, head_deleted AS ( DELETE FROM ${headTable.quotedName} - WHERE (fn, version) IN (SELECT fn, version FROM head_row) - RETURNING id, fn, version + WHERE ($FN, $VERSION) IN (SELECT $FN, $VERSION FROM head_row) + RETURNING $ID, $FN, $VERSION )""" else "" // For PURGE only: also write a tombstone record into history to explicitly mark // end-of-lifetime. The tombstone's next_version == version (closed interval). - val history_tombstone = if (purge && insert_into_history != null) """, history_tombstone AS ( - INSERT INTO ${insert_into_history.quotedName} - (${if (hasCc) "${PgColumn.cc}, " else ""}${PgColumn.fn}, ${PgColumn.version}, ${PgColumn.next_version}, ${PgColumn.base_tn}, ${PgColumn.tombstoneColumns.joinToString(", ")}) - SELECT ${if (hasCc) "head_row.cc, " else ""}head_row.fn, $deleted_version, $deleted_version, null::bytea, - ${PgColumn.tombstoneColumns.joinToString(", ") { "head_row.${it.name} AS ${it.name}" }} + val history_tombstone = if (purge && historyTable != null) """, history_tombstone AS ( + INSERT INTO ${historyTable.quotedName} + ($VERSION, $NEXT_VERSION, + ${pgCollection.joinColumns { if (VERSION eq it || NEXT_VERSION eq it) null else it.ident }}) + SELECT $deleted_version AS $VERSION, $deleted_version AS $NEXT_VERSION, + ${pgCollection.joinColumns { column -> if (VERSION eq column || NEXT_VERSION eq column) null else "head_row.$column AS $column" }} FROM head_row - RETURNING id, fn, version + RETURNING $ID, $FN, $VERSION )""" else "" // The returned row for DELETE is the updated HEAD row (same data, new version/cc). // We reconstruct it from head_row overriding the two changed columns. - val effHeadNoCcVersionFn = effHead.filter { it !== PgColumn.cc && it !== PgColumn.version && it !== PgColumn.fn } val SQL = """$query$head_select$head_row$head_to_history$head_updated$head_deleted$history_tombstone SELECT - head_row.fn AS fn, - $deleted_version AS version, - ${if (hasCc) "COALESCE(head_updated.${PgColumn.cc.name}, head_row.${PgColumn.cc.name} + 1) AS ${PgColumn.cc}," else ""} - ${effHeadNoCcVersionFn.joinToString(", ") { "head_row.${it.name} AS ${it.name}" }}${if (effHeadNoCcVersionFn.isNotEmpty()) "," else ""} - null::int8 AS ${PgColumn.next_version}, - ${if (head_to_history.isNotEmpty()) "head_to_history.version AS head_history_version," else ""} - ${if (history_tombstone.isNotEmpty()) "history_tombstone.version AS history_version," else ""} - head_select.fn AS select_fn, - head_select.version AS select_version, - head_row.version AS head_version, - ${if (!purge) "head_updated.version AS deleted_version," else "head_deleted.version AS deleted_version,"} - query.id AS query_id, + head_row.$FN AS $FN, + $deleted_version AS $VERSION, + null::int8 AS $NEXT_VERSION, + ${if (CC!=null) "COALESCE(head_updated.$CC, head_row.$CC + 1) AS $CC," else ""} + ${pgCollection.joinColumns { col -> if (col eq CC || col eq FN || col eq VERSION || col eq NEXT_VERSION) null else "head_row.$col AS $col" }}, + ${if (head_to_history.isNotEmpty()) "head_to_history.$VERSION AS head_history_version," else "null AS head_history_version"} + ${if (history_tombstone.isNotEmpty()) "history_tombstone.version AS history_version," else "null AS history_version"} + head_select.$FN AS select_fn, + head_select.$VERSION AS select_version, + head_row.$VERSION AS head_version, + ${if (!purge) "head_updated.$VERSION AS deleted_version," else "head_deleted.$VERSION AS deleted_version,"} + query.$ID AS query_id, query.expected_version AS query_expected_version FROM query -LEFT JOIN head_row ON head_row.id = query.id -${if (!purge) "LEFT JOIN head_updated ON head_updated.id = query.id" else ""} -${if (head_to_history.isNotEmpty()) "LEFT JOIN head_to_history ON head_to_history.id = query.id" else ""} -${if (history_tombstone.isNotEmpty()) "LEFT JOIN history_tombstone ON history_tombstone.id = query.id" else ""} -LEFT JOIN head_select ON head_select.id = query.id -${if (purge) "LEFT JOIN head_deleted ON head_deleted.id = query.id" else ""} +LEFT JOIN head_row ON head_row.$ID = query.$ID +${if (!purge) "LEFT JOIN head_updated ON head_updated.$ID = query.$ID" else ""} +${if (head_to_history.isNotEmpty()) "LEFT JOIN head_to_history ON head_to_history.$ID = query.$ID" else ""} +${if (history_tombstone.isNotEmpty()) "LEFT JOIN history_tombstone ON history_tombstone.$ID = query.$ID" else ""} +LEFT JOIN head_select ON head_select.ID = query.$ID +${if (purge) "LEFT JOIN head_deleted ON head_deleted.$ID = query.$ID" else ""} ;""" val typeNames = inRows.typeNames() val pgPlan = conn.prepare(SQL, typeNames) @@ -143,22 +157,28 @@ ${if (purge) "LEFT JOIN head_deleted ON head_deleted.id = query.id" else ""} } override fun doExecute(conn: PgConnection) { - if (writes.isEmpty()) return + if (pgWrites.isEmpty()) return val outRows = PgRows() .withDatabaseNumber(storageNumber) .withCatalogNumber(catalogNumber) .withCollectionNumber(collectionNumber) - .withDefaultDataEncoding(collection.head.dataEncoding ?: Naksha.DEFAULT_DATA_ENCODING) - .addColumns(collection.effectiveHistoryColumns) - .addColumn("head_history_version", PgType.INT64) - .addColumn("history_version", PgType.INT64) - .addColumn("select_fn", PgType.INT64) - .addColumn("select_version", PgType.INT64) - .addColumn("head_version", PgType.INT64) - .addColumn("deleted_version", PgType.INT64) - .addColumn("query_id", PgType.STRING) - .addColumn("query_expected_version", PgType.INT64) - val plan = plan(conn, collection) + outRows.addColumn(FN) + outRows.addColumn(VERSION) + outRows.addColumn(NEXT_VERSION) + if (CC!=null) outRows.addColumn(CC) + for (col in pgCollection.columns) { + if (col eq CC || col eq FN || col eq VERSION || col eq NEXT_VERSION) continue + outRows.addColumn(col) + } + outRows.addColumn("head_history_version", MemberType.INT64) + .addColumn("history_version", MemberType.INT64) + .addColumn("select_fn", MemberType.INT64) + .addColumn("select_version", MemberType.INT64) + .addColumn("head_version", MemberType.INT64) + .addColumn("deleted_version", MemberType.INT64) + .addColumn("query_id", MemberType.STRING) + .addColumn("query_expected_version", MemberType.INT64) + val plan = plan(conn, pgCollection) val array = inRows.values() if (PlatformUtil.ENABLE_INFO) { if (session.logQueries) { @@ -173,24 +193,24 @@ ${if (purge) "LEFT JOIN head_deleted ON head_deleted.id = query.id" else ""} val cursor = plan.pgPlan.execute(array) val end = Platform.currentNanos() val seconds = (end.toDouble() - start.toDouble()) / 1e9 - if (writes.size != 1 || writes[0].isFeatureModification) { + if (pgWrites.size != 1 || pgWrites[0].isFeatureModification) { logger.info( - "${if (purge) "PURGE" else "DELETE"} for ${writes.size} rows resulted in ${inRows.size} rows deleted, ${seconds * 1000}ms, ${inRows.size / seconds} features/s, partitions: $featureCountByPartitionJoined" + "${if (purge) "PURGE" else "DELETE"} for ${pgWrites.size} rows resulted in ${inRows.size} rows deleted, ${seconds * 1000}ms, ${inRows.size / seconds} features/s, partitions: $featureCountByPartitionJoined" ) } cursor.fetch().use { cursor -> - outRows.addAll(cursor) + outRows.readAll(cursor) for (row in 0 until outRows.size) { - val write = writes[row] + val write = pgWrites[row] val id = outRows.getString(row, "query_id") ?: throw generalException("Missing 'query_id' in result") val tuple = outRows[row] if (tuple != null) write.tuple = tuple - val tombstone_fn = outRows.getInt64(row, PgColumn.fn) - val tombstone_version = outRows.getInt64(row, PgColumn.version) + val tombstone_fn = outRows.getInt64(row, FN) + val tombstone_version = outRows.getInt64(row, VERSION) val tn = if (tombstone_fn != null && tombstone_version != null) { - TupleNumber(storageNumber, catalogNumber, collectionNumber, tombstone_fn, Version(tombstone_version)) + TupleNumber(storageNumber, catalogNumber, collectionNumber, tombstone_fn, tombstone_version) } else null write.tupleNumber = tn diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterInsert.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterInsert.kt index bb0291c10..4b3d30d56 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterInsert.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterInsert.kt @@ -99,8 +99,8 @@ LEFT JOIN head_inserted ON head_inserted.id = new_row.id } override fun doExecute(conn: PgConnection) { - if (writes.isEmpty()) return - val plan = plan(conn, collection) + if (pgWrites.isEmpty()) return + val plan = plan(conn, pgCollection) val array = inRows.values() if (PlatformUtil.ENABLE_INFO) { if (session.logQueries) { @@ -116,7 +116,7 @@ LEFT JOIN head_inserted ON head_inserted.id = new_row.id plan.pgPlan.execute(array).close() val end = Platform.currentNanos() val seconds = (end.toDouble() - start.toDouble()) / 1e9 - if (writes.size != 1 || writes[0].isFeatureModification) { + if (pgWrites.size != 1 || pgWrites[0].isFeatureModification) { logger.info("INSERT of ${inRows.size} rows took ${seconds * 1000}ms, therefore ${inRows.size / seconds} features/s, partitions: $featureCountByPartitionJoined") } } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt index 9214e0cb6..4fe25bea5 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt @@ -129,10 +129,10 @@ LEFT JOIN inserted ON inserted.id = new_row.id } override fun doExecute(conn: PgConnection) { - if (writes.isEmpty()) return + if (pgWrites.isEmpty()) return // All nullable BYTE_ARRAY columns may carry the "keep if undefined" sentinel and must be // read back from the DB so the in-memory tuple reflects the final stored value. - val keepableByteCols = collection.effectiveHeadColumns.filter { it.type == PgType.BYTE_ARRAY && it !== PgColumn.feature } + val keepableByteCols = pgCollection.effectiveHeadColumns.filter { it.type == PgType.BYTE_ARRAY && it !== PgColumn.feature } val rows = PgRows() .withDatabaseNumber(storageNumber) .withCatalogNumber(catalogNumber) @@ -142,7 +142,7 @@ LEFT JOIN inserted ON inserted.id = new_row.id .addColumn("existing_version", PgType.INT64) .addColumn("head_id", PgType.STRING) for (col in keepableByteCols) rows.addColumn(col.name, PgType.BYTE_ARRAY) - val plan = plan(conn, collection) + val plan = plan(conn, pgCollection) val array = this.inRows.values() if (PlatformUtil.ENABLE_INFO) { if (session.logQueries) { @@ -157,7 +157,7 @@ LEFT JOIN inserted ON inserted.id = new_row.id val cursor = plan.pgPlan.execute(array) val end = Platform.currentNanos() val seconds = (end.toDouble() - start.toDouble()) / 1e9 - if (writes.size != 1 || writes[0].isFeatureModification) { + if (pgWrites.size != 1 || pgWrites[0].isFeatureModification) { logger.info("UPDATE of ${rows.size} rows took ${seconds * 1000}ms, therefore ${rows.size / seconds} features/s, partitions: $featureCountByPartitionJoined") } cursor.fetch().use { diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt index 84c254c8b..d299fb332 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt @@ -145,7 +145,7 @@ ${if (head_to_history.isNotEmpty()) "LEFT JOIN head_to_history ON head_to_histor } override fun doExecute(conn: PgConnection) { - val keepableByteCols = collection.effectiveHeadColumns.filter { it.type == PgType.BYTE_ARRAY && it !== PgColumn.feature } + val keepableByteCols = pgCollection.effectiveHeadColumns.filter { it.type == PgType.BYTE_ARRAY && it !== PgColumn.feature } val outRows = PgRows() .withDatabaseNumber(storageNumber) .withCatalogNumber(catalogNumber) @@ -162,8 +162,8 @@ ${if (head_to_history.isNotEmpty()) "LEFT JOIN head_to_history ON head_to_histor .addColumn("head_inserted_version", PgType.INT64) .addColumn("head_to_history_version", PgType.INT64) for (col in keepableByteCols) outRows.addColumn(col.name, PgType.BYTE_ARRAY) - if (writes.isEmpty()) return - val plan = plan(conn, collection) + if (pgWrites.isEmpty()) return + val plan = plan(conn, pgCollection) // TupleNumber.fromB128(inRows.columns[11].values_field[0] as ByteArray, naksha.base.Int64(0), 0, 0).partitionNumber % 16 val array = inRows.values() val session = this.session @@ -180,7 +180,7 @@ ${if (head_to_history.isNotEmpty()) "LEFT JOIN head_to_history ON head_to_histor val cursor = plan.pgPlan.execute(array) val end = Platform.currentNanos() val seconds = (end.toDouble() - start.toDouble()) / 1e9 - if (writes.size != 1 || writes[0].isFeatureModification) { + if (pgWrites.size != 1 || pgWrites[0].isFeatureModification) { logger.info("UPSERT of ${inRows.size} rows took ${seconds * 1000}ms, therefore ${inRows.size / seconds} features/s, partitions: $featureCountByPartitionJoined") } cursor.fetch().use { @@ -198,7 +198,7 @@ ${if (head_to_history.isNotEmpty()) "LEFT JOIN head_to_history ON head_to_histor if (updatedFn != null && updatedVersionTxn != null) { val updated_tn = TupleNumber(storageNumber, catalogNumber, collectionNumber, updatedFn, Version(updatedVersionTxn)) // If an update was done, we need the following values to be available: - val hasCc = PgColumn.cc in collection.effectiveHeadColumns + val hasCc = PgColumn.cc in pgCollection.effectiveHeadColumns val changeCount: Int = if (hasCc) { outRows.getInt(row, "cc") ?: throw generalException("Missing 'cc' in update result for feature '$id'") From eee40c667b849a6290f042b9bee3b2e704c1744e Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Thu, 25 Jun 2026 10:29:14 +0200 Subject: [PATCH 38/57] Improve ID verification. Signed-off-by: Alexander Lowey-Weber --- .../commonMain/kotlin/naksha/model/Naksha.kt | 143 +----------- .../kotlin/naksha/model/NakshaIdType.kt | 213 ++++++++++++++++++ .../kotlin/naksha/model/objects/Member.kt | 5 +- .../kotlin/naksha/model/objects/MemberList.kt | 4 +- .../naksha/model/objects/NakshaCollection.kt | 9 +- .../kotlin/naksha/model/request/Write.kt | 31 ++- .../kotlin/naksha/psql/PgAdminCatalog.kt | 16 +- .../commonMain/kotlin/naksha/psql/PgRows.kt | 10 + .../commonMain/kotlin/naksha/psql/PgWriter.kt | 2 +- .../kotlin/naksha/psql/PgWriterDelete.kt | 5 +- .../kotlin/naksha/psql/PgWriterInsert.kt | 37 ++- 11 files changed, 289 insertions(+), 186 deletions(-) create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/NakshaIdType.kt diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt index 1ec0f2851..9bea39feb 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/Naksha.kt @@ -101,11 +101,17 @@ class Naksha private constructor() { const val BOOKS_COL_FN = 3 /** - * The maximum length of identifiers _(`42`)_ . + * The maximum length of identifiers _(`42`)_. * @since 3.0 */ const val MAX_ID_LENGTH = 42 // The answer to everything ;-) + /** + * The maximum length of internal identifiers. + * @since 3.0 + */ + const val MAX_INTERNAL_ID_LENGTH = 63 + // TODO: This shows why we really need a special TupleNumberArray next to TupleNumberList, which stores all tuple-numbers in a single // byte-array, compressed by adding the database-number, catalog-number and collection-number upfront, then followed by all the // tuple-numbers. It will reduce memory consumption from 1 GiB to around 256 MiB. @@ -147,141 +153,6 @@ class Naksha private constructor() { @JvmField var DEFAULT_SESSION_LOG_LEVEL: String? = null - /** - * Tests if the given **id** is a valid identifier, so matches: - * - * `[a-z][a-z0-9_:-]{42}` - * - * **Beware**: Identifiers must not contain upper-case letters, because many storages does not make a difference between upper- and lower-cased letters. - * @param id the identifier. - * @param internal if _true_, then extends the allowed character set to `[a-z_][a-z0-9_:-~$]{31}`. - * @return _true_ if the identifier is valid; _false_ otherwise. - * @since 3.0 - * @see [verifyId] - * @see [verifyInternalId] - * @see [MAX_ID_LENGTH] - */ - @JsStatic - @JvmStatic - @JvmOverloads - fun isValidId(id: String?, internal: Boolean = false): Boolean { - if (id.isNullOrEmpty() || "naksha" == id || id.length > MAX_ID_LENGTH) return false - var i = 0 - var c = id[i++] - // First character must be a-z - if (c.code < 'a'.code || c.code > 'z'.code) return false - while (i < id.length) { - c = id[i++] - when (c.code) { - in 'a'.code..'z'.code -> continue - in '0'.code..'9'.code -> continue - '_'.code, ':'.code, '-'.code -> continue - else -> return false - } - } - return true - } - - /** - * Tests if the given **id** is a valid identifier, so matches: - * - * `[a-z][a-z0-9_:-]{31}` - * - * If the given identifier is invalid, the methods throws [NakshaError.ILLEGAL_ID]. - * @param id the identifier to test. - * @return the given identifier, tested. - * @since 3.0 - * @see [isValidId] - */ - @JsStatic - @JvmStatic - fun verifyId(id: String?): String { - verifyId(id, internal = false, throwOnError = true) - return id!! - } - - /** - * Tests if the given **id** is a valid identifier, so matches: - * - * `[a-z_][a-z0-9_:-~$]{31}` - * - * If the given identifier is invalid, the methods throws [NakshaError.ILLEGAL_ID]. - * @param id the identifier to test. - * @return the given identifier, tested. - * @since 3.0 - * @see [isValidId] - */ - @JsStatic - @JvmStatic - fun verifyInternalId(id: String?): String { - verifyId(id, internal = true, throwOnError = true) - return id!! - } - - @JvmStatic - private fun verifyId(id: String?, internal: Boolean, throwOnError: Boolean): Boolean { - if (id.isNullOrEmpty()) { - if (throwOnError) throw illegalId("The given identifier is null or empty") - else return false - } - if (id == "naksha") { - if (throwOnError) throw illegalId("The identifier 'naksha' is forbidden") - else return false - } - if (id.length > MAX_ID_LENGTH) { - if (throwOnError) throw illegalId("The identifier '$id' is too long: ${id.length}, must be maximal $MAX_ID_LENGTH") - else return false - } - var i = 0 - var c = id[i++] - if (c.code < 'a'.code || c.code > 'z'.code) { - if (!internal) { - if (throwOnError) throw illegalId("The first character must be a-z, but was $c") - else return false - } - // Internal identifiers may start with `_` - if (c.code != '_'.code) { - if (throwOnError) throw illegalId("The first character must be a-z or '_' (underscore), but was $c") - else return false - } - } - while (i < id.length) { - c = id[i++] - when (c.code) { - in 'a'.code..'z'.code -> continue - in '0'.code..'9'.code -> continue - '_'.code, ':'.code, '-'.code -> continue - '~'.code, '$'.code -> if (!internal) { - if (throwOnError) throw illegalId("Invalid character at index $i: '$c', expected [a-z0-9_:-]") - else return false - } else continue - else -> if (!internal) { - if (throwOnError) throw illegalId("Invalid character at index $i: '$c', expected [a-z0-9_:-]") - else return false - } else { - if (throwOnError) throw illegalId("Invalid character at index $i: '$c', expected [a-z0-9_:-~$]") - else return false - } - } - } - return true - } - - /** - * Tests if the given identifier is an internal one. - * @param id the identifier to test. - * @return _true_ if this is an internal identifier; _false_ otherwise. - * @since 3.0 - */ - @JsStatic - @JvmStatic - fun isInternalId(id: String?): Boolean = - // Every identifier that is a valid internal identifier - verifyId(id, internal = true, throwOnError = false) - // but not a valid normal identifier - && !verifyId(id, internal = false, throwOnError = false) - // is actually an internal identifier - /** * Generates an [MD5](https://en.wikipedia.org/wiki/MD5) hash above the given identifier, which is used to extract many values from it. * @param id the identifier to hash. diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/NakshaIdType.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/NakshaIdType.kt new file mode 100644 index 000000000..e3187c0ea --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/NakshaIdType.kt @@ -0,0 +1,213 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model + +import naksha.model.Naksha.NakshaCompanion.MAX_ID_LENGTH +import naksha.model.Naksha.NakshaCompanion.MAX_INTERNAL_ID_LENGTH +import kotlin.js.JsExport +import kotlin.jvm.JvmOverloads + +/** + * All possible identifier types in Naksha with their validation rules. + * @property start the characters allowed as first character. + * @property chars the characters allowed at the rest of the identifier. + * @property rangeInfo informational string for error messages which characters are allowed. + */ +@JsExport +enum class NakshaIdType( + internal val start: Map, + internal val chars: Map, + internal val maxLength: Int, + internal val rangeInfo: String, + internal val isInternal: Boolean = false, +) { + /** + * The identifiers for `NakshaDatabase` _(not yet implemented)_. + * @since 3.0 + */ + DATABASE(_start(), _container(), MAX_ID_LENGTH, "[a-z][a-z0-9_-:]{${MAX_ID_LENGTH-1}}"), + + /** + * The identifiers for internal `NakshaDatabase` _(not yet implemented)_. + * @since 3.0 + */ + INTERNAL_DATABASE(_start(true), _container(true), MAX_INTERNAL_ID_LENGTH, "[a-z][a-z0-9_-:$~]{${MAX_INTERNAL_ID_LENGTH-1}}", true), + + /** + * The identifiers for [NakshaCatalog][naksha.model.objects.NakshaCatalog]. + * @since 3.0 + */ + CATALOG(_start(), _container(), MAX_ID_LENGTH, "[a-z][a-z0-9_-:]{${MAX_ID_LENGTH}}"), + + /** + * The identifiers for internal [NakshaCatalog][naksha.model.objects.NakshaCatalog]. + * @since 3.0 + */ + INTERNAL_CATALOG(_start(true), _container(true), MAX_INTERNAL_ID_LENGTH, "[a-z][a-z0-9_-:$~]{${MAX_INTERNAL_ID_LENGTH-1}}", true), + + /** + * The identifiers for [NakshaCollection][naksha.model.objects.NakshaCollection]. + * @since 3.0 + */ + COLLECTION(_start(), _container(), MAX_ID_LENGTH, "[a-z][a-z0-9_-:]{${MAX_ID_LENGTH-1}}"), + + /** + * The identifiers for internal [NakshaCollection][naksha.model.objects.NakshaCollection]. + * @since 3.0 + */ + INTERNAL_COLLECTION(_start(true), _container(true), MAX_INTERNAL_ID_LENGTH, "[a-z][a-z0-9_-:$~]{${MAX_INTERNAL_ID_LENGTH-1}}", true), + + /** + * The identifiers for [Member][naksha.model.objects.Member]. + * @since 3.0 + */ + MEMBER(_start(), _member(), MAX_ID_LENGTH, "[a-z][a-z0-9_]{${MAX_ID_LENGTH-1}}"), + + /** + * The identifiers for internal [Member][naksha.model.objects.Member]. + * @since 3.0 + */ + INTERNAL_MEMBER(_start(true), _member(true), MAX_ID_LENGTH, "[a-z_][a-z0-9_]{${MAX_ID_LENGTH-1}}", true), + + /** + * The identifiers for [Index][naksha.model.objects.Index]. + * @since 3.0 + */ + INDEX(_start(), _member(), MAX_ID_LENGTH, "[a-z][a-z0-9_]{${MAX_ID_LENGTH-1}}"), + + /** + * The identifiers for internal [Index][naksha.model.objects.Index]. + * @since 3.0 + */ + INTERNAL_INDEX(_start(true), _member(true), MAX_ID_LENGTH, "[a-z_][a-z0-9_]{${MAX_ID_LENGTH-1}}", true), + + /** + * The identifiers for `Book` _(not yet implemented)_. + * @since 3.0 + */ + BOOK(_start(), _member(), MAX_ID_LENGTH, "[a-z][a-z0-9_]{${MAX_ID_LENGTH-1}}"), + + /** + * The identifiers for internal `Book` _(not yet implemented)_. + * @since 3.0 + */ + INTERNAL_BOOK(_start(true), _member(true), MAX_INTERNAL_ID_LENGTH, "[a-z_][a-z0-9_]{${MAX_INTERNAL_ID_LENGTH-1}}"), + + /** + * The identifiers for `Book` _(not yet implemented)_. + * @since 3.0 + */ + TRANSACTION(_start_tx(), _tx(), 16, "[1-9][0-9]{15}"), + + /** + * The identifiers for [NakshaFeature][naksha.model.objects.NakshaFeature], actually without limits. + * @since 3.0 + */ + FEATURE(mapOf(), mapOf(), Int.MAX_VALUE, ".*"); + + /** + * Tests if the given **id** is a valid identifier of this kind. + * + * - `DATABASE` - `[a-z][a-z0-9_:-]{Naksha.MAX_ID_LENGTH}` + * - `CATALOG` - `[a-z][a-z0-9_:-]{Naksha.MAX_ID_LENGTH}` + * - `COLLECTION` - `[a-z][a-z0-9_:-]{Naksha.MAX_ID_LENGTH}` + * - `MEMBER` - `[a-z][a-z0-9_]{Naksha.MAX_ID_LENGTH}` + * - `BOOK` - `[a-z][a-z0-9_:-]{Naksha.MAX_ID_LENGTH}` + * - `TRANSACTION` - `[1-9][0-9]{15}` + * - `FEATURE` - no limit + * + * @return the given identifier, if it is valid; otherwise throws an exception. + * @throws NakshaException with [ILLEGAL_ID][naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_ID], when `throwOnError` is _true_ and the identifier is not valid for the selected purpose (`idType`). + * @since 3.0 + * @see [isValidId] + */ + fun verify(id: String?): String { + isValidId(id, true) + return id!! + } + + /** + * Tests if the given **id** is a valid identifier of this kind. + * + * - `DATABASE` - `[a-z][a-z0-9_:-]{Naksha.MAX_ID_LENGTH}` + * - `CATALOG` - `[a-z][a-z0-9_:-]{Naksha.MAX_ID_LENGTH}` + * - `COLLECTION` - `[a-z][a-z0-9_:-]{Naksha.MAX_ID_LENGTH}` + * - `MEMBER` - `[a-z][a-z0-9_]{Naksha.MAX_ID_LENGTH}` + * - `BOOK` - `[a-z][a-z0-9_:-]{Naksha.MAX_ID_LENGTH}` + * - `TRANSACTION` - `[1-9][0-9]{15}` + * - `FEATURE` - no limit + * + * **Beware**: Identifiers must not contain upper-case letters, because many storages does not make a difference between upper- and lower-cased letters. + * @param id the identifier to test. + * @param throwOnError if an exception should be thrown, when the verification failed. + * @return _true_ if the identifier is valid; _false_ otherwise. + * @throws NakshaException with [ILLEGAL_ID][naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_ID], when `throwOnError` is _true_ and the identifier is not valid for the selected purpose (`idType`). + * @since 3.0 + * @see [verify] + */ + @JvmOverloads + fun isValidId(id: String?, throwOnError: Boolean = false): Boolean { + if (id.isNullOrEmpty()) { + if (throwOnError) throw illegalId("The given identifier is null or empty") + else return false + } + if (id == "naksha") { + if (isInternal) return true + if (throwOnError) throw illegalId("The identifier 'naksha' is forbidden") + else return false + } + if (id.length > maxLength) { + if (throwOnError) throw illegalId("The identifier '$id' is too long: ${id.length}, must be maximal $maxLength") + else return false + } + var i = 0 + var c = id[i++] + if (!start.containsKey(c)) { + if (throwOnError) throw illegalId("The first character must be $rangeInfo, but was $c") + else return false + } + while (i < id.length) { + c = id[i++] + if (!chars.containsKey(c)) { + if (throwOnError) throw illegalId("Invalid character at index $i: '$c', expected $rangeInfo") + else return false + } + } + return true + } +} + +private fun _start_tx(): Map { + val map = mutableMapOf() + for (c in '1' .. '9') map[c] = true + return map.toMap() +} +private fun _tx(): Map { + val map = mutableMapOf() + for (c in '0' .. '9') map[c] = true + return map.toMap() +} +private fun _start(internal: Boolean = false): Map { + val map = mutableMapOf() + for (c in 'a' .. 'z') map[c] = true + if (internal) map['_'] = true + return map.toMap() +} +private fun _member(internal: Boolean = false): Map { + val map = mutableMapOf() + for (c in 'a' .. 'z') map[c] = true + map['_'] = true + return map.toMap() +} +private fun _container(internal: Boolean = false): Map { + val map = mutableMapOf() + for (c in 'a' .. 'z') map[c] = true + map['_'] = true + map['-'] = true + map[':'] = true + if (internal) { + map['~'] = true + map['$'] = true + } + return map.toMap() +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt index 9cf6f09a1..30f763e25 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt @@ -14,10 +14,10 @@ import naksha.base.PlatformList import naksha.base.PlatformMap import naksha.base.Proxy import naksha.geo.SpGeometry -import naksha.model.Naksha import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_ARGUMENT import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_STATE import naksha.model.NakshaException +import naksha.model.NakshaIdType.INTERNAL_MEMBER import naksha.model.TagList import naksha.model.TagMap import naksha.model.TupleNumber @@ -53,8 +53,7 @@ open class Member() : AnyObject(), Comparator { */ @JsName("of") constructor(name: String, dataType: MemberType = MemberType.STRING, path: JsonPath? = null) : this() { - Naksha.verifyInternalId(name) - this.name = name + this.name = INTERNAL_MEMBER.verify(name) this.dataType = dataType this.path = path ?: JsonPath(listOf("properties", name)) this.path.validate() diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberList.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberList.kt index 94ade30a5..a4afa5fce 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberList.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/MemberList.kt @@ -7,6 +7,8 @@ import naksha.model.Naksha import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_STATE import naksha.model.NakshaError.NakshaErrorCompanion.INTERNAL_ERROR import naksha.model.NakshaException +import naksha.model.NakshaIdType +import naksha.model.NakshaIdType.INTERNAL_MEMBER import kotlin.js.JsExport import kotlin.js.JsName import kotlin.jvm.JvmStatic @@ -127,7 +129,7 @@ open class MemberList() : ListProxy(Member::class) { for (i in 0 until this.size) { val member = this[i] ?: throw NakshaException(ILLEGAL_STATE, "Member at index $i is null") val memberName = member.name - if (!Naksha.isValidId(memberName, internal = true)) { + if (INTERNAL_MEMBER.isValidId(memberName)) { throw NakshaException(ILLEGAL_STATE, "Member at index $i has invalid name: $memberName") } for (j in (i + 1) until this.size) { diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt index e390dd7c6..747164550 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt @@ -11,6 +11,7 @@ import naksha.model.NakshaError import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_ARGUMENT import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_STATE import naksha.model.NakshaException +import naksha.model.NakshaIdType import naksha.model.TupleNumber import kotlin.js.JsExport import kotlin.js.JsName @@ -316,7 +317,7 @@ open class NakshaCollection() : NakshaFeature() { * @since 3.0 */ open fun addMember(value: Member): NakshaCollection { - Naksha.verifyId(value.name) + NakshaIdType.INTERNAL_MEMBER.verify(value.name) var list = this.members if (list == null) { list = MemberList() @@ -324,11 +325,11 @@ open class NakshaCollection() : NakshaFeature() { } for (existing in list) { if (existing != null && existing.name == value.name) { - throw NakshaException(NakshaError.ILLEGAL_ARGUMENT, "Duplicate member name: '${value.name}'") + throw NakshaException(ILLEGAL_ARGUMENT, "Duplicate member name: '${value.name}'") } } if (list.size >= MemberList.MAX_MEMBERS) { - throw NakshaException(NakshaError.ILLEGAL_ARGUMENT, "Cannot add more than ${MemberList.MAX_MEMBERS} members to a collection") + throw NakshaException(ILLEGAL_ARGUMENT, "Cannot add more than ${MemberList.MAX_MEMBERS} members to a collection") } list.add(value) return this @@ -485,7 +486,7 @@ open class NakshaCollection() : NakshaFeature() { * @since 3.0 */ open fun addIndex(value: Index): NakshaCollection { - Naksha.verifyId(value.name) + NakshaIdType.INDEX.verify(value.name) var list = this.indices if (list == null) { list = IndexList() diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/Write.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/Write.kt index 5ec613f7e..68e61643b 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/Write.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/Write.kt @@ -8,8 +8,8 @@ import naksha.model.Naksha.NakshaCompanion.ADMIN_CATALOG_ID import naksha.model.Naksha.NakshaCompanion.COLLECTIONS_COL_ID import naksha.model.Naksha.NakshaCompanion.BOOKS_COL_ID import naksha.model.Naksha.NakshaCompanion.CATALOGS_COL_ID +import naksha.model.Naksha.NakshaCompanion.TRANSACTIONS_COL_ID import naksha.model.Naksha.NakshaCompanion.featureNumber -import naksha.model.Naksha.NakshaCompanion.isInternalId import naksha.model.Naksha.NakshaCompanion.partitionNumber import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_STATE import naksha.model.objects.NakshaFeature @@ -244,6 +244,7 @@ open class Write : AnyObject() { get() { val raw = getRaw("tupleNumber") if (raw is String) { + @Suppress("StringReferentialEquality") // Intentional reference compare ! if (raw === tupleNumberRaw) return tupleNumberValue tupleNumberValue = TupleNumber.fromUrn(raw) tupleNumberRaw = raw @@ -280,7 +281,7 @@ open class Write : AnyObject() { var atomic: Boolean get() { val raw = getRaw("atomic") - return if (raw is Boolean) raw else false + return raw as? Boolean ?: false } set(value) { if (value) setRaw("atomic", true) else removeRaw("atomic") @@ -359,10 +360,14 @@ open class Write : AnyObject() { if (fn == null) fn = StandardMembers.Tn.getTupleNumber(feature)?.featureNumber } if (fn != null) return fn + // TODO: Eventually this boils down to how we handle collisions. In practise we rarely ever encountered a collision. + // It seems that a 63-bit hash is so unique, that the few collisions can be ignored and the burden can be given + // to the user to generate or use an alternative identifier, if two identifiers hash to the same number! val id = this.id val cachedId = featureNumberId val cachedNumber = featureNumberValue + @Suppress("StringReferentialEquality") // Intentional reference compare ! if (id === cachedId && cachedNumber != null) return cachedNumber val number = featureNumber(id) featureNumberId = id @@ -928,14 +933,24 @@ open class Write : AnyObject() { * @see [WriteOp] */ fun validate(): Write { - if (catalogId == ADMIN_CATALOG_ID || collectionId == COLLECTIONS_COL_ID) { - if (isInternalId(id)) { - throw NakshaException(ILLEGAL_STATE, "Modification of internal features forbidden: '$id'") + // Writing into the admin-catalog means that we either want to mutate administrative objects. + if (catalogId == ADMIN_CATALOG_ID) { + if (collectionId == CATALOGS_COL_ID) { // Mutation of catalog. + NakshaIdType.CATALOG.verify(id) + } else if (collectionId == BOOKS_COL_ID) { // Mutation of book. + NakshaIdType.BOOK.verify(id) + } else if (collectionId == COLLECTIONS_COL_ID) { // Mutation of collection. + NakshaIdType.COLLECTION.verify(id) + } else if (collectionId == TRANSACTIONS_COL_ID) { // Mutation of transaction. + NakshaIdType.TRANSACTION.verify(id) + } else { + throw illegalArg("Write operation for invalid collection in admin catalog; id: '$id' ") } + return this } - if (!Naksha.isValidId(id)) { - throw NakshaException(ILLEGAL_STATE, "Invalid feature-id: '$id'") - } + if (collectionId == COLLECTIONS_COL_ID) { // Mutation of collection in custom catalog. + NakshaIdType.COLLECTION.verify(id) + } // otherwise, arbitrary feature is modified, we allow any identifier. return this } } \ No newline at end of file diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgAdminCatalog.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgAdminCatalog.kt index b6286c2be..b4258a1b6 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgAdminCatalog.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgAdminCatalog.kt @@ -16,7 +16,6 @@ import naksha.model.* import naksha.model.Naksha.NakshaCompanion.ADMIN_CATALOG_ID import naksha.model.Naksha.NakshaCompanion.ADMIN_CATALOG_FN import naksha.model.NakshaError.NakshaErrorCompanion.EXCEPTION -import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_ARGUMENT import naksha.model.NakshaError.NakshaErrorCompanion.ILLEGAL_STATE import naksha.model.NakshaError.NakshaErrorCompanion.STORAGE_ID_MISMATCH import naksha.model.objects.NakshaCatalog @@ -475,7 +474,7 @@ SELECT basics.*, procs.* FROM basics, procs; } /** - * Create a new [catalog][PgCatalog] using the given connection, and return it. + * Create a new custom [catalog][PgCatalog] using the given connection, and return it. * * ### Note * Does not commit the given connection, therefore the catalog _(aka schema)_ is not yet persisted, but can be used through the given connection. The method neither creates the corresponding entry in the collection's collection of the admin-catalog, it only creates the schema. @@ -485,8 +484,7 @@ SELECT basics.*, procs.* FROM basics, procs; * @since 3.0.0 */ fun createPgCatalog(conn: PgConnection, catalog: PgCatalog) { - if (Naksha.isInternalId(catalog.id)) throw NakshaException(ILLEGAL_ARGUMENT, "Can't create internal catalogs: ${catalog.id}") - if (!Naksha.isValidId(catalog.id)) throw NakshaException(ILLEGAL_ARGUMENT, "Invalid catalog identifier: ${catalog.id}") + NakshaIdType.CATALOG.verify(catalog.id) // TODO: We need to ensure that there is no other schema with the same feature-number: // Write: `ALTER SCHEMA ${catalog.quotedId} SET SCHEMA OPTION 'featureNumber' 'stringifiedFN';` // Read: @@ -505,13 +503,13 @@ SELECT basics.*, procs.* FROM basics, procs; * ### Note * Does not commit the given connection, therefore the map is not yet physically deleted. The method neither deletes the corresponding entry from the collection's collection of the admin-map, it only drops the schema! * @param conn the connection to use to access the database. - * @param map the map to delete. + * @param catalog the map to delete. * @since 3.0.0 */ - fun deletePgCatalog(conn: PgConnection, map: PgCatalog) { - if (Naksha.isInternalId(map.id)) throw NakshaException(ILLEGAL_ARGUMENT, "Can't delete internal maps: ${map.id}") - conn.execute("DROP SCHEMA IF EXISTS ${map.quotedId} CASCADE").close() - invalidateCatalog(map) + fun deletePgCatalog(conn: PgConnection, catalog: PgCatalog) { + NakshaIdType.CATALOG.verify(catalog.id) + conn.execute("DROP SCHEMA IF EXISTS ${catalog.quotedId} CASCADE").close() + invalidateCatalog(catalog) } /** diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRows.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRows.kt index fc510318c..a9900dfea 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRows.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRows.kt @@ -144,6 +144,16 @@ internal class PgRows { return this } + fun addColumns(columns: Array): PgRows { + for (column in columns) { + val alias = column.name + val existing = getColumn(alias) + if (existing != null) continue + this.columns.add(PgColumnWithValues(column, alias).withSize(size)) + } + return this + } + fun addColumn(column: PgColumn, alias: String = column.name): PgRows { clearCache() val existing = getColumn(alias) diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt index e0f6e6ffd..9ca0c10e4 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt @@ -347,7 +347,7 @@ open class PgWriter internal constructor( } // if (e > s) { - val tupleWriter = PgWriterDelete(this, pgCollection, partition, purges, purge = true) + val tupleWriter = PgWriterDelete(this, pgCollection, pgWrites, s, e, purge = false) tupleWriter.execute(conn) e = s } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterDelete.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterDelete.kt index 0ad132349..6fc65b65e 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterDelete.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterDelete.kt @@ -55,8 +55,7 @@ internal class PgWriterDelete( val ID: PgColumn = pgCollection.column(StandardMembers.Id) ?: throw illegalState("The collection does not have an 'id' column.") val CC: PgColumn? = pgCollection.column(StandardMembers.ChangeCount) - private fun plan(conn: PgConnection, collection: PgCollection): PgWriterPlan { - + private fun plan(conn: PgConnection): PgWriterPlan { // The new version with action bits set to DELETED (2). val deleted_version = "(${tx.version.number}::int8 | 2)" @@ -178,7 +177,7 @@ ${if (purge) "LEFT JOIN head_deleted ON head_deleted.$ID = query.$ID" else ""} .addColumn("deleted_version", MemberType.INT64) .addColumn("query_id", MemberType.STRING) .addColumn("query_expected_version", MemberType.INT64) - val plan = plan(conn, pgCollection) + val plan = plan(conn) val array = inRows.values() if (PlatformUtil.ENABLE_INFO) { if (session.logQueries) { diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterInsert.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterInsert.kt index 4b3d30d56..4c8f6003f 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterInsert.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterInsert.kt @@ -3,6 +3,8 @@ package naksha.psql import naksha.base.Platform import naksha.base.Platform.PlatformCompanion.logger import naksha.base.PlatformUtil +import naksha.model.illegalState +import naksha.model.objects.StandardMembers import naksha.model.objects.StoreMode /** @@ -16,30 +18,23 @@ import naksha.model.objects.StoreMode * @since 3.0 * @see [PgWriter] */ -internal class PgWriterInsert(writer: PgWriter, collection: PgCollection, partition: Int, writes: List) - : PgWriterBase(writer, collection, partition, writes) -{ +internal class PgWriterInsert( + pgWriter: PgWriter, + pgCollection: PgCollection, + pgWrites: List, + start: Int, + end: Int +) : PgWriterBase(pgWriter, pgCollection, pgWrites, start, end) { init { - // Transactions HEAD is the one HEAD partitioned by `next_version` and must include the column. - val targetColumns = if (collection.headTable.partitionByColumn == PgColumn.next_version) - collection.effectiveHistoryColumns else collection.effectiveHeadColumns - inRows.addColumns(targetColumns) - val members = collection.head.members - inRows.addCustomMembers(members) - var i = 0 - for (write in writes) { - val tuple = write.tuple - if (tuple != null) { - inRows[i] = tuple - inRows.setCustomMembers(i, write.feature, members) - i++ - } - } + inRows.addColumns(pgCollection.columns) } - private fun plan(conn: PgConnection, collection: PgCollection): PgWriterPlan { - val insert_into_history = if (historyTable != null && collection.head.storeHistory == StoreMode.ON) historyTable else null + val headTable = pgCollection.headTable + val historyTable = if (pgCollection.storeHistory) pgCollection.historyTable else null + val ID: PgColumn = pgCollection.column(StandardMembers.Id) ?: throw illegalState("The collection does not have an 'id' column.") + val CC: PgColumn? = pgCollection.column(StandardMembers.ChangeCount) + private fun plan(conn: PgConnection): PgWriterPlan { val new_row = """WITH new_row AS ( SELECT * FROM UNNEST(${inRows.placeholders()}) AS t(${inRows.aliases()}) )""" @@ -100,7 +95,7 @@ LEFT JOIN head_inserted ON head_inserted.id = new_row.id override fun doExecute(conn: PgConnection) { if (pgWrites.isEmpty()) return - val plan = plan(conn, pgCollection) + val plan = plan(conn) val array = inRows.values() if (PlatformUtil.ENABLE_INFO) { if (session.logQueries) { From 48c82751d7e9584fe19ac0cf4a6c0e60ba60d8b7 Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Thu, 25 Jun 2026 10:31:56 +0200 Subject: [PATCH 39/57] Fix PgWriter. Signed-off-by: Alexander Lowey-Weber --- .../commonMain/kotlin/naksha/psql/PgWriter.kt | 17 ++++------------- .../kotlin/naksha/psql/PgWriterUpdate.kt | 10 +++++++--- .../kotlin/naksha/psql/PgWriterUpsert.kt | 10 +++++++--- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt index 9ca0c10e4..1b333a334 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriter.kt @@ -299,15 +299,6 @@ open class PgWriter internal constructor( } } - private fun MutableMap>.getOrCreate(collection: PgCollection): MutableList { - var list = this[collection] - if (list == null) { - list = ArrayList() - this[collection] = list - } - return list - } - /** * Execute all writes between `start` _(inclusive)_ and `end` _(exclusive)_. * @param pgWrites the list of ordered writes. @@ -347,7 +338,7 @@ open class PgWriter internal constructor( } // if (e > s) { - val tupleWriter = PgWriterDelete(this, pgCollection, pgWrites, s, e, purge = false) + val tupleWriter = PgWriterDelete(this, pgCollection, pgWrites, s, e, purge = true) tupleWriter.execute(conn) e = s } @@ -361,7 +352,7 @@ open class PgWriter internal constructor( e++ } if (e > s) { - val tupleWriter = PgWriterInsert(this, pgCollection, partition, inserts) + val tupleWriter = PgWriterInsert(this, pgCollection, pgWrites, s, e) tupleWriter.execute(conn) e = s } @@ -375,7 +366,7 @@ open class PgWriter internal constructor( e++ } if (e > s) { - val tupleWriter = PgWriterUpsert(this, pgCollection, partition, upserts) + val tupleWriter = PgWriterUpsert(this, pgCollection, pgWrites, s, e) tupleWriter.execute(conn) e = s } @@ -389,7 +380,7 @@ open class PgWriter internal constructor( e++ } if (e > s) { - val tupleWriter = PgWriterUpdate(this, pgCollection, partition, updates) + val tupleWriter = PgWriterUpdate(this, pgCollection, pgWrites, s, e) tupleWriter.execute(conn) e = s } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt index 4fe25bea5..00c275043 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt @@ -12,9 +12,13 @@ import naksha.model.objects.StoreMode * @since 3.0 * @see [PgWriter] */ -internal class PgWriterUpdate(writer: PgWriter, collection: PgCollection, partition: Int, writes: List) - : PgWriterBase(writer, collection, partition, writes) -{ +internal class PgWriterUpdate( + pgWriter: PgWriter, + pgCollection: PgCollection, + pgWrites: List, + start: Int, + end: Int +) : PgWriterBase(pgWriter, pgCollection, pgWrites, start, end) { private val writeById = mutableMapOf() init { diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt index d299fb332..65ae99579 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt @@ -12,9 +12,13 @@ import naksha.model.objects.StoreMode * @since 3.0 * @see [PgWriter] */ -internal class PgWriterUpsert(writer: PgWriter, collection: PgCollection, partition: Int, writes: List) - : PgWriterBase(writer, collection, partition, writes) -{ +internal class PgWriterUpsert( + pgWriter: PgWriter, + pgCollection: PgCollection, + pgWrites: List, + start: Int, + end: Int +) : PgWriterBase(pgWriter, pgCollection, pgWrites, start, end) { private val writeByTn = mutableMapOf() init { From 7ac9ab3f6933bc4e9632064c8025ac7e90e76b24 Mon Sep 17 00:00:00 2001 From: kkin-here <284318677+kkin-here@users.noreply.github.com> Date: Thu, 25 Jun 2026 10:34:25 +0200 Subject: [PATCH 40/57] Add query converter methods for new members (#607) * Add query converter methods for new members Signed-off-by: kkin-here <284318677+kkin-here@users.noreply.github.com> * Add TagMatches op Signed-off-by: kkin-here <284318677+kkin-here@users.noreply.github.com> --------- Signed-off-by: kkin-here <284318677+kkin-here@users.noreply.github.com> --- .../kotlin/naksha/model/request/ops/Op.kt | 2 + .../model/request/ops/QueryConverter.kt | 275 +++++++++++++++++- .../naksha/model/request/ops/TagMatches.kt | 34 +++ .../model/request/ops/QueryConverterTest.kt | 225 ++++++++++++++ 4 files changed, 532 insertions(+), 4 deletions(-) create mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagMatches.kt create mode 100644 here-naksha-lib-model/src/commonTest/kotlin/naksha/model/request/ops/QueryConverterTest.kt diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Op.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Op.kt index c47fcb9ed..0cd9053af 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Op.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/Op.kt @@ -46,6 +46,7 @@ open class Op : AnyObject() { const val TAG_LT = "tag_lt" const val TAG_LTE = "tag_lte" const val TAG_STARTS_WITH = "tag_starts_with" + const val TAG_MATCHES = "tag_matches" @Suppress("SpellCheckingInspection") const val TAGLIST_CONTAINS = "taglist_contains" @Suppress("SpellCheckingInspection") @@ -83,6 +84,7 @@ open class Op : AnyObject() { TAG_IS_NULL -> op.proxy(TagIsNull::class) TAG_EQ -> op.proxy(TagEquals::class) TAG_STARTS_WITH -> op.proxy(TagStartsWith::class) + TAG_MATCHES -> op.proxy(TagMatches::class) TAG_GT -> op.proxy(TagGt::class) TAG_GTE -> op.proxy(TagGte::class) TAG_LT -> op.proxy(TagLt::class) diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/QueryConverter.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/QueryConverter.kt index ac7055ec4..13dd2589c 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/QueryConverter.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/QueryConverter.kt @@ -1,16 +1,283 @@ -@file:Suppress("OPT_IN_USAGE") +@file:Suppress("OPT_IN_USAGE", "DEPRECATION") package naksha.model.request.ops +import naksha.base.IntList +import naksha.base.JsEnum +import naksha.geo.HereTile +import naksha.model.NakshaError.NakshaErrorCompanion.UNSUPPORTED_OPERATION +import naksha.model.NakshaException +import naksha.model.illegalArg +import naksha.model.objects.Member +import naksha.model.objects.XyzMembers import naksha.model.request.RequestQuery +import naksha.model.request.query.AnyOp +import naksha.model.request.query.DoubleOp +import naksha.model.request.query.IMemberQuery +import naksha.model.request.query.ISpatialQuery +import naksha.model.request.query.ITagQuery +import naksha.model.request.query.MemberAnd +import naksha.model.request.query.MemberNot +import naksha.model.request.query.MemberOr +import naksha.model.request.query.MemberQuery +import naksha.model.request.query.SpAnd +import naksha.model.request.query.SpIntersects +import naksha.model.request.query.SpNot +import naksha.model.request.query.SpOr +import naksha.model.request.query.SpRefInHereTile +import naksha.model.request.query.StringOp +import naksha.model.request.query.TagAnd +import naksha.model.request.query.TagExists +import naksha.model.request.query.TagNot +import naksha.model.request.query.TagOr +import naksha.model.request.query.TagQuery +import naksha.model.request.query.TagSetContains +import naksha.model.request.query.TagValueIsBool +import naksha.model.request.query.TagValueIsDouble +import naksha.model.request.query.TagValueIsNull +import naksha.model.request.query.TagValueIsString +import naksha.model.request.query.TagValueMatches import kotlin.js.JsExport +import naksha.model.request.query.SpBuffer as QuerySpBuffer +import naksha.model.request.query.SpTransformation as QuerySpTransformation +/** + * Converts the deprecated, fixed-column XYZ queries _([RequestQuery])_ into the generalized, + * member-based [operations][Op]. + * + * @since 3.0 + */ @JsExport class QueryConverter private constructor() { companion object PgQueryConverter_C { + /** + * Convert the given [request-query][RequestQuery] into a single [operation][Op]. + * + * @param query the deprecated request-query to convert. + * @return the equivalent [operation][Op]; or `null`, if there is nothing to convert. + * @throws NakshaException with [UNSUPPORTED_OPERATION], if a query has no generalized equivalent. + * @since 3.0 + */ fun convert(query: RequestQuery): Op? { - // TODO: Implement a convertion. - return null + val ops = ArrayList() + query.tags?.let { ops.add(convertTag(it)) } + query.spatial?.let { ops.add(convertSpatial(it)) } + query.members?.let { ops.add(convertMember(it)) } + convertRefTiles(query.refTiles)?.let { ops.add(it) } + return when (ops.size) { + 0 -> null + 1 -> ops[0] + else -> And(*ops.toTypedArray()) + } } + + // --------------------------------------------------------< tags >--------------------------------------------------------- + + /** + * Convert a single _(possibly nested)_ [tag-query][ITagQuery] into an [operation][Op]. + * + */ + private fun convertTag(query: ITagQuery): Op = when (query) { + // Logical combinators become their generalized counterparts. + is TagAnd -> And(*query.filterNotNull().map { convertTag(it) }.toTypedArray()) + is TagOr -> Or(*query.filterNotNull().map { convertTag(it) }.toTypedArray()) + is TagNot -> Not(convertTag(query.query)) + + // Element containment in a tag-list. + is TagSetContains -> { + val element = query.element ?: throw NakshaException( + UNSUPPORTED_OPERATION, "TagSetContains without an element can't be converted" + ) + TagListContains(XyzMembers.XyzTags, element) + } + + // TagExists just tests for the existence of the key/element, ignoring the value. + is TagExists -> TagMapHasKey(XyzMembers.XyzTags, query.name) + + // TagValueIsNull tests that the key exists and is explicitly assigned the value `null`. + is TagValueIsNull -> TagIsNull(XyzMembers.XyzTags, query.name) + + is TagValueIsBool -> TagEquals(XyzMembers.XyzTags, query.name, query.value) + is TagValueIsString -> TagEquals(XyzMembers.XyzTags, query.name, query.value) + is TagValueIsDouble -> convertTagValueIsDouble(query) + + // Regular-expression matching on the tag value. + is TagValueMatches -> TagMatches(XyzMembers.XyzTags, query.name, query.regex) + + is TagQuery -> throw NakshaException( + UNSUPPORTED_OPERATION, "Can't convert tag-query of type '${query::class.simpleName}'" + ) + else -> throw NakshaException( + UNSUPPORTED_OPERATION, "Can't convert tag-query of type '${query::class.simpleName}'" + ) + } + + /** + * Convert a [TagValueIsDouble] into the matching tag operation, depending on its [operation][DoubleOp]. + */ + private fun convertTagValueIsDouble(query: TagValueIsDouble): Op { + val tags = XyzMembers.XyzTags + val name = query.name + val value = query.value + return when (query.op) { + DoubleOp.EQ -> TagEquals(tags, name, value) + DoubleOp.NE -> Not(TagEquals(tags, name, value)) + DoubleOp.GT -> TagGt(tags, name, value) + DoubleOp.GTE -> TagGte(tags, name, value) + DoubleOp.LT -> TagLt(tags, name, value) + DoubleOp.LTE -> TagLte(tags, name, value) + else -> throw NakshaException( + UNSUPPORTED_OPERATION, "Unsupported double operation '${query.op}' in TagValueIsDouble" + ) + } + } + + // -------------------------------------------------------< spatial >------------------------------------------------------- + + /** + * Convert a single _(possibly nested)_ [spatial-query][ISpatialQuery] into an [operation][Op]. + */ + private fun convertSpatial(query: ISpatialQuery): Op = when (query) { + is SpAnd -> And(*query.filterNotNull().map { convertSpatial(it) }.toTypedArray()) + is SpOr -> Or(*query.filterNotNull().map { convertSpatial(it) }.toTypedArray()) + is SpNot -> Not(convertSpatial(query.query)) + is SpIntersects -> { + val transformers = flattenTransformation(query.transformation) + Intersects(XyzMembers.XyzGeometry, query.geometry, *transformers.toTypedArray()) + } + is SpRefInHereTile -> tileRange(query.getHereTile()) + else -> throw NakshaException( + UNSUPPORTED_OPERATION, "Can't convert spatial-query of type '${query::class.simpleName}'" + ) + } + + /** + * Flatten the old single-link transformation chain _(each transformation has an optional + * [child][QuerySpTransformation.childTransformation] executed before it)_ into the ordered list of + * generalized [transformations][SpTransformation] expected by [Intersects], deepest child first. + */ + private fun flattenTransformation(transformation: QuerySpTransformation?): List { + val chain = ArrayList() + var current = transformation + while (current != null) { + chain.add(current) + current = current.childTransformation + } + return chain.asReversed().map { convertTransformation(it) } + } + + /** + * Convert a single old [transformation][QuerySpTransformation] into its generalized counterpart. + */ + private fun convertTransformation(transformation: QuerySpTransformation): SpTransformation = + when (transformation) { + is QuerySpBuffer -> SpBuffer( + distance = transformation.distance, + geography = transformation.geography, + quadSegments = transformation.quadSegments, + joinStyle = transformation.joinStyle?.value?.let { JsEnum.getDefined(it, SpJoinStyle::class) }, + joinLimit = transformation.joinLimit, + endCap = transformation.endCap?.value?.let { JsEnum.getDefined(it, SpEndCap::class) }, + side = transformation.side?.value?.let { JsEnum.getDefined(it, SpSide::class) } + ) + else -> throw NakshaException( + UNSUPPORTED_OPERATION, + "Can't convert spatial transformation of type '${transformation::class.simpleName}'" + ) + } + + // ------------------------------------------------------< ref-tiles >------------------------------------------------------ + + /** + * Convert the deprecated [refTiles][RequestQuery.refTiles] list into a single [operation][Op] that + * matches features whose reference point lies in any of the given tiles; `null`, if the list is empty. + */ + private fun convertRefTiles(refTiles: IntList): Op? { + val ranges = refTiles.filterNotNull().map { tileRange(HereTile(it)) } + return when (ranges.size) { + 0 -> null + 1 -> ranges[0] + else -> Or(*ranges.toTypedArray()) + } + } + + /** + * Build the here-tile range operation `here_tile BETWEEN lower AND upper` for the given [tile] + */ + private fun tileRange(tile: HereTile): Op = And( + Gte(XyzMembers.XyzHereTile, tile.maxLevelLowerBound().intKey), + Lte(XyzMembers.XyzHereTile, tile.maxLevelUpperBound().intKey) + ) + + // -------------------------------------------------------< members >------------------------------------------------------- + + /** + * Convert a single _(possibly nested)_ [member-query][IMemberQuery] into an [operation][Op]. + * + * The queried column is taken from the [member][MemberQuery.member] of the query, so any member can + * be queried. + */ + private fun convertMember(query: IMemberQuery): Op = when (query) { + is MemberAnd -> And(*query.filterNotNull().map { convertMember(it) }.toTypedArray()) + is MemberOr -> Or(*query.filterNotNull().map { convertMember(it) }.toTypedArray()) + is MemberNot -> Not(convertMember(query.query)) + is MemberQuery -> convertMemberQuery(query) + else -> throw NakshaException( + UNSUPPORTED_OPERATION, "Can't convert member-query of type '${query::class.simpleName}'" + ) + } + + /** + * Convert a single [MemberQuery] into the matching operation, depending on its + * [operation][MemberQuery.op]. + */ + private fun convertMemberQuery(query: MemberQuery): Op { + val member: Member = query.member + val value: Any? = query.value + return when (val op = query.op) { + is StringOp -> when (op) { + StringOp.EQUALS -> Equals(member, value) + StringOp.NOT_EQUALS -> Not(Equals(member, value)) + StringOp.STARTS_WITH -> StartsWith( + member, value as? String ?: throw illegalArg("STARTS_WITH requires a string value") + ) + else -> throw NakshaException(UNSUPPORTED_OPERATION, "Unsupported string operation '$op'") + } + is DoubleOp -> when (op) { + DoubleOp.EQ -> Equals(member, value) + DoubleOp.NE -> Not(Equals(member, value)) + DoubleOp.GT -> Gt(member, requireValue(value)) + DoubleOp.GTE -> Gte(member, requireValue(value)) + DoubleOp.LT -> Lt(member, requireValue(value)) + DoubleOp.LTE -> Lte(member, requireValue(value)) + else -> throw NakshaException(UNSUPPORTED_OPERATION, "Unsupported double operation '$op'") + } + AnyOp.IS_NULL -> IsNull(member) + AnyOp.IS_NOT_NULL -> Not(IsNull(member)) + AnyOp.IS_TRUE -> IsTrue(member) + AnyOp.IS_FALSE -> IsFalse(member) + AnyOp.EXISTS -> Not(IsNull(member)) + AnyOp.IS_ANY_OF -> buildIsAnyOf(member, value) + else -> throw NakshaException( + UNSUPPORTED_OPERATION, "Member operation '$op' has no generalized equivalent" + ) + } + } + + /** + * Build an [IsAnyOf] operation from a member and an array/list-shaped [value]. + */ + private fun buildIsAnyOf(member: Member, value: Any?): Op { + val items: List = when (value) { + null -> emptyList() + is List<*> -> value.filterNotNull() + is Array<*> -> value.filterNotNull() + else -> listOf(value) + } + return IsAnyOf(member, *items.toTypedArray()) + } + + private fun requireValue(value: Any?): Any = + value ?: throw illegalArg("The operation requires a non-null value") } -} \ No newline at end of file +} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagMatches.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagMatches.kt new file mode 100644 index 000000000..a2afbb475 --- /dev/null +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/request/ops/TagMatches.kt @@ -0,0 +1,34 @@ +@file:Suppress("OPT_IN_USAGE") + +package naksha.model.request.ops + +import naksha.base.NotNullProperty +import naksha.model.objects.Member +import kotlin.js.JsExport +import kotlin.js.JsName + +/** + * Tests if the value of the tag [key] on the member at [at] matches the given regular expression [regex]. + * @since 3.0 + */ +@JsExport +class TagMatches() : Op() { + companion object TagMatches_C { + private val KEY = NotNullProperty(String::class) { _,_ -> "" } + private val REGEX = NotNullProperty(String::class) { _,_ -> ".*" } + } + + @JsName("forName") + constructor(at: String, key: String, regex: String) : this() { + this.op = TAG_MATCHES + this.at = at + this.key = key + this.regex = regex + } + + @JsName("forMember") + constructor(at: Member, key: String, regex: String) : this(at.name, key, regex) + + var key: String by KEY + var regex: String by REGEX +} diff --git a/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/request/ops/QueryConverterTest.kt b/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/request/ops/QueryConverterTest.kt new file mode 100644 index 000000000..b3cae354f --- /dev/null +++ b/here-naksha-lib-model/src/commonTest/kotlin/naksha/model/request/ops/QueryConverterTest.kt @@ -0,0 +1,225 @@ +@file:Suppress("DEPRECATION") + +package naksha.model.request.ops + +import naksha.geo.HereTile +import naksha.geo.PointCoord +import naksha.geo.SpPoint +import naksha.model.NakshaException +import naksha.model.objects.XyzMembers +import naksha.model.request.RequestQuery +import naksha.model.request.query.AnyOp +import naksha.model.request.query.DoubleOp +import naksha.model.request.query.MemberQuery +import naksha.model.request.query.SpIntersects +import naksha.model.request.query.SpRefInHereTile +import naksha.model.request.query.StringOp +import naksha.model.request.query.TagAnd +import naksha.model.request.query.TagExists +import naksha.model.request.query.TagNot +import naksha.model.request.query.TagSetContains +import naksha.model.request.query.TagValueIsBool +import naksha.model.request.query.TagValueIsDouble +import naksha.model.request.query.TagValueIsNull +import naksha.model.request.query.TagValueIsString +import naksha.model.request.query.TagValueMatches +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertIs +import kotlin.test.assertNull + +class QueryConverterTest { + + private companion object { + const val TAGS = "tags" // XyzMembers.XyzTags.name + const val HERE_TILE = "here_tile" // XyzMembers.XyzHereTile.name + const val GEO = "geo" // XyzMembers.XyzGeometry.name + } + + /** + * Resolves the concrete operation subtype (operations read back from a list/child are typed as the + * [Op] base) and asserts it is of the expected type. + */ + private inline fun Op.expect(): T { + val real = Op.detect(this) ?: error("operation '${op}' is not detectable") + assertIs(real) + return real + } + + private fun convert(query: RequestQuery): Op? = QueryConverter.convert(query) + + private fun tagQuery(query: naksha.model.request.query.ITagQuery): Op = + convert(RequestQuery().apply { tags = query }) ?: error("expected a non-null op") + + // ------------------------------------------------------------< tags >----------------------------------------------------------- + + @Test + fun tagExistsBecomesTagMapHasKey() { + val op = tagQuery(TagExists("sample")).expect() + assertEquals(TAGS, op.at) + assertEquals("sample", op.key) + } + + @Test + fun tagValueIsNullBecomesTagIsNull() { + val op = tagQuery(TagValueIsNull("ref")).expect() + assertEquals(TAGS, op.at) + assertEquals("ref", op.key) + } + + @Test + fun tagValueIsStringBecomesTagEquals() { + val op = tagQuery(TagValueIsString("name", "john")).expect() + assertEquals(TAGS, op.at) + assertEquals("name", op.key) + assertEquals("john", op.value) + } + + @Test + fun tagValueIsBoolBecomesTagEquals() { + val op = tagQuery(TagValueIsBool("flag", true)).expect() + assertEquals("flag", op.key) + assertEquals(true, op.value) + } + + @Test + fun tagValueIsDoubleGtBecomesTagGt() { + val op = tagQuery(TagValueIsDouble("speed", DoubleOp.GT, 5.0)).expect() + assertEquals("speed", op.key) + assertEquals(5.0, op.value) + } + + @Test + fun tagValueIsDoubleNeBecomesNotTagEquals() { + val not = tagQuery(TagValueIsDouble("speed", DoubleOp.NE, 5.0)).expect() + val eq = not.child.expect() + assertEquals("speed", eq.key) + assertEquals(5.0, eq.value) + } + + @Test + fun tagValueMatchesBecomesTagMatches() { + val op = tagQuery(TagValueMatches("code", "^[a-z][0-9]+$")).expect() + assertEquals(TAGS, op.at) + assertEquals("code", op.key) + assertEquals("^[a-z][0-9]+$", op.regex) + } + + @Test + fun tagSetContainsBecomesTagListContains() { + val op = tagQuery(TagSetContains("flag:=true")).expect() + assertEquals(TAGS, op.at) + assertEquals("flag:=true", op.item) + } + + @Test + fun tagAndBecomesAndWithConvertedChildren() { + val and = tagQuery(TagAnd(TagExists("a"), TagExists("b"))).expect() + assertEquals(2, and.children.size) + assertEquals("a", and.children[0]!!.expect().key) + assertEquals("b", and.children[1]!!.expect().key) + } + + @Test + fun tagNotBecomesNot() { + val not = tagQuery(TagNot(TagExists("a"))).expect() + assertEquals("a", not.child.expect().key) + } + + // -----------------------------------------------------------< members >--------------------------------------------------------- + + @Test + fun memberEqualsBecomesEquals() { + val q = RequestQuery().apply { members = MemberQuery(XyzMembers.XyzAppId, StringOp.EQUALS, "app-1") } + val op = convert(q)!!.expect() + assertEquals(XyzMembers.XyzAppId.name, op.at) + assertEquals("app-1", op.value) + } + + @Test + fun memberStartsWithBecomesStartsWith() { + val q = RequestQuery().apply { members = MemberQuery(XyzMembers.XyzAuthor, StringOp.STARTS_WITH, "Jo") } + val op = convert(q)!!.expect() + assertEquals(XyzMembers.XyzAuthor.name, op.at) + assertEquals("Jo", op.value) + } + + @Test + fun memberIsAnyOfBecomesIsAnyOf() { + val q = RequestQuery().apply { members = MemberQuery(XyzMembers.XyzAppId, AnyOp.IS_ANY_OF, arrayOf("a", "b")) } + val op = convert(q)!!.expect() + assertEquals(2, op.items.size) + assertEquals("a", op.items[0]) + assertEquals("b", op.items[1]) + } + + @Test + fun unsupportedMemberOpThrows() { + val q = RequestQuery().apply { members = MemberQuery(XyzMembers.XyzAppId, AnyOp.CONTAINS, "x") } + assertFailsWith { convert(q) } + } + + // -----------------------------------------------------------< spatial >--------------------------------------------------------- + + @Test + fun spatialIntersectsBecomesIntersects() { + val q = RequestQuery().apply { spatial = SpIntersects(SpPoint(PointCoord(1.0, 2.0))) } + val op = convert(q)!!.expect() + assertEquals(GEO, op.at) + } + + @Test + fun spatialRefInHereTileBecomesHereTileRange() { + val tile = HereTile("122010112103") + val q = RequestQuery().apply { spatial = SpRefInHereTile(tile) } + val and = convert(q)!!.expect() + assertEquals(2, and.children.size) + val gte = and.children[0]!!.expect() + val lte = and.children[1]!!.expect() + assertEquals(HERE_TILE, gte.at) + assertEquals(HERE_TILE, lte.at) + assertEquals(tile.maxLevelLowerBound().intKey, gte.value) + assertEquals(tile.maxLevelUpperBound().intKey, lte.value) + } + + // ----------------------------------------------------------< ref-tiles >-------------------------------------------------------- + + @Test + fun refTilesBecomeOrOfRanges() { + val a = HereTile("122010112103") + val b = HereTile("122010322102") + val q = RequestQuery().apply { refTiles += listOf(a.intKey, b.intKey) } + val or = convert(q)!!.expect() + assertEquals(2, or.children.size) + or.children[0]!!.expect() + or.children[1]!!.expect() + } + + @Test + fun singleRefTileBecomesSingleRange() { + val a = HereTile("122010112103") + val q = RequestQuery().apply { refTiles += a.intKey } + // A single tile is not wrapped in an OR. + convert(q)!!.expect() + } + + // -----------------------------------------------------------< combine >--------------------------------------------------------- + + @Test + fun multipleCategoriesAreAndCombined() { + val q = RequestQuery().apply { + tags = TagExists("a") + members = MemberQuery(XyzMembers.XyzAppId, StringOp.EQUALS, "app-1") + } + val and = convert(q)!!.expect() + assertEquals(2, and.children.size) + and.children[0]!!.expect() + and.children[1]!!.expect() + } + + @Test + fun emptyQueryConvertsToNull() { + assertNull(convert(RequestQuery())) + } +} From 4e9de953fdfcf801c284f355b627ce356b264cf9 Mon Sep 17 00:00:00 2001 From: phmai Date: Thu, 25 Jun 2026 10:55:33 +0200 Subject: [PATCH 41/57] implement TagList ops Signed-off-by: phmai --- .../kotlin/naksha/psql/PgQueryWhereBuilder.kt | 51 +++++++++++++++---- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt index af3192616..a355a33dd 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt @@ -57,6 +57,7 @@ internal class PgQueryWhereBuilder(private val request: ReadFeatures, private va for (child in children) { if (child == null) continue if (first) first = false else where.append(" AND ") + // TODO optimization if only TagMapHasKey applyOp(child) } if (children.size > 1) where.append(") ") else where.append(" ") @@ -70,6 +71,7 @@ internal class PgQueryWhereBuilder(private val request: ReadFeatures, private va for (child in children) { if (child == null) continue if (first) first = false else where.append(" OR ") + // TODO optimization if only TagMapHasKey applyOp(child) } if (children.size > 1) where.append(") ") else where.append(" ") @@ -217,13 +219,44 @@ internal class PgQueryWhereBuilder(private val request: ReadFeatures, private va .append(", ").append(placeholderForArg(value, pgType)).append(") ") } is TagListContains -> { - // TODO: Implement me + val pgType = PgType.ofValue(op.item) + ?: throw illegalArg("The given value is no valid argument for ${op.op}}: ${op.item}") + val value = pgType.convertValue(op.item) + // [NOT ]foo::jsonb @> $1::jsonb + val placeholder = placeholderForArg(value, pgType) + if (negate) where.append("NOT ") + where.append(at).append("::jsonb @> ").append(placeholder).append("::jsonb ") } is TagListContainsAllOf -> { - // TODO: Implement me + val pgType = PgType.ofValue(op.items) + ?: throw illegalArg("The given value is no valid argument for ${op.op}}: ${op.items}") + val value = pgType.convertValue(op.items) + val placeholder = placeholderForArg(toJSON(value), pgType) + if (negate) where.append("NOT ") + where.append(at).append("::jsonb @> ").append(placeholder).append("::jsonb ") } is TagListContainsAnyOf -> { - // TODO: Implement me + val items = op.items.filterNotNull() + // Any-of over empty set -> false; negated -> true + if (items.isEmpty()) { + if (negate) where.append("TRUE ") else where.append("FALSE ") + return + } + + if (negate) where.append("NOT ") + // Multiple items: build OR of single-element containment checks + where.append('(') + val pgType = PgType.ofValue(op.items.first()) + ?: throw illegalArg("The given value is no valid argument for ${op.op}}: ${op.items.first()}") + var first = true + for (item in items) { + if (!first) where.append(" OR ") + first = false + val value = pgType.convertValue(item) + val placeholder = placeholderForArg(value, pgType) + where.append(at).append("::jsonb @> ").append(placeholder).append("::jsonb") + } + where.append(") ") } is Intersects -> { // TODO: Implement me @@ -325,12 +358,12 @@ internal class PgQueryWhereBuilder(private val request: ReadFeatures, private va // val version = request.version // if (version != null) { // if (where.isNotEmpty()) where.append(" AND ") -// where.append("$VERSION <= ${version.number}") +// where.append("$VERSION <= ${version.toInt()}") // } // val minVersion = request.minVersion // if (minVersion != null) { // if (where.isNotEmpty()) where.append(" AND ") -// where.append("$VERSION >= ${minVersion.number}") +// where.append("$VERSION >= ${minVersion.toInt()}") // } // } // @@ -370,7 +403,7 @@ internal class PgQueryWhereBuilder(private val request: ReadFeatures, private va // null -> queryGeometry // else -> resolveTransformation(transformation, queryGeometry) // } -// where.append("ST_Intersects(naksha_2d(${PgColumn.geo}), $geometryToCompare)") +// where.append("ST_Intersects(naksha_2d(${StandardMembers.Geometry}), $geometryToCompare)") // } // // is SpRefInHereTile -> { @@ -481,7 +514,7 @@ internal class PgQueryWhereBuilder(private val request: ReadFeatures, private va // hereTile.maxLevelUpperBound().intKey, // PgType.INT // ) -// return "(${PgColumn.here_tile} >= $lowerBoundPlaceholder AND ${PgColumn.here_tile} <= $upperBoundPlaceholder)" +// return "(${StandardMembers.HereTile} >= $lowerBoundPlaceholder AND ${StandardMembers.HereTile} <= $upperBoundPlaceholder)" // } // // private fun whereMetadata() { @@ -518,7 +551,7 @@ internal class PgQueryWhereBuilder(private val request: ReadFeatures, private va // val isActionQuery = metaQuery.member == MetaColumn.action() // val pgColumn = // if (isActionQuery) { -// PgColumn.version +// StandardMembers.Version // } else { // PgColumn.ofRowColumn(metaQuery.member) ?: throw NakshaException( // NakshaError.ILLEGAL_STATE, @@ -543,7 +576,7 @@ internal class PgQueryWhereBuilder(private val request: ReadFeatures, private va // val placeholder = placeholderForArg(metaQuery.value, placeholderType) // resolveDoubleOp(op, leftOperand, placeholder) // } -// AnyOp.IS_ANY_OF -> { +// is AnyOp.IS_ANY_OF -> { // val placeholder = placeholderForArg(metaQuery.value, arrayTypeFor(placeholderType)) // "$leftOperand = ANY($placeholder)" // } From 192293caba47e086a40b3f4ebbae93ecc8655429 Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Thu, 25 Jun 2026 11:27:13 +0200 Subject: [PATCH 42/57] Fix id usage in delete, fix insert Signed-off-by: Alexander Lowey-Weber --- .../kotlin/naksha/psql/PgCollection.kt | 4 +- .../commonMain/kotlin/naksha/psql/PgRows.kt | 17 +++-- .../kotlin/naksha/psql/PgWriterBase.kt | 41 ++++++++++++ .../kotlin/naksha/psql/PgWriterDelete.kt | 67 ++++++++----------- .../kotlin/naksha/psql/PgWriterInsert.kt | 59 ++++++++-------- .../kotlin/naksha/psql/PgWriterUpdate.kt | 25 +++---- .../kotlin/naksha/psql/PgWriterUpsert.kt | 20 ++---- 7 files changed, 129 insertions(+), 104 deletions(-) diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt index 0f7e3d8e1..0f661a7f9 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt @@ -152,16 +152,18 @@ open class PgCollection internal constructor( /** * Join the identities of all [columns], separated by comma, optionally filtered by the given filter. + * @param prefix an optional prefix to be added in front of each column being joined. * @param filter an optional filter method to remove _(or replace)_ certain columns. * @return a comma separated list of [ident][PgColumn.ident] strings. * @since 3.0 */ - fun joinColumns(filter: Fn1? = null): String { + fun joinColumns(prefix: String? = null, filter: Fn1? = null): String { val sb = StringBuilder() for (column in columns) { val ident: String? = if (filter != null) filter.call(column) else column.ident if (ident == null) continue if (sb.isNotEmpty()) sb.append(", ") + if (prefix != null) sb.append(prefix) sb.append(ident) } return sb.toString() diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRows.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRows.kt index a9900dfea..55bacc9a4 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRows.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgRows.kt @@ -407,13 +407,15 @@ internal class PgRows { /** * Returns the placeholders of all columns as comma separated string _($1, $2, ...)_, usage: * - * ```sql - * WITH new_row AS ( + * ```kotlin + * val sql = """WITH new_row AS ( * SELECT * FROM UNNEST(${rows.placeholders()}) * AS t(${rows.names()}) * ) - * INSERT INTO ${collection.head.quotedName} (${rows.names()}) - * SELECT * FROM new_row + * INSERT INTO ${collection.head.quotedName} (${rows.aliases()}) + * SELECT * FROM new_row""" + * val plan = conn.prepare(sql, rows.typeNames()) + * val cursor = plan.execute(rows.values()) * ``` * * @return the placeholders of all columns as comma separated string. @@ -440,9 +442,10 @@ internal class PgRows { * SELECT * FROM UNNEST(${rows.placeholders()}) * AS t(${rows.names()}) * ) - * INSERT INTO ${collection.head.quotedName} (${rows.names()}) + * INSERT INTO ${collection.head.quotedName} (${rows.aliases()}) * SELECT * FROM new_row""" * val plan = conn.prepare(sql, rows.typeNames()) + * val cursor = plan.execute(rows.values()) * ``` * @return the array type-names of all columns. * @since 3.0 @@ -457,10 +460,10 @@ internal class PgRows { * SELECT * FROM UNNEST(${rows.placeholders()}) * AS t(${rows.names()}) * ) - * INSERT INTO ${collection.head.quotedName} (${rows.names()}) + * INSERT INTO ${collection.head.quotedName} (${rows.aliases()}) * SELECT * FROM new_row""" * val plan = conn.prepare(sql, rows.typeNames()) - * val cursor = plan.execute(rows.valuesExecutable()) + * val cursor = plan.execute(rows.values()) * ``` * Beware that the array really is two-dimensional: `Array>`. * @return the values of all columns as `Array`. diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterBase.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterBase.kt index ec53835bc..b41b66ec1 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterBase.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterBase.kt @@ -2,8 +2,18 @@ package naksha.psql import naksha.base.Int64 import naksha.base.IntMutable +import naksha.base.fn.Fn1 +import naksha.base.fn.Fx1 +import naksha.base.fn.Fx2 +import naksha.base.fn.Fx3 +import naksha.model.Tuple +import naksha.model.TupleNumber import naksha.model.Version +import naksha.model.illegalArg +import naksha.model.illegalState import naksha.model.objects.NakshaTx +import naksha.model.objects.StandardMembers +import kotlin.collections.mutableMapOf /** * Base class for all operations, so for: @@ -55,6 +65,19 @@ internal abstract class PgWriterBase protected constructor( val collectionNumber: Int get() = pgCollection.collectionNumber + /** The _HEAD_ table. */ + val headTable = pgCollection.headTable + /** The quoted name of the _HEAD_ table. */ + val headIdent = headTable.quotedName + /** The _HISTORY_ table or `null`, if _HISTORY_ is disabled. */ + val historyTable = if (pgCollection.storeHistory) pgCollection.historyTable else null + /** The quoted name of the _HISTORY_ table or `null`, if _HISTORY_ is disabled. */ + val historyIdent = historyTable?.quotedName + /** The `id` column */ + val ID: PgColumn = pgCollection.column(StandardMembers.Id) ?: throw illegalState("The collection does not have an 'id' column.") + /** The change-count column, if there is any defined. */ + val CC: PgColumn? = pgCollection.column(StandardMembers.ChangeCount) + /** * The transaction to operate upon. * @since 3.0 @@ -129,6 +152,24 @@ internal abstract class PgWriterBase protected constructor( return doExecute(conn) } + /** + * Add all tuple from [pgWrites], expects that the columns are prepared. + * @param lambda a lambda optionally being called after every imported tuple, with `row`, `tuple` and `pgWrite` as arguments. + * @return the number of rows loaded. + */ + protected fun loadAllTuple(lambda: Fx3? = null): Int { + var row = 0 + inRows.setMinRows(inRows.size + (end - start)) + for (i in start ..< end) { + val pgWrite = pgWrites[i] + val tuple = pgWrite.tuple ?: throw illegalArg("The write #$i has no tuple, failed to load all tuple") + inRows[row] = tuple + lambda?.call(row, tuple, pgWrite) + row++ + } + return row + } + /** * Execute the operation. * @param conn the connection to be used. diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterDelete.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterDelete.kt index 6fc65b65e..08633a3af 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterDelete.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterDelete.kt @@ -38,46 +38,41 @@ internal class PgWriterDelete( val purge: Boolean = false ) : PgWriterBase(pgWriter, pgCollection, pgWrites, start, end) { init { - inRows.addColumn("id", MemberType.STRING) + inRows.addColumn(FN.ident, MemberType.INT64) inRows.addColumn("expected_version", MemberType.INT64) var row = 0 for (i in start until end) { val pgWrite = pgWrites[i] - inRows.set(row, "id", pgWrite.id) + inRows.set(row, FN.ident, pgWrite.tupleNumber!!.featureNumber) inRows.set(row, "expected_version", pgWrite.version?.number) row++ } check(row == (end-start)) } - val headTable = pgCollection.headTable - val historyTable = if (pgCollection.storeHistory) pgCollection.historyTable else null - val ID: PgColumn = pgCollection.column(StandardMembers.Id) ?: throw illegalState("The collection does not have an 'id' column.") - val CC: PgColumn? = pgCollection.column(StandardMembers.ChangeCount) - private fun plan(conn: PgConnection): PgWriterPlan { // The new version with action bits set to DELETED (2). val deleted_version = "(${tx.version.number}::int8 | 2)" // All input provided by client, `id` and optionally `expected_version` val query = """WITH query AS ( - SELECT * FROM UNNEST($1, $2) AS t($ID, expected_version) + SELECT * FROM UNNEST($1, $2) AS t($FN, expected_version) )""" // Select id and version of all rows matching query.id — used for conflict detection. val head_select = """, head_select AS ( - SELECT head.$ID AS $ID, head.$FN AS $FN, head.$VERSION AS $VERSION - FROM ${headTable.quotedName} AS head, query - WHERE head.$ID = query.$ID + SELECT head.$FN AS $FN, head.$VERSION AS $VERSION + FROM $headIdent AS head, query + WHERE head.$FN = query.$FN )""" // Select the HEAD rows to act on: // - Optional atomic version check (expected_version). // - Skip rows already deleted ((version & 3) >= 2) — idempotent. val head_row = """, head_row AS ( - SELECT ${pgCollection.columns.joinToString(", ") { column -> "head.$column AS $column" }} - FROM ${headTable.quotedName} AS head, query - WHERE head.$ID = query.$ID + SELECT ${pgCollection.joinColumns { column -> "head.$column AS $column" }} + FROM $headIdent AS head, query + WHERE head.$FN = query.$FN AND (query.expected_version IS NULL OR (query.expected_version & -4) = (head.$VERSION & -4)) AND (head.$VERSION & 3) < 2 )""" @@ -85,32 +80,28 @@ internal class PgWriterDelete( // Archive the current HEAD row into history (identical to how UPDATE does it). // next_version = the new deleted version, signaling "succeeded by a deletion". val head_to_history = if (historyTable != null) """, head_to_history AS ( - INSERT INTO ${historyTable.quotedName} ($NEXT_VERSION, ${pgCollection.joinColumns { if (it.name != NEXT_VERSION.name) it.ident else null }}) + INSERT INTO $historyIdent ($NEXT_VERSION, ${pgCollection.joinColumns { if (it.name != NEXT_VERSION.name) it.ident else null }}) SELECT $deleted_version AS $NEXT_VERSION, ${pgCollection.joinColumns { column -> if (column eq NEXT_VERSION) null else "head_row.$column AS $column" }} FROM head_row - RETURNING $ID, $FN, $VERSION + RETURNING $FN, $VERSION )""" else "" // For DELETE: UPDATE version (action bits = DELETED) and cc in HEAD. // Only these two control-columns change; all data columns remain identical. val head_updated = if (!purge) """, head_updated AS ( - UPDATE ${headTable.quotedName} - SET $VERSION = $deleted_version - ${if (CC!=null) ", $CC = ${headTable.quotedName}.$CC + 1" else ""} + UPDATE $headIdent + SET $VERSION = $deleted_version${if (CC!=null) ", $CC = $headIdent.$CC + 1" else ""} FROM head_row - WHERE ${headTable.quotedName}.$FN = head_row.$FN - RETURNING ${headTable.quotedName}.$ID - , ${headTable.quotedName}.$FN - , ${headTable.quotedName}.$VERSION -${if (CC!=null) " , ${headTable.quotedName}.$CC" else ""} + WHERE $headIdent.$FN = head_row.$FN + RETURNING $headIdent.$FN, $headIdent.$VERSION${if (CC!=null) ", $headIdent.$CC" else ""} )""" else "" // For PURGE: DELETE the HEAD row entirely. val head_deleted = if (purge) """, head_deleted AS ( - DELETE FROM ${headTable.quotedName} + DELETE FROM $headIdent WHERE ($FN, $VERSION) IN (SELECT $FN, $VERSION FROM head_row) - RETURNING $ID, $FN, $VERSION + RETURNING $FN, $VERSION )""" else "" // For PURGE only: also write a tombstone record into history to explicitly mark @@ -122,7 +113,7 @@ ${if (CC!=null) " , ${headTable.quotedName}.$CC" else ""} SELECT $deleted_version AS $VERSION, $deleted_version AS $NEXT_VERSION, ${pgCollection.joinColumns { column -> if (VERSION eq column || NEXT_VERSION eq column) null else "head_row.$column AS $column" }} FROM head_row - RETURNING $ID, $FN, $VERSION + RETURNING $FN, $VERSION )""" else "" // The returned row for DELETE is the updated HEAD row (same data, new version/cc). @@ -140,15 +131,15 @@ SELECT head_select.$VERSION AS select_version, head_row.$VERSION AS head_version, ${if (!purge) "head_updated.$VERSION AS deleted_version," else "head_deleted.$VERSION AS deleted_version,"} - query.$ID AS query_id, + query.$FN AS query_fn, query.expected_version AS query_expected_version FROM query -LEFT JOIN head_row ON head_row.$ID = query.$ID -${if (!purge) "LEFT JOIN head_updated ON head_updated.$ID = query.$ID" else ""} -${if (head_to_history.isNotEmpty()) "LEFT JOIN head_to_history ON head_to_history.$ID = query.$ID" else ""} -${if (history_tombstone.isNotEmpty()) "LEFT JOIN history_tombstone ON history_tombstone.$ID = query.$ID" else ""} -LEFT JOIN head_select ON head_select.ID = query.$ID -${if (purge) "LEFT JOIN head_deleted ON head_deleted.$ID = query.$ID" else ""} +LEFT JOIN head_row ON head_row.$FN = query.$FN +${if (!purge) "LEFT JOIN head_updated ON head_updated.$FN = query.$FN" else ""} +${if (head_to_history.isNotEmpty()) "LEFT JOIN head_to_history ON head_to_history.$FN = query.$FN" else ""} +${if (history_tombstone.isNotEmpty()) "LEFT JOIN history_tombstone ON history_tombstone.$FN = query.$FN" else ""} +LEFT JOIN head_select ON head_select.$FN = query.$ID +${if (purge) "LEFT JOIN head_deleted ON head_deleted.$FN = query.$FN" else ""} ;""" val typeNames = inRows.typeNames() val pgPlan = conn.prepare(SQL, typeNames) @@ -175,7 +166,7 @@ ${if (purge) "LEFT JOIN head_deleted ON head_deleted.$ID = query.$ID" else ""} .addColumn("select_version", MemberType.INT64) .addColumn("head_version", MemberType.INT64) .addColumn("deleted_version", MemberType.INT64) - .addColumn("query_id", MemberType.STRING) + .addColumn("query_fn", MemberType.INT64) .addColumn("query_expected_version", MemberType.INT64) val plan = plan(conn) val array = inRows.values() @@ -201,7 +192,7 @@ ${if (purge) "LEFT JOIN head_deleted ON head_deleted.$ID = query.$ID" else ""} outRows.readAll(cursor) for (row in 0 until outRows.size) { val write = pgWrites[row] - val id = outRows.getString(row, "query_id") ?: throw generalException("Missing 'query_id' in result") + val fn = outRows.getString(row, "query_fn") ?: throw generalException("Missing 'query_fn' in result") val tuple = outRows[row] if (tuple != null) write.tuple = tuple @@ -218,7 +209,7 @@ ${if (purge) "LEFT JOIN head_deleted ON head_deleted.$ID = query.$ID" else ""} if (select_fn == null || select_version == null) { if (write.version != null) { throw featureNotFound( - "Expected feature '$id' in version '${write.version}', but no such feature exists" + "Expected feature '$fn' in version '${write.version}', but no such feature exists" ) } continue @@ -226,7 +217,7 @@ ${if (purge) "LEFT JOIN head_deleted ON head_deleted.$ID = query.$ID" else ""} val head_version = outRows.getInt64(row, "head_version") if (head_version == null || select_version != head_version) { throw conflict( - "The feature '$id' was expected in version '${write.version}', but found in '${Version(select_version)}'" + "The feature '$fn' was expected in version '${write.version}', but found in '${Version(select_version)}'" ) } } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterInsert.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterInsert.kt index 4c8f6003f..5cf107eb9 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterInsert.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterInsert.kt @@ -6,6 +6,9 @@ import naksha.base.PlatformUtil import naksha.model.illegalState import naksha.model.objects.StandardMembers import naksha.model.objects.StoreMode +import naksha.psql.PgColumn.PgColumn_C.FN +import naksha.psql.PgColumn.PgColumn_C.NEXT_VERSION +import naksha.psql.PgColumn.PgColumn_C.VERSION /** * Execute an **INSERT** _(aka [CREATE][naksha.model.request.WriteOp.CREATE])_ into a collection. @@ -25,68 +28,62 @@ internal class PgWriterInsert( start: Int, end: Int ) : PgWriterBase(pgWriter, pgCollection, pgWrites, start, end) { + init { inRows.addColumns(pgCollection.columns) + loadAllTuple() } - val headTable = pgCollection.headTable - val historyTable = if (pgCollection.storeHistory) pgCollection.historyTable else null - val ID: PgColumn = pgCollection.column(StandardMembers.Id) ?: throw illegalState("The collection does not have an 'id' column.") - val CC: PgColumn? = pgCollection.column(StandardMembers.ChangeCount) - private fun plan(conn: PgConnection): PgWriterPlan { val new_row = """WITH new_row AS ( SELECT * FROM UNNEST(${inRows.placeholders()}) AS t(${inRows.aliases()}) )""" // Detect any existing tombstone in HEAD for the same id (auto-purge target). - val effectiveHead = collection.effectiveHeadColumns val head_tombstone = """, head_tombstone AS ( - SELECT ${effectiveHead.joinToString(", ") { "head.${it.name} AS ${it.name}" }} - FROM ${headTable.quotedName} AS head - JOIN new_row ON head.id = new_row.id - WHERE (head.version & 3) >= 2 + SELECT ${pgCollection.joinColumns { column -> "head.$column AS $column" }} + FROM $headIdent AS head + JOIN new_row ON head.$FN = new_row.$FN + WHERE (head.$VERSION & 3) >= 2 )""" // Archive the tombstone into history so the deletion is preserved in the audit trail. // next_version = new feature's version (the tombstone is succeeded by the new creation). - val effCopyHistory = collection.effectiveCopyIntoHistoryColumns - val tombstone_to_history = if (insert_into_history != null) """, tombstone_to_history AS ( - INSERT INTO ${insert_into_history.quotedName} (${PgColumn.next_version}, ${effCopyHistory.joinToString(",") { it.name }}) - SELECT new_row.version AS ${PgColumn.next_version}, - ${effCopyHistory.joinToString(", ") { "head_tombstone.${it.name} AS ${it.name}" }} + val tombstone_to_history = if (historyTable != null) """, tombstone_to_history AS ( + INSERT INTO $historyIdent ($NEXT_VERSION, ${pgCollection.joinColumns { column -> if (NEXT_VERSION eq column) null else column.ident }}) + SELECT new_row.$VERSION AS $NEXT_VERSION, + ${pgCollection.joinColumns { column -> if (NEXT_VERSION eq column) null else "head_tombstone.$column" }} FROM head_tombstone - JOIN new_row ON new_row.id = head_tombstone.id - RETURNING id, fn, version + JOIN new_row ON new_row.$FN = head_tombstone.$FN + RETURNING $FN, $VERSION )""" else "" - // Overwrite the tombstone in HEAD with the new feature (UPDATE in-place, keeps the same fn). + // Overwrite the tombstone in HEAD (except for fn) with the new feature (UPDATE in-place). // All columns are replaced; cc resets to 1 for the new lifecycle. val head_overwrite = """, head_overwrite AS ( - UPDATE ${headTable.quotedName} - SET ${effectiveHead.filter { it !== PgColumn.fn }.joinToString(", ") { "${it.name} = new_row.${it.name}" }} + UPDATE $headIdent + SET ${pgCollection.joinColumns { column -> if (column eq FN) null else "$column = new_row.$column" }} FROM new_row - JOIN head_tombstone ON head_tombstone.id = new_row.id - WHERE ${headTable.quotedName}.fn = head_tombstone.fn - RETURNING ${headTable.quotedName}.id, ${headTable.quotedName}.fn, ${headTable.quotedName}.version + JOIN head_tombstone ON head_tombstone.$FN = new_row.$FN + WHERE $headIdent.$FN = head_tombstone.$FN + RETURNING $headIdent.$FN, $headIdent.$VERSION )""" // Plain INSERT for features that have no tombstone in HEAD (the normal case). val head_inserted = """, head_inserted AS ( - INSERT INTO ${headTable.quotedName} (${inRows.aliases()}) + INSERT INTO $headIdent (${inRows.aliases()}) SELECT * FROM new_row - WHERE new_row.id NOT IN (SELECT id FROM head_tombstone) - RETURNING id, fn, version + WHERE new_row.$FN NOT IN (SELECT $FN FROM head_tombstone) + RETURNING $FN, $VERSION )""" val SQL = """$new_row$head_tombstone${tombstone_to_history}$head_overwrite$head_inserted SELECT - COALESCE(head_overwrite.id, head_inserted.id) AS id, - COALESCE(head_overwrite.fn, head_inserted.fn) AS fn, - COALESCE(head_overwrite.version, head_inserted.version) AS version + COALESCE(head_overwrite.$FN, head_inserted.$FN) AS $FN, + COALESCE(head_overwrite.$VERSION, head_inserted.$VERSION) AS $VERSION FROM new_row -LEFT JOIN head_overwrite ON head_overwrite.id = new_row.id -LEFT JOIN head_inserted ON head_inserted.id = new_row.id +LEFT JOIN head_overwrite ON head_overwrite.$FN = new_row.$FN +LEFT JOIN head_inserted ON head_inserted.$FN = new_row.$FN """ val typeNames = inRows.typeNames() val pgPlan = conn.prepare(SQL, typeNames) diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt index 00c275043..56b35c2e2 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt @@ -4,6 +4,7 @@ import naksha.base.Platform import naksha.base.Platform.PlatformCompanion.logger import naksha.base.PlatformUtil import naksha.model.* +import naksha.model.objects.MemberType import naksha.model.objects.StandardMembers import naksha.model.objects.StoreMode @@ -19,25 +20,21 @@ internal class PgWriterUpdate( start: Int, end: Int ) : PgWriterBase(pgWriter, pgCollection, pgWrites, start, end) { + + val headTable = pgCollection.headTable + val historyTable = if (pgCollection.storeHistory) pgCollection.historyTable else null + val ID: PgColumn = pgCollection.column(StandardMembers.Id) ?: throw illegalState("The collection does not have an 'id' column.") + val CC: PgColumn? = pgCollection.column(StandardMembers.ChangeCount) private val writeById = mutableMapOf() init { - inRows.addColumns(collection.effectiveHeadColumns) + inRows.addColumns(pgCollection.columns) // Separate column for the expected/atomic version, because `version` itself // is now a real HEAD column carrying the new tuple's version. - inRows.addColumn("expected_version", PgType.INT64) // needed to do atomic updates - val members = collection.head.members - inRows.addCustomMembers(members) - var i = 0 - for (write in writes) { - val tuple = write.tuple - if (tuple != null) { - writeById[write.id] = write - inRows[i] = tuple - inRows.set(i, "expected_version", write.version?.number) - inRows.setCustomMembers(i, write.feature, members) - i++ - } + inRows.addColumn("expected_version", MemberType.INT64) // needed to do atomic updates + loadAllTuple { row, _, pgWrite -> + writeById[pgWrite.id] = pgWrite + inRows.set(row, "expected_version", pgWrite.version?.number) } } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt index 65ae99579..e8b20dee5 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt @@ -19,22 +19,16 @@ internal class PgWriterUpsert( start: Int, end: Int ) : PgWriterBase(pgWriter, pgCollection, pgWrites, start, end) { + + val headTable = pgCollection.headTable + val historyTable = if (pgCollection.storeHistory) pgCollection.historyTable else null + val ID: PgColumn = pgCollection.column(StandardMembers.Id) ?: throw illegalState("The collection does not have an 'id' column.") + val CC: PgColumn? = pgCollection.column(StandardMembers.ChangeCount) private val writeByTn = mutableMapOf() init { - inRows.addColumns(collection.effectiveHeadColumns) - val members = collection.head.members - inRows.addCustomMembers(members) - var i = 0 - for (write in writes) { - val tuple = write.tuple - if (tuple != null) { - inRows[i] = tuple - inRows.setCustomMembers(i, write.feature, members) - writeByTn[tuple.tupleNumber] = write - i++ - } - } + inRows.addColumns(pgCollection.columns) + loadAllTuple { _, tuple, pgWrite -> writeByTn[tuple.tupleNumber] = pgWrite } } private fun plan(conn: PgConnection, collection: PgCollection): PgWriterPlan { From 7c53c4ccadc4240ba980bf293b0d0856231ab693 Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Thu, 25 Jun 2026 14:27:03 +0200 Subject: [PATCH 43/57] Fix parts of upsert, minor improvements to members and heap book. Signed-off-by: Alexander Lowey-Weber --- .../commonMain/kotlin/naksha/jbon/HeapBook.kt | 37 +++ .../naksha/model/objects/StandardMembers.kt | 24 +- .../kotlin/naksha/model/objects/XyzMembers.kt | 6 +- .../naksha/model/request/ReadFeatures.kt | 1 - .../kotlin/naksha/psql/PgCollection.kt | 6 +- .../kotlin/naksha/psql/PgWriterUpsert.kt | 261 +++++++++--------- 6 files changed, 180 insertions(+), 155 deletions(-) diff --git a/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/HeapBook.kt b/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/HeapBook.kt index 6d8ecbc5c..43caf0fd9 100644 --- a/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/HeapBook.kt +++ b/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/HeapBook.kt @@ -4,7 +4,9 @@ package naksha.jbon import naksha.base.Int64 import kotlin.js.JsExport +import kotlin.js.JsStatic import kotlin.jvm.JvmOverloads +import kotlin.jvm.JvmStatic /** * A mutable [IBook] implementation on the Java _HEAP_. @@ -14,6 +16,41 @@ import kotlin.jvm.JvmOverloads class HeapBook( override var bookType: BookType ) : IBook { + companion object HeapBook_C { + /** + * Creates a copy of the given book. + * @param other The [IBook] to make a copy of. + * @param databaseNumber The database-number of the copy, if different. + * @param featureNumber The feature-number of the copy, if different. + * @return a new [HeapBook] with the same entries. + * @since 3.0.0 + */ + @JvmStatic + @JsStatic + @JvmOverloads + fun copyOf(other: IBook, databaseNumber: Int64? = other.databaseNumber, featureNumber: Int64? = other.featureNumber): HeapBook { + val c = HeapBook(other.bookType) + c.id = other.id + c.databaseNumber = other.databaseNumber + c.featureNumber = other.featureNumber + if (other is HeapBook) { + c._names.addAll(other._names) + c._values.addAll(other._values) + for ((name,index) in other._nameIndex) c._nameIndex[name] = index + } else { + val namesLen = other.namesLength() + for (i in 0..(String::class) private val STRING_LIST = NotNullProperty(StringList::class) { _, _ -> StringList() } private val BOOLEAN_OR_FALSE = NotNullProperty(Boolean::class) { _, _ -> false } - private val INT_OR_1 = NotNullProperty(Int::class) { _, _ -> 1 } private val ORDER_BY_OR_NULL = NullableProperty(OrderBy::class) private val GUID_LIST = NotNullProperty(GuidList::class) private val QUERY = NotNullProperty(RequestQuery::class) diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt index 0f661a7f9..b00e22d2c 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt @@ -153,14 +153,14 @@ open class PgCollection internal constructor( /** * Join the identities of all [columns], separated by comma, optionally filtered by the given filter. * @param prefix an optional prefix to be added in front of each column being joined. - * @param filter an optional filter method to remove _(or replace)_ certain columns. + * @param toIdent an optional convertion lambda that turns the column into a string; if it returns `null`, the column is skipped. * @return a comma separated list of [ident][PgColumn.ident] strings. * @since 3.0 */ - fun joinColumns(prefix: String? = null, filter: Fn1? = null): String { + fun joinColumns(prefix: String? = null, toIdent: Fn1? = null): String { val sb = StringBuilder() for (column in columns) { - val ident: String? = if (filter != null) filter.call(column) else column.ident + val ident: String? = if (toIdent != null) toIdent.call(column) else column.ident if (ident == null) continue if (sb.isNotEmpty()) sb.append(", ") if (prefix != null) sb.append(prefix) diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt index e8b20dee5..ce07464cf 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt @@ -1,11 +1,16 @@ package naksha.psql +import naksha.base.Int64 import naksha.base.Platform import naksha.base.Platform.PlatformCompanion.logger import naksha.base.PlatformUtil +import naksha.jbon.HeapBook import naksha.model.* +import naksha.model.objects.MemberType import naksha.model.objects.StandardMembers -import naksha.model.objects.StoreMode +import naksha.psql.PgColumn.PgColumn_C.FN +import naksha.psql.PgColumn.PgColumn_C.NEXT_VERSION +import naksha.psql.PgColumn.PgColumn_C.VERSION /** * Execute [UPSERT][naksha.model.request.WriteOp.UPSERT] into a collection. @@ -20,20 +25,17 @@ internal class PgWriterUpsert( end: Int ) : PgWriterBase(pgWriter, pgCollection, pgWrites, start, end) { - val headTable = pgCollection.headTable - val historyTable = if (pgCollection.storeHistory) pgCollection.historyTable else null - val ID: PgColumn = pgCollection.column(StandardMembers.Id) ?: throw illegalState("The collection does not have an 'id' column.") - val CC: PgColumn? = pgCollection.column(StandardMembers.ChangeCount) - private val writeByTn = mutableMapOf() - + // All columns that are no BYTE_ARRAY and that care not CC, FN, VERSION or NEXT_VERSION + private val copyCols = pgCollection.columns.filter { !(it eq CC || it eq FN || it eq VERSION || it eq NEXT_VERSION || it.memberType != MemberType.BYTE_ARRAY) } + // All columns that are BYTE_ARRAYs (can be empty) + private val byteArrayCols = pgCollection.columns.filter { it.memberType == MemberType.BYTE_ARRAY } + private val writeByFn = mutableMapOf() init { inRows.addColumns(pgCollection.columns) - loadAllTuple { _, tuple, pgWrite -> writeByTn[tuple.tupleNumber] = pgWrite } + loadAllTuple { _, tuple, pgWrite -> writeByFn[tuple.tupleNumber.featureNumber] = pgWrite } } private fun plan(conn: PgConnection, collection: PgCollection): PgWriterPlan { - val insert_into_history = if (historyTable != null && collection.head.storeHistory == StoreMode.ON) historyTable else null - // This is what we should INSERT or UPDATE. val new_row = """WITH new_row AS ( SELECT * FROM UNNEST(${inRows.placeholders()}) AS t(${inRows.aliases()}) @@ -41,128 +43,122 @@ internal class PgWriterUpsert( // Select existing. val head_row = """, head_row AS ( - SELECT * FROM ${headTable.quotedName} - WHERE fn IN (SELECT fn FROM new_row) + SELECT * FROM $headIdent + WHERE $FN IN (SELECT $FN FROM new_row) )""" - // Insert the current `head_row` into history. next_version is the new tuple's version with action set to UPDATE. - val effCopyHistory = collection.effectiveCopyIntoHistoryColumns - val effUpdate = collection.effectiveUpdateColumns - val head_to_history = if (insert_into_history != null) """, head_to_history AS ( - INSERT INTO ${insert_into_history.quotedName} (${PgColumn.next_version}, ${effCopyHistory.joinToString(",") { it.name }}) - SELECT ((new_row.version & -4) | 1) AS ${PgColumn.next_version}, - ${effCopyHistory.joinToString(", ") { "head_row.${it.name} AS ${it.name}" }} + // TODO: This is not fully correct, NEXT_VERSION must be equal to VERSION, when the action is DELETED. + // Only when the current HEAD is in CREATE or UPDATE action, then the NEXT_VERSION is VERSION. + // The reason is that tombstone states end the life-cycle. Clearly, still they need to be copied to history. + // Basically, we need to split this into two parts, `tombstones_to_history` and `head_to_history`! + // Insert the current `head_row` into history. `next_version` is the new tuple's version with action set to UPDATE. + val head_to_history = if (historyTable != null) """, head_to_history AS ( + INSERT INTO $historyIdent ($NEXT_VERSION, + ${pgCollection.joinColumns { column -> if (column eq NEXT_VERSION) null else column.ident }}) + SELECT ((new_row.$VERSION & -4) | 1) AS $NEXT_VERSION, + ${pgCollection.joinColumns { column -> if (column eq NEXT_VERSION) null else "head_row.$column AS $column" }} FROM head_row - LEFT JOIN new_row ON new_row.fn = head_row.fn - RETURNING id, fn, version + LEFT JOIN new_row ON new_row.$FN = head_row.$FN + RETURNING $FN, $VERSION )""" else "" // Delete `head_row` from HEAD. val head_deleted = """, head_deleted AS ( - DELETE FROM ${headTable.quotedName} - WHERE fn IN (SELECT fn FROM head_row) - RETURNING id, fn, version + DELETE FROM $headIdent + WHERE $FN IN (SELECT $FN FROM head_row) + RETURNING $FN, $VERSION )""" - // All nullable BYTE_ARRAY columns support the "keep if undefined" sentinel. - // `feature` is mandatory (NOT NULL) and never carries the sentinel. - val keepableByteCols = collection.effectiveHeadColumns.filter { it.type == PgType.BYTE_ARRAY && it !== PgColumn.feature } + // TODO: Fix the two queries: + // The `head_inserted` need to select from the new tombstone deletion + // The `head_updated` need to select from the new head deletion - // Insert new rows for which there was no existing HEAD version. + // Insert new rows for which there was no existing HEAD version or HEAD was in action DELETE. // Sentinel "undefined" on any BYTE_ARRAY column is treated as NULL on insert (no prior value to retain). + // Note: We expact that the ASCII string `undefined` is put into the BYTE_ARRAY, when it should be undefined. + // That is why we check for `CASE WHEN colum = convert_to('undefined', 'UTF8') ...` val head_inserted = """, head_inserted AS ( - INSERT INTO ${headTable.quotedName} (${inRows.aliases()}) - SELECT ${inRows.columns.joinToString(", ") { col -> - val q = PgUtil.quoteIdent(col.name) - if (keepableByteCols.any { it.name == col.name }) - "CASE WHEN ${col.name} = convert_to('undefined', 'UTF8') THEN null ELSE ${col.name} END AS ${col.name}" - else - q + INSERT INTO $headIdent (${inRows.aliases()}) + SELECT ${inRows.columns.joinToString(", ") { colWithValue -> + val ident = PgUtil.quoteIdent(colWithValue.alias) + if (colWithValue.pgColumn.memberType == MemberType.BYTE_ARRAY) { + "CASE WHEN $ident = convert_to('undefined', 'UTF8') THEN null ELSE $ident END AS $ident" + } else ident }} FROM new_row - WHERE new_row.fn NOT IN (SELECT fn FROM head_deleted) - RETURNING id, fn, version + WHERE new_row.$FN NOT IN (SELECT $FN FROM head_deleted) + RETURNING $VERSION )""" - // Update means insert new_rows, but with patched values. - // For any BYTE_ARRAY column that carries the sentinel, keep the value from the existing HEAD row. - // The action is encoded in the lower two bits of `version`; we set it to UPDATED (=1). - // Only include columns that exist in the table (effectiveUpdateColumns). - val hasCc = PgColumn.cc in collection.effectiveHeadColumns - val updColNames = effUpdate.joinToString(",") { it.name } - // keepable cols that are NOT in effUpdate need their own INSERT slot (currently: attachment) - val keepableExtraCols = keepableByteCols.filter { it !in effUpdate } - val keepableExtraColNames = keepableExtraCols.joinToString(",") { it.name } + // TODO: Instead of deleting HEAD, then INSERT, we can just UPDATE the HEAD, this could be beneficial. + // We patch VERSION and change the lower two bit from 0 (CREATED) to 1 (UPDATED) val head_updated = """, head_updated AS ( - INSERT INTO ${headTable.quotedName} ( - ${if (hasCc) "${PgColumn.cc}," else ""} - ${if (keepableExtraCols.isNotEmpty()) "$keepableExtraColNames," else ""} - ${PgColumn.fn}, - ${PgColumn.version}${if (updColNames.isNotEmpty()) ",\n $updColNames" else ""}) + INSERT INTO $headIdent ( + ${if (copyCols.isNotEmpty()) "${copyCols.joinToString(", ") { it.ident }},\n " else ""} + ${if (byteArrayCols.isNotEmpty()) "${byteArrayCols.joinToString(", ") { it.ident }},\n " else ""} + ${if (CC != null) "$CC,\n " else ""} + $FN, + $VERSION, + $NEXT_VERSION + ) SELECT - ${if (hasCc) "(head_row.cc + 1) AS ${PgColumn.cc}," else ""} - ${if (keepableExtraCols.isNotEmpty()) keepableExtraCols.joinToString(", ") { col -> - "CASE WHEN new_row.${col.name} = convert_to('undefined', 'UTF8') THEN head_row.${col.name} ELSE new_row.${col.name} END AS ${col.name}" - } + "," else ""} - new_row.fn AS ${PgColumn.fn}, - ((new_row.version & -4) | 1) AS ${PgColumn.version}${if (effUpdate.isNotEmpty()) ",\n ${effUpdate.joinToString(", ") { col -> - if (col in keepableByteCols) - "CASE WHEN new_row.${col.name} = convert_to('undefined', 'UTF8') THEN head_row.${col.name} ELSE new_row.${col.name} END AS ${col.name}" - else - "new_row.${col.name} AS ${col.name}" - }}" else ""} + ${if (copyCols.isNotEmpty()) "${copyCols.joinToString(", ") { column -> "new_row.$column" }},\n " else ""} + ${if (byteArrayCols.isNotEmpty()) byteArrayCols.joinToString(", ") { column -> + "CASE WHEN new_row.$column = convert_to('undefined', 'UTF8') THEN head_row.$column ELSE new_row.$column END AS $column" + } + ",\n " else ""} + ${if (CC != null) "(head_row.$CC + 1) AS $CC," else ""} + new_row.$FN AS $FN, + ((new_row.version & -4) | 1) AS $VERSION, + null AS $NEXT_VERSION FROM new_row - LEFT JOIN head_row ON head_row.fn = new_row.fn - WHERE new_row.fn IN (SELECT fn FROM head_deleted) - RETURNING id, fn, version${if (hasCc) ", cc" else ""}${keepableByteCols.joinToString("") { ", ${it.name}" }} + LEFT JOIN head_row ON head_row.$FN = new_row.$FN + WHERE new_row.$FN IN (SELECT $FN FROM head_deleted) + RETURNING $VERSION${byteArrayCols.joinToString("") { column -> column.ident }}${if (CC!=null) ", $CC" else ""} )""" val SQL = """$new_row$head_row$head_deleted$head_to_history$head_inserted$head_updated SELECT - new_row.id AS id, - new_row.fn AS fn, - new_row.version AS version, - head_updated.fn AS updated_fn, - head_updated.version AS updated_version, - ${if (hasCc) "head_updated.cc AS cc," else "null::int4 AS cc,"} - ${if (keepableByteCols.isNotEmpty()) keepableByteCols.joinToString(",\n ") { "head_updated.${it.name} AS ${it.name}" } + "," else "null::bytea AS attachment,"} - head_row.version AS head_row_version, - head_deleted.version AS head_deleted_version, - head_inserted.version AS head_inserted_version, - null AS clear_shadow_version, - ${if (head_to_history.isNotEmpty()) "head_to_history.version AS head_to_history_version" else "null AS head_to_history_version"} + new_row.$FN AS $FN, + new_row.$VERSION AS $VERSION, + ${if (byteArrayCols.isNotEmpty()) byteArrayCols.joinToString(",\n ") { column -> "head_updated.$column AS $column" } + ",\n " else ""} + ${if (CC!=null) "head_updated.$CC AS $CC," else "null::int4 AS $CC,"} + head_updated.$FN AS _updated_fn, + head_updated.$VERSION AS _updated_version, + head_row.$VERSION AS _head_row_version, + head_deleted.$VERSION AS _head_deleted_version, + head_inserted.$VERSION AS _head_inserted_version, + ${if (head_to_history.isNotEmpty()) "head_to_history.$VERSION AS _head_to_history_version" else "null AS _head_to_history_version"} FROM new_row -LEFT JOIN head_updated ON head_updated.fn = new_row.fn -LEFT JOIN head_row ON head_row.fn = new_row.fn -LEFT JOIN head_deleted ON head_deleted.fn = new_row.fn -LEFT JOIN head_inserted ON head_inserted.fn = new_row.fn -${if (head_to_history.isNotEmpty()) "LEFT JOIN head_to_history ON head_to_history.fn = new_row.fn" else ""} +LEFT JOIN head_updated ON head_updated.$FN = new_row.$FN +LEFT JOIN head_row ON head_row.$FN = new_row.$FN +LEFT JOIN head_deleted ON head_deleted.$FN = new_row.$FN +LEFT JOIN head_inserted ON head_inserted.$FN = new_row.$FN +${if (head_to_history.isNotEmpty()) "LEFT JOIN head_to_history ON head_to_history.$FN = new_row.$FN" else ""} ;""" - val typeNames = inRows.typeNames(); - val pgPlan = conn.prepare(SQL, typeNames); + // Notes: + // head_updated contains all rows that have been executed an UPDATE, all others performed an INSERT + val typeNames = inRows.typeNames() + val pgPlan = conn.prepare(SQL, typeNames) return PgWriterPlan(pgPlan, SQL, typeNames) } override fun doExecute(conn: PgConnection) { - val keepableByteCols = pgCollection.effectiveHeadColumns.filter { it.type == PgType.BYTE_ARRAY && it !== PgColumn.feature } val outRows = PgRows() .withDatabaseNumber(storageNumber) .withCatalogNumber(catalogNumber) .withCollectionNumber(collectionNumber) - .addColumn(PgColumn.id) - .addColumn(PgColumn.fn) - .addColumn(PgColumn.version) - .addColumn(PgColumn.cc) - .addColumn("updated_fn", PgType.INT64) - .addColumn("updated_version", PgType.INT64) - .addColumn("head_row_version", PgType.INT64) - .addColumn("clear_shadow_version", PgType.INT64) - .addColumn("head_deleted_version", PgType.INT64) - .addColumn("head_inserted_version", PgType.INT64) - .addColumn("head_to_history_version", PgType.INT64) - for (col in keepableByteCols) outRows.addColumn(col.name, PgType.BYTE_ARRAY) + outRows.addColumn(FN) + .addColumn(VERSION) + for (column in byteArrayCols) outRows.addColumn(column) + if (CC!=null) outRows.addColumn(CC) + outRows.addColumn("_updated_fn", MemberType.INT64) + .addColumn("_updated_version", MemberType.INT64) + .addColumn("_head_row_version", MemberType.INT64) + .addColumn("_head_deleted_version", MemberType.INT64) + .addColumn("_head_inserted_version", MemberType.INT64) + .addColumn("_clear_shadow_version", MemberType.INT64) + .addColumn("_head_to_history_version", MemberType.INT64) if (pgWrites.isEmpty()) return val plan = plan(conn, pgCollection) - // TupleNumber.fromB128(inRows.columns[11].values_field[0] as ByteArray, naksha.base.Int64(0), 0, 0).partitionNumber % 16 val array = inRows.values() val session = this.session if (PlatformUtil.ENABLE_INFO) { @@ -182,49 +178,40 @@ ${if (head_to_history.isNotEmpty()) "LEFT JOIN head_to_history ON head_to_histor logger.info("UPSERT of ${inRows.size} rows took ${seconds * 1000}ms, therefore ${inRows.size / seconds} features/s, partitions: $featureCountByPartitionJoined") } cursor.fetch().use { - outRows.addAll(cursor) + outRows.readAll(cursor) for (row in 0 until outRows.size) { - val fn = outRows.getInt64(row, "fn") ?: throw generalException("Missing 'fn' in SQL result") - val id = outRows.getString(row, "id") ?: fn.toString() - val versionTxn = outRows.getInt64(row, "version") ?: throw generalException("Missing 'version' in SQL result") - val tn = TupleNumber(storageNumber, catalogNumber, collectionNumber, fn, Version(versionTxn)) - - // We need to patch the tuple of all inserts, that were replaced with updates! - // The content is the same, but the action, operation, and change-count change. - val updatedFn = outRows.getInt64(row, "updated_fn") - val updatedVersionTxn = outRows.getInt64(row, "updated_version") - if (updatedFn != null && updatedVersionTxn != null) { - val updated_tn = TupleNumber(storageNumber, catalogNumber, collectionNumber, updatedFn, Version(updatedVersionTxn)) + val updated_fn = outRows.getInt64(row, "_updated_fn") + val updated_version = outRows.getInt64(row, "_updated_version") + if (updated_fn != null && updated_version != null) { + // UPDATE executed + val pgWrite = writeByFn[updated_fn] ?: throw generalException("Received _updated_fn '$updated_fn', but found no matching PgWrite") + val updatedTupleNumber = TupleNumber(storageNumber, catalogNumber, collectionNumber, updated_fn, updated_version) + val + val previousTupleNumber = TupleNumber(storageNumber, catalogNumber, collectionNumber, updated_fn, updated_version) // If an update was done, we need the following values to be available: - val hasCc = PgColumn.cc in pgCollection.effectiveHeadColumns - val changeCount: Int = if (hasCc) { - outRows.getInt(row, "cc") ?: - throw generalException("Missing 'cc' in update result for feature '$id'") + val change_count: Int = if (CC!=null) { + outRows.getInt(row, CC) ?: throw generalException("Missing '$CC' in update result for feature '${pgWrite.id}'") } else 1 - val write = writeByTn[tn] ?: throw generalException("Missing write state for feature '$id'") - val tuple = write.tuple ?: throw generalException("Missing tuple for feature '$id'") - // Read back all keepable BYTE_ARRAY columns — the DB may have substituted the sentinel - // with the existing value, so the in-memory tuple must reflect the final stored state. - val geo = if (PgColumn.geo in keepableByteCols) outRows.getByteArray(row, PgColumn.geo.name) else tuple.getByteArray(StandardMembers.Geometry) - val referencePoint = if (PgColumn.ref_point in keepableByteCols) outRows.getByteArray(row, PgColumn.ref_point.name) else tuple.getByteArray(StandardMembers.ReferencePoint) - val tags = tuple.getString(StandardMembers.XyzTags) - val attachment = if (PgColumn.attachment in keepableByteCols) outRows.getByteArray(row, PgColumn.attachment.name) else tuple.getByteArray(StandardMembers.XyzAttachment) - write.tupleNumber = updated_tn - val m = tuple.membersBook - val newMembers = if (m is naksha.jbon.HeapBook) { - val dict = m.copy() - dict.put(StandardMembers.Geometry.name, geo) - dict.put(StandardMembers.ReferencePoint.name, referencePoint) - dict.put(StandardMembers.XyzTags.name, tags) - dict.put(StandardMembers.XyzAttachment.name, attachment) - dict - } else m - write.tuple = tuple.copy( - version = updated_tn.version, - membersBook = newMembers + + // Update all BYTE_ARRAY rows that have been updated. + val insertTuple = pgWrite.tuple ?: throw generalException("Missing tuple for feature '${pgWrite.id}}'") + val insertMemberBook = insertTuple.membersBook + val updatedMembersBook = HeapBook.copyOf(insertMemberBook) + for (column in byteArrayCols) { + val value = outRows.getByteArray(row, column) + updatedMembersBook.put(column.name, value) + } + updatedMembersBook.put(StandardMembers.Tn.name, updatedTupleNumber) + val updatedTuple = insertTuple.copy( + membersBook = updatedMembersBook, + previousTupleNumber = inser ) - write.action = Action.UPDATE - } + pgWrite.tuple = updatedTuple + pgWrite.tupleNumber = updatedTupleNumber + } // else INSERT executed, that means the tuple was inserted as given. +// val fn = outRows.getInt64(row, FN) ?: throw generalException("Missing 'fn' in SQL result") +// val new_version = outRows.getInt64(row, VERSION) ?: throw generalException("Missing 'version' in SQL result") +// val new_tn = TupleNumber(storageNumber, catalogNumber, collectionNumber, fn, new_version) } } } From 59295088a02f4634e1b314b34c170b6524316075 Mon Sep 17 00:00:00 2001 From: phmai Date: Thu, 25 Jun 2026 15:22:22 +0200 Subject: [PATCH 44/57] implement TagList ops Signed-off-by: phmai --- .../kotlin/naksha/psql/PgQueryWhereBuilder.kt | 225 +++++++++--------- 1 file changed, 113 insertions(+), 112 deletions(-) diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt index a355a33dd..89c28cb2d 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgQueryWhereBuilder.kt @@ -1,19 +1,15 @@ package naksha.psql -import naksha.base.AnyList import naksha.base.Int64 -import naksha.base.ListProxy import naksha.base.Platform.PlatformCompanion.toJSON import naksha.base.StringList -import naksha.geo.HereTile -import naksha.geo.SpGeometry -import naksha.model.* +import naksha.model.Naksha +import naksha.model.NakshaError +import naksha.model.NakshaException +import naksha.model.illegalArg import naksha.model.objects.StandardMembers import naksha.model.request.ReadFeatures import naksha.model.request.ops.* -import kotlin.collections.get -import kotlin.text.append -import kotlin.text.iterator /** * Helper to convert a [ReadFeatures] request into a sql `WHERE` query. @@ -38,10 +34,11 @@ internal class PgQueryWhereBuilder(private val request: ReadFeatures, private va fun build(): PgQueryWhereClause? { var op: Op? = request.queryMembers if (op == null) op = QueryConverter.convert(request.query) - // TODO: Convert `featureIds` - // TODO: Convert `guids` if (op == null) return null applyOp(op) + if (request.featureIds.isNotEmpty()) { // backward compatibility for feature IDs read requests + whereFeatureId() + } return PgQueryWhereClause(collection, where.toString(), argValues, argTypes) } @@ -259,7 +256,14 @@ internal class PgQueryWhereBuilder(private val request: ReadFeatures, private va where.append(") ") } is Intersects -> { - // TODO: Implement me + val geoBytes = Naksha.encodeGeometry(op.value) + val geoBytesPlaceholder = placeholderForArg(geoBytes, PgType.BYTE_ARRAY) + val queryGeometry = "naksha_2d($geoBytesPlaceholder)" + val transformation = op.transformers + val geometryToCompare = + if (transformation.isEmpty()) queryGeometry + else resolveTransformation(transformation, queryGeometry) + where.append("ST_Intersects(naksha_2d(${StandardMembers.Geometry}), $geometryToCompare)") } else -> throw illegalArg("Unknown operation: '$op'") } @@ -300,43 +304,106 @@ internal class PgQueryWhereBuilder(private val request: ReadFeatures, private va return "\$${argTypes.size}" } + private fun resolveTransformation( + transformationList: SpTransformationList, + basicGeometry: String + ): String { + var geometry = basicGeometry + for (transformation in transformationList) { + geometry = when (transformation) { + is SpBuffer -> resolveBuffer(transformation, geometry) + else -> throw NakshaException( + NakshaError.UNSUPPORTED_OPERATION, + "This transformation is not yet supported: ${transformation!!::class.simpleName}" + ) + } + } + return geometry + } + + private fun resolveBuffer(buffer: SpBuffer, basicGeometry: String): String { + val geo = if (buffer.geography) { + "$basicGeometry::geography" + } else { + basicGeometry + } + val distancePlaceholder = placeholderForArg(buffer.distance, PgType.DOUBLE) + val bufferStyleParams = bufferStyleParams(buffer) + return if (bufferStyleParams != null) { + "ST_Buffer($geo, $distancePlaceholder, $bufferStyleParams)" + } else { + "ST_Buffer($geo, $distancePlaceholder)" + } + } + + private fun bufferStyleParams(buffer: SpBuffer): String? { + val bufferStyleParams = StringBuilder() + if (buffer.quadSegments != null) { + val quadSegPlaceholder = placeholderForArg(buffer.quadSegments, PgType.INT) + bufferStyleParams.append("quad_segs=$quadSegPlaceholder") + } + if (buffer.joinStyle != null) { + val joinStylePlaceholder = placeholderForArg(buffer.joinStyle!!.value, PgType.STRING) + if (bufferStyleParams.isNotEmpty()) bufferStyleParams.append(" ") + bufferStyleParams.append("join=$joinStylePlaceholder") + } + if (buffer.joinLimit != null) { + val joinLimitPlaceholder = placeholderForArg(buffer.joinLimit, PgType.DOUBLE) + if (bufferStyleParams.isNotEmpty()) bufferStyleParams.append(" ") + bufferStyleParams.append("mitre_limit=$joinLimitPlaceholder") + } + if (buffer.endCap != null) { + val endCapPlaceholder = placeholderForArg(buffer.endCap!!.value, PgType.STRING) + if (bufferStyleParams.isNotEmpty()) bufferStyleParams.append(" ") + bufferStyleParams.append("endcap=$endCapPlaceholder") + } + if (buffer.side != null) { + val sidePlaceholder = placeholderForArg(buffer.side!!.value, PgType.STRING) + if (bufferStyleParams.isNotEmpty()) bufferStyleParams.append(" ") + bufferStyleParams.append("side=$sidePlaceholder") + } + return if (bufferStyleParams.isNotEmpty()) { + bufferStyleParams.toString() + } else { + null + } + } + + private fun whereFeatureId() { + // Partition into numeric IDs (fn >= 0, id stored as NULL in DB) and named IDs (fn < 0, id NOT NULL). + val reqIds: StringList = request.featureIds + val featureNumbers: MutableList = mutableListOf() + val featureIds: MutableList = mutableListOf() + for (id in reqIds) { + if (id == null) continue + val fn = Naksha.featureNumber(id) + if (fn >= Int64(0)) { + featureNumbers.add(fn) + } else { + featureIds.add(id) + } + } + if (featureNumbers.isEmpty() && featureIds.isEmpty()) return + + // For each collection: + if (where.isNotEmpty()) where.append(" AND ") + + where.append("( ") + if (featureIds.isNotEmpty()) { + val op = IsAnyOf(at = StandardMembers.Id, items = featureIds.toTypedArray()) + applyOp(op) + } + if (featureNumbers.isNotEmpty()) { + if (featureIds.isNotEmpty()) where.append(" OR ") + + val op = IsAnyOf(at = StandardMembers.FeatureNumber, items = featureNumbers.toTypedArray()) + applyOp(op) + } + where.append(")") + } + // --------------------------------------------------------< OLD CODE >------------------------------------------------------------- // -// private fun whereFeatureId() { -// // Partition into numeric IDs (fn >= 0, id stored as NULL in DB) and named IDs (fn < 0, id NOT NULL). -// val reqIds: StringList = request.featureIds -// val featureNumbers: MutableList = mutableListOf() -// val featureIds: MutableList = mutableListOf() -// for (id in reqIds) { -// if (id == null) continue -// val fn = Naksha.featureNumber(id) -// if (fn >= Int64(0)) { -// featureNumbers.add(fn) -// } else { -// featureIds.add(id) -// } -// } -// if (featureNumbers.isEmpty() && featureIds.isEmpty()) return -// -// // For each collection: -// if (where.isNotEmpty()) where.append(" AND ") -// -// where.append("( ") -// if (featureIds.isNotEmpty()) { -// val placeholder: String = placeholderForArg(featureIds.toTypedArray(), PgType.STRING_ARRAY) -// val ID = collection.column(StandardMembers.Id) ?: throw illegalArg("Collection does not defined `id` column") -// where.append(ID.ident).append(" = ANY(").append(placeholder).append(")") -// } -// if (featureNumbers.isNotEmpty()) { -// if (featureIds.isNotEmpty()) where.append(" OR ") -// -// val placeholder: String = placeholderForArg(featureNumbers.toTypedArray(), PgType.INT64_ARRAY) -// if (where.isNotEmpty()) where.append(" AND ") -// where.append(FN.ident).append(" = ANY(").append(placeholder).append(")") -// } -// where.append(")") -// } -// // private fun whereGuids() { // val tupleNumbers = request.guids.mapNotNull { it?.tupleNumber } // if (tupleNumbers.isNotEmpty()) { @@ -417,72 +484,6 @@ internal class PgQueryWhereBuilder(private val request: ReadFeatures, private va // } // } // -// private fun nakshaGeometry(geometry: SpGeometry): String { -// val geoBytes = Naksha.encodeGeometry(geometry) -// val geoBytesPlaceholder = placeholderForArg(geoBytes, PgType.BYTE_ARRAY) -// return "naksha_2d($geoBytesPlaceholder)" -// } -// -// private fun resolveTransformation( -// transformation: SpTransformation, -// basicGeometry: String -// ): String { -// return when (transformation) { -// is SpBuffer -> resolveBuffer(transformation, basicGeometry) -// else -> throw NakshaException( -// NakshaError.UNSUPPORTED_OPERATION, -// "This transformation is not yet supported: ${transformation::class.simpleName}" -// ) -// } -// } -// -// private fun resolveBuffer(buffer: SpBuffer, basicGeometry: String): String { -// val geo = if (buffer.geography) { -// "$basicGeometry::geography" -// } else { -// basicGeometry -// } -// val distancePlaceholder = placeholderForArg(buffer.distance, PgType.DOUBLE) -// val bufferStyleParams = bufferStyleParams(buffer) -// return if (bufferStyleParams != null) { -// "ST_Buffer($geo, $distancePlaceholder, $bufferStyleParams)" -// } else { -// "ST_Buffer($geo, $distancePlaceholder)" -// } -// } -// -// private fun bufferStyleParams(buffer: SpBuffer): String? { -// val bufferStyleParams = StringBuilder() -// if (buffer.quadSegments != null) { -// val quadSegPlaceholder = placeholderForArg(buffer.quadSegments, PgType.INT) -// bufferStyleParams.append("quad_segs=$quadSegPlaceholder") -// } -// if (buffer.joinStyle != null) { -// val joinStylePlaceholder = placeholderForArg(buffer.joinStyle!!.value, PgType.STRING) -// if (bufferStyleParams.isNotEmpty()) bufferStyleParams.append(" ") -// bufferStyleParams.append("join=$joinStylePlaceholder") -// } -// if (buffer.joinLimit != null) { -// val joinLimitPlaceholder = placeholderForArg(buffer.joinLimit, PgType.DOUBLE) -// if (bufferStyleParams.isNotEmpty()) bufferStyleParams.append(" ") -// bufferStyleParams.append("mitre_limit=$joinLimitPlaceholder") -// } -// if (buffer.endCap != null) { -// val endCapPlaceholder = placeholderForArg(buffer.endCap!!.value, PgType.STRING) -// if (bufferStyleParams.isNotEmpty()) bufferStyleParams.append(" ") -// bufferStyleParams.append("endcap=$endCapPlaceholder") -// } -// if (buffer.side != null) { -// val sidePlaceholder = placeholderForArg(buffer.side!!.value, PgType.STRING) -// if (bufferStyleParams.isNotEmpty()) bufferStyleParams.append(" ") -// bufferStyleParams.append("side=$sidePlaceholder") -// } -// return if (bufferStyleParams.isNotEmpty()) { -// bufferStyleParams.toString() -// } else { -// null -// } -// } // // private fun whereRefTiles() { // val hereTiles = request.query.refTiles From 1b2ca10f50b0a4315274d199aadafc99084b6b05 Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Thu, 25 Jun 2026 16:42:36 +0200 Subject: [PATCH 45/57] Fixed WriterUpsert Signed-off-by: Alexander Lowey-Weber --- .../kotlin/naksha/psql/PgWriterBase.kt | 4 + .../kotlin/naksha/psql/PgWriterUpsert.kt | 122 ++++++++++-------- 2 files changed, 72 insertions(+), 54 deletions(-) diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterBase.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterBase.kt index b41b66ec1..588fea7a0 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterBase.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterBase.kt @@ -53,6 +53,10 @@ internal abstract class PgWriterBase protected constructor( */ val end: Int, ) { + companion object PgWriterBase_C { + protected val UNDEFINED: ByteArray = "undefined".encodeToByteArray() + } + val session: PgSession get() = pgWriter.session diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt index ce07464cf..89d26e052 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpsert.kt @@ -25,8 +25,6 @@ internal class PgWriterUpsert( end: Int ) : PgWriterBase(pgWriter, pgCollection, pgWrites, start, end) { - // All columns that are no BYTE_ARRAY and that care not CC, FN, VERSION or NEXT_VERSION - private val copyCols = pgCollection.columns.filter { !(it eq CC || it eq FN || it eq VERSION || it eq NEXT_VERSION || it.memberType != MemberType.BYTE_ARRAY) } // All columns that are BYTE_ARRAYs (can be empty) private val byteArrayCols = pgCollection.columns.filter { it.memberType == MemberType.BYTE_ARRAY } private val writeByFn = mutableMapOf() @@ -35,7 +33,7 @@ internal class PgWriterUpsert( loadAllTuple { _, tuple, pgWrite -> writeByFn[tuple.tupleNumber.featureNumber] = pgWrite } } - private fun plan(conn: PgConnection, collection: PgCollection): PgWriterPlan { + private fun plan(conn: PgConnection): PgWriterPlan { // This is what we should INSERT or UPDATE. val new_row = """WITH new_row AS ( SELECT * FROM UNNEST(${inRows.placeholders()}) AS t(${inRows.aliases()}) @@ -47,11 +45,8 @@ internal class PgWriterUpsert( WHERE $FN IN (SELECT $FN FROM new_row) )""" - // TODO: This is not fully correct, NEXT_VERSION must be equal to VERSION, when the action is DELETED. - // Only when the current HEAD is in CREATE or UPDATE action, then the NEXT_VERSION is VERSION. - // The reason is that tombstone states end the life-cycle. Clearly, still they need to be copied to history. - // Basically, we need to split this into two parts, `tombstones_to_history` and `head_to_history`! - // Insert the current `head_row` into history. `next_version` is the new tuple's version with action set to UPDATE. + // Copy all current head row's into history that are in CREATE or UPDATE action. + // We need to set `next_version` to the new tuple's version, which is new row's version plus action UPDATE. val head_to_history = if (historyTable != null) """, head_to_history AS ( INSERT INTO $historyIdent ($NEXT_VERSION, ${pgCollection.joinColumns { column -> if (column eq NEXT_VERSION) null else column.ident }}) @@ -59,6 +54,24 @@ internal class PgWriterUpsert( ${pgCollection.joinColumns { column -> if (column eq NEXT_VERSION) null else "head_row.$column AS $column" }} FROM head_row LEFT JOIN new_row ON new_row.$FN = head_row.$FN + WHERE (head_row.$VERSION & -4) < 2 -- action = CREATE or UPDATE + RETURNING $FN, $VERSION${if (byteArrayCols.isNotEmpty()) ", ${byteArrayCols.joinToString(", ") { column -> + // We return NULL, when the input contained data, because the client knows this already, no need to send back. + // If the client provided `undefined`, we return the actual value so we can build a correct tuple. + "CASE WHEN new_row.$column = convert_to('undefined', 'UTF8') THEN $column ELSE null END AS $column" + }}" else ""} +)""" else "" + + // Copy all current head row's into history that are in DELETE action. + // In this case, we need to set `next_version` to the old tuple's version to signal end of lifetime. + val tombstone_to_history = if (historyTable != null) """, tombstone_to_history AS ( + INSERT INTO $historyIdent ($NEXT_VERSION, + ${pgCollection.joinColumns { column -> if (column eq NEXT_VERSION) null else column.ident }}) + SELECT head_row.$VERSION AS $NEXT_VERSION, + ${pgCollection.joinColumns { column -> if (column eq NEXT_VERSION) null else "head_row.$column AS $column" }} + FROM head_row + LEFT JOIN new_row ON new_row.$FN = head_row.$FN + WHERE (head_row.$VERSION & -4) == 2 -- action = DELETE RETURNING $FN, $VERSION )""" else "" @@ -69,53 +82,53 @@ internal class PgWriterUpsert( RETURNING $FN, $VERSION )""" - // TODO: Fix the two queries: - // The `head_inserted` need to select from the new tombstone deletion - // The `head_updated` need to select from the new head deletion - - // Insert new rows for which there was no existing HEAD version or HEAD was in action DELETE. + // Copy new rows for which there was no existing HEAD version or HEAD was in action DELETE. // Sentinel "undefined" on any BYTE_ARRAY column is treated as NULL on insert (no prior value to retain). // Note: We expact that the ASCII string `undefined` is put into the BYTE_ARRAY, when it should be undefined. - // That is why we check for `CASE WHEN colum = convert_to('undefined', 'UTF8') ...` - val head_inserted = """, head_inserted AS ( + // That is why we check for `CASE WHEN $ident = convert_to('undefined', 'UTF8') ...` + + // ------------------------------------------ DO UPDATE -------------------------------------------------------- + val head_updated = """, head_updated AS ( INSERT INTO $headIdent (${inRows.aliases()}) SELECT ${inRows.columns.joinToString(", ") { colWithValue -> val ident = PgUtil.quoteIdent(colWithValue.alias) - if (colWithValue.pgColumn.memberType == MemberType.BYTE_ARRAY) { - "CASE WHEN $ident = convert_to('undefined', 'UTF8') THEN null ELSE $ident END AS $ident" + val pgColumn = colWithValue.pgColumn + if (pgColumn eq CC) { + "(head_to_history.$CC + 1) AS $CC" // Increment Change-Count + } else if (pgColumn eq VERSION){ + "((new_row.$VERSION & -4) | 1) AS $VERSION" // Set lower 2 bit to UPDATE (1) + } else if (pgColumn eq NEXT_VERSION){ + "NULL AS $NEXT_VERSION" // in HEAD, next version must always be NULL + } else if (pgColumn.memberType == MemberType.BYTE_ARRAY) { + "CASE WHEN $ident = convert_to('undefined', 'UTF8') THEN head_to_history.$ident ELSE new_row.$ident END AS $ident" } else ident }} FROM new_row - WHERE new_row.$FN NOT IN (SELECT $FN FROM head_deleted) - RETURNING $VERSION -)""" + LEFT JOIN head_to_history ON new_row.$FN = head_to_history.$FN + WHERE new_row.$FN IN (SELECT $FN FROM head_to_history) + RETURNING $FN, $VERSION +)""" // Note: head_to_history contains all existing HEAD row in CREATE or UPDATE action. - // TODO: Instead of deleting HEAD, then INSERT, we can just UPDATE the HEAD, this could be beneficial. - // We patch VERSION and change the lower two bit from 0 (CREATED) to 1 (UPDATED) - val head_updated = """, head_updated AS ( - INSERT INTO $headIdent ( - ${if (copyCols.isNotEmpty()) "${copyCols.joinToString(", ") { it.ident }},\n " else ""} - ${if (byteArrayCols.isNotEmpty()) "${byteArrayCols.joinToString(", ") { it.ident }},\n " else ""} - ${if (CC != null) "$CC,\n " else ""} - $FN, - $VERSION, - $NEXT_VERSION - ) - SELECT - ${if (copyCols.isNotEmpty()) "${copyCols.joinToString(", ") { column -> "new_row.$column" }},\n " else ""} - ${if (byteArrayCols.isNotEmpty()) byteArrayCols.joinToString(", ") { column -> - "CASE WHEN new_row.$column = convert_to('undefined', 'UTF8') THEN head_row.$column ELSE new_row.$column END AS $column" - } + ",\n " else ""} - ${if (CC != null) "(head_row.$CC + 1) AS $CC," else ""} - new_row.$FN AS $FN, - ((new_row.version & -4) | 1) AS $VERSION, - null AS $NEXT_VERSION - FROM new_row - LEFT JOIN head_row ON head_row.$FN = new_row.$FN - WHERE new_row.$FN IN (SELECT $FN FROM head_deleted) - RETURNING $VERSION${byteArrayCols.joinToString("") { column -> column.ident }}${if (CC!=null) ", $CC" else ""} -)""" + // ------------------------------------------ DO INSERT -------------------------------------------------------- + val head_inserted = """, head_inserted AS ( + INSERT INTO $headIdent (${inRows.aliases()}) + SELECT ${inRows.columns.joinToString(", ") { colWithValue -> + val ident = PgUtil.quoteIdent(colWithValue.alias) + val pgColumn = colWithValue.pgColumn + if (pgColumn eq CC) { + "1 AS $CC" // Change-Count = 1 + } else if (pgColumn eq VERSION){ + "($VERSION & -4) AS $VERSION" // Clear lower 2 bit to set action = CREATE (0) + } else if (pgColumn eq NEXT_VERSION){ + "NULL AS $NEXT_VERSION" // in HEAD, next version must always be NULL + } else if (pgColumn.memberType == MemberType.BYTE_ARRAY) { + "CASE WHEN $ident = convert_to('undefined', 'UTF8') THEN null ELSE $ident END AS $ident" + } else ident +}} FROM new_row + WHERE new_row.$FN NOT IN (SELECT $FN FROM head_updated) + RETURNING $FN, $VERSION +)""" // Note: for the real inserts, we do not need to return byte-array columns, because we send them, so its clear what the content will be. - val SQL = """$new_row$head_row$head_deleted$head_to_history$head_inserted$head_updated + val SQL = """$new_row$head_row$head_to_history$tombstone_to_history$head_deleted$head_updated$head_inserted SELECT new_row.$FN AS $FN, new_row.$VERSION AS $VERSION, @@ -158,7 +171,7 @@ ${if (head_to_history.isNotEmpty()) "LEFT JOIN head_to_history ON head_to_histor .addColumn("_clear_shadow_version", MemberType.INT64) .addColumn("_head_to_history_version", MemberType.INT64) if (pgWrites.isEmpty()) return - val plan = plan(conn, pgCollection) + val plan = plan(conn) val array = inRows.values() val session = this.session if (PlatformUtil.ENABLE_INFO) { @@ -183,28 +196,29 @@ ${if (head_to_history.isNotEmpty()) "LEFT JOIN head_to_history ON head_to_histor val updated_fn = outRows.getInt64(row, "_updated_fn") val updated_version = outRows.getInt64(row, "_updated_version") if (updated_fn != null && updated_version != null) { - // UPDATE executed + // UPDATE was executed. val pgWrite = writeByFn[updated_fn] ?: throw generalException("Received _updated_fn '$updated_fn', but found no matching PgWrite") val updatedTupleNumber = TupleNumber(storageNumber, catalogNumber, collectionNumber, updated_fn, updated_version) - val val previousTupleNumber = TupleNumber(storageNumber, catalogNumber, collectionNumber, updated_fn, updated_version) // If an update was done, we need the following values to be available: val change_count: Int = if (CC!=null) { outRows.getInt(row, CC) ?: throw generalException("Missing '$CC' in update result for feature '${pgWrite.id}'") } else 1 - // Update all BYTE_ARRAY rows that have been updated. val insertTuple = pgWrite.tuple ?: throw generalException("Missing tuple for feature '${pgWrite.id}}'") val insertMemberBook = insertTuple.membersBook val updatedMembersBook = HeapBook.copyOf(insertMemberBook) + if (CC != null) updatedMembersBook.put(CC.name, change_count) + updatedMembersBook.put(StandardMembers.Tn.name, updatedTupleNumber) + // Update all BYTE_ARRAY members that have been updated. for (column in byteArrayCols) { - val value = outRows.getByteArray(row, column) - updatedMembersBook.put(column.name, value) + val inValue = insertTuple.membersBook[column.name] as ByteArray? + val newValue = if (inValue == null || UNDEFINED.contentEquals(inValue)) outRows.getByteArray(row, column) else inValue + updatedMembersBook.put(column.name, newValue) } - updatedMembersBook.put(StandardMembers.Tn.name, updatedTupleNumber) val updatedTuple = insertTuple.copy( membersBook = updatedMembersBook, - previousTupleNumber = inser + previousTupleNumber = previousTupleNumber ) pgWrite.tuple = updatedTuple pgWrite.tupleNumber = updatedTupleNumber From 2f562183548562bd3cf51ecb6b55938190f2565b Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Thu, 25 Jun 2026 17:34:21 +0200 Subject: [PATCH 46/57] Fix issues in WriterUpdate Signed-off-by: Alexander Lowey-Weber --- .../kotlin/naksha/psql/PgWriterBase.kt | 2 + .../kotlin/naksha/psql/PgWriterUpdate.kt | 213 ++++++++---------- 2 files changed, 101 insertions(+), 114 deletions(-) diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterBase.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterBase.kt index 588fea7a0..4ec1f825a 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterBase.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterBase.kt @@ -14,6 +14,7 @@ import naksha.model.illegalState import naksha.model.objects.NakshaTx import naksha.model.objects.StandardMembers import kotlin.collections.mutableMapOf +import kotlin.jvm.JvmStatic /** * Base class for all operations, so for: @@ -54,6 +55,7 @@ internal abstract class PgWriterBase protected constructor( val end: Int, ) { companion object PgWriterBase_C { + @JvmStatic protected val UNDEFINED: ByteArray = "undefined".encodeToByteArray() } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt index 56b35c2e2..604a65394 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt @@ -1,5 +1,6 @@ package naksha.psql +import naksha.base.Int64 import naksha.base.Platform import naksha.base.Platform.PlatformCompanion.logger import naksha.base.PlatformUtil @@ -7,6 +8,9 @@ import naksha.model.* import naksha.model.objects.MemberType import naksha.model.objects.StandardMembers import naksha.model.objects.StoreMode +import naksha.psql.PgColumn.PgColumn_C.FN +import naksha.psql.PgColumn.PgColumn_C.NEXT_VERSION +import naksha.psql.PgColumn.PgColumn_C.VERSION /** * Execute a [UPDATE][naksha.model.request.WriteOp.UPDATE]. @@ -21,108 +25,86 @@ internal class PgWriterUpdate( end: Int ) : PgWriterBase(pgWriter, pgCollection, pgWrites, start, end) { - val headTable = pgCollection.headTable - val historyTable = if (pgCollection.storeHistory) pgCollection.historyTable else null - val ID: PgColumn = pgCollection.column(StandardMembers.Id) ?: throw illegalState("The collection does not have an 'id' column.") - val CC: PgColumn? = pgCollection.column(StandardMembers.ChangeCount) - private val writeById = mutableMapOf() - + // All columns that are BYTE_ARRAYs (can be empty) + private val byteArrayCols = pgCollection.columns.filter { it.memberType == MemberType.BYTE_ARRAY } + private val writeByFn = mutableMapOf() init { inRows.addColumns(pgCollection.columns) - // Separate column for the expected/atomic version, because `version` itself - // is now a real HEAD column carrying the new tuple's version. inRows.addColumn("expected_version", MemberType.INT64) // needed to do atomic updates - loadAllTuple { row, _, pgWrite -> - writeById[pgWrite.id] = pgWrite + loadAllTuple { row, tuple, pgWrite -> + writeByFn[tuple.tupleNumber.featureNumber] = pgWrite + // Separate column for the expected HEAD version. inRows.set(row, "expected_version", pgWrite.version?.number) } } - private fun plan(conn: PgConnection, collection: PgCollection): PgWriterPlan { - val insert_into_history = if (historyTable != null && collection.head.storeHistory == StoreMode.ON) historyTable else null - + private fun plan(conn: PgConnection): PgWriterPlan { // All input provided by client (the updates) val query = """WITH new_row AS ( SELECT * FROM UNNEST(${inRows.placeholders()}) AS t(${inRows.aliases()}) )""" - val effHead = collection.effectiveHeadColumns - val effCopyHistory = collection.effectiveCopyIntoHistoryColumns - - // select `id` and version of all rows that match new_row.id - val head_select = """, head_select AS ( - SELECT head.id AS id, head.fn AS fn, head.version AS version - FROM ${headTable.quotedName} AS head, new_row - WHERE head.id = new_row.id -)""" - - val effHeadNames = effHead.joinToString(", ") { it.name } - // All nullable BYTE_ARRAY columns support the "keep if undefined" sentinel. - // `feature` is mandatory (NOT NULL) and never carries the sentinel. - val keepableByteCols = effHead.filter { it.type == PgType.BYTE_ARRAY && it !== PgColumn.feature } - - // If the client requested an atomic update, so it provided an `expected_version`, then - // we only update the head row, when the version matches. - // If we need to create a history entry, select all HEAD columns, otherwise the minimal set - // needed: id/fn/version for control flow, plus all keepable BYTE_ARRAY columns so the CASE - // expressions in `inserted` can reference head_row.. - val leanHeadRowCols = (listOf(PgColumn.id, PgColumn.fn, PgColumn.version) + keepableByteCols).distinct() + // Select head rows that we want to update, lock the rows for update + // We do not select rows in deleted state, as they can't be updated. + // The reason is: Update explicitly only updates a living object + // So it must fail when there is no existing object or only a tombstone exists (logically the same as not existing) val head_row = """, head_row AS ( - SELECT ${if (insert_into_history != null) - effHead.joinToString(", ") { "head.${it.name} AS ${it.name}" } - else leanHeadRowCols.joinToString(", ") { "head.${it.name} AS ${it.name}" }} - FROM ${headTable.quotedName} AS head, new_row - WHERE head.id = new_row.id AND (new_row.expected_version IS NULL OR (new_row.expected_version & -4) = (head.version & -4)) + SELECT ${pgCollection.joinColumns { column -> "head.$column" }} + FROM $headIdent AS head, new_row + WHERE head.$FN = new_row.$FN AND (new_row.expected_version IS NULL OR (new_row.expected_version & -4) = (head.$VERSION & -4)) FOR UPDATE NOWAIT )""" - // Insert the current `head_row` into history. The new tuple's version becomes the demoted row's next_version. - val head_to_history = if (insert_into_history != null) """, head_to_history AS ( - INSERT INTO ${insert_into_history.quotedName} (${PgColumn.next_version}, ${effCopyHistory.joinToString(",") { it.name }}) - SELECT new_row.version AS ${PgColumn.next_version}, - ${effCopyHistory.joinToString(", ") { "head_row.${it.name} AS ${it.name}" }} + // Copy the current HEAD row into HISTORY; set the next version to the version for the history row. + val head_to_history = if (historyTable != null) """, head_to_history AS ( + INSERT INTO $historyIdent ($NEXT_VERSION, ${pgCollection.joinColumns { column -> if (column eq NEXT_VERSION) null else column.ident }}) + SELECT new_row.$VERSION AS $NEXT_VERSION, ${pgCollection.joinColumns { column -> if (column eq NEXT_VERSION) null else "head_row.$column" }}) FROM head_row - LEFT JOIN new_row ON new_row.id = head_row.id - RETURNING id, fn, version + LEFT JOIN new_row ON new_row.$FN = head_row.$FN + RETURNING $FN, $VERSION )""" else "" - // Delete `head_row` from HEAD. + // Delete HEAD rows that have been copied into history. val head_deleted = """, head_deleted AS ( - DELETE FROM ${headTable.quotedName} - WHERE (fn, version) IN (SELECT fn, version FROM head_row) - RETURNING id, fn, version + DELETE FROM $headIdent + WHERE $FN IN (SELECT $FN FROM head_row) + RETURNING $FN, $VERSION )""" val inserted = """, inserted AS ( -INSERT INTO ${headTable.quotedName} ($effHeadNames) -SELECT ${effHead.joinToString(", ") { col -> - if (col in keepableByteCols) - "CASE WHEN new_row.${col.name} = convert_to('undefined', 'UTF8') THEN head_row.${col.name} ELSE new_row.${col.name} END AS ${col.name}" - else "new_row.${col.name} AS ${col.name}" - }} +INSERT INTO $headIdent ($NEXT_VERSION, ${pgCollection.joinColumns { column -> if (column eq NEXT_VERSION) null else column.ident }}) +SELECT NULL AS $NEXT_VERSION, ${pgCollection.joinColumns { column -> + if (column eq NEXT_VERSION) + null + else if (column.memberType == MemberType.BYTE_ARRAY) + "CASE WHEN new_row.$column = convert_to('undefined', 'UTF8') THEN head_row.$column ELSE new_row.$column END AS $column" + else + "head_row.$column" +}}) FROM new_row -LEFT JOIN head_row ON head_row.id = new_row.id -RETURNING id, fn, version${if (keepableByteCols.isNotEmpty()) keepableByteCols.joinToString("") { ", ${it.name}" } else ""} +LEFT JOIN head_row ON head_row.$FN = new_row.$FN +RETURNING $FN, $VERSION${if (byteArrayCols.isNotEmpty()) ", ${byteArrayCols.joinToString(", ") { column -> + // We return NULL, when the input contained data, because the client knows this already, no need to send back. + // If the client provided `undefined`, we return the actual value so we can build a correct tuple. + "CASE WHEN new_row.$column = convert_to('undefined', 'UTF8') THEN $column ELSE null END AS $column" + }}" else ""} )""" - val SQL = """$query$head_select$head_row$head_to_history$head_deleted$inserted + val SQL = """$query$head_row$head_to_history$head_deleted$inserted SELECT - new_row.id AS id, - new_row.fn AS fn, - new_row.version AS version, - head_select.id AS existing_id, - head_select.version AS existing_version, - head_row.id AS head_id, - ${if (head_to_history.isNotEmpty()) "head_to_history.id AS history_id," else ""} - head_deleted.id AS head_deleted_id, - inserted.id AS inserted_id, - ${if (keepableByteCols.isNotEmpty()) keepableByteCols.joinToString(",\n ") { "inserted.${it.name} AS ${it.name}" } else "null::bytea AS attachment"} + new_row.$FN AS $FN, + new_row.$VERSION AS $VERSION, + head_row.$FN AS _existing_fn, + head_row.$VERSION AS _existing_version, + ${if (byteArrayCols.isNotEmpty()) byteArrayCols.joinToString(",\n ") { column -> "inserted.$column AS $column" } + ",\n " else ""} + ${if (head_to_history.isNotEmpty()) "head_to_history.$FN AS _history_fn," else "NULL AS _history_fn"} + head_deleted.$FN AS _head_deleted_fn, + inserted.$FN AS _inserted_fn, FROM new_row -LEFT JOIN head_select ON head_select.id = new_row.id -LEFT JOIN head_row ON head_row.id = new_row.id -${if (head_to_history.isNotEmpty()) "LEFT JOIN head_to_history ON head_to_history.id = new_row.id" else ""} -LEFT JOIN head_deleted ON head_deleted.id = new_row.id -LEFT JOIN inserted ON inserted.id = new_row.id +LEFT JOIN head_row ON head_row.$FN = new_row.$FN +${if (head_to_history.isNotEmpty()) "LEFT JOIN head_to_history ON head_to_history.$FN = new_row.$FN" else ""} +LEFT JOIN head_deleted ON head_deleted.$FN = new_row.$FN +LEFT JOIN inserted ON inserted.$FN = new_row.$FN ;""" val typeNames = inRows.typeNames() val pgPlan = conn.prepare(SQL, typeNames) @@ -133,17 +115,20 @@ LEFT JOIN inserted ON inserted.id = new_row.id if (pgWrites.isEmpty()) return // All nullable BYTE_ARRAY columns may carry the "keep if undefined" sentinel and must be // read back from the DB so the in-memory tuple reflects the final stored value. - val keepableByteCols = pgCollection.effectiveHeadColumns.filter { it.type == PgType.BYTE_ARRAY && it !== PgColumn.feature } val rows = PgRows() .withDatabaseNumber(storageNumber) .withCatalogNumber(catalogNumber) .withCollectionNumber(collectionNumber) - .addColumn("id", PgType.STRING) - .addColumn("existing_id", PgType.STRING) - .addColumn("existing_version", PgType.INT64) - .addColumn("head_id", PgType.STRING) - for (col in keepableByteCols) rows.addColumn(col.name, PgType.BYTE_ARRAY) - val plan = plan(conn, pgCollection) + rows.addColumn(FN) + .addColumn(VERSION) + .addColumn("_existing_fn", MemberType.INT64) + .addColumn("_existing_version", MemberType.INT64) + for (column in byteArrayCols) rows.addColumn(column) + rows.addColumn("_history_fn", MemberType.INT64) + rows.addColumn("_head_deleted_fn", MemberType.INT64) + rows.addColumn("_inserted_fn", MemberType.INT64) + + val plan = plan(conn) val array = this.inRows.values() if (PlatformUtil.ENABLE_INFO) { if (session.logQueries) { @@ -162,7 +147,7 @@ LEFT JOIN inserted ON inserted.id = new_row.id logger.info("UPDATE of ${rows.size} rows took ${seconds * 1000}ms, therefore ${rows.size / seconds} features/s, partitions: $featureCountByPartitionJoined") } cursor.fetch().use { - rows.addAll(cursor) + rows.readAll(cursor) for (rowNum in 0 until rows.size) { // The original `id` of the feature to update. val id = rows.getString(rowNum, "id") ?: throw illegalState("Column 'id' in result must not be null") @@ -173,38 +158,38 @@ LEFT JOIN inserted ON inserted.id = new_row.id } val existing_version = rows.getInt64(rowNum, "existing_version") ?: throw illegalState("Missing version in HEAD select for feature '$id'") // Fetch the original write and tuple for this row. - val write = writeById[id] ?: throw illegalState("Missing write state for feature '$id'") - val tuple = write.tuple ?: throw generalException("Missing tuple for feature '$id'") - // The `id` from the eventually read head-row, this is only available, if the existing_id is the expected version! - val head_id = rows.getString(rowNum, "head_id") - if (head_id != id) { // Conflict! - val expectedVersion = write.version ?: throw illegalState("Missing expected version for feature '$id'") - throw conflict("The feature '$id' was expected in version $expectedVersion, but actually found in ${Version(existing_version)}") - } - // Patch back all BYTE_ARRAY columns whose stored value may differ from what the client sent - // (sentinel "undefined" causes the DB to retain the existing value). - val geo = if (PgColumn.geo in keepableByteCols) rows.getByteArray(rowNum, PgColumn.geo.name) else tuple.getByteArray(StandardMembers.Geometry) - val referencePoint = if (PgColumn.ref_point in keepableByteCols) rows.getByteArray(rowNum, PgColumn.ref_point.name) else tuple.getByteArray(StandardMembers.ReferencePoint) - val tags = tuple.getString(StandardMembers.XyzTags) - val attachment = if (PgColumn.attachment in keepableByteCols) rows.getByteArray(rowNum, PgColumn.attachment.name) else tuple.getByteArray(StandardMembers.XyzAttachment) - val oldGeo = tuple.getByteArray(StandardMembers.Geometry) - val oldRefPoint = tuple.getByteArray(StandardMembers.ReferencePoint) - val oldAttachment = tuple.getByteArray(StandardMembers.XyzAttachment) - val needsPatch = (oldGeo == null || !oldGeo.contentEquals(geo ?: ByteArray(0))) - || (oldRefPoint == null || !oldRefPoint.contentEquals(referencePoint ?: ByteArray(0))) - || (oldAttachment == null || !oldAttachment.contentEquals(attachment ?: ByteArray(0))) - if (needsPatch) { - val m = tuple.membersBook - val newMembers = if (m is naksha.jbon.HeapBook) { - val dict = m.copy() - dict.put(StandardMembers.Geometry.name, geo) - dict.put(StandardMembers.ReferencePoint.name, referencePoint) - dict.put(StandardMembers.XyzTags.name, tags) - dict.put(StandardMembers.XyzAttachment.name, attachment) - dict - } else m - write.tuple = tuple.copy(membersBook = newMembers) - } +// val write = writeById[id] ?: throw illegalState("Missing write state for feature '$id'") +// val tuple = write.tuple ?: throw generalException("Missing tuple for feature '$id'") +// // The `id` from the eventually read head-row, this is only available, if the existing_id is the expected version! +// val head_id = rows.getString(rowNum, "head_id") +// if (head_id != id) { // Conflict! +// val expectedVersion = write.version ?: throw illegalState("Missing expected version for feature '$id'") +// throw conflict("The feature '$id' was expected in version $expectedVersion, but actually found in ${Version(existing_version)}") +// } +// // Patch back all BYTE_ARRAY columns whose stored value may differ from what the client sent +// // (sentinel "undefined" causes the DB to retain the existing value). +// val geo = if (PgColumn.geo in keepableByteCols) rows.getByteArray(rowNum, PgColumn.geo.name) else tuple.getByteArray(StandardMembers.Geometry) +// val referencePoint = if (PgColumn.ref_point in keepableByteCols) rows.getByteArray(rowNum, PgColumn.ref_point.name) else tuple.getByteArray(StandardMembers.ReferencePoint) +// val tags = tuple.getString(StandardMembers.XyzTags) +// val attachment = if (PgColumn.attachment in keepableByteCols) rows.getByteArray(rowNum, PgColumn.attachment.name) else tuple.getByteArray(StandardMembers.XyzAttachment) +// val oldGeo = tuple.getByteArray(StandardMembers.Geometry) +// val oldRefPoint = tuple.getByteArray(StandardMembers.ReferencePoint) +// val oldAttachment = tuple.getByteArray(StandardMembers.XyzAttachment) +// val needsPatch = (oldGeo == null || !oldGeo.contentEquals(geo ?: ByteArray(0))) +// || (oldRefPoint == null || !oldRefPoint.contentEquals(referencePoint ?: ByteArray(0))) +// || (oldAttachment == null || !oldAttachment.contentEquals(attachment ?: ByteArray(0))) +// if (needsPatch) { +// val m = tuple.membersBook +// val newMembers = if (m is naksha.jbon.HeapBook) { +// val dict = m.copy() +// dict.put(StandardMembers.Geometry.name, geo) +// dict.put(StandardMembers.ReferencePoint.name, referencePoint) +// dict.put(StandardMembers.XyzTags.name, tags) +// dict.put(StandardMembers.XyzAttachment.name, attachment) +// dict +// } else m +// write.tuple = tuple.copy(membersBook = newMembers) +// } } } } From 0ee5df5f47a71b33b263d0dfc6aa8046ffd69601 Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Thu, 25 Jun 2026 18:04:07 +0200 Subject: [PATCH 47/57] Final fixes for update. Signed-off-by: Alexander Lowey-Weber --- .../kotlin/naksha/psql/PgWriterUpdate.kt | 128 ++++++++++-------- 1 file changed, 69 insertions(+), 59 deletions(-) diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt index 604a65394..3eda03b3b 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgWriterUpdate.kt @@ -4,10 +4,10 @@ import naksha.base.Int64 import naksha.base.Platform import naksha.base.Platform.PlatformCompanion.logger import naksha.base.PlatformUtil +import naksha.jbon.HeapBook import naksha.model.* import naksha.model.objects.MemberType import naksha.model.objects.StandardMembers -import naksha.model.objects.StoreMode import naksha.psql.PgColumn.PgColumn_C.FN import naksha.psql.PgColumn.PgColumn_C.NEXT_VERSION import naksha.psql.PgColumn.PgColumn_C.VERSION @@ -44,15 +44,19 @@ internal class PgWriterUpdate( SELECT * FROM UNNEST(${inRows.placeholders()}) AS t(${inRows.aliases()}) )""" - // Select head rows that we want to update, lock the rows for update - // We do not select rows in deleted state, as they can't be updated. - // The reason is: Update explicitly only updates a living object - // So it must fail when there is no existing object or only a tombstone exists (logically the same as not existing) + // Select rows from HEAD that we want to update, lock the rows for update + val existing_rows = """, existing_rows AS ( + SELECT head.$FN AS $FN, head.$VERSION AS $VERSION, + FROM $headIdent AS head, new_row + WHERE head.$FN = new_row.$FN + FOR UPDATE NOWAIT +)""" + + // Select all rows from HEAD that we want to update AND that have the correct version. val head_row = """, head_row AS ( SELECT ${pgCollection.joinColumns { column -> "head.$column" }} FROM $headIdent AS head, new_row WHERE head.$FN = new_row.$FN AND (new_row.expected_version IS NULL OR (new_row.expected_version & -4) = (head.$VERSION & -4)) - FOR UPDATE NOWAIT )""" // Copy the current HEAD row into HISTORY; set the next version to the version for the history row. @@ -90,17 +94,18 @@ RETURNING $FN, $VERSION${if (byteArrayCols.isNotEmpty()) ", ${byteArrayCols.join }}" else ""} )""" - val SQL = """$query$head_row$head_to_history$head_deleted$inserted + val SQL = """$query$existing_rows$head_row$head_to_history$head_deleted$inserted SELECT new_row.$FN AS $FN, new_row.$VERSION AS $VERSION, - head_row.$FN AS _existing_fn, - head_row.$VERSION AS _existing_version, + existing_rows.$FN AS _existing_fn, + existing_rows.$VERSION AS _existing_version, ${if (byteArrayCols.isNotEmpty()) byteArrayCols.joinToString(",\n ") { column -> "inserted.$column AS $column" } + ",\n " else ""} ${if (head_to_history.isNotEmpty()) "head_to_history.$FN AS _history_fn," else "NULL AS _history_fn"} head_deleted.$FN AS _head_deleted_fn, - inserted.$FN AS _inserted_fn, + inserted.$FN AS _inserted_fn FROM new_row +LEFT JOIN existing_rows ON existing_rows.$FN = new_row.$FN LEFT JOIN head_row ON head_row.$FN = new_row.$FN ${if (head_to_history.isNotEmpty()) "LEFT JOIN head_to_history ON head_to_history.$FN = new_row.$FN" else ""} LEFT JOIN head_deleted ON head_deleted.$FN = new_row.$FN @@ -115,18 +120,18 @@ LEFT JOIN inserted ON inserted.$FN = new_row.$FN if (pgWrites.isEmpty()) return // All nullable BYTE_ARRAY columns may carry the "keep if undefined" sentinel and must be // read back from the DB so the in-memory tuple reflects the final stored value. - val rows = PgRows() + val outRows = PgRows() .withDatabaseNumber(storageNumber) .withCatalogNumber(catalogNumber) .withCollectionNumber(collectionNumber) - rows.addColumn(FN) + outRows.addColumn(FN) .addColumn(VERSION) .addColumn("_existing_fn", MemberType.INT64) .addColumn("_existing_version", MemberType.INT64) - for (column in byteArrayCols) rows.addColumn(column) - rows.addColumn("_history_fn", MemberType.INT64) - rows.addColumn("_head_deleted_fn", MemberType.INT64) - rows.addColumn("_inserted_fn", MemberType.INT64) + for (column in byteArrayCols) outRows.addColumn(column) + outRows.addColumn("_history_fn", MemberType.INT64) + outRows.addColumn("_head_deleted_fn", MemberType.INT64) + outRows.addColumn("_inserted_fn", MemberType.INT64) val plan = plan(conn) val array = this.inRows.values() @@ -144,52 +149,57 @@ LEFT JOIN inserted ON inserted.$FN = new_row.$FN val end = Platform.currentNanos() val seconds = (end.toDouble() - start.toDouble()) / 1e9 if (pgWrites.size != 1 || pgWrites[0].isFeatureModification) { - logger.info("UPDATE of ${rows.size} rows took ${seconds * 1000}ms, therefore ${rows.size / seconds} features/s, partitions: $featureCountByPartitionJoined") + logger.info("UPDATE of ${outRows.size} rows took ${seconds * 1000}ms, therefore ${outRows.size / seconds} features/s, partitions: $featureCountByPartitionJoined") } cursor.fetch().use { - rows.readAll(cursor) - for (rowNum in 0 until rows.size) { - // The original `id` of the feature to update. - val id = rows.getString(rowNum, "id") ?: throw illegalState("Column 'id' in result must not be null") - // The `id` and `tuple-number` currently in HEAD table. - val existing_id = rows.getString(rowNum, "existing_id") - if (existing_id != id) { - throw featureNotFound("Failed to update feature '$id', no such feature exists") + outRows.readAll(cursor) + for (row in 0 until outRows.size) { + val fn = outRows.getInt64(row, FN) ?: throw illegalState("Column '$FN' in result must not be null") + val version = outRows.getInt64(row, VERSION) ?: throw illegalState("Column '$VERSION' in result must not be null") + val newTn = TupleNumber(storageNumber, catalogNumber, collectionNumber, fn, version) + val pgWrite = writeByFn[fn] ?: throw illegalState("Missing write record for feature-number: $fn") + val expected_version: Int64? = pgWrite.version?.number + + // Feature should have existed. + val existing_fn = outRows.getInt64(row, "_existing_fn") + ?: throw featureNotFound("Failed to update feature '${pgWrite.id}', no such feature exists") + if (existing_fn != fn) { + // We do not expect this to ever happen! + throw generalException("Internal error, feature-number mismatch for feature '${pgWrite.id}', expected fn: $fn, existing fn: $existing_fn") + } + val existing_version = outRows.getInt64(row, "_existing_version") + // We do not expect this to ever happen, when we have an existing_fn there must be as well an existing_version! + ?: throw generalException("Internal error, missing existing version for feature '${pgWrite.id}' in result-set") + val previousTupleNumber = TupleNumber(storageNumber, catalogNumber, collectionNumber, existing_fn, existing_version) + + // We should have updated the feature + val inserted_fn = outRows.getInt64(row, "_inserted_fn") ?: { + // The only defined reason is that the expected version did not match. + if (expected_version != null && (expected_version and Int64(-4)) != (existing_version and Int64(-4))) { + throw conflict("Atomic update failed, feature '${pgWrite.id}' was expected in version $existing_version, but found to be in $existing_version" ) + } + // Otherwise, there is an internal error. + throw generalException("Internal error, failed to update feature '${pgWrite.id}', update was skipped for unknown reason") + } + + val tuple = pgWrite.tuple ?: throw generalException("Missing tuple for feature '${pgWrite.id}}'") + val memberBook = tuple.membersBook + val updatedMembersBook = HeapBook.copyOf(memberBook) + // TODO: Fix change-count ! + // if (CC != null) updatedMembersBook.put(CC.name, change_count) + updatedMembersBook.put(StandardMembers.Tn.name, newTn) + // Update all BYTE_ARRAY members that have been updated. + for (column in byteArrayCols) { + val inValue = tuple.membersBook[column.name] as ByteArray? + val newValue = if (inValue == null || UNDEFINED.contentEquals(inValue)) outRows.getByteArray(row, column) else inValue + updatedMembersBook.put(column.name, newValue) } - val existing_version = rows.getInt64(rowNum, "existing_version") ?: throw illegalState("Missing version in HEAD select for feature '$id'") - // Fetch the original write and tuple for this row. -// val write = writeById[id] ?: throw illegalState("Missing write state for feature '$id'") -// val tuple = write.tuple ?: throw generalException("Missing tuple for feature '$id'") -// // The `id` from the eventually read head-row, this is only available, if the existing_id is the expected version! -// val head_id = rows.getString(rowNum, "head_id") -// if (head_id != id) { // Conflict! -// val expectedVersion = write.version ?: throw illegalState("Missing expected version for feature '$id'") -// throw conflict("The feature '$id' was expected in version $expectedVersion, but actually found in ${Version(existing_version)}") -// } -// // Patch back all BYTE_ARRAY columns whose stored value may differ from what the client sent -// // (sentinel "undefined" causes the DB to retain the existing value). -// val geo = if (PgColumn.geo in keepableByteCols) rows.getByteArray(rowNum, PgColumn.geo.name) else tuple.getByteArray(StandardMembers.Geometry) -// val referencePoint = if (PgColumn.ref_point in keepableByteCols) rows.getByteArray(rowNum, PgColumn.ref_point.name) else tuple.getByteArray(StandardMembers.ReferencePoint) -// val tags = tuple.getString(StandardMembers.XyzTags) -// val attachment = if (PgColumn.attachment in keepableByteCols) rows.getByteArray(rowNum, PgColumn.attachment.name) else tuple.getByteArray(StandardMembers.XyzAttachment) -// val oldGeo = tuple.getByteArray(StandardMembers.Geometry) -// val oldRefPoint = tuple.getByteArray(StandardMembers.ReferencePoint) -// val oldAttachment = tuple.getByteArray(StandardMembers.XyzAttachment) -// val needsPatch = (oldGeo == null || !oldGeo.contentEquals(geo ?: ByteArray(0))) -// || (oldRefPoint == null || !oldRefPoint.contentEquals(referencePoint ?: ByteArray(0))) -// || (oldAttachment == null || !oldAttachment.contentEquals(attachment ?: ByteArray(0))) -// if (needsPatch) { -// val m = tuple.membersBook -// val newMembers = if (m is naksha.jbon.HeapBook) { -// val dict = m.copy() -// dict.put(StandardMembers.Geometry.name, geo) -// dict.put(StandardMembers.ReferencePoint.name, referencePoint) -// dict.put(StandardMembers.XyzTags.name, tags) -// dict.put(StandardMembers.XyzAttachment.name, attachment) -// dict -// } else m -// write.tuple = tuple.copy(membersBook = newMembers) -// } + val updatedTuple = tuple.copy( + membersBook = updatedMembersBook, + previousTupleNumber = previousTupleNumber + ) + pgWrite.tuple = updatedTuple + pgWrite.tupleNumber = newTn } } } From 12a50a55df3662bd3d9c12a23c3008529f7d42cb Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Fri, 26 Jun 2026 10:14:02 +0200 Subject: [PATCH 48/57] Add missing documentation to members. Signed-off-by: Alexander Lowey-Weber --- .../commonMain/kotlin/naksha/model/objects/BoolMember.kt | 7 ++++++- .../kotlin/naksha/model/objects/ByteArrayMember.kt | 5 +++++ .../kotlin/naksha/model/objects/Float32Member.kt | 5 +++++ .../kotlin/naksha/model/objects/Float64Member.kt | 5 +++++ .../commonMain/kotlin/naksha/model/objects/Int16Member.kt | 5 +++++ .../commonMain/kotlin/naksha/model/objects/Int32Member.kt | 5 +++++ .../commonMain/kotlin/naksha/model/objects/Int64Member.kt | 5 +++++ .../commonMain/kotlin/naksha/model/objects/Int8Member.kt | 5 +++++ .../kotlin/naksha/model/objects/SpatialMember.kt | 5 +++++ .../commonMain/kotlin/naksha/model/objects/StringMember.kt | 5 +++++ .../kotlin/naksha/model/objects/TagListMember.kt | 5 +++++ .../commonMain/kotlin/naksha/model/objects/TagsMember.kt | 5 +++++ .../kotlin/naksha/model/objects/TupleNumberMember.kt | 5 +++++ 13 files changed, 66 insertions(+), 1 deletion(-) diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/BoolMember.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/BoolMember.kt index d4799b9d8..88056c3bb 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/BoolMember.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/BoolMember.kt @@ -13,6 +13,7 @@ class BoolMember() : TypedMember() { return this } + /** Creates a new boolean member with the given name and an optional custom JSON path. */ @JsName("of") constructor(name: String, path: JsonPath? = null) : this() { this.name = name @@ -21,14 +22,18 @@ class BoolMember() : TypedMember() { this.path.validate() } + /** Creates a boolean member from an existing [Member], validating its type. */ @JsName("from") constructor(member: Member, path: JsonPath? = null) : this() { if (member.dataType != BOOLEAN) throw illegalArg("The given member is not of boolean type") this.name = member.name this.dataType = BOOLEAN - this.path = path?.validate() ?: member.path // Only verify modified path. + this.path = path?.validate() ?: member.path } + /** Retrieves the boolean value of this member from the given feature. */ fun get(feature: NakshaFeature): Boolean? = getBoolean(feature) + + /** Sets the boolean value of this member on the given feature. */ fun set(feature: NakshaFeature, value: Boolean): Boolean? = setPath(feature, path, value) as Boolean? } \ No newline at end of file diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/ByteArrayMember.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/ByteArrayMember.kt index 3a9fcc7ce..260e5737d 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/ByteArrayMember.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/ByteArrayMember.kt @@ -13,6 +13,7 @@ class ByteArrayMember() : TypedMember() { return this } + /** Creates a new byte array member with the given name and an optional custom JSON path. */ @JsName("of") constructor(name: String, path: JsonPath? = null) : this() { this.name = name @@ -21,6 +22,7 @@ class ByteArrayMember() : TypedMember() { this.path.validate() } + /** Creates a byte array member from an existing [Member], validating its type. */ @JsName("from") constructor(member: Member, path: JsonPath? = null) : this() { if (member.dataType != BYTE_ARRAY) throw illegalArg("The given member is not of byte_array type") @@ -29,6 +31,9 @@ class ByteArrayMember() : TypedMember() { this.path = path?.validate() ?: member.path } + /** Retrieves the byte array value of this member from the given feature. */ fun get(feature: NakshaFeature): ByteArray? = getByteArray(feature) + + /** Sets the byte array value of this member on the given feature. */ fun set(feature: NakshaFeature, value: ByteArray): Any? = setPath(feature, path, value) } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Float32Member.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Float32Member.kt index ab95a317f..d0da8f903 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Float32Member.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Float32Member.kt @@ -13,6 +13,7 @@ class Float32Member() : TypedMember() { return this } + /** Creates a new float32 member with the given name and an optional custom JSON path. */ @JsName("of") constructor(name: String, path: JsonPath? = null) : this() { this.name = name @@ -21,6 +22,7 @@ class Float32Member() : TypedMember() { this.path.validate() } + /** Creates a float32 member from an existing [Member], validating its type. */ @JsName("from") constructor(member: Member, path: JsonPath? = null) : this() { if (member.dataType != FLOAT32) throw illegalArg("The given member is not of float32 type") @@ -29,6 +31,9 @@ class Float32Member() : TypedMember() { this.path = path?.validate() ?: member.path } + /** Retrieves the float32 value of this member from the given feature. */ fun get(feature: NakshaFeature): Float? = getDouble(feature)?.toFloat() + + /** Sets the float32 value of this member on the given feature. */ fun set(feature: NakshaFeature, value: Float): Any? = setPath(feature, path, value) } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Float64Member.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Float64Member.kt index 15a727c13..d3b175b78 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Float64Member.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Float64Member.kt @@ -13,6 +13,7 @@ class Float64Member() : TypedMember() { return this } + /** Creates a new float64 member with the given name and an optional custom JSON path. */ @JsName("of") constructor(name: String, path: JsonPath? = null) : this() { this.name = name @@ -21,6 +22,7 @@ class Float64Member() : TypedMember() { this.path.validate() } + /** Creates a float64 member from an existing [Member], validating its type. */ @JsName("from") constructor(member: Member, path: JsonPath? = null) : this() { if (member.dataType != FLOAT64) throw illegalArg("The given member is not of float64 type") @@ -29,6 +31,9 @@ class Float64Member() : TypedMember() { this.path = path?.validate() ?: member.path } + /** Retrieves the float64 value of this member from the given feature. */ fun get(feature: NakshaFeature): Double? = getDouble(feature) + + /** Sets the float64 value of this member on the given feature. */ fun set(feature: NakshaFeature, value: Double): Any? = setPath(feature, path, value) } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int16Member.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int16Member.kt index 9341c72b2..7c1e6be0a 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int16Member.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int16Member.kt @@ -13,6 +13,7 @@ class Int16Member() : TypedMember() { return this } + /** Creates a new int16 member with the given name and an optional custom JSON path. */ @JsName("of") constructor(name: String, path: JsonPath? = null) : this() { this.name = name @@ -21,6 +22,7 @@ class Int16Member() : TypedMember() { this.path.validate() } + /** Creates an int16 member from an existing [Member], validating its type. */ @JsName("from") constructor(member: Member, path: JsonPath? = null) : this() { if (member.dataType != INT16) throw illegalArg("The given member is not of int16 type") @@ -29,6 +31,9 @@ class Int16Member() : TypedMember() { this.path = path?.validate() ?: member.path } + /** Retrieves the int16 value of this member from the given feature. */ fun get(feature: NakshaFeature): Short? = getInt64(feature)?.toShort() + + /** Sets the int16 value of this member on the given feature. */ fun set(feature: NakshaFeature, value: Short): Any? = setPath(feature, path, value) } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int32Member.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int32Member.kt index 1628515ad..e96a9f57b 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int32Member.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int32Member.kt @@ -13,6 +13,7 @@ class Int32Member() : TypedMember() { return this } + /** Creates a new int32 member with the given name and an optional custom JSON path. */ @JsName("of") constructor(name: String, path: JsonPath? = null) : this() { this.name = name @@ -21,6 +22,7 @@ class Int32Member() : TypedMember() { this.path.validate() } + /** Creates an int32 member from an existing [Member], validating its type. */ @JsName("from") constructor(member: Member, path: JsonPath? = null) : this() { if (member.dataType != INT32) throw illegalArg("The given member is not of int32 type") @@ -29,6 +31,9 @@ class Int32Member() : TypedMember() { this.path = path?.validate() ?: member.path } + /** Retrieves the int32 value of this member from the given feature. */ fun get(feature: NakshaFeature): Int? = getInt64(feature)?.toInt() + + /** Sets the int32 value of this member on the given feature. */ fun set(feature: NakshaFeature, value: Int): Any? = setPath(feature, path, value) } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int64Member.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int64Member.kt index aa9f19294..3f4bd3497 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int64Member.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int64Member.kt @@ -14,6 +14,7 @@ class Int64Member() : TypedMember() { return this } + /** Creates a new int64 member with the given name and an optional custom JSON path. */ @JsName("of") constructor(name: String, path: JsonPath? = null) : this() { this.name = name @@ -22,6 +23,7 @@ class Int64Member() : TypedMember() { this.path.validate() } + /** Creates an int64 member from an existing [Member], validating its type. */ @JsName("from") constructor(member: Member, path: JsonPath? = null) : this() { if (member.dataType != INT64) throw illegalArg("The given member is not of int64 type") @@ -30,6 +32,9 @@ class Int64Member() : TypedMember() { this.path = path?.validate() ?: member.path } + /** Retrieves the int64 value of this member from the given feature. */ fun get(feature: NakshaFeature): Int64? = getInt64(feature) + + /** Sets the int64 value of this member on the given feature. */ fun set(feature: NakshaFeature, value: Int64): Any? = setPath(feature, path, value) } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int8Member.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int8Member.kt index 3a5e4873a..b14f9e70a 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int8Member.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int8Member.kt @@ -13,6 +13,7 @@ class Int8Member() : TypedMember() { return this } + /** Creates a new int8 member with the given name and an optional custom JSON path. */ @JsName("of") constructor(name: String, path: JsonPath? = null) : this() { this.name = name @@ -21,6 +22,7 @@ class Int8Member() : TypedMember() { this.path.validate() } + /** Creates an int8 member from an existing [Member], validating its type. */ @JsName("from") constructor(member: Member, path: JsonPath? = null) : this() { if (member.dataType != INT8) throw illegalArg("The given member is not of int8 type") @@ -29,6 +31,9 @@ class Int8Member() : TypedMember() { this.path = path?.validate() ?: member.path } + /** Retrieves the int8 value of this member from the given feature. */ fun get(feature: NakshaFeature): Byte? = getInt64(feature)?.toByte() + + /** Sets the int8 value of this member on the given feature. */ fun set(feature: NakshaFeature, value: Byte): Any? = setPath(feature, path, value) } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/SpatialMember.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/SpatialMember.kt index 40e3d589a..dd8829296 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/SpatialMember.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/SpatialMember.kt @@ -14,6 +14,7 @@ class SpatialMember() : TypedMember() { return this } + /** Creates a new spatial member with the given name and an optional custom JSON path. */ @JsName("of") constructor(name: String, path: JsonPath? = null) : this() { this.name = name @@ -22,6 +23,7 @@ class SpatialMember() : TypedMember() { this.path.validate() } + /** Creates a spatial member from an existing [Member], validating its type. */ @JsName("from") constructor(member: Member, path: JsonPath? = null) : this() { if (member.dataType != SPATIAL) throw illegalArg("The given member is not of spatial type") @@ -30,6 +32,9 @@ class SpatialMember() : TypedMember() { this.path = path?.validate() ?: member.path } + /** Retrieves the spatial geometry value of this member from the given feature. */ fun get(feature: NakshaFeature): SpGeometry? = getGeometry(feature) + + /** Sets the spatial geometry value of this member on the given feature. */ fun set(feature: NakshaFeature, value: SpGeometry): Any? = setPath(feature, path, value) } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StringMember.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StringMember.kt index 02080ff21..9956eba97 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StringMember.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StringMember.kt @@ -13,6 +13,7 @@ class StringMember() : TypedMember() { return this } + /** Creates a new string member with the given name and an optional custom JSON path. */ @JsName("of") constructor(name: String, path: JsonPath? = null) : this() { this.name = name @@ -21,6 +22,7 @@ class StringMember() : TypedMember() { this.path.validate() } + /** Creates a string member from an existing [Member], validating its type. */ @JsName("from") constructor(member: Member, path: JsonPath? = null) : this() { if (member.dataType != STRING) throw illegalArg("The given member is not of string type") @@ -29,6 +31,9 @@ class StringMember() : TypedMember() { this.path = path?.validate() ?: member.path } + /** Retrieves the string value of this member from the given feature. */ fun get(feature: NakshaFeature): String? = getString(feature) + + /** Sets the string value of this member on the given feature. */ fun set(feature: NakshaFeature, value: String): Any? = setPath(feature, path, value) } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TagListMember.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TagListMember.kt index 4cb05b478..10dd723c5 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TagListMember.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TagListMember.kt @@ -15,6 +15,7 @@ class TagListMember() : TypedMember() { return this } + /** Creates a new tag list member with the given name and an optional custom JSON path. */ @JsName("of") constructor(name: String, path: JsonPath? = null) : this() { this.name = name @@ -23,6 +24,7 @@ class TagListMember() : TypedMember() { this.path.validate() } + /** Creates a tag list member from an existing [Member], validating its type. */ @JsName("from") constructor(member: Member, path: JsonPath? = null) : this() { if (member.dataType != TAG_LIST) throw illegalArg("The given member is not of tag_list type") @@ -31,6 +33,9 @@ class TagListMember() : TypedMember() { this.path = path?.validate() ?: member.path } + /** Retrieves the tag list value of this member from the given feature. */ fun get(feature: NakshaFeature): TagList? = getTagList(feature) + + /** Sets the tag list value of this member on the given feature. */ fun set(feature: NakshaFeature, value: ListProxy<*>): Any? = setPath(feature, path, value) } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TagsMember.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TagsMember.kt index 4521c4d16..5b1f76451 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TagsMember.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TagsMember.kt @@ -14,6 +14,7 @@ class TagsMember() : TypedMember() { return this } + /** Creates a new tag map member with the given name and an optional custom JSON path. */ @JsName("of") constructor(name: String, path: JsonPath? = null) : this() { this.name = name @@ -22,6 +23,7 @@ class TagsMember() : TypedMember() { this.path.validate() } + /** Creates a tag map member from an existing [Member], validating its type. */ @JsName("from") constructor(member: Member, path: JsonPath? = null) : this() { if (member.dataType != TAG_MAP) throw illegalArg("The given member is not of tags type") @@ -30,6 +32,9 @@ class TagsMember() : TypedMember() { this.path = path?.validate() ?: member.path } + /** Retrieves the tag map value of this member from the given feature. */ fun get(feature: NakshaFeature): TagMap? = getTagMap(feature) + + /** Sets the tag map value of this member on the given feature. */ fun set(feature: NakshaFeature, value: TagMap): Any? = setPath(feature, path, value) } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TupleNumberMember.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TupleNumberMember.kt index f07c4f7be..e741b3413 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TupleNumberMember.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TupleNumberMember.kt @@ -14,6 +14,7 @@ class TupleNumberMember() : TypedMember() { return this } + /** Creates a new tuple number member with the given name and an optional custom JSON path. */ @JsName("of") constructor(name: String, path: JsonPath? = null) : this() { this.name = name @@ -22,6 +23,7 @@ class TupleNumberMember() : TypedMember() { this.path.validate() } + /** Creates a tuple number member from an existing [Member], validating its type. */ @JsName("from") constructor(member: Member, path: JsonPath? = null) : this() { if (member.dataType != TUPLE_NUMBER) throw illegalArg("The given member is not of tuple_number type") @@ -30,6 +32,9 @@ class TupleNumberMember() : TypedMember() { this.path = path?.validate() ?: member.path } + /** Retrieves the tuple number value of this member from the given feature. */ fun get(feature: NakshaFeature): TupleNumber? = getTupleNumber(feature) + + /** Sets the tuple number value of this member on the given feature. */ fun set(feature: NakshaFeature, value: TupleNumber): Any? = setPath(feature, path, value) } From 8125be55538018790dd045a3d55722e99229a4a8 Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Fri, 26 Jun 2026 10:14:51 +0200 Subject: [PATCH 49/57] Add equals infix operator to member and allow TupleNumber.fromByteArray to auto-detect by array length. --- .../src/commonMain/kotlin/naksha/model/TupleNumber.kt | 6 +++--- .../src/commonMain/kotlin/naksha/model/objects/Member.kt | 9 +++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumber.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumber.kt index 9ab3bc856..4fb4ef9cc 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumber.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TupleNumber.kt @@ -353,7 +353,7 @@ data class TupleNumber( * Restore a [TupleNumber] from a binary encoding. * @param bytes the binary to read. * @param offset the index of the first byte to read. - * @param variant the variant to read. + * @param variant the variant to read, if omitted, the value is auto-detected by the byte-array size. * @param storageNumber if the binary does not encode the storage-number _(anything other than [B256])_, so variant is [B64], [B128], [B160], or [B192]. * @param mapNumber if the binary does not encode the map-number, so variant is [B64], [B128], or [B160]. * @param collectionNumber if the binary does not encode the collection-number, so variant is [B64] or [B128]. @@ -364,8 +364,8 @@ data class TupleNumber( @JvmOverloads fun fromByteArray( bytes: ByteArray, - offset: Int, - variant: TupleNumberVariant, + offset: Int = 0, + variant: TupleNumberVariant = TupleNumberVariant.fromValue(bytes.size - offset), storageNumber: Int64? = null, mapNumber: Int? = null, collectionNumber: Int? = null, diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt index 30f763e25..a296b4e94 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt @@ -180,6 +180,14 @@ open class Member() : AnyObject(), Comparator { return this } + /** + * Check if this member is the same as the given one, so [name] and [dataType] match. + * @param other the other member to test against. + * @return _true_ if the two members are the same; _false_ otherwise. + * @since 3.0 + */ + infix fun eq(other: Member?): Boolean = isSameAs(other) + /** * Ensures that the given `found` member is the same as the `expected` member, allow different path. * @param other The member to compare this member with. @@ -234,6 +242,7 @@ open class Member() : AnyObject(), Comparator { val raw = feature.getPath(path) if (raw is TupleNumber) return raw if (raw is String) return TupleNumber.fromString(raw) + if (raw is ByteArray) return TupleNumber.fromByteArray(raw) return null } From 0c015480a1bc00fa3b0586de9077d0060e5ae5b0 Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Fri, 26 Jun 2026 10:15:12 +0200 Subject: [PATCH 50/57] Make default XYZ member being typed members. Signed-off-by: Alexander Lowey-Weber --- .../kotlin/naksha/model/objects/XyzMembers.kt | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzMembers.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzMembers.kt index fde841494..30b6fd865 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzMembers.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzMembers.kt @@ -40,21 +40,21 @@ class XyzMembers private constructor() { * @since 3.0 */ @JvmField @JsStatic - val XyzGlobalBookFeatureNumber = Member(StandardMembers.GlobalBookFeatureNumber, JsonPath("properties", "@ns:com:here:xyz", "globalBookFn")) + val XyzGlobalBookFeatureNumber = Int64Member(StandardMembers.GlobalBookFeatureNumber, JsonPath("properties", "@ns:com:here:xyz", "globalBookFn")) /** * The same as [StandardMembers.Feature]. * @since 3.0 */ @JvmField @JsStatic - val XyzFeature = Member(StandardMembers.Feature, JsonPath()) + val XyzFeature = ByteArrayMember(StandardMembers.Feature, JsonPath()) /** * The same as [StandardMembers.Feature]. * @since 3.0 */ @JvmField @JsStatic - val XyzId = Member(StandardMembers.Id, JsonPath("id")) + val XyzId = StringMember(StandardMembers.Id, JsonPath("id")) // ------------------------------------------------------------------------- // Optional members. @@ -66,14 +66,14 @@ class XyzMembers private constructor() { * @since 3.0 */ @JvmField @JsStatic - val XyzGeometry = Member("geo", MemberType.SPATIAL, JsonPath("geometry")) + val XyzGeometry = SpatialMember(StandardMembers.Geometry, JsonPath("geometry")) /** * `updated_at` — millisecond epoch timestamp of the last modification. Default member. * @since 3.0 */ @JvmField @JsStatic - val XyzUpdatedAt = Member("updated_at", MemberType.INT64, JsonPath("properties", "@ns:com:here:xyz", "updatedAt")) + val XyzUpdatedAt = Int64Member("updated_at", JsonPath("properties", "@ns:com:here:xyz", "updatedAt")) /** * `created_at` — millisecond epoch timestamp of the initial creation. `null` means the @@ -81,7 +81,7 @@ class XyzMembers private constructor() { * @since 3.0 */ @JvmField @JsStatic - val XyzCreatedAt = Member("created_at", MemberType.INT64, JsonPath("properties", "@ns:com:here:xyz", "createdAt")) + val XyzCreatedAt = Int64Member("created_at", JsonPath("properties", "@ns:com:here:xyz", "createdAt")) /** * `author_ts` — millisecond epoch timestamp of the last author change. `null` means the @@ -89,7 +89,7 @@ class XyzMembers private constructor() { * @since 3.0 */ @JvmField @JsStatic - val XyzAuthorTimestamp = Member("author_ts", MemberType.INT64, JsonPath("properties", "@ns:com:here:xyz", "authorTs")) + val XyzAuthorTimestamp = Int64Member("author_ts", JsonPath("properties", "@ns:com:here:xyz", "authorTs")) /** * `hash` — content hash of the tuple, computed by the storage. `null` if not recorded. @@ -97,7 +97,7 @@ class XyzMembers private constructor() { * @since 3.0 */ @JvmField @JsStatic - val XyzHash = Member("hash", MemberType.INT32, JsonPath("properties", "@ns:com:here:xyz", "hash")) + val XyzHash = Int32Member("hash", JsonPath("properties", "@ns:com:here:xyz", "hash")) /** * `here_tile` — HERE tile key (binary) of the reference point. `null` if not known. @@ -105,14 +105,14 @@ class XyzMembers private constructor() { * @since 3.0 */ @JvmField @JsStatic - val XyzHereTile = Member("here_tile", MemberType.INT32, JsonPath("properties", "@ns:com:here:xyz", "hereTile")) + val XyzHereTile = Int32Member("here_tile", JsonPath("properties", "@ns:com:here:xyz", "hereTile")) /** * `cc` — change-count: how many times this feature has been modified. Default member. * @since 3.0 */ @JvmField @JsStatic - val XyzChangeCount = Member(StandardMembers.ChangeCount, JsonPath("properties", "@ns:com:here:xyz", "changeCount")) + val XyzChangeCount = Int32Member(StandardMembers.ChangeCount, JsonPath("properties", "@ns:com:here:xyz", "changeCount")) /** * `base_tn` — base tuple-number (`BYTE_ARRAY`), set when a three-way merge was performed. @@ -120,21 +120,21 @@ class XyzMembers private constructor() { * @since 3.0 */ @JvmField @JsStatic - val XyzBaseTn = Member("base_tn", MemberType.BYTE_ARRAY, JsonPath("properties", "@ns:com:here:xyz", "base")) + val XyzBaseTn = TupleNumberMember("base_tn", JsonPath("properties", "@ns:com:here:xyz", "base")) /** * `app_id` — identifier of the application that wrote this tuple. Default member. * @since 3.0 */ @JvmField @JsStatic - val XyzAppId = Member("app_id", MemberType.STRING, JsonPath("properties", "@ns:com:here:xyz", "appId")) + val XyzAppId = StringMember("app_id", JsonPath("properties", "@ns:com:here:xyz", "appId")) /** * `author` — identifier of the human author that takes ownership for this tuple. Default member. * @since 3.0 */ @JvmField @JsStatic - val XyzAuthor = Member("author", MemberType.STRING, JsonPath("properties", "@ns:com:here:xyz", "author")) + val XyzAuthor = StringMember("author", JsonPath("properties", "@ns:com:here:xyz", "author")) /** * `origin` — stringified reference to the originating feature when this feature was forked or @@ -142,7 +142,7 @@ class XyzMembers private constructor() { * @since 3.0 */ @JvmField @JsStatic - val XyzOrigin = Member("origin", MemberType.STRING, JsonPath("properties", "@ns:com:here:xyz", "origin")) + val XyzOrigin = StringMember("origin", JsonPath("properties", "@ns:com:here:xyz", "origin")) /** * `target` — stringified reference to the feature into which this feature was joined. @@ -150,7 +150,7 @@ class XyzMembers private constructor() { * @since 3.0 */ @JvmField @JsStatic - val XyzTarget = Member("target", MemberType.STRING, JsonPath("properties", "@ns:com:here:xyz", "target")) + val XyzTarget = StringMember("target", JsonPath("properties", "@ns:com:here:xyz", "target")) /** * `ft` — feature-type string. `null` when it matches the collection's @@ -159,63 +159,63 @@ class XyzMembers private constructor() { * @since 3.0 */ @JvmField @JsStatic - val XyzFeatureType = Member("ft", MemberType.STRING, JsonPath("properties", "featureType")) + val XyzFeatureType = StringMember("ft", JsonPath("properties", "featureType")) /** * `cv0` — custom numeric value 0 (`FLOAT64`). `null` if not used. Default member. * @since 3.0 */ @JvmField @JsStatic - val XyzCustomValue0 = Member("cv0", MemberType.FLOAT64, JsonPath("properties", "@ns:com:here:xyz", "cv0")) + val XyzCustomValue0 = Float64Member("cv0", JsonPath("properties", "@ns:com:here:xyz", "cv0")) /** * `cv1` — custom numeric value 1 (`FLOAT64`). `null` if not used. Default member. * @since 3.0 */ @JvmField @JsStatic - val XyzCustomValue1 = Member("cv1", MemberType.FLOAT64, JsonPath("properties", "@ns:com:here:xyz", "cv1")) + val XyzCustomValue1 = Float64Member("cv1", JsonPath("properties", "@ns:com:here:xyz", "cv1")) /** * `cv2` — custom numeric value 2 (`FLOAT64`). `null` if not used. Default member. * @since 3.0 */ @JvmField @JsStatic - val XyzCustomValue2 = Member("cv2", MemberType.FLOAT64, JsonPath("properties", "@ns:com:here:xyz", "cv2")) + val XyzCustomValue2 = Float64Member("cv2", JsonPath("properties", "@ns:com:here:xyz", "cv2")) /** * `cv3` — custom numeric value 3 (`FLOAT64`). `null` if not used. Default member. * @since 3.0 */ @JvmField @JsStatic - val XyzCustomValue3 = Member("cv3", MemberType.FLOAT64, JsonPath("properties", "@ns:com:here:xyz", "cv3")) + val XyzCustomValue3 = Float64Member("cv3", JsonPath("properties", "@ns:com:here:xyz", "cv3")) /** * `cs0` — custom string value 0. `null` if not used. Default member. * @since 3.0 */ @JvmField @JsStatic - val XyzCustomString0 = Member("cs0", MemberType.STRING, JsonPath("properties", "@ns:com:here:xyz", "cs0")) + val XyzCustomString0 = StringMember("cs0", JsonPath("properties", "@ns:com:here:xyz", "cs0")) /** * `cs1` — custom string value 1. `null` if not used. Default member. * @since 3.0 */ @JvmField @JsStatic - val XyzCustomString1 = Member("cs1", MemberType.STRING, JsonPath("properties", "@ns:com:here:xyz", "cs1")) + val XyzCustomString1 = StringMember("cs1", JsonPath("properties", "@ns:com:here:xyz", "cs1")) /** * `cs2` — custom string value 2. `null` if not used. Default member. * @since 3.0 */ @JvmField @JsStatic - val XyzCustomString2 = Member("cs2", MemberType.STRING, JsonPath("properties", "@ns:com:here:xyz", "cs2")) + val XyzCustomString2 = StringMember("cs2", JsonPath("properties", "@ns:com:here:xyz", "cs2")) /** * `cs3` — custom string value 3. `null` if not used. Default member. * @since 3.0 */ @JvmField @JsStatic - val XyzCustomString3 = Member("cs3", MemberType.STRING, JsonPath("properties", "@ns:com:here:xyz", "cs3")) + val XyzCustomString3 = StringMember("cs3", JsonPath("properties", "@ns:com:here:xyz", "cs3")) /** * `tags` — feature tags, the classic XYZ tags array located at @@ -226,7 +226,7 @@ class XyzMembers private constructor() { * @since 3.0 */ @JvmField @JsStatic - val XyzTags = Member("tags", MemberType.TAG_LIST, JsonPath("properties", "@ns:com:here:xyz", "tags")) + val XyzTags = TagListMember("tags", JsonPath("properties", "@ns:com:here:xyz", "tags")) /** * `ref_point` — geometry reference point (always a single point), stored as TWKB. Used to compute the [XyzHereTile] value. `null` if the feature has no explicit reference point. @@ -234,7 +234,7 @@ class XyzMembers private constructor() { * @since 3.0 */ @JvmField @JsStatic - val XyzReferencePoint = Member("ref_point", MemberType.SPATIAL, JsonPath("referencePoint")) + val XyzReferencePoint = SpatialMember("ref_point", JsonPath("referencePoint")) /** * All members of XYZ compatible features. From 96f9e163a5e9cf893d1429a4923207857e6ce45d Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Fri, 26 Jun 2026 10:15:32 +0200 Subject: [PATCH 51/57] Fix compilation errors of test. Signed-off-by: Alexander Lowey-Weber --- .../kotlin/naksha/psql/ChainCollectionTest.kt | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ChainCollectionTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ChainCollectionTest.kt index eed809654..997e7547b 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ChainCollectionTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ChainCollectionTest.kt @@ -7,6 +7,7 @@ import naksha.model.objects.Member import naksha.model.objects.MemberType import naksha.model.objects.NakshaCollection import naksha.model.objects.NakshaFeature +import naksha.model.objects.XyzMembers.XyzMembers_C.XyzTn import naksha.model.request.ReadFeatures import naksha.model.request.Write import naksha.model.request.WriteRequest @@ -69,11 +70,11 @@ class ChainCollectionTest : PgTestBase( val members = assertNotNull(collection.members) assertEquals(2, members.size) assertEquals("left_fn", assertNotNull(members[0]).name) - assertContentEquals(listOf("properties", "left_fn"), assertNotNull(members[0]).effectivePath()) + assertContentEquals(listOf("properties", "left_fn"), assertNotNull(members[0]).path) assertNull(assertNotNull(members[0]).path) assertEquals("right_fn", assertNotNull(members[1]).name) - assertContentEquals(listOf("properties", "right_fn"), assertNotNull(members[1]).effectivePath()) + assertContentEquals(listOf("properties", "right_fn"), assertNotNull(members[1]).path) assertNull(assertNotNull(members[1]).path) } @@ -106,7 +107,7 @@ class ChainCollectionTest : PgTestBase( // Then: verify head val headBack = assertNotNull(response.features.find { it?.id == headFn.toString() }) - assertEquals(Int64(headFn), headBack.featureNumber) + assertEquals(Int64(headFn), XyzTn.get(headBack)?.featureNumber) assertNull( headBack.properties["left_fn"], "head.left_fn should be null" @@ -119,7 +120,7 @@ class ChainCollectionTest : PgTestBase( // Then: verify mid val midBack = assertNotNull(response.features.find { it?.id == midFn.toString() }) - assertEquals(Int64(midFn), midBack.featureNumber) + assertEquals(Int64(midFn), XyzTn.get(midBack)?.featureNumber) assertEquals( Int64(headFn), toInt64(midBack.properties["left_fn"]), @@ -133,7 +134,7 @@ class ChainCollectionTest : PgTestBase( // Then: verify tail val tailBack = assertNotNull(response.features.find { it?.id == tailFn.toString() }) - assertEquals(Int64(tailFn), tailBack.featureNumber) + assertEquals(Int64(tailFn), XyzTn.get(tailBack)?.featureNumber) assertEquals( Int64(midFn), toInt64(tailBack.properties["left_fn"]), From e4b89ee933bd18c91617792c14f652b838f6871c Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Fri, 26 Jun 2026 11:01:09 +0200 Subject: [PATCH 52/57] Update TagList to be a string-list. Signed-off-by: Alexander Lowey-Weber --- docs/latest/JBON2.md | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/docs/latest/JBON2.md b/docs/latest/JBON2.md index e34e7e1ed..bd6fd674d 100644 --- a/docs/latest/JBON2.md +++ b/docs/latest/JBON2.md @@ -28,7 +28,7 @@ As the format name indicates, this format is object-oriented. All **JBON** data - `Primitive`: The following _**units**_ are called _primitives_: `null`, `boolean`, `integer`, `float`, `timestamp`, `string`, and `tuple-number`. - `String`: A special _primitive_ that encodes a list of [UNICODE] code points, optionally including [references] to sub-strings. Strings are split using the [UNICODE] word boundary algorithm from [ICU4J]. - `Array` _(`List`)_: A list of arbitrary _**units**_ with significant order _(changing the order creates a different array)_. -- `TagList` _(`List`)_: A list of unique non-null _**primitives**_; the order of the elements is significant, and the list must not have duplicates, null or undefined. +- `TagList` _(`List`)_: A list of unique non-null _**strings**_; the order of the elements is significant, and the list must not have duplicates, null or undefined. - `Object` _(`Map`)_: A list of key-value pairs with all keys being unique non-null _**strings**_; the values can be any _**unit**_. The order of the entries is not significant, therefore the encoder will optimize by sorting the entries by their keys. - `Map` _(`Map`)_: A list of key-value pairs with keys limited to be unique non-null _**primitives**_; values can be any _**unit**_. The order of the entries is not significant, therefore the encoder will optimize by sorting the entries by their keys. - `Dictionary` _(`Map`)_: A list of key-value pairs with keys being unique non-null _**strings**_ and values being non-null _**strings**_. The order of the entries is not significant, therefore the encoder will optimize by sorting the entries by their keys. @@ -589,14 +589,14 @@ For the keys, the [primitive-stringification] is used, if needed. --- ### TagList (2) -A TagList is a list of unique non-null _**primitives**_; the order of the elements is significant, and the list must not have duplicates, null or undefined. The **lead-in** byte is `11ss_0010`; with `ss` encoding the size of the size, as usual. +A TagList is a list of unique non-null _**string**_; the order of the elements is significant, and the list must not have duplicates, null or undefined. The **lead-in** byte is `11ss_0010`; with `ss` encoding the size of the size, as usual. -| Name | Type | Description | -|-----------|------------------|-----------------------------------------------------------------------| -| lead_in | `byte` | The **lead-in** byte, `11ss_0010`. | -| byte_size | `int32` | The total size of the structure, including the **lead-in**, in bytes. | -| | | | -| entries | ([primitive])... | The entries of the TagList. | +| Name | Type | Description | +|-----------|---------------|-----------------------------------------------------------------------| +| lead_in | `byte` | The **lead-in** byte, `11ss_0010`. | +| byte_size | `int32` | The total size of the structure, including the **lead-in**, in bytes. | +| | | | +| entries | ([string])... | The entries of the TagList. | If `ss=00` _(**lead-in** is `1100_0010`)_, this implies an empty TagList _(`{"@type":"naksha:taglist"}`)_. @@ -615,8 +615,6 @@ var tagList = [ ] ``` -For the entries, the [primitive-stringification] is used, if needed. - ### Object (3) An object is a special [map] that only allows strings as keys. The **lead-in** byte is `11ss_0011`; with `ss` encoding the size of the size, as usual. All keys must be [strings]. From 1284acf0709973daab7fb35abe6da266389909cf Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Fri, 26 Jun 2026 11:24:42 +0200 Subject: [PATCH 53/57] AIs fixes of tests compilation. Signed-off-by: Alexander Lowey-Weber --- .../activitylog/ActivityLogHandler.java | 10 ++-- .../ActivityLogRequestTranslationUtil.java | 5 +- .../storage/ReadFeaturesProxyWrapper.java | 2 +- .../assertions/PropertyQueryAssertions.java | 6 +- .../lib/handlers/DefaultStorageHandler.java | 4 +- .../naksha/lib/handlers/SourceIdHandler.java | 4 +- .../lib/hub/storages/NHAdminStorage.java | 3 +- .../hub/storages/NHAdminStorageReader.java | 9 ++- .../lib/hub/storages/NHSpaceStorage.java | 1 - .../hub/storages/NHSpaceStorageReader.java | 21 +++---- .../kotlin/naksha/psql/CollectionTests.kt | 55 ++++++++----------- .../java/com/here/naksha/lib/view/View.java | 1 - .../here/naksha/lib/view/ViewReadSession.java | 10 +++- .../naksha/lib/view/ViewWriteSession.java | 2 +- .../lib/view/concurrent/LayerReadRequest.java | 2 +- .../concurrent/ParallelQueryExecutor.java | 2 +- .../here/naksha/storage/http/HttpStorage.java | 1 - .../storage/http/HttpStorageReadSession.java | 8 ++- .../ConnectorInterfaceReadExecute.java | 19 +++---- .../pop/IPropertyQueryToPropertiesQuery.java | 2 +- .../http/ffw/FfwInterfaceReadExecute.java | 2 +- 21 files changed, 83 insertions(+), 86 deletions(-) diff --git a/here-naksha-handler-activitylog/src/jvmMain/java/com/here/naksha/handler/activitylog/ActivityLogHandler.java b/here-naksha-handler-activitylog/src/jvmMain/java/com/here/naksha/handler/activitylog/ActivityLogHandler.java index f74ca7b9e..546189d47 100644 --- a/here-naksha-handler-activitylog/src/jvmMain/java/com/here/naksha/handler/activitylog/ActivityLogHandler.java +++ b/here-naksha-handler-activitylog/src/jvmMain/java/com/here/naksha/handler/activitylog/ActivityLogHandler.java @@ -56,8 +56,8 @@ import naksha.model.request.SuccessResponse; import naksha.model.request.WriteRequest; import naksha.model.request.query.AnyOp; -import naksha.model.request.query.MetaColumn; import naksha.model.request.query.MemberQuery; +import naksha.model.objects.StandardMembers; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -158,7 +158,7 @@ private static boolean isOrphanTombstone(FeatureWithPredecessor fwp) { private void collectMissingPredecessors(CollectedFeatures collectedFeatures, NakshaContext context) { List tnsOfRootsMissingPredecessor = collectedFeatures.activityLogRoots.stream() .filter(f -> !collectedFeatures.allByNuuid.containsKey(f.getId())) - .map(NakshaFeature::getTupleNumber) + .map(f -> StandardMembers.Tn.getTupleNumber(f)) .toList(); if (!tnsOfRootsMissingPredecessor.isEmpty()) { List missingPredecessorsByNextVersion = @@ -171,11 +171,11 @@ private ReadFeatures featuresWhereNextVersionIsOneOf(List tupleNumb // next_version is a plain int8 column, so we pass an Int64[] of the version values. Int64[] versions = new Int64[tupleNumbers.size()]; for (int i = 0; i < tupleNumbers.size(); i++) { - versions[i] = tupleNumbers.get(i).version.value; + versions[i] = tupleNumbers.get(i).version; } - MemberQuery nextVersionQuery = new MemberQuery(MetaColumn.nextVersion(), AnyOp.IS_ANY_OF, versions); + MemberQuery nextVersionQuery = new MemberQuery(StandardMembers.NextVersion, AnyOp.IS_ANY_OF, versions); ReadFeatures requestPredecessors = new ReadFeatures(); - requestPredecessors.setCollectionId(StringList.of(properties.getSpaceId())); + requestPredecessors.setCollectionId(properties.getSpaceId()); requestPredecessors.setQueryHistory(true); requestPredecessors.getQuery().setMembers(nextVersionQuery); return requestPredecessors; diff --git a/here-naksha-handler-activitylog/src/jvmMain/java/com/here/naksha/handler/activitylog/ActivityLogRequestTranslationUtil.java b/here-naksha-handler-activitylog/src/jvmMain/java/com/here/naksha/handler/activitylog/ActivityLogRequestTranslationUtil.java index cab733d70..ecbde1693 100644 --- a/here-naksha-handler-activitylog/src/jvmMain/java/com/here/naksha/handler/activitylog/ActivityLogRequestTranslationUtil.java +++ b/here-naksha-handler-activitylog/src/jvmMain/java/com/here/naksha/handler/activitylog/ActivityLogRequestTranslationUtil.java @@ -58,7 +58,7 @@ static void transformOriginalRequest(ReadFeatures readFeatures, String spaceId) readFeatures.setQueryHistory(true); readFeatures.setQueryDeleted(true); readFeatures.setVersions(Integer.MAX_VALUE); - readFeatures.setCollectionId(StringList.of(spaceId)); + readFeatures.setCollectionId(spaceId); // extract UUIDs from featureIds, reset featureIds StringList rawGuids = readFeatures.getFeatureIds(); @@ -82,6 +82,7 @@ static void transformOriginalRequest(ReadFeatures readFeatures, String spaceId) } private static boolean isSingleActivityLogIdEqualityQuery(PQuery pQuery) { - return StringOp.EQUALS.equals(pQuery.getOp()) && pQuery.getProperty().getPath().containsStringsInOrder(ACTIVITY_LOG_ID_PATH); + return StringOp.EQUALS.equals(pQuery.getOp()) + && pQuery.getProperty().getPath().asList().stream().allMatch(s -> java.util.Arrays.asList(ACTIVITY_LOG_ID_PATH).contains(s)); } } diff --git a/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/storage/ReadFeaturesProxyWrapper.java b/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/storage/ReadFeaturesProxyWrapper.java index c2a221447..58a4e96be 100644 --- a/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/storage/ReadFeaturesProxyWrapper.java +++ b/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/lib/core/models/storage/ReadFeaturesProxyWrapper.java @@ -106,7 +106,7 @@ public ReadFeaturesProxyWrapper withLimit(int limit){ } public ReadFeaturesProxyWrapper withCollection(String collectionId){ - getCollectionId().add(collectionId); + setCollectionId(collectionId); return this; } diff --git a/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/test/common/assertions/PropertyQueryAssertions.java b/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/test/common/assertions/PropertyQueryAssertions.java index a970773b0..91c666720 100644 --- a/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/test/common/assertions/PropertyQueryAssertions.java +++ b/here-naksha-lib-core/src/jvmMain/java/com/here/naksha/test/common/assertions/PropertyQueryAssertions.java @@ -62,7 +62,11 @@ public PropertyQueryAssertions isPQuery(){ } public PropertyQueryAssertions hasProperty(Property expected) { - return hasProperty(expected.getPath()); + java.util.List path = new java.util.ArrayList<>(); + for (Object segment : expected.getPath().asList()) { + path.add(segment.toString()); + } + return hasProperty(path); } public PropertyQueryAssertions hasProperty(List expected) { diff --git a/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/DefaultStorageHandler.java b/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/DefaultStorageHandler.java index 4873cd14b..06ad4cc8a 100644 --- a/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/DefaultStorageHandler.java +++ b/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/DefaultStorageHandler.java @@ -526,7 +526,7 @@ private void applyMapIdAndCollectionId( if (request instanceof ReadFeatures) { ReadFeatures rf = (ReadFeatures) request; rf.setCatalogId(mapId); - rf.setCollectionId(StringList.of(collectionId)); + rf.setCollectionId(collectionId); } else if (request instanceof WriteRequest) { WriteRequest wr = (WriteRequest) request; if (isOnlyWriteCollections(wr)) { @@ -537,7 +537,7 @@ private void applyMapIdAndCollectionId( } String finalCollectionId = isOnlyWriteCollections(wr) ? Naksha.COLLECTIONS_COL_ID : collectionId; wr.getWrites().forEach(write -> { - write.setMapId(mapId); + write.setCatalogId(mapId); write.setCollectionId(finalCollectionId); }); } diff --git a/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/SourceIdHandler.java b/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/SourceIdHandler.java index d3b23fddf..ba7b93d67 100644 --- a/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/SourceIdHandler.java +++ b/here-naksha-lib-handlers/src/jvmMain/java/com/here/naksha/lib/handlers/SourceIdHandler.java @@ -204,8 +204,8 @@ else if ((propertyComponent instanceof PAnd) && (((PAnd) propertyComponent).size } private static boolean propertyReferenceEqualsSourceId(Property pRef) { - List<@NotNull String> path = pRef.getPath(); - return path.size() == PREF_PATHS_SIZE && path.containsAll(List.of(NakshaProperties.META_KEY, SOURCE_ID)); + naksha.model.objects.JsonPath jp = pRef.getPath(); + return jp.size() == PREF_PATHS_SIZE && jp.asList().containsAll(List.of(NakshaProperties.META_KEY, SOURCE_ID)); } private static boolean sourceIdTransformationCapable(PQuery propertyOperation) { diff --git a/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHAdminStorage.java b/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHAdminStorage.java index 9b5535572..e40636b8c 100644 --- a/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHAdminStorage.java +++ b/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHAdminStorage.java @@ -88,9 +88,8 @@ public int getHardCap() { return psqlStorage.getNumber(); } - @Override public @NotNull DataEncoding getDataEncoding(@Nullable Object feature, @Nullable Object context) { - return psqlStorage.getDataEncoding(feature, context); + throw new UnsupportedOperationException("Not supported by NHAdminStorage"); } @Override diff --git a/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHAdminStorageReader.java b/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHAdminStorageReader.java index 7f7d76a6f..8f11445f3 100644 --- a/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHAdminStorageReader.java +++ b/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHAdminStorageReader.java @@ -113,8 +113,13 @@ public Response executeParallel(@NotNull Request request) { } @Override - public @Nullable NakshaCatalog getMapById(@NotNull String mapId) { - return session.getCatalogById(mapId); + public @Nullable NakshaCatalog getCatalogById(@NotNull String catalogId) { + return session.getCatalogById(catalogId); + } + + @Override + public @NotNull naksha.model.MemberProcessorMap getProcessors() { + return session.getProcessors(); } @Override diff --git a/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHSpaceStorage.java b/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHSpaceStorage.java index f38a7d963..07dee798d 100644 --- a/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHSpaceStorage.java +++ b/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHSpaceStorage.java @@ -132,7 +132,6 @@ public IReadSession newReadSession(@Nullable SessionOptions options) { throw new UnsupportedOperationException("Unsupported by NHSpaceStorage"); } - @Override public @NotNull DataEncoding getDataEncoding(@Nullable Object feature, @Nullable Object context) { throw new UnsupportedOperationException("Unsupported by NHSpaceStorage"); } diff --git a/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHSpaceStorageReader.java b/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHSpaceStorageReader.java index 5999c6c88..c15c64859 100644 --- a/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHSpaceStorageReader.java +++ b/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHSpaceStorageReader.java @@ -117,11 +117,7 @@ public NHSpaceStorageReader( } private @NotNull Response executeReadFeatures(final @NotNull ReadFeatures rf) { - List collectionIds = rf.getCollectionId(); - if (collectionIds.size() > 1) { - throw new UnsupportedOperationException("Reading from multiple spaces not supported!"); - } - final String spaceId = collectionIds.get(0); + String spaceId = rf.getCollectionId(); logger.info("ReadFeatures Request against spaceId={}", spaceId); addSpaceIdToStreamInfo(spaceId); if (virtualSpaces.containsKey(spaceId)) { @@ -161,13 +157,7 @@ public NHSpaceStorageReader( } private @NotNull Response executeReadFeaturesFromCustomSpaces(final @NotNull ReadFeatures rf) { - List collectionIds = rf.getCollectionId(); - if (collectionIds.size() > 1) { - return new ErrorResponse(new NakshaError( - NakshaError.UNSUPPORTED_OPERATION, - "ReadFeatures from multiple collections not supported at present!")); - } - final String spaceId = collectionIds.get(0); + final String spaceId = rf.getCollectionId(); final EventPipeline eventPipeline = pipelineFactory.eventPipeline(); final Response response = setupEventPipelineForSpaceId(spaceId, eventPipeline); if (!(response instanceof SuccessResponse)) { @@ -337,7 +327,12 @@ public Response executeParallel(@NotNull Request request) { } @Override - public @Nullable NakshaCatalog getMapById(@NotNull String mapId) { + public @Nullable NakshaCatalog getCatalogById(@NotNull String catalogId) { + throw NOT_SUPPORTED_ERROR; + } + + @Override + public @NotNull naksha.model.MemberProcessorMap getProcessors() { throw NOT_SUPPORTED_ERROR; } diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/CollectionTests.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/CollectionTests.kt index 07c0890b2..8038659a2 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/CollectionTests.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/CollectionTests.kt @@ -30,6 +30,9 @@ import naksha.model.objects.IndexType import naksha.model.objects.Member import naksha.model.objects.MemberList import naksha.model.objects.MemberType +import naksha.model.objects.XyzIndices +import naksha.model.objects.XyzMembers +import naksha.model.objects.XyzMembers.XyzMembers_C.XyzTn import naksha.model.request.ErrorResponse import naksha.model.request.ReadFeatures import naksha.model.request.Write @@ -99,9 +102,9 @@ class CollectionTests : PgTestBase(collection = null, mapId = "") { args = arrayOf(collection.id) ).use { cursor -> while (cursor.next()) columns.add(cursor["column_name"]) - // HEAD has no `next_version` column (intrinsically HEAD); the table should match `headColumns`. - assertEquals(PgColumn.headColumns.size, columns.size) - assertTrue(PgColumn.headColumns.all { column -> columns.contains(column.name) }) + assertEquals(XyzMembers.ALL.size + 1, columns.size) + // Note: We will not find TN in the database, because `lib-psql` stores `fn` and `version` instead. + assertTrue(XyzMembers.ALL.all { column -> if (column eq XyzTn) true else columns.contains(column.name) }) } } } @@ -148,24 +151,12 @@ class CollectionTests : PgTestBase(collection = null, mapId = "") { sql = "SELECT indexname FROM pg_indexes WHERE tablename = $1;", args = arrayOf(tableName) ).use { cursor -> - val addedIndices = mutableListOf() - while (cursor.next()) addedIndices.add(cursor["indexname"]) - check(indices.size <= addedIndices.size) { "Too few indices" } - indices.forEach { indexName -> - check(indexName != null) - val pgIndex = PgIndex.of(indexName) - check(pgIndex != null) { "pgIndex of $indexName should not be null" } - // Note: We know that the `id` index is replaced with `id_unique` internally for HEAD tables! - if (pgIndex == PgIndex.id) { - check(addedIndices.contains(pgIndex.id(tableName)) - || addedIndices.contains(PgIndex.id_unique.id(tableName))) { - "Missing index ${pgIndex.name} aka $indexName" - } - } else { - check(addedIndices.contains(pgIndex.id(tableName))) { - "Missing index ${pgIndex.name} aka $indexName" - } - } + val existingIndices = mutableListOf() + while (cursor.next()) existingIndices.add(cursor["indexname"]) + check(indices.size <= existingIndices.size) { "Too few indices" } + XyzIndices.ALL.forEach { index -> + val existingName = existingIndices.find { index.name == it } + check(existingName != null) { "Index ${index.name} not found" } } } } @@ -208,7 +199,7 @@ class CollectionTests : PgTestBase(collection = null, mapId = "") { val readFeatureRequest = ReadFeatures() readFeatureRequest.catalogId = map.id - readFeatureRequest.collectionId.add(collectionName) + readFeatureRequest.collectionId = collectionName readFeatureRequest.featureIds.add(feature.id) val readFeaturesResponse = executeRead(readFeatureRequest) assertEquals(1, readFeaturesResponse.features.size) @@ -281,7 +272,7 @@ class CollectionTests : PgTestBase(collection = null, mapId = "") { val readFeature = ReadFeatures() readFeature.catalogId = map.id - readFeature.collectionId.add(collectionId) + readFeature.collectionId= collectionId readFeature.featureIds.add(feature.id) val readFeatureResponse = executeRead(readFeature) assertEquals(1, readFeatureResponse.features.size) @@ -429,21 +420,22 @@ class CollectionTests : PgTestBase(collection = null, mapId = "") { @Test fun membersUndefined_shouldCreateAllColumnsAndDefaultIndices() { val collection = NakshaCollection("members_null_test", map.id) - // members is null by default — do NOT set it + // members are null by default — do NOT set them, then they will automatically become XyzMember.ALL! executeWrite(WriteRequest().add(Write().createCollection(collection))) storage.adminConnection().use { conn -> - // Columns: must include all headColumns (28 columns, no next_version). + // Columns: must include all headColumns (28 columns). val columns = mutableListOf() conn.execute( "SELECT column_name FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2", arrayOf(map.id, collection.id) ).use { cursor -> while (cursor.next()) columns.add(cursor["column_name"]) } + // Note: `lib-psql` does split `tn` into `fn` and `version` columns, so we have one more column than members! assertEquals( - PgColumn.headColumns.size, columns.size, - "Expected all ${PgColumn.headColumns.size} head columns, got: $columns" + XyzMembers.ALL.size + 1, columns.size, + "Expected to find ${XyzMembers.ALL+1} columns, found: ${columns.size}, being: $columns" ) - assertTrue(PgColumn.headColumns.all { it.name in columns }) + assertTrue(XyzMembers.ALL.all { XyzMembers.XyzTn eq it || it.name in columns }) // Indices: must include all default optional indices on the HEAD table. val indexNames = mutableListOf() @@ -451,11 +443,8 @@ class CollectionTests : PgTestBase(collection = null, mapId = "") { "SELECT indexname FROM pg_indexes WHERE schemaname = $1 AND tablename = $2", arrayOf(map.id, collection.id) ).use { cursor -> while (cursor.next()) indexNames.add(cursor["indexname"]) } - val defaultIndices = PgIndex.DEFAULT_INDICES.filter { !it.internal } - for (pgIdx in defaultIndices) { - val expectedId = pgIdx.id(collection.id) - assertTrue(expectedId in indexNames || PgIndex.id_unique.id(collection.id) in indexNames, - "Expected default index '${pgIdx.name}' (id='$expectedId') to be present, found: $indexNames") + for (index in XyzIndices.ALL) { + assertTrue( index.name in indexNames,"Expected index '${index.name}' to be present, found: $indexNames") } } } diff --git a/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/View.java b/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/View.java index dc67c7102..3918cfedb 100644 --- a/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/View.java +++ b/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/View.java @@ -57,7 +57,6 @@ public void setViewLayerCollection(@NotNull ViewLayerCollection viewLayerCollect this.viewLayerCollection = viewLayerCollection; } - @Override public @NotNull naksha.model.DataEncoding getDataEncoding(@Nullable Object feature, @Nullable Object context) { throw new NotImplementedException("Not supported by View storage"); } diff --git a/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/ViewReadSession.java b/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/ViewReadSession.java index bd7036942..c1a799a8b 100644 --- a/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/ViewReadSession.java +++ b/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/ViewReadSession.java @@ -29,6 +29,7 @@ import java.util.*; import naksha.model.*; +import naksha.model.MemberProcessorMap; import naksha.model.objects.NakshaCollection; import naksha.model.objects.NakshaCatalog; import naksha.model.request.*; @@ -188,7 +189,7 @@ private boolean isRequestOnlyById(ReadRequest request) { } if (propertyQuery instanceof PQuery) { final PQuery query = ((PQuery) propertyQuery); - return query.getProperty().getPath().contains(Property.ID) + return query.getProperty().getPath().contains("id") && query.getOp().equals(AnyOp.IS_ANY_OF); } } @@ -256,7 +257,7 @@ public void loadTuples(@NotNull List featureTuples, int } @Override - public @Nullable NakshaCatalog getMapById(@NotNull String mapId) { + public @Nullable NakshaCatalog getCatalogById(@NotNull String catalogId) { throw new UnsupportedOperationException(); } @@ -279,4 +280,9 @@ public void loadTuples(@NotNull List featureTuples, int public @NotNull SessionOptions getOptions() { throw new UnsupportedOperationException(); } + + @Override + public @NotNull MemberProcessorMap getProcessors() { + throw new UnsupportedOperationException(); + } } diff --git a/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/ViewWriteSession.java b/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/ViewWriteSession.java index 78cc3f6d2..29b2fdbaa 100644 --- a/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/ViewWriteSession.java +++ b/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/ViewWriteSession.java @@ -64,7 +64,7 @@ public ViewWriteSession withWriteLayer(ViewLayer viewLayer) { } else if (request instanceof ReadFeatures) { final ReadFeatures readFeatures = (ReadFeatures) request; readFeatures.setCatalogId(writeLayer.getMapId()); - readFeatures.setCollectionId(new StringList(writeLayer.getCollectionId())); + readFeatures.setCollectionId(writeLayer.getCollectionId()); } else { throw new IllegalArgumentException("Unsupported request type: " + request.getClass()); } diff --git a/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/concurrent/LayerReadRequest.java b/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/concurrent/LayerReadRequest.java index 8d44c874d..ac69763fc 100644 --- a/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/concurrent/LayerReadRequest.java +++ b/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/concurrent/LayerReadRequest.java @@ -35,7 +35,7 @@ public LayerReadRequest(@NotNull ReadFeatures request, @NotNull ViewLayer viewLa // because the view is always fixed to certain map/collection! this.request = request.copy(false); this.request.setCatalogId(viewLayer.getMapId()); - this.request.setCollectionId(new StringList(viewLayer.getCollectionId())); + this.request.setCollectionId(viewLayer.getCollectionId()); this.viewLayer = viewLayer; this.session = session; } diff --git a/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/concurrent/ParallelQueryExecutor.java b/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/concurrent/ParallelQueryExecutor.java index 0e60c26f0..a513a3c10 100644 --- a/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/concurrent/ParallelQueryExecutor.java +++ b/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/concurrent/ParallelQueryExecutor.java @@ -113,7 +113,7 @@ private Stream executeSingle( final String collectionId = layer.getCollectionId(); final ReadFeatures readRequest = request.copy(false); readRequest.setCatalogId(layer.getMapId()); - readRequest.setCollectionId(new StringList(collectionId)); + readRequest.setCollectionId(collectionId); final @NotNull Response readResponse = session.execute(readRequest); final FeatureTupleList featureList = getFeatureTuples(readResponse); diff --git a/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/HttpStorage.java b/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/HttpStorage.java index 6e72c1096..92d7023f7 100644 --- a/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/HttpStorage.java +++ b/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/HttpStorage.java @@ -121,7 +121,6 @@ public int getHardCap() { throw new NotImplementedException("Not supported by HTTP storage"); } - @Override public @NotNull naksha.model.DataEncoding getDataEncoding(@Nullable Object feature, @Nullable Object context) { throw new NotImplementedException("Not supported by HTTP storage"); } diff --git a/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/HttpStorageReadSession.java b/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/HttpStorageReadSession.java index 310465178..84022c956 100644 --- a/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/HttpStorageReadSession.java +++ b/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/HttpStorageReadSession.java @@ -22,6 +22,7 @@ import com.here.naksha.storage.http.connector.ConnectorInterfaceReadExecute; import com.here.naksha.storage.http.ffw.FfwInterfaceReadExecute; import naksha.model.IReadSession; +import naksha.model.MemberProcessorMap; import naksha.model.IStorage; import naksha.model.NakshaContext; import naksha.model.NakshaError; @@ -136,7 +137,7 @@ public Response executeParallel(@NotNull Request request) { } @Override - public @Nullable NakshaCatalog getMapById(@NotNull String mapId) { + public @Nullable NakshaCatalog getCatalogById(@NotNull String catalogId) { throw new NotImplementedException("Not supported by HTTP storage"); } @@ -160,6 +161,11 @@ public void loadTuples(@NotNull List featureTuples, int throw new NotImplementedException("Not supported by HTTP storage"); } + @Override + public @NotNull MemberProcessorMap getProcessors() { + throw new NotImplementedException("Not supported by HTTP storage"); + } + @Override public @Nullable NakshaCollection getCollectionById(@NotNull NakshaCatalog map, @NotNull String collectionId) { // TODO: Technically, this translates into creating an ReadCollections query! diff --git a/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/connector/ConnectorInterfaceReadExecute.java b/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/connector/ConnectorInterfaceReadExecute.java index 39917cf6e..2064e5534 100644 --- a/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/connector/ConnectorInterfaceReadExecute.java +++ b/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/connector/ConnectorInterfaceReadExecute.java @@ -167,17 +167,12 @@ private static Event createFeaturesByTileEvent(ReadFeaturesProxyWrapper readRequ } } - private static String firstCollectionIdOrThrow(ReadFeaturesProxyWrapper request) { - StringList ids = request.getCollectionId(); - if (ids == null || ids.isEmpty()) { - throw new NakshaException(NakshaError.ILLEGAL_ARGUMENT, - "collectionIds must contain at least one non-empty id"); - } - String id0 = ids.get(0); - if (id0 == null || id0.isBlank()) { - throw new NakshaException(NakshaError.ILLEGAL_ARGUMENT, - "First collectionId must be non-empty"); - } - return id0; + private static String firstCollectionIdOrThrow(ReadFeaturesProxyWrapper request) { + String id = request.getCollectionId(); + if (id == null || id.isBlank()) { + throw new NakshaException(NakshaError.ILLEGAL_ARGUMENT, + "collectionId must be non-empty"); } + return id; + } } \ No newline at end of file diff --git a/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/connector/pop/IPropertyQueryToPropertiesQuery.java b/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/connector/pop/IPropertyQueryToPropertiesQuery.java index 3a8ff2515..dfe304634 100644 --- a/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/connector/pop/IPropertyQueryToPropertiesQuery.java +++ b/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/connector/pop/IPropertyQueryToPropertiesQuery.java @@ -98,7 +98,7 @@ private static PropertyQuery simpleLeaf(@NotNull PQuery leaf) { AnyOp anyOp = leaf.getOp(); Object value = leaf.getValue(); - String key = String.join(".", leaf.getProperty().getPath().asList()); + String key = leaf.getProperty().getPath().stream().map(Object::toString).collect(java.util.stream.Collectors.joining(".")); String op = normalizeOp(anyOp); switch (op) { diff --git a/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/ffw/FfwInterfaceReadExecute.java b/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/ffw/FfwInterfaceReadExecute.java index 46db2f3c9..6a4c0a6f4 100644 --- a/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/ffw/FfwInterfaceReadExecute.java +++ b/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/ffw/FfwInterfaceReadExecute.java @@ -179,6 +179,6 @@ private static String keysToKeyValuesStrings(ReadFeaturesProxyWrapper readReques } private static String baseEndpoint(ReadFeaturesProxyWrapper request) { - return request.getCollectionId().get(0); + return request.getCollectionId(); } } From 4df4225a4635f1e63ceec804a3cfe7af7e913b7f Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Fri, 26 Jun 2026 15:21:22 +0200 Subject: [PATCH 54/57] Fix indices Signed-off-by: Alexander Lowey-Weber --- .../commonMain/kotlin/naksha/model/TagList.kt | 6 +- .../kotlin/naksha/model/objects/Index.kt | 26 +------ .../kotlin/naksha/model/objects/IndexType.kt | 60 ---------------- .../naksha/model/objects/NakshaCollection.kt | 4 +- .../naksha/model/objects/StandardIndices.kt | 20 +++--- .../kotlin/naksha/model/objects/XyzIndices.kt | 50 ++++++------- .../kotlin/naksha/model/objects/XyzMembers.kt | 3 +- .../kotlin/naksha/psql/PgCollection.kt | 3 +- .../commonMain/kotlin/naksha/psql/PgIndex.kt | 72 +++++++++++-------- .../commonMain/kotlin/naksha/psql/PgTable.kt | 2 +- .../commonMain/kotlin/naksha/psql/PgType.kt | 8 +-- 11 files changed, 86 insertions(+), 168 deletions(-) delete mode 100644 here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/IndexType.kt diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TagList.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TagList.kt index 5d9533687..335e1caac 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TagList.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/TagList.kt @@ -2,9 +2,7 @@ package naksha.model -import naksha.base.ListProxy -import naksha.base.NormalizerForm -import naksha.base.Platform +import naksha.base.StringList import naksha.model.TagNormalizer.TagNormalizer_C.normalizeTag import kotlin.js.JsExport import kotlin.js.JsName @@ -15,7 +13,7 @@ import kotlin.jvm.JvmStatic * A list of tags. */ @JsExport -class TagList() : ListProxy(String::class) { +class TagList() : StringList() { /** * Create a tag list from the given arguments; the tags are normalized. diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Index.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Index.kt index e49d6cde0..c9d5df02f 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Index.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Index.kt @@ -32,14 +32,12 @@ open class Index() : AnyObject() { /** * Construct a user-defined index. * @param name the index name (must be unique within the collection). - * @param type the index type. * @param on the member names to index. * @since 3.0 */ @JsName("of") - constructor(name: String, type: IndexType, vararg on: String) : this() { + constructor(name: String, vararg on: String) : this() { this.name = name - this.type = type val cols = StringList() cols.setCapacity(on.size) for (c in on) cols.add(c) @@ -67,27 +65,6 @@ open class Index() : AnyObject() { return this } - /** - * The index type. - * @since 3.0 - */ - var type: IndexType by TYPE - - /** True iff the underlying map has an entry for [type]. */ - fun hasType(): Boolean = hasRaw("type") - - /** Remove [type] from the underlying map; returns this for chaining. */ - fun removeType(): Index { - removeRaw("type") - return this - } - - /** Fluent setter for [type]; returns this for chaining. */ - fun withType(value: IndexType): Index { - type = value - return this - } - /** * The names of the members to index. May be names of mandatory/default built-in members * (see [StandardMembers]) or names of custom [Member]s declared on the same collection. @@ -177,7 +154,6 @@ open class Index() : AnyObject() { companion object Index_C { private val NAME = NotNullProperty(String::class) { _, _ -> "" } - private val TYPE = NotNullEnum(IndexType::class) { _, _ -> IndexType.BTREE } private val ON = NotNullProperty(StringList::class) private val INCLUDE = NullableProperty(StringList::class) private val UNIQUE = NotNullProperty(Boolean::class) { _, _ -> false } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/IndexType.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/IndexType.kt deleted file mode 100644 index 3415b6054..000000000 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/IndexType.kt +++ /dev/null @@ -1,60 +0,0 @@ -@file:Suppress("OPT_IN_USAGE") - -package naksha.model.objects - -import naksha.base.JsEnum -import kotlin.js.JsExport -import kotlin.jvm.JvmField -import kotlin.reflect.KClass - -/** - * The kind of index to create for an [Index]. - * - * - [BTREE] — ordered index for equality and range queries on primitive columns (numbers, booleans, strings, byte-arrays). - * - [SPATIAL] — spatial index over a geometry column ([MemberType.SPATIAL]) (e.g. the built-in `geo`). - * - [TAG_MAP] — inverted index over a tags column ([MemberType.TAG_MAP] or [MemberType.TAG_MAP_FROM_ARRAY]); - * supports key/value containment lookups. - * - [TAG_LIST] — inverted index over a tag-list column ([MemberType.TAG_LIST]); supports element containment lookups. - * @since 3.0 - */ -@JsExport -class IndexType : JsEnum() { - - @Suppress("NON_EXPORTABLE_TYPE") - override fun namespace(): KClass = IndexType::class - - override fun initClass() {} - - companion object IndexType_C { - /** - * Ordered index for equality and range queries on primitive columns. - * @since 3.0 - */ - @JvmField - val BTREE = defIgnoreCase(IndexType::class, "btree") - - /** - * Spatial index over a geometry column. - * @since 3.0 - */ - @JvmField - val SPATIAL = defIgnoreCase(IndexType::class, "spatial") - - /** - * Inverted index over a [MemberType.TAG_MAP] or [MemberType.TAG_MAP_FROM_ARRAY] column. - * Supports key/value containment lookups. - * @since 3.0 - */ - @JvmField - val TAG_MAP = defIgnoreCase(IndexType::class, "tag_map") - - /** - * Inverted index over a [MemberType.TAG_LIST] column. Supports element containment lookups, - * e.g. find all features whose tags list contains the element `"foo"`. This is the default - * index for the standard `tags` member. - * @since 3.0 - */ - @JvmField - val TAG_LIST = defIgnoreCase(IndexType::class, "tag_list") - } -} diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt index 747164550..b83cc017a 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/NakshaCollection.kt @@ -433,13 +433,13 @@ open class NakshaCollection() : NakshaFeature() { var indices: IndexList? by INDICES /** - * Initializes the [indices] to the bare minimum, therefore [mandatory indices][StandardIndices.MANDATORY]. + * Initializes the [indices] to the bare minimum. * * The method should be called, if next to the minimal indices additional proprietary indices should be added. * @since 3.0 */ fun withMinimalIndices(): NakshaCollection { - indices = IndexList(StandardIndices.MANDATORY) + indices = IndexList() return this } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardIndices.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardIndices.kt index 5d0c51471..b606c15cc 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardIndices.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StandardIndices.kt @@ -29,7 +29,7 @@ class StandardIndices private constructor() { * @since 3.0 */ @JvmField @JsStatic - val FeatureNumberUnique = Index("fn_unique", IndexType.BTREE, "fn").withInternal(true) + val FeatureNumberUnique = Index("fn_unique", "fn").withInternal(true) /** * `id_unique` — UNIQUE index on `id` (WHERE `id IS NOT NULL`). Present in HEAD, DELETED, and @@ -37,7 +37,7 @@ class StandardIndices private constructor() { * @since 3.0 */ @JvmField @JsStatic - val IdUnique = Index("id_unique", IndexType.BTREE, "id").withInternal(true).withUnique(true) + val IdUnique = Index("id_unique", "id").withInternal(true).withUnique(true) /** * `id` — non-unique index on `id`, `fn`, `version` (WHERE `id IS NOT NULL`). Present in @@ -45,14 +45,14 @@ class StandardIndices private constructor() { * @since 3.0 */ @JvmField @JsStatic - val Id = Index("id", IndexType.BTREE, "id", "fn", "version").withInternal(true) + val Id = Index("id", "id", "fn", "version").withInternal(true) /** * `version` — non-unique index on `version`. Present in all tables. Mandatory. * @since 3.0 */ @JvmField @JsStatic - val Version = Index("version", IndexType.BTREE, "version").withInternal(true) + val Version = Index("version", "version").withInternal(true) /** * `gbn` — conditional non-unique index on `gbn` WHERE `gbn IS NOT NULL`. Used by the sequencer @@ -61,7 +61,7 @@ class StandardIndices private constructor() { * @since 3.0 */ @JvmField @JsStatic - val GlobalBookNumber = Index("gbn", IndexType.BTREE, "gbn").withInternal(true) + val GlobalBookNumber = Index("gbn", "gbn").withInternal(true) /** * All mandatory indices, in declaration order. These are always created by the storage. @@ -82,13 +82,13 @@ class StandardIndices private constructor() { // ------------------------------------------------------------------------- /** - * `geo` — spatial ([IndexType.SPATIAL]) GIST index over the geometry member (WHERE `geo IS NOT NULL`). + * `geo` — spatial index over the geometry member. * * Geometry is a **standard** member (part of the GeoJSON standard, see [StandardMembers.Geometry]). * @since 3.0 */ @JvmField @JsStatic - val Geometry = Index("geo", IndexType.SPATIAL, "geo") + val Geometry = Index("geo", "geo") // ------------------------------------------------------------------------- // Special indices — not added automatically; declared explicitly per collection @@ -101,7 +101,7 @@ class StandardIndices private constructor() { * @since 3.0 */ @JvmField @JsStatic - val PublishNumber = Index("pn", IndexType.BTREE, "pn") + val PublishNumber = Index("pn", "pn") /** * `pt` — BTREE index on `pt` (WHERE `pt IS NOT NULL`). Enables efficient range scans @@ -109,7 +109,7 @@ class StandardIndices private constructor() { * @since 3.0 */ @JvmField @JsStatic - val PublishTime = Index("pt", IndexType.BTREE, "pt") + val PublishTime = Index("pt", "pt") /** * `gv` — BTREE index on `gv` (WHERE `gv IS NOT NULL`). Enables efficient range scans @@ -117,7 +117,7 @@ class StandardIndices private constructor() { * @since 3.0 */ @JvmField @JsStatic - val GlobalVersion = Index("gv", IndexType.BTREE, "gv") + val GlobalVersion = Index("gv", "gv") /** * All special indices — not added automatically but recognised by all storage implementations. diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzIndices.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzIndices.kt index fd4858af7..06bec6abe 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzIndices.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzIndices.kt @@ -9,14 +9,9 @@ import kotlin.jvm.JvmField /** * The default indices created for a Data-Hub (XYZ) compatible collection. * - * This is the index counterpart of [XyzMembers]: it indexes the members declared there (e.g. - * [XyzMembers.XyzTags], [XyzMembers.XyzAppId], [XyzMembers.XyzHereTile]) and is applied via - * [NakshaCollection.withXyzIndices]. + * This is the index counterpart of [XyzMembers]: It indexes the members declared there _(e.g. [XyzTags][XyzMembers.XyzTags], [XyzAppId][XyzMembers.XyzAppId], [XyzHereTile][XyzMembers.XyzHereTile])_ and is applied via [NakshaCollection.withXyzIndices]. * - * An index refers to a member by its identity (name + type), not by JSON path, so indices that - * target a member which is also standard (e.g. the geometry member, see [StandardIndices.Geometry]) - * are **referenced** from [StandardIndices] rather than redeclared here. The storage-managed indices - * that every collection always has live in [StandardIndices.MANDATORY]. + * An index refers to a member by its identity _(name)_, **not** by JSON path. Therefore, the name of a member is very significant. * @since 3.0 */ @JsExport @@ -30,7 +25,7 @@ class XyzIndices private constructor() { * @since 3.0 */ @JvmField @JsStatic - val XyzHereTile = Index("here_tile", IndexType.BTREE, "here_tile", "fn", "version") + val XyzHereTile = Index("here_tile", "here_tile", "fn", "version") /** * `app_id` — index on `app_id`, `updated_at`, `fn`, `version` (WHERE `app_id IS NOT NULL`). @@ -38,7 +33,7 @@ class XyzIndices private constructor() { * @since 3.0 */ @JvmField @JsStatic - val XyzAppId = Index("app_id", IndexType.BTREE, "app_id", "updated_at", "fn", "version") + val XyzAppId = Index("app_id", "app_id", "updated_at", "fn", "version") /** * `author` — index on the effective author and author timestamp, `fn`, `version` @@ -46,7 +41,7 @@ class XyzIndices private constructor() { * @since 3.0 */ @JvmField @JsStatic - val XyzAuthor = Index("author", IndexType.BTREE, "author", "author_ts", "fn", "version") + val XyzAuthor = Index("author", "author", "author_ts", "fn", "version") /** * `tags` — inverted ([IndexType.TAG_LIST]) index over the `tags` member, supporting element @@ -54,7 +49,7 @@ class XyzIndices private constructor() { * @since 3.0 */ @JvmField @JsStatic - val XyzTags = Index("tags", IndexType.TAG_LIST, "tags") + val XyzTags = Index("tags", "tags") /** * `feature_type` — index on `ft`, `fn`, `version` (WHERE `ft IS NOT NULL`). @@ -62,83 +57,78 @@ class XyzIndices private constructor() { * @since 3.0 */ @JvmField @JsStatic - val XyzFeatureType = Index("feature_type", IndexType.BTREE, "ft", "fn", "version") + val XyzFeatureType = Index("feature_type", "ft", "fn", "version") /** * `cv0` — index on custom numeric value 0, `fn`, `version` (WHERE `cv0 IS NOT NULL`). * @since 3.0 */ @JvmField @JsStatic - val XyzCustomValue0 = Index("cv0", IndexType.BTREE, "cv0", "fn", "version") + val XyzCustomValue0 = Index("cv0", "cv0", "fn", "version") /** * `cv1` — index on custom numeric value 1, `fn`, `version` (WHERE `cv1 IS NOT NULL`). * @since 3.0 */ @JvmField @JsStatic - val XyzCustomValue1 = Index("cv1", IndexType.BTREE, "cv1", "fn", "version") + val XyzCustomValue1 = Index("cv1", "cv1", "fn", "version") /** * `cv2` — index on custom numeric value 2, `fn`, `version` (WHERE `cv2 IS NOT NULL`). * @since 3.0 */ @JvmField @JsStatic - val XyzCustomValue2 = Index("cv2", IndexType.BTREE, "cv2", "fn", "version") + val XyzCustomValue2 = Index("cv2", "cv2", "fn", "version") /** * `cv3` — index on custom numeric value 3, `fn`, `version` (WHERE `cv3 IS NOT NULL`). * @since 3.0 */ @JvmField @JsStatic - val XyzCustomValue3 = Index("cv3", IndexType.BTREE, "cv3", "fn", "version") + val XyzCustomValue3 = Index("cv3", "cv3", "fn", "version") /** * `cs0` — index on custom string value 0, `fn`, `version` (WHERE `cs0 IS NOT NULL`). * @since 3.0 */ @JvmField @JsStatic - val XyzCustomString0 = Index("cs0", IndexType.BTREE, "cs0", "fn", "version") + val XyzCustomString0 = Index("cs0", "cs0", "fn", "version") /** * `cs1` — index on custom string value 1, `fn`, `version` (WHERE `cs1 IS NOT NULL`). * @since 3.0 */ @JvmField @JsStatic - val XyzCustomString1 = Index("cs1", IndexType.BTREE, "cs1", "fn", "version") + val XyzCustomString1 = Index("cs1", "cs1", "fn", "version") /** * `cs2` — index on custom string value 2, `fn`, `version` (WHERE `cs2 IS NOT NULL`). * @since 3.0 */ @JvmField @JsStatic - val XyzCustomString2 = Index("cs2", IndexType.BTREE, "cs2", "fn", "version") + val XyzCustomString2 = Index("cs2", "cs2", "fn", "version") /** * `cs3` — index on custom string value 3, `fn`, `version` (WHERE `cs3 IS NOT NULL`). * @since 3.0 */ @JvmField @JsStatic - val XyzCustomString3 = Index("cs3", IndexType.BTREE, "cs3", "fn", "version") + val XyzCustomString3 = Index("cs3", "cs3", "fn", "version") /** - * `ref_point` — spatial ([IndexType.SPATIAL]) index over the reference-point geometry member - * (WHERE `ref_point IS NOT NULL`). See [XyzMembers.XyzReferencePoint]. + * `ref_point` — spatial index over the reference-point geometry member. * @since 3.0 + * @see [XyzMembers.XyzReferencePoint] */ @JvmField @JsStatic - val XyzReferencePoint = Index("ref_point", IndexType.SPATIAL, "ref_point") + val XyzReferencePoint = Index("ref_point", "ref_point") /** - * All indices for a default XYZ collection, in declaration order: the [StandardIndices.MANDATORY] - * indices (always present), followed by the XYZ default indices, followed by the geometry index - * (referenced from [StandardIndices.Geometry], since geometry is a standard member). - * - * Does **not** include the [StandardIndices.SPECIAL] indices (`pn`/`pt`/`gv`), which are declared - * explicitly only where needed (e.g. `naksha~transactions`). + * All indices for a default XYZ collection. * @since 3.0 */ @JvmField @JsStatic - val ALL: List = StandardIndices.MANDATORY + listOf( + val ALL: List = listOf( XyzHereTile, XyzAppId, XyzAuthor, diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzMembers.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzMembers.kt index 30b6fd865..a3ac9c56e 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzMembers.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/XyzMembers.kt @@ -2,6 +2,7 @@ package naksha.model.objects +import naksha.geo.SpGeometry import naksha.model.TupleNumber import kotlin.js.ExperimentalJsExport import kotlin.js.ExperimentalJsStatic @@ -10,7 +11,7 @@ import kotlin.js.JsStatic import kotlin.jvm.JvmField /** - * All members being part of the classic XYZ-Hub architecture, plus further extensions added later in Data-Hub and Naksha v1, v2. All internal adminstrative object are stored in this format. + * All members being part of the classic XYZ-Hub architecture, plus further extensions added later in Data-Hub and Naksha v1, v2. All internal administrative object are stored in this format. * @since 3.0 */ @JsExport diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt index b00e22d2c..8183f8d74 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgCollection.kt @@ -175,7 +175,6 @@ open class PgCollection internal constructor( val indices: IndexList = nakshaCollection.indices!! val index = indices[i] ?: throw NakshaException(ILLEGAL_STATE, "Index #$i must not be null") val indexName = index.name - val indexType = index.type val nakshaOn = index.on val on: ArrayList = ArrayList(nakshaOn.size) @@ -208,7 +207,7 @@ open class PgCollection internal constructor( } else { include = null } - PgIndex(indexName, indexType, on.toTypedArray(), include?.toTypedArray() ?: emptyArray()) + PgIndex(indexName, on.toTypedArray(), include?.toTypedArray() ?: emptyArray()) } } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgIndex.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgIndex.kt index b45902383..8e2425f10 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgIndex.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgIndex.kt @@ -1,13 +1,8 @@ package naksha.psql -import naksha.model.NakshaError.NakshaErrorCompanion.INTERNAL_ERROR -import naksha.model.NakshaException +import naksha.model.illegalArg import naksha.model.objects.Index -import naksha.model.objects.IndexType -import naksha.model.objects.IndexType.IndexType_C.BTREE -import naksha.model.objects.IndexType.IndexType_C.SPATIAL -import naksha.model.objects.IndexType.IndexType_C.TAG_MAP -import naksha.model.objects.IndexType.IndexType_C.TAG_LIST +import naksha.model.objects.MemberType import naksha.psql.PgUtil.PgUtilCompanion.quoteIdent import kotlin.js.JsExport import kotlin.jvm.JvmField @@ -27,13 +22,6 @@ data class PgIndex( @JvmField val name: String, - /** - * The index type. - * @since 3.0 - */ - @JvmField - var type: IndexType, - /** * The columns to index. * @since 3.0 @@ -49,20 +37,48 @@ data class PgIndex( var includes: Array = emptyArray() ) { - internal fun create(conn: PgConnection, tableName: String) { - val includeClause = if (includes.isEmpty()) "" else " INCLUDE (${includes.joinToString(", ")})" - val using = when (type) { - BTREE -> "btree" - SPATIAL -> "gist" - TAG_MAP, TAG_LIST -> "gin" - else -> throw NakshaException(INTERNAL_ERROR, "Invalid index type for index $name on table $tableName") + companion object PgIndex_C { + private fun indexAndOpsOf(column: PgColumn, indexName: String): Pair = when (column.memberType) { + MemberType.INT8, + MemberType.INT16, + MemberType.INT32, + MemberType.INT64, + MemberType.FLOAT32, + MemberType.FLOAT64, + MemberType.BYTE_ARRAY -> Pair("btree", "") + MemberType.STRING -> Pair("btree", " COLLATE \"C\" text_pattern_ops") + MemberType.SPATIAL -> Pair("gist_btree", " gist_geometry_ops_2d") + MemberType.TAG_MAP -> Pair("gin", " jsonb_ops") + MemberType.TAG_MAP_FROM_ARRAY, + MemberType.TAG_LIST -> Pair("gin", " array_ops") + else -> throw illegalArg("The member type ${column.memberType} of column '$column' of index '$indexName' is not a valid index target") } - val indexName = quoteIdent(tableName, "\$i_", tableName) - val fillFactor = if (PgTable.isAnyHead(tableName)) "(fillfactor=50)" else "(fillfactor=100)" - val sql = """CREATE INDEX IF NOT EXISTS $indexName -ON ${quoteIdent(tableName)} -USING $using$includeClause -WITH $fillFactor""" + } + + internal fun create(conn: PgConnection, table: PgTable) { + if (on.isEmpty()) throw illegalArg("Index without target columns: $name") + val primaryColumn = on.first() + val (primaryIndex, primaryDeclaration) = indexAndOpsOf(primaryColumn, name) + val secondaries = mutableListOf>() + for (i in 0 ..< on.size) { + val pgColumn = on[i] + val secondary = indexAndOpsOf(pgColumn, name) + if (secondary.first != "btree" || !primaryIndex.contains(secondary.first)) { + throw illegalArg("The member #$i ($pgColumn) can not be used as secondary index element, only primitives are allows") + } + secondaries.add(secondary) + } + val includeClause = if (includes.isEmpty()) "" else " INCLUDE (${includes.joinToString(", ") { column -> + val (index, _) = indexAndOpsOf(column, "include") + if (index != "btree") throw illegalArg("The include of column $column is not possible, because it is no primitive") + column.ident + }})" + val indexIdent = quoteIdent(table.name, "\$i_", table.name) + val fillFactor = if (PgTable.isAnyHead(table.name)) "(fillfactor=50)" else "(fillfactor=100)" + val sql = """CREATE INDEX IF NOT EXISTS $indexIdent +ON ${table.quotedName} +USING $primaryIndex ($primaryDeclaration${secondaries.joinToString(", ") { secondary -> secondary.second }})$includeClause +WITH $fillFactor""" // like "gin jsonb_ops" conn.execute(sql).close() } @@ -77,7 +93,6 @@ WITH $fillFactor""" other as PgIndex if (name != other.name) return false - if (type != other.type) return false if (!on.contentEquals(other.on)) return false if (!includes.contentEquals(other.on)) return false return true @@ -85,7 +100,6 @@ WITH $fillFactor""" override fun hashCode(): Int { var result = name.hashCode() - result = 31 * result + type.hashCode() result = 31 * result + on.contentHashCode() result = 31 * result + includes.contentHashCode() return result diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgTable.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgTable.kt index 366fd556b..799f68671 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgTable.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgTable.kt @@ -201,7 +201,7 @@ abstract class PgTable( */ open fun createIndex(conn: PgConnection, index: PgIndex) { if (!indices.contains(index)) { - index.create(conn, name) + index.create(conn, this) indices = indices + index } } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgType.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgType.kt index ff8f0aba0..43dd0c9cc 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgType.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgType.kt @@ -195,12 +195,12 @@ class PgType : JsEnum() { MemberType.FLOAT32 -> FLOAT MemberType.FLOAT64 -> DOUBLE MemberType.STRING -> STRING + MemberType.TAG_MAP -> JSONB + MemberType.TAG_MAP_FROM_ARRAY -> STRING_ARRAY + MemberType.TAG_LIST -> STRING_ARRAY // MemberType.BYTE_ARRAY -> BYTE_ARRAY // MemberType.TUPLE_NUMBER -> BYTE_ARRAY - // MemberType.SPATIAL -> BYTE_ARRAY - MemberType.TAG_MAP -> JSONB - MemberType.TAG_MAP_FROM_ARRAY -> JSONB - MemberType.TAG_LIST -> JSONB + // MemberType.SPATIAL -> BYTE_ARRAY (TWKB) else -> BYTE_ARRAY } } From c510861cc221fc8a9419e433d51314809c9344d0 Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Fri, 26 Jun 2026 16:05:50 +0200 Subject: [PATCH 55/57] Fix compilation errors. Signed-off-by: Alexander Lowey-Weber --- .../app/service/http/auth/JWTPayload.java | 2 +- .../http/tasks/ReadFeatureApiTask.java | 6 ++--- .../http/tasks/WriteFeatureApiTask.java | 2 +- .../naksha/cli/copy/service/CopyService.java | 4 +--- .../cli/storages/GeneratingSession.java | 13 ++++++----- .../cli/storages/GeneratingStorage.java | 12 ---------- .../storages/GeneratingStorageService.java | 2 +- .../hub/storages/NHAdminStorageReader.java | 11 ++------- .../hub/storages/NHSpaceStorageReader.java | 15 ++---------- .../kotlin/naksha/jbon/IDictReader.kt | 2 +- .../kotlin/naksha/model/ISession.kt | 23 +++---------------- .../kotlin/naksha/psql/PgSession.kt | 5 +--- .../here/naksha/lib/view/ViewReadSession.java | 6 +---- .../storage/http/HttpStorageReadSession.java | 13 ++--------- 14 files changed, 26 insertions(+), 90 deletions(-) diff --git a/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/auth/JWTPayload.java b/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/auth/JWTPayload.java index 2f615e9a3..bc97289c5 100644 --- a/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/auth/JWTPayload.java +++ b/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/auth/JWTPayload.java @@ -60,7 +60,7 @@ public class JWTPayload { if (urm == null) { return null; } - final ServiceUserRights hereActionMatrix = urm.getPath(URMServiceId.NAKSHA); + final ServiceUserRights hereActionMatrix = (ServiceUserRights) urm.getPath(URMServiceId.NAKSHA); if (hereActionMatrix == null) { return null; } diff --git a/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/ReadFeatureApiTask.java b/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/ReadFeatureApiTask.java index d26ab1eab..092e5a77b 100644 --- a/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/ReadFeatureApiTask.java +++ b/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/ReadFeatureApiTask.java @@ -337,7 +337,7 @@ protected void init() { .withPropertyQuery(propertyQuery) .withTagQuery(tagQuery); rdRequest.setFeatureIds(suppliedFeatureIds); - rdRequest.setCollectionId(StringList.of(spaceId)); + rdRequest.setCollectionId(spaceId); rdRequest.setLimit(limit); // Forward request to NH Space Storage reader instance @@ -422,7 +422,7 @@ protected void init() { query.setTags(tagQuery); final ReadFeatures rdRequest = new ReadFeatures(); rdRequest.setFeatureIds(suppliedFeatureIds); - rdRequest.setCollectionId(StringList.of(spaceId)); + rdRequest.setCollectionId(spaceId); rdRequest.setQuery(query); rdRequest.withPropertyQuery(propertyQuery); @@ -502,7 +502,7 @@ protected void init() { query.setSpatial(radiusQuery); query.setTags(tagQuery); final ReadFeatures rdRequest = new ReadFeatures(); - rdRequest.setCollectionId(StringList.of(spaceId)); + rdRequest.setCollectionId(spaceId); rdRequest.setFeatureIds(suppliedFeatureIds); rdRequest.setQuery(query); rdRequest.withPropertyQuery(propertyQuery); diff --git a/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/WriteFeatureApiTask.java b/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/WriteFeatureApiTask.java index 1759bce52..6bbae57be 100644 --- a/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/WriteFeatureApiTask.java +++ b/here-naksha-app-service/src/jvmMain/java/com/here/naksha/app/service/http/tasks/WriteFeatureApiTask.java @@ -327,7 +327,7 @@ private XyzResponse patchAndPreProcess( // Prepare WriteRequest - separating insert from updates and keeping the order of the features from the request WriteRequest insertsAndUpdates = new WriteRequest(); for (NakshaFeature featureFromRequest : featuresFromRequest) { - NakshaFeature correspondingExistingFeature = existingFeaturesById.getPath(featureFromRequest.getId()); + NakshaFeature correspondingExistingFeature = (NakshaFeature) existingFeaturesById.getPath(featureFromRequest.getId()); if (correspondingExistingFeature == null) { // Feature not yet persisted - just insert preProcessor.preProcess(featureFromRequest); diff --git a/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/copy/service/CopyService.java b/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/copy/service/CopyService.java index dc90d64ec..8f0ccb767 100644 --- a/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/copy/service/CopyService.java +++ b/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/copy/service/CopyService.java @@ -112,9 +112,7 @@ private SuccessResponse requireSuccessResponse( private ReadFeatures createReadFeaturesRequest(CopyElement source) { ReadFeatures readFeatures = new ReadFeatures(); - readFeatures.setCollectionId( - StringList.of(source.getCollectionId()) - ); + readFeatures.setCollectionId(source.getCollectionId()); readFeatures.setCatalogId(source.getMapId()); return readFeatures; diff --git a/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/storages/GeneratingSession.java b/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/storages/GeneratingSession.java index 182012a37..1f988087f 100644 --- a/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/storages/GeneratingSession.java +++ b/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/storages/GeneratingSession.java @@ -34,15 +34,15 @@ public Response execute(@NotNull Request request) { } @Override - public void loadTuples(@NotNull List featureTuples) { + public void loadTuples(@NotNull List featureTuples, int from, int to) { GeneratingStorageService service = storage.getService(); List generatedFeatures = service.generateFeatures( - featureTuples.size(), + to - from, storage.getTileIds(), storage.getIdsPrefix(), templateFeature ); - for (int i = 0; i < featureTuples.size(); ++i) { + for (int i = from; i < to; ++i) { FeatureTuple featureTuple = featureTuples.get(i); NakshaFeature feature = generatedFeatures.get(i); featureTuple.setFeature(feature); @@ -109,7 +109,7 @@ public void setLockTimeout(int i) { @Nullable @Override - public NakshaCatalog getMapById(@NotNull String mapId) { + public NakshaCatalog getCatalogById(@NotNull String mapId) { throw new NakshaException(NakshaError.UNSUPPORTED_OPERATION, ""); } @@ -131,8 +131,9 @@ public NakshaCollection getCollectionByNumber(@NotNull NakshaCatalog catalog, in throw new NakshaException(NakshaError.UNSUPPORTED_OPERATION, ""); } + private final MemberProcessorMap processors = new MemberProcessorMap(); @Override - public void loadTuples(@NotNull List featureTuples, int from, int to, int mode) { - throw new NakshaException(NakshaError.UNSUPPORTED_OPERATION, ""); + public @NotNull MemberProcessorMap getProcessors() { + return processors; } } diff --git a/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/storages/GeneratingStorage.java b/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/storages/GeneratingStorage.java index 85fc9ff09..53d07a803 100644 --- a/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/storages/GeneratingStorage.java +++ b/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/storages/GeneratingStorage.java @@ -13,7 +13,6 @@ import kotlin.reflect.KClass; import naksha.base.Platform; import naksha.base.StringList; -import naksha.jbon.JbDictionary; import naksha.model.*; import naksha.model.objects.NakshaFeature; import org.jetbrains.annotations.NotNull; @@ -67,17 +66,6 @@ public int getHardCap() { return Integer.MAX_VALUE; } - @Override - public @NotNull DataEncoding getDataEncoding(@Nullable Object feature, @Nullable Object context) { - return Naksha.DEFAULT_DATA_ENCODING; - } - - @Nullable - @Override - public JbDictionary getDictionary(@NotNull String id) { - return null; - } - @Override protected void initStorage( @NotNull GeneratingStorageConfig storageConfig, diff --git a/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/storages/GeneratingStorageService.java b/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/storages/GeneratingStorageService.java index 3daa126b9..4f3cd1847 100644 --- a/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/storages/GeneratingStorageService.java +++ b/here-naksha-cli/src/jvmMain/java/com/here/naksha/cli/storages/GeneratingStorageService.java @@ -29,7 +29,7 @@ FeatureTupleList generateDummyFeatureTuples(Int64 storageNumber, int numOfTuples dummyFeatureTuples.setCapacity(numOfTuplesToGenerate); for (int i = 0; i < numOfTuplesToGenerate; ++i) { TupleNumber dummyTupleNumber = new TupleNumber( - storageNumber, 0, 0, Platform.intToInt64(0), Version.HEAD + storageNumber, 0, 0, Platform.intToInt64(0), Version.HEAD.number ); FeatureTuple dummyFeatureTuple = new FeatureTuple(dummyTupleNumber, null); dummyFeatureTuples.add(dummyFeatureTuple); diff --git a/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHAdminStorageReader.java b/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHAdminStorageReader.java index 8f11445f3..c1e3ccd0e 100644 --- a/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHAdminStorageReader.java +++ b/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHAdminStorageReader.java @@ -35,8 +35,6 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import static naksha.model.LibModelKt.FETCH_ALL; - public class NHAdminStorageReader implements IReadSession { /** @@ -133,17 +131,12 @@ public Response executeParallel(@NotNull Request request) { } @Override - public void loadTuples(@NotNull List featureTuples, int from, int to, int mode) { - session.loadTuples(featureTuples, from, to, mode); + public void loadTuples(@NotNull List featureTuples, int from, int to) { + session.loadTuples(featureTuples, from, to); } @Override public @Nullable NakshaCollection getCollectionByNumber(@NotNull NakshaCatalog catalog, int collectionNumber) { return session.getCollectionByNumber(catalog, collectionNumber); } - - @Override - public void loadTuples(@NotNull List featureTuples) { - loadTuples(featureTuples, 0, featureTuples.size(), FETCH_ALL); - } } diff --git a/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHSpaceStorageReader.java b/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHSpaceStorageReader.java index c15c64859..cc9c1f768 100644 --- a/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHSpaceStorageReader.java +++ b/here-naksha-lib-hub/src/jvmMain/java/com/here/naksha/lib/hub/storages/NHSpaceStorageReader.java @@ -47,13 +47,7 @@ import naksha.model.StreamInfo; import naksha.model.objects.NakshaCollection; import naksha.model.objects.NakshaCatalog; -import naksha.model.request.ErrorResponse; -import naksha.model.request.FeatureTuple; -import naksha.model.request.ReadCollections; -import naksha.model.request.ReadFeatures; -import naksha.model.request.Request; -import naksha.model.request.Response; -import naksha.model.request.SuccessResponse; +import naksha.model.request.*; import naksha.model.util.ResultHelper; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -166,11 +160,6 @@ public NHSpaceStorageReader( return eventPipeline.sendEvent(rf); } - @Override - public void loadTuples(@NotNull List featureTuples) { - loadTuples(featureTuples, 0, featureTuples.size(), FETCH_ALL); - } - record SpaceAndHandlerConfigs(Space space, List eventHandlerConfigs) { } @@ -347,7 +336,7 @@ public Response executeParallel(@NotNull Request request) { } @Override - public void loadTuples(@NotNull List featureTuples, int from, int to, int mode) { + public void loadTuples(@NotNull List featureTuples, int from, int to) { throw NOT_SUPPORTED_ERROR; } diff --git a/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/IDictReader.kt b/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/IDictReader.kt index d998d5ce8..292456b72 100644 --- a/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/IDictReader.kt +++ b/here-naksha-lib-jbon/src/commonMain/kotlin/naksha/jbon/IDictReader.kt @@ -17,7 +17,7 @@ interface IDictReader { * @return The global dictionary with the given identifier; _null_ when no such dictionary exists. * @since 3.0 */ - fun getDictionary(id: String): JbDictionary? + fun getDictionary(id: String): JbDictionary? = null /** * The best dictionary to encode the given feature. diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/ISession.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/ISession.kt index 34cffdcdf..c75dc664f 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/ISession.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/ISession.kt @@ -7,6 +7,7 @@ import naksha.model.objects.NakshaCatalog import naksha.model.request.* import kotlin.js.JsExport import kotlin.js.JsName +import kotlin.jvm.JvmOverloads /** * When a session is opened, it is bound to the context in which the session shall operate. @@ -149,19 +150,7 @@ interface ISession : AutoCloseable { */ fun getCollectionByNumber(catalog: NakshaCatalog, collectionNumber: Int): NakshaCollection? - /** - * Load all tuples into the given [feature-tuples][FeatureTuple]. - * - * [Tuple] that can't be fetched will still be `null` after the method returns. The method should query the [Naksha.cache] before actually loading the [Tuple] from the storage _(without asking the cache to load from storage, otherwise this would be a recursion)_ . - * - * @param featureTuples a list of result-tuples to fetch. - * @since 3.0 - * @see [Naksha.cache] - */ - @JsName("loadAllTuples") - fun loadTuples(featureTuples: List) - - /** + /** * Load all tuples into the given [feature-tuples][FeatureTuple]. * * [Tuple] that can't be fetched will still be `null` after the method returns. The method should query the [Naksha.cache] before actually loading the [Tuple] from the storage _(without asking the cache to load from storage, otherwise this would be a recursion)_ . @@ -169,14 +158,8 @@ interface ISession : AutoCloseable { * @param featureTuples a list of result-tuples to fetch. * @param from the index of the first result-tuples to fetch; default is `0`. * @param to the index of the first result-tuples to ignore; default is `featureTuples.size`. - * @param mode the fetch mode; default is [FETCH_ALL]. * @since 3.0 * @see [Naksha.cache] */ - fun loadTuples( - featureTuples: List, - from: Int = 0, - to: Int = featureTuples.size, - mode: FetchMode = FETCH_ALL - ) + fun loadTuples(featureTuples: List, from: Int = 0,to: Int = featureTuples.size) } diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgSession.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgSession.kt index c7b5bb284..40c61db46 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgSession.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/PgSession.kt @@ -372,10 +372,7 @@ open class PgSession( return PgLock(this, useConnection(), lockId, false) } - // TODO: We should only have one method being this one! - override fun loadTuples(featureTuples: List) = loadTuples(featureTuples, 0, featureTuples.size, FETCH_ALL) - - override fun loadTuples(featureTuples: List, from: Int, to: Int, mode: FetchMode) { + override fun loadTuples(featureTuples: List, from: Int, to: Int) { val missing = featureTuples.subList(from, to).mapNotNull { if (it != null && it.tuple == null) it else null } if (missing.isNotEmpty()) { (if (mayReadParallel) newReadConnection() else readConnection()).use { readConn -> diff --git a/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/ViewReadSession.java b/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/ViewReadSession.java index c1a799a8b..dbb72fc29 100644 --- a/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/ViewReadSession.java +++ b/here-naksha-lib-view/src/jvmMain/java/com/here/naksha/lib/view/ViewReadSession.java @@ -235,12 +235,8 @@ public Response executeParallel(@NotNull Request request) { return execute(request); } - public void loadTuples(@NotNull List featureTuples) { - loadTuples(featureTuples, 0, featureTuples.size(), FETCH_ALL); - } - @Override - public void loadTuples(@NotNull List featureTuples, int from, int to, int mode) { + public void loadTuples(@NotNull List featureTuples, int from, int to) { final @NotNull ViewLayerCollection viewCollection = view.getViewCollection(); // TODO: We need to group the tuples by layer using: // viewCollection.getByTupleNumber() diff --git a/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/HttpStorageReadSession.java b/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/HttpStorageReadSession.java index 84022c956..f79f11d10 100644 --- a/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/HttpStorageReadSession.java +++ b/here-naksha-storage-http/src/jvmMain/java/com/here/naksha/storage/http/HttpStorageReadSession.java @@ -29,10 +29,7 @@ import naksha.model.SessionOptions; import naksha.model.objects.NakshaCollection; import naksha.model.objects.NakshaCatalog; -import naksha.model.request.ErrorResponse; -import naksha.model.request.FeatureTuple; -import naksha.model.request.Request; -import naksha.model.request.Response; +import naksha.model.request.*; import org.apache.commons.lang3.NotImplementedException; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -130,7 +127,6 @@ public Response executeParallel(@NotNull Request request) { return execute(request); } - @Override public @NotNull IStorage getStorage() { throw new NotImplementedException("Not supported by HTTP storage"); @@ -147,7 +143,7 @@ public Response executeParallel(@NotNull Request request) { } @Override - public void loadTuples(@NotNull List featureTuples, int from, int to, int mode) { + public void loadTuples(@NotNull List featureTuples, int from, int to) { throw new NotImplementedException("Not supported by HTTP storage"); } @@ -172,11 +168,6 @@ public void loadTuples(@NotNull List featureTuples, int throw new NotImplementedException("Not supported by HTTP storage"); } - @Override - public void loadTuples(@NotNull List featureTuples) { - loadTuples(featureTuples, 0, featureTuples.size(), FETCH_ALL); - } - @NotNull RequestSender getRequestSender() { return requestSender; From 5bd995fb7588dc6e61d338e676109abbdd29a354 Mon Sep 17 00:00:00 2001 From: Alexander Lowey-Weber Date: Fri, 26 Jun 2026 16:48:57 +0200 Subject: [PATCH 56/57] Add support in the get methods to read from Tuple, next to read from feature. Signed-off-by: Alexander Lowey-Weber --- .../kotlin/naksha/model/objects/BoolMember.kt | 8 + .../naksha/model/objects/ByteArrayMember.kt | 8 + .../naksha/model/objects/Float32Member.kt | 8 + .../naksha/model/objects/Float64Member.kt | 8 + .../naksha/model/objects/Int16Member.kt | 8 + .../naksha/model/objects/Int32Member.kt | 8 + .../naksha/model/objects/Int64Member.kt | 8 + .../kotlin/naksha/model/objects/Int8Member.kt | 8 + .../kotlin/naksha/model/objects/Member.kt | 147 +++++++++++++++++- .../naksha/model/objects/SpatialMember.kt | 8 + .../naksha/model/objects/StringMember.kt | 8 + .../naksha/model/objects/TagListMember.kt | 8 + .../kotlin/naksha/model/objects/TagsMember.kt | 8 + .../naksha/model/objects/TupleNumberMember.kt | 8 + 14 files changed, 249 insertions(+), 2 deletions(-) diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/BoolMember.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/BoolMember.kt index 88056c3bb..2d176ea25 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/BoolMember.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/BoolMember.kt @@ -1,5 +1,6 @@ package naksha.model.objects +import naksha.model.Tuple import naksha.model.illegalArg import naksha.model.illegalState import naksha.model.objects.MemberType.MemberType_C.BOOLEAN @@ -34,6 +35,13 @@ class BoolMember() : TypedMember() { /** Retrieves the boolean value of this member from the given feature. */ fun get(feature: NakshaFeature): Boolean? = getBoolean(feature) + /** + * Retrieves the boolean value of this member from the given tuple. + * TODO: When no such member exists in membersBook, should search along [path] in [tuple.featureBytes], but currently cannot due to JbDecoder2 limits. + */ + @JsName("getFromTuple") + fun get(tuple: Tuple): Boolean? = getBoolean(tuple) + /** Sets the boolean value of this member on the given feature. */ fun set(feature: NakshaFeature, value: Boolean): Boolean? = setPath(feature, path, value) as Boolean? } \ No newline at end of file diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/ByteArrayMember.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/ByteArrayMember.kt index 260e5737d..eacf9e78e 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/ByteArrayMember.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/ByteArrayMember.kt @@ -1,5 +1,6 @@ package naksha.model.objects +import naksha.model.Tuple import naksha.model.illegalArg import naksha.model.illegalState import naksha.model.objects.MemberType.MemberType_C.BYTE_ARRAY @@ -34,6 +35,13 @@ class ByteArrayMember() : TypedMember() { /** Retrieves the byte array value of this member from the given feature. */ fun get(feature: NakshaFeature): ByteArray? = getByteArray(feature) + /** + * Retrieves the byte array value of this member from the given tuple. + * TODO: When no such member exists in membersBook, should search along [path] in [tuple.featureBytes], but currently cannot due to JbDecoder2 limits. + */ + @JsName("getFromTuple") + fun get(tuple: Tuple): ByteArray? = getByteArray(tuple) + /** Sets the byte array value of this member on the given feature. */ fun set(feature: NakshaFeature, value: ByteArray): Any? = setPath(feature, path, value) } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Float32Member.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Float32Member.kt index d0da8f903..d2a3a5658 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Float32Member.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Float32Member.kt @@ -1,5 +1,6 @@ package naksha.model.objects +import naksha.model.Tuple import naksha.model.illegalArg import naksha.model.illegalState import naksha.model.objects.MemberType.MemberType_C.FLOAT32 @@ -34,6 +35,13 @@ class Float32Member() : TypedMember() { /** Retrieves the float32 value of this member from the given feature. */ fun get(feature: NakshaFeature): Float? = getDouble(feature)?.toFloat() + /** + * Retrieves the float32 value of this member from the given tuple. + * TODO: When no such member exists in membersBook, should search along [path] in [tuple.featureBytes], but currently cannot due to JbDecoder2 limits. + */ + @JsName("getFromTuple") + fun get(tuple: Tuple): Float? = getDouble(tuple)?.toFloat() + /** Sets the float32 value of this member on the given feature. */ fun set(feature: NakshaFeature, value: Float): Any? = setPath(feature, path, value) } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Float64Member.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Float64Member.kt index d3b175b78..d92d8e539 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Float64Member.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Float64Member.kt @@ -1,5 +1,6 @@ package naksha.model.objects +import naksha.model.Tuple import naksha.model.illegalArg import naksha.model.illegalState import naksha.model.objects.MemberType.MemberType_C.FLOAT64 @@ -34,6 +35,13 @@ class Float64Member() : TypedMember() { /** Retrieves the float64 value of this member from the given feature. */ fun get(feature: NakshaFeature): Double? = getDouble(feature) + /** + * Retrieves the float64 value of this member from the given tuple. + * TODO: When no such member exists in membersBook, should search along [path] in [tuple.featureBytes], but currently cannot due to JbDecoder2 limits. + */ + @JsName("getFromTuple") + fun get(tuple: Tuple): Double? = getDouble(tuple) + /** Sets the float64 value of this member on the given feature. */ fun set(feature: NakshaFeature, value: Double): Any? = setPath(feature, path, value) } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int16Member.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int16Member.kt index 7c1e6be0a..09b441469 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int16Member.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int16Member.kt @@ -1,5 +1,6 @@ package naksha.model.objects +import naksha.model.Tuple import naksha.model.illegalArg import naksha.model.illegalState import naksha.model.objects.MemberType.MemberType_C.INT16 @@ -34,6 +35,13 @@ class Int16Member() : TypedMember() { /** Retrieves the int16 value of this member from the given feature. */ fun get(feature: NakshaFeature): Short? = getInt64(feature)?.toShort() + /** + * Retrieves the int16 value of this member from the given tuple. + * TODO: When no such member exists in membersBook, should search along [path] in [tuple.featureBytes], but currently cannot due to JbDecoder2 limits. + */ + @JsName("getFromTuple") + fun get(tuple: Tuple): Short? = getInt64(tuple)?.toShort() + /** Sets the int16 value of this member on the given feature. */ fun set(feature: NakshaFeature, value: Short): Any? = setPath(feature, path, value) } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int32Member.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int32Member.kt index e96a9f57b..68d6e3b85 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int32Member.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int32Member.kt @@ -1,5 +1,6 @@ package naksha.model.objects +import naksha.model.Tuple import naksha.model.illegalArg import naksha.model.illegalState import naksha.model.objects.MemberType.MemberType_C.INT32 @@ -34,6 +35,13 @@ class Int32Member() : TypedMember() { /** Retrieves the int32 value of this member from the given feature. */ fun get(feature: NakshaFeature): Int? = getInt64(feature)?.toInt() + /** + * Retrieves the int32 value of this member from the given tuple. + * TODO: When no such member exists in membersBook, should search along [path] in [tuple.featureBytes], but currently cannot due to JbDecoder2 limits. + */ + @JsName("getFromTuple") + fun get(tuple: Tuple): Int? = getInt64(tuple)?.toInt() + /** Sets the int32 value of this member on the given feature. */ fun set(feature: NakshaFeature, value: Int): Any? = setPath(feature, path, value) } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int64Member.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int64Member.kt index 3f4bd3497..299350681 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int64Member.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int64Member.kt @@ -1,6 +1,7 @@ package naksha.model.objects import naksha.base.Int64 +import naksha.model.Tuple import naksha.model.illegalArg import naksha.model.illegalState import naksha.model.objects.MemberType.MemberType_C.INT64 @@ -35,6 +36,13 @@ class Int64Member() : TypedMember() { /** Retrieves the int64 value of this member from the given feature. */ fun get(feature: NakshaFeature): Int64? = getInt64(feature) + /** + * Retrieves the int64 value of this member from the given tuple. + * TODO: When no such member exists in membersBook, should search along [path] in [tuple.featureBytes], but currently cannot due to JbDecoder2 limits. + */ + @JsName("getFromTuple") + fun get(tuple: Tuple): Int64? = getInt64(tuple) + /** Sets the int64 value of this member on the given feature. */ fun set(feature: NakshaFeature, value: Int64): Any? = setPath(feature, path, value) } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int8Member.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int8Member.kt index b14f9e70a..4551d4042 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int8Member.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Int8Member.kt @@ -1,5 +1,6 @@ package naksha.model.objects +import naksha.model.Tuple import naksha.model.illegalArg import naksha.model.illegalState import naksha.model.objects.MemberType.MemberType_C.INT8 @@ -34,6 +35,13 @@ class Int8Member() : TypedMember() { /** Retrieves the int8 value of this member from the given feature. */ fun get(feature: NakshaFeature): Byte? = getInt64(feature)?.toByte() + /** + * Retrieves the int8 value of this member from the given tuple. + * TODO: When no such member exists in membersBook, should search along [path] in [tuple.featureBytes], but currently cannot due to JbDecoder2 limits. + */ + @JsName("getFromTuple") + fun get(tuple: Tuple): Byte? = getInt64(tuple)?.toByte() + /** Sets the int8 value of this member on the given feature. */ fun set(feature: NakshaFeature, value: Byte): Any? = setPath(feature, path, value) } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt index a296b4e94..55d4bba08 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/Member.kt @@ -20,6 +20,7 @@ import naksha.model.NakshaException import naksha.model.NakshaIdType.INTERNAL_MEMBER import naksha.model.TagList import naksha.model.TagMap +import naksha.model.Tuple import naksha.model.TupleNumber import kotlin.js.JsExport import kotlin.js.JsName @@ -341,10 +342,137 @@ open class Member() : AnyObject(), Comparator { return null } - // TODO: We need support for real sets! + /** + * Helper to read a [TupleNumber] from the given tuple. + * @param tuple The tuple to read from. + * @return the read value or `null`, if the tuple does not store a valid value for this member. + * TODO: When no such member exists in membersBook, should search along [path] in [tuple.featureBytes], but currently cannot due to JbDecoder2 limits. + */ + @JsName("getTupleNumberFromTuple") + fun getTupleNumber(tuple: Tuple): TupleNumber? { + val raw = tuple.membersBook[this.name] ?: return null + if (raw is TupleNumber) return raw + if (raw is String) return TupleNumber.fromString(raw) + if (raw is ByteArray) return TupleNumber.fromByteArray(raw) + return null + } + + /** + * Helper to read a boolean from the given tuple. + * @param tuple The tuple to read from. + * @return the read value or `null`, if the tuple does not store a valid value for this member. + * TODO: When no such member exists in membersBook, should search along [path] in [tuple.featureBytes], but currently cannot due to JbDecoder2 limits. + */ + @JsName("getBooleanFromTuple") + fun getBoolean(tuple: Tuple): Boolean? { + val raw = tuple.membersBook[this.name] + if (raw is Boolean) return raw + return null + } + + /** + * Helper to read a string from the given tuple. + * @param tuple The tuple to read from. + * @return the read value or `null`, if the tuple does not store a valid value for this member. + * TODO: When no such member exists in membersBook, should search along [path] in [tuple.featureBytes], but currently cannot due to JbDecoder2 limits. + */ + @JsName("getStringFromTuple") + fun getString(tuple: Tuple): String? { + val raw = tuple.membersBook[this.name] + if (raw is String) return raw + return null + } /** - * Helper to read a set form the given feature. + * Helper to read a 64-bit integer from the given tuple. + * @param tuple The tuple to read from. + * @return the read value or `null`, if the tuple does not store a valid value for this member. + * TODO: When no such member exists in membersBook, should search along [path] in [tuple.featureBytes], but currently cannot due to JbDecoder2 limits. + */ + @JsName("getInt64FromTuple") + fun getInt64(tuple: Tuple): Int64? { + val raw = tuple.membersBook[this.name] + if (raw is Int64) return raw + if (raw is Long) return Int64(raw) + if (raw is Number) return Int64(raw.toLong()) + return null + } + + /** + * Helper to read a 64-bit floating point number from the given tuple. + * @param tuple The tuple to read from. + * @return the read value or `null`, if the tuple does not store a valid value for this member. + * TODO: When no such member exists in membersBook, should search along [path] in [tuple.featureBytes], but currently cannot due to JbDecoder2 limits. + */ + @JsName("getDoubleFromTuple") + fun getDouble(tuple: Tuple): Double? { + val raw = tuple.membersBook[this.name] + if (raw is Double) return raw + if (raw is Number) return raw.toDouble() + return null + } + + /** + * Helper to read a geometry from the given tuple. + * @param tuple The tuple to read from. + * @return the read value or `null`, if the tuple does not store a valid value for this member. + * TODO: When no such member exists in membersBook, should search along [path] in [tuple.featureBytes], but currently cannot due to JbDecoder2 limits. + */ + @JsName("getGeometryFromTuple") + fun getGeometry(tuple: Tuple): SpGeometry? { + val raw = tuple.membersBook[this.name] + if (raw is SpGeometry) return raw + return null + } + + /** + * Helper to read a byte array from the given tuple. + * @param tuple The tuple to read from. + * @return the read value or `null`, if the tuple does not store a valid value for this member. + * TODO: When no such member exists in membersBook, should search along [path] in [tuple.featureBytes], but currently cannot due to JbDecoder2 limits. + */ + @JsName("getByteArrayFromTuple") + fun getByteArray(tuple: Tuple): ByteArray? { + val raw = tuple.membersBook[this.name] + if (raw is ByteArray) return raw + return null + } + + /** + * Helper to read a [TagMap] from the given tuple. + * @param tuple The tuple to read from. + * @return the read value or `null`, if the tuple does not store a valid value for this member. + * TODO: When no such member exists in membersBook, should search along [path] in [tuple.featureBytes], but currently cannot due to JbDecoder2 limits. + */ + @JsName("getTagMapFromTuple") + fun getTagMap(tuple: Tuple): TagMap? { + val raw = tuple.membersBook[this.name] + if (raw is TagMap) return raw + if (raw is MapProxy<*,*>) return raw.proxy(TagMap::class) + if (raw is PlatformMap) return raw.proxy(TagMap::class) + return null + } + + /** + * Helper to read a [TagList] from the given tuple. + * @param tuple The tuple to read from. + * @return the read value or `null`, if the tuple does not store a valid value for this member. + * TODO: When no such member exists in membersBook, should search along [path] in [tuple.featureBytes], but currently cannot due to JbDecoder2 limits. + */ + @JsName("getTagListFromTuple") + fun getTagList(tuple: Tuple): TagList? { + val raw = tuple.membersBook[this.name] + if (raw is TagList) return raw + if (raw is ListProxy<*>) return raw.proxy(TagList::class) + if (raw is PlatformList) return raw.proxy(TagList::class) + return null + } + + /** + * Helper to write a member value to the given feature. + * @param feature The feature to write to. + * @return the previous value. + * Helper to read a set from the given feature. * @param feature The feature to read from. * @return the read value or `null`, if the feature does not store a valid value at the member path. */ @@ -356,6 +484,21 @@ open class Member() : AnyObject(), Comparator { return null } + /** + * Helper to read a set from the given tuple. + * @param tuple The tuple to read from. + * @return the read value or `null`, if the tuple does not store a valid value for this member. + * TODO: When no such member exists in membersBook, should search along [path] in [tuple.featureBytes], but currently cannot due to JbDecoder2 limits. + */ + @JsName("getSetFromTuple") + fun getSet(tuple: Tuple): AnyList? { + val raw = tuple.membersBook[this.name] + if (raw is AnyList) return raw + if (raw is ListProxy<*>) return raw.proxy(AnyList::class) + if (raw is PlatformList) return raw.proxy(AnyList::class) + return null + } + /** * Helper to write a member value to the given feature. * @param feature The feature to write to. diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/SpatialMember.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/SpatialMember.kt index dd8829296..3b0afd3e7 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/SpatialMember.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/SpatialMember.kt @@ -1,6 +1,7 @@ package naksha.model.objects import naksha.geo.SpGeometry +import naksha.model.Tuple import naksha.model.illegalArg import naksha.model.illegalState import naksha.model.objects.MemberType.MemberType_C.SPATIAL @@ -35,6 +36,13 @@ class SpatialMember() : TypedMember() { /** Retrieves the spatial geometry value of this member from the given feature. */ fun get(feature: NakshaFeature): SpGeometry? = getGeometry(feature) + /** + * Retrieves the spatial geometry value of this member from the given tuple. + * TODO: When no such member exists in membersBook, should search along [path] in [tuple.featureBytes], but currently cannot due to JbDecoder2 limits. + */ + @JsName("getFromTuple") + fun get(tuple: Tuple): SpGeometry? = getGeometry(tuple) + /** Sets the spatial geometry value of this member on the given feature. */ fun set(feature: NakshaFeature, value: SpGeometry): Any? = setPath(feature, path, value) } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StringMember.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StringMember.kt index 9956eba97..33876de60 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StringMember.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/StringMember.kt @@ -1,5 +1,6 @@ package naksha.model.objects +import naksha.model.Tuple import naksha.model.illegalArg import naksha.model.illegalState import naksha.model.objects.MemberType.MemberType_C.STRING @@ -34,6 +35,13 @@ class StringMember() : TypedMember() { /** Retrieves the string value of this member from the given feature. */ fun get(feature: NakshaFeature): String? = getString(feature) + /** + * Retrieves the string value of this member from the given tuple. + * TODO: When no such member exists in membersBook, should search along [path] in [tuple.featureBytes], but currently cannot due to JbDecoder2 limits. + */ + @JsName("getFromTuple") + fun get(tuple: Tuple): String? = getString(tuple) + /** Sets the string value of this member on the given feature. */ fun set(feature: NakshaFeature, value: String): Any? = setPath(feature, path, value) } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TagListMember.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TagListMember.kt index 10dd723c5..a417d7e23 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TagListMember.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TagListMember.kt @@ -2,6 +2,7 @@ package naksha.model.objects import naksha.base.ListProxy import naksha.model.TagList +import naksha.model.Tuple import naksha.model.illegalArg import naksha.model.illegalState import naksha.model.objects.MemberType.MemberType_C.TAG_LIST @@ -36,6 +37,13 @@ class TagListMember() : TypedMember() { /** Retrieves the tag list value of this member from the given feature. */ fun get(feature: NakshaFeature): TagList? = getTagList(feature) + /** + * Retrieves the tag list value of this member from the given tuple. + * TODO: When no such member exists in membersBook, should search along [path] in [tuple.featureBytes], but currently cannot due to JbDecoder2 limits. + */ + @JsName("getFromTuple") + fun get(tuple: Tuple): TagList? = getTagList(tuple) + /** Sets the tag list value of this member on the given feature. */ fun set(feature: NakshaFeature, value: ListProxy<*>): Any? = setPath(feature, path, value) } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TagsMember.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TagsMember.kt index 5b1f76451..1311184dc 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TagsMember.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TagsMember.kt @@ -1,6 +1,7 @@ package naksha.model.objects import naksha.model.TagMap +import naksha.model.Tuple import naksha.model.illegalArg import naksha.model.illegalState import naksha.model.objects.MemberType.MemberType_C.TAG_MAP @@ -35,6 +36,13 @@ class TagsMember() : TypedMember() { /** Retrieves the tag map value of this member from the given feature. */ fun get(feature: NakshaFeature): TagMap? = getTagMap(feature) + /** + * Retrieves the tag map value of this member from the given tuple. + * TODO: When no such member exists in membersBook, should search along [path] in [tuple.featureBytes], but currently cannot due to JbDecoder2 limits. + */ + @JsName("getFromTuple") + fun get(tuple: Tuple): TagMap? = getTagMap(tuple) + /** Sets the tag map value of this member on the given feature. */ fun set(feature: NakshaFeature, value: TagMap): Any? = setPath(feature, path, value) } diff --git a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TupleNumberMember.kt b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TupleNumberMember.kt index e741b3413..9a2c09146 100644 --- a/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TupleNumberMember.kt +++ b/here-naksha-lib-model/src/commonMain/kotlin/naksha/model/objects/TupleNumberMember.kt @@ -1,5 +1,6 @@ package naksha.model.objects +import naksha.model.Tuple import naksha.model.TupleNumber import naksha.model.illegalArg import naksha.model.illegalState @@ -35,6 +36,13 @@ class TupleNumberMember() : TypedMember() { /** Retrieves the tuple number value of this member from the given feature. */ fun get(feature: NakshaFeature): TupleNumber? = getTupleNumber(feature) + /** + * Retrieves the tuple number value of this member from the given tuple. + * TODO: When no such member exists in membersBook, should search along [path] in [tuple.featureBytes], but currently cannot due to JbDecoder2 limits. + */ + @JsName("getFromTuple") + fun get(tuple: Tuple): TupleNumber? = getTupleNumber(tuple) + /** Sets the tuple number value of this member on the given feature. */ fun set(feature: NakshaFeature, value: TupleNumber): Any? = setPath(feature, path, value) } From ff27444553d700846ea6c32c479d971057e20bd7 Mon Sep 17 00:00:00 2001 From: phmai Date: Mon, 29 Jun 2026 10:35:23 +0200 Subject: [PATCH 57/57] fix some activitylog tests Signed-off-by: phmai --- .../activitylog/ActivityLogHandlerTest.java | 17 +++++++---------- .../ActivityLogRequestTranslationUtilTest.java | 17 ++++++++--------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/here-naksha-handler-activitylog/src/jvmTest/java/com/here/naksha/handler/activitylog/ActivityLogHandlerTest.java b/here-naksha-handler-activitylog/src/jvmTest/java/com/here/naksha/handler/activitylog/ActivityLogHandlerTest.java index 5d55f1852..8a6f189df 100644 --- a/here-naksha-handler-activitylog/src/jvmTest/java/com/here/naksha/handler/activitylog/ActivityLogHandlerTest.java +++ b/here-naksha-handler-activitylog/src/jvmTest/java/com/here/naksha/handler/activitylog/ActivityLogHandlerTest.java @@ -7,7 +7,6 @@ import static com.here.naksha.handler.activitylog.assertions.ActivityLogSuccessResultAssertions.assertThatResult; import static java.util.Collections.emptyList; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -29,10 +28,8 @@ import java.util.List; import java.util.Map; import java.util.stream.Stream; -import naksha.base.AnyList; -import naksha.base.Int64; -import naksha.base.JvmInt64; -import naksha.base.Timestamp; + +import naksha.base.*; import naksha.model.Action; import naksha.model.Guid; import naksha.model.IReadSession; @@ -44,6 +41,7 @@ import naksha.model.XyzNs; import naksha.model.objects.NakshaFeature; import naksha.model.objects.NakshaProperties; +import naksha.model.objects.XyzMembers; import naksha.model.request.ErrorResponse; import naksha.model.request.ReadCollections; import naksha.model.request.ReadFeatures; @@ -55,7 +53,6 @@ import naksha.model.request.WriteRequest; import naksha.model.request.query.AnyOp; import naksha.model.request.query.IMemberQuery; -import naksha.model.request.query.MetaColumn; import naksha.model.request.query.MemberQuery; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; @@ -463,7 +460,7 @@ private ArgumentMatcher predecessorRequestMatcher() { private boolean isHistoryAwareReadFeatures(ReadRequest readRequest) { if (readRequest instanceof ReadFeatures rf) { - return rf.getQueryHistory() && rf.getCollectionId().size() == 1; + return rf.getQueryHistory(); } return false; } @@ -471,7 +468,7 @@ private boolean isHistoryAwareReadFeatures(ReadRequest readRequest) { private boolean containsNextVersionMetaQuery(ReadFeatures readFeatures, TupleNumber... expectedTns) { IMemberQuery metaQuery = readFeatures.getQuery().getMembers(); if (!(metaQuery instanceof MemberQuery mq)) return false; - boolean basicCheck = mq.getMember().equals(MetaColumn.nextVersion()) + boolean basicCheck = ((Proxy)XyzMembers.XyzNextVersion).equals(mq.getMember()) && mq.getOp().equals(AnyOp.IS_ANY_OF); if (!basicCheck) return false; if (expectedTns.length == 0) return mq.getValue() != null; @@ -483,7 +480,7 @@ private boolean containsNextVersionMetaQuery(ReadFeatures readFeatures, TupleNum versions = Arrays.asList(arr); } else if (value instanceof AnyList list) { versions = new java.util.ArrayList<>(); - for (int i = 0; i < list.size(); i++) { + for (int i = 0; i < list.getSize(); i++) { Object item = list.get(i); if (item instanceof Int64 v) versions.add(v); else return false; @@ -493,7 +490,7 @@ private boolean containsNextVersionMetaQuery(ReadFeatures readFeatures, TupleNum } if (versions.size() != expectedTns.length) return false; return Arrays.stream(expectedTns) - .map(tn -> tn.version.value) + .map(tn -> tn.version) .allMatch(expected -> versions.stream().anyMatch(expected::equals)); } diff --git a/here-naksha-handler-activitylog/src/jvmTest/java/com/here/naksha/handler/activitylog/ActivityLogRequestTranslationUtilTest.java b/here-naksha-handler-activitylog/src/jvmTest/java/com/here/naksha/handler/activitylog/ActivityLogRequestTranslationUtilTest.java index c99a068b9..52de81934 100644 --- a/here-naksha-handler-activitylog/src/jvmTest/java/com/here/naksha/handler/activitylog/ActivityLogRequestTranslationUtilTest.java +++ b/here-naksha-handler-activitylog/src/jvmTest/java/com/here/naksha/handler/activitylog/ActivityLogRequestTranslationUtilTest.java @@ -44,7 +44,7 @@ void shouldTranslateSingleGuidPassedAsFeatureId() { // And: there is a single guid passed from original featureId GuidList finalGuids = readFeatures.getGuids(); - assertEquals(1, finalGuids.size()); + assertEquals(1, finalGuids.getSize()); assertEquals(guid, finalGuids.get(0)); } @@ -74,7 +74,7 @@ void shouldTranslateMultipleGuidsPassedAsFeatureIds() { // And: all guids defined in featureIds were moved to ReadFeatures.guids GuidList finalGuids = readFeatures.getGuids(); - assertEquals(guids.size(), finalGuids.size()); + assertEquals(guids.getSize(), finalGuids.getSize()); assertTrue(finalGuids.containsAll(guids)); } @@ -97,7 +97,7 @@ void shouldTranslateActivityLogIdToFeatureId() { // And: there is a single featureId withing the request StringList featureIds = readFeatures.getFeatureIds(); - assertEquals(1, featureIds.size()); + assertEquals(1, featureIds.getSize()); assertEquals(featureId, featureIds.get(0)); // And: @@ -127,7 +127,7 @@ void shouldTranslateActivityLogIdsToFeatureIds() { // And: all ids defined in AcitvityLogNs are now part of featureIds StringList featureIds = readFeatures.getFeatureIds(); - assertEquals(2, featureIds.size()); + assertEquals(2, featureIds.getSize()); assertTrue(featureIds.containsAll(List.of(firstId, secondId))); // And: the pQuery left is effectively dead @@ -159,20 +159,19 @@ void shouldApplyMixedTranslations() { // And: feature ids are populated from ActivityLogNs StringList finalFeatureIds = readFeatures.getFeatureIds(); - assertEquals(1, finalFeatureIds.size()); + assertEquals(1, finalFeatureIds.getSize()); assertEquals(activityLogId, finalFeatureIds.get(0)); // And: guuids are populared from original feature ids GuidList finalGuids = readFeatures.getGuids(); - assertEquals(1, finalGuids.size()); + assertEquals(1, finalGuids.getSize()); assertEquals(guid, finalGuids.get(0)); } private void verifyAllHistoricalVersionsInCollection(ReadFeatures readFeatures) { assertTrue(readFeatures.getQueryHistory()); - StringList collectionIds = readFeatures.getCollectionId(); - assertEquals(1, collectionIds.size()); - assertEquals(TEST_SPACE_ID, collectionIds.get(0)); + String collectionId = readFeatures.getCollectionId(); + assertEquals(TEST_SPACE_ID, collectionId); assertEquals(Integer.MAX_VALUE, readFeatures.getVersions()); } } \ No newline at end of file