Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e06b06000b
|
||
|
|
2b627e767a |
No files matched your search
+68
-11
@@ -1,14 +1,71 @@
|
||||
version: 2.1
|
||||
setup: true
|
||||
|
||||
orbs:
|
||||
qqq-orb: kingsrook/qqq-orb@0.7.15
|
||||
qqq-orb: kingsrook/qqq-orb@0.6.8
|
||||
|
||||
commands:
|
||||
configure-package-install:
|
||||
steps:
|
||||
- run:
|
||||
name: Keep package installation unattended
|
||||
command: |
|
||||
# These build machines are discarded after the job.
|
||||
sudo install -d /etc/needrestart/conf.d
|
||||
printf '%s\n' '$nrconf{restart} = "l";' | sudo tee /etc/needrestart/conf.d/99-qqq-ci.conf >/dev/null
|
||||
|
||||
workflows:
|
||||
setup:
|
||||
jobs:
|
||||
- qqq-orb/generate_pipeline:
|
||||
orb_version: 0.7.15
|
||||
filters:
|
||||
branches:
|
||||
ignore: automation/qbit-rc-pending
|
||||
tags:
|
||||
only: /v.*/
|
||||
test_only:
|
||||
jobs:
|
||||
- qqq-orb/mvn_test_only:
|
||||
pre-steps:
|
||||
- configure-package-install
|
||||
context: [qqq-maven-registry-credentials]
|
||||
filters:
|
||||
branches:
|
||||
ignore: /(develop|release\/.*|hotfix\/.*)/
|
||||
|
||||
publish_snapshot:
|
||||
jobs:
|
||||
- qqq-orb/mvn_publish:
|
||||
pre-steps:
|
||||
- configure-package-install
|
||||
context: [qqq-maven-registry-credentials]
|
||||
branch_type: snapshot
|
||||
filters:
|
||||
branches:
|
||||
only: [develop]
|
||||
|
||||
publish_release_candidate:
|
||||
jobs:
|
||||
- qqq-orb/mvn_publish:
|
||||
pre-steps:
|
||||
- configure-package-install
|
||||
context: [qqq-maven-registry-credentials]
|
||||
branch_type: release_candidate
|
||||
filters:
|
||||
branches:
|
||||
only: [/release\/.*/]
|
||||
|
||||
publish_release:
|
||||
jobs:
|
||||
- qqq-orb/mvn_publish:
|
||||
pre-steps:
|
||||
- configure-package-install
|
||||
context: [qqq-maven-registry-credentials]
|
||||
branch_type: release
|
||||
filters:
|
||||
branches:
|
||||
ignore: /.*/
|
||||
tags:
|
||||
only: [/v.*/]
|
||||
|
||||
publish_hotfix_release:
|
||||
jobs:
|
||||
- qqq-orb/mvn_publish:
|
||||
pre-steps:
|
||||
- configure-package-install
|
||||
context: [qqq-maven-registry-credentials]
|
||||
branch_type: hotfix
|
||||
filters:
|
||||
branches:
|
||||
only: [/hotfix\/.*/]
|
||||
@@ -1,23 +0,0 @@
|
||||
name: CodeQL
|
||||
'on':
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
- release/**
|
||||
- hotfix/**
|
||||
pull_request: {}
|
||||
schedule:
|
||||
- cron: 41 16 * * 0
|
||||
workflow_dispatch: {}
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
security-events: write
|
||||
packages: read
|
||||
concurrency:
|
||||
group: codeql-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
jobs:
|
||||
analyze:
|
||||
uses: QRun-IO/qqq-orb/.github/workflows/qbit-codeql.yml@c288a8864fe9054f68410e1d78f5863aea4c4d22
|
||||
@@ -1,74 +0,0 @@
|
||||
name: Release candidate
|
||||
'on':
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: Plan a cut without changing release state
|
||||
type: boolean
|
||||
default: false
|
||||
finalize:
|
||||
description: Retry finalization of an existing cut; do not cut another RC
|
||||
type: boolean
|
||||
default: false
|
||||
recover:
|
||||
description: Recover the same published RC after a deployment timeout; never
|
||||
reupload
|
||||
type: boolean
|
||||
default: false
|
||||
circle_build:
|
||||
description: Original Circle publisher build number (recovery only)
|
||||
type: string
|
||||
default: ''
|
||||
merge_sha:
|
||||
description: Existing cut merge_sha (finalize retry only)
|
||||
type: string
|
||||
default: ''
|
||||
bump_sha:
|
||||
description: Existing cut bump_sha (finalize retry only)
|
||||
type: string
|
||||
default: ''
|
||||
branch:
|
||||
description: Existing cut branch (finalize retry only)
|
||||
type: string
|
||||
default: ''
|
||||
version:
|
||||
description: Existing cut version (finalize retry only)
|
||||
type: string
|
||||
default: ''
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
branches:
|
||||
- develop
|
||||
repository_dispatch:
|
||||
types:
|
||||
- qbit-rc-published
|
||||
permissions:
|
||||
contents: write
|
||||
actions: read
|
||||
checks: read
|
||||
jobs:
|
||||
release-rc:
|
||||
if: github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch'
|
||||
|| (github.event_name == 'pull_request' && github.event.pull_request.merged
|
||||
== true && github.event.pull_request.base.ref == 'develop' && contains(github.event.pull_request.labels.*.name,
|
||||
'release:rc'))
|
||||
uses: QRun-IO/qqq-orb/.github/workflows/qbit-release-rc.yml@cc30f599e63c8e41629c99d23395bac1adb8bc44
|
||||
with:
|
||||
dry_run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || false
|
||||
}}
|
||||
finalize: ${{ github.event_name == 'repository_dispatch' || (github.event_name
|
||||
== 'workflow_dispatch' && inputs.finalize) }}
|
||||
recover: ${{ github.event_name == 'workflow_dispatch' && inputs.recover || false
|
||||
}}
|
||||
circle_build: ${{ github.event_name == 'workflow_dispatch' && inputs.circle_build
|
||||
|| '' }}
|
||||
merge_sha: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.merge_sha
|
||||
|| inputs.merge_sha || '' }}
|
||||
bump_sha: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.bump_sha
|
||||
|| inputs.bump_sha || '' }}
|
||||
branch: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.branch
|
||||
|| inputs.branch || '' }}
|
||||
version: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.version
|
||||
|| inputs.version || '' }}
|
||||
secrets: inherit
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
profile: qbit
|
||||
orb_version: 0.7.15
|
||||
default_branch: develop
|
||||
java_version: '21'
|
||||
browsers: true
|
||||
api_checks: false
|
||||
docs_only: false
|
||||
release_on_main: false
|
||||
release_tag_policy: v-prefix
|
||||
test_branch_policy: non-release-no-hotfix
|
||||
publish:
|
||||
snapshot: true
|
||||
release_candidate: true
|
||||
release: true
|
||||
hotfix: true
|
||||
asciidoc: false
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
All notable changes to this project are documented here. The format follows Keep a Changelog; versions follow semantic versioning.
|
||||
|
||||
## [1.0.0-RC.2] - 2026-10-06
|
||||
|
||||
Second release candidate, published to Maven Central from `release/1.0`. Fixes the five pre-GA findings from the PR #10 review (#13 to #17); the changes are listed under 1.0.0 below. Requires QQQ 4.1. No schema change, but two new status values: `REBUILDING` on `quickSearchIndex.status` and `AWAITING_REINDEX` on `quickSearchFailedEvent.status`; a host that sized its own `quickSearchFailedEvent.status` column needs at least 16 characters. Known gaps before GA are tracked in #12.
|
||||
|
||||
## [1.0.0-RC.1] - 2026-10-05
|
||||
|
||||
First release candidate of 1.0, published to Maven Central from `release/1.0`. It contains every change listed under 1.0.0 below. Requires QQQ 4.1 (built against 4.1.0-RC.1). Known gaps before GA are tracked in #12.
|
||||
@@ -35,13 +39,23 @@ Requires QQQ 4.1. Closes every blocker of the 2026-10-04 production-readiness au
|
||||
- `IndexingUtils.normalizeSearchText` no longer lower-cases (analyzers do).
|
||||
- `enableScheduledProcesses` is deprecated in favour of `enableBasepullProcess` and `enableMaintenanceProcesses`.
|
||||
- `QuickSearchQBitContext` is deprecated; state lives on `QuickSearchRuntime`, resolved from the `QInstance`.
|
||||
- Search `offset` counts results the session can read. With record security locks on, hits are scanned from the start in batches of at most 1,000, filtered for access and deduplicated; one call reads at most 10,000 hits over 32 requests, and in per-table mode each of T tables gets max(10,000 / T, 2 x `limitPerTable`) hits. When that share runs out `hasMore` is false and `totalHitsIsLowerBound` is true. With locks on, `totalHits` counts hits confirmed readable. Per-table mode now honours `offset` (#16).
|
||||
- A full reindex of all tables marks each table `REBUILDING` while it runs; drift detection is suspended for those tables. After a killed run, run the full reindex again (#14).
|
||||
|
||||
### Removed
|
||||
- The three table customizers (`QuickSearchPostInsertCustomizer`, `QuickSearchPostUpdateCustomizer`, `QuickSearchPostDeleteCustomizer`), replaced by `QuickSearchRecordChangeListener`.
|
||||
- Support for QQQ 4.0.
|
||||
|
||||
### Security
|
||||
- `opensearchUrl` with userinfo (`https://user:pass@host`) fails validation; an `${env.X}` or `${prop.X}` credential or TLS path that resolves to an empty or blank value fails transport creation and names the field, never the value; `tls.hostnameVerification=false` logs a warning (#15).
|
||||
|
||||
### Fixed
|
||||
- Rolled-back writes could be indexed; real-time failures were silent; basepull lost rows modified during a run and rows at the boundary second; unordered paging skipped or duplicated rows; full reindex blanked search; search leaked labels and snippets across tables regardless of permissions; mixed field types across tables broke indexing; non-text searchable fields failed every search; record labels were null for annotation tables; duplicate `quickSearchIndex` rows on concurrent first runs; unbounded `limit`/`offset`; unchecked `OpenSearchException`; unreachable cluster at startup left the host healthy with search broken.
|
||||
- Deletes made during a full reindex of all tables were lost from the new index; they are now captured and applied after the alias swap (#14).
|
||||
- An update was dropped without a `quickSearchFailedEvent` row when the listener's re-read failed or the optional AWS SDK was missing (`LinkageError`); both are now recorded for basepull, and a `DEGRADED` start without the AWS SDK no longer aborts host startup (#13).
|
||||
- Search pages overlapped under record locks, per-table mode ignored `offset`, and a large `maxSearchLimit` could exceed the 10,000 result window (#16).
|
||||
- A failing document count was swallowed and cleared `documentCount`; it is now logged at warn and the previous count is kept (#17).
|
||||
- A real-time insert was indexed with raw values (a foreign key as its id, the record label as "2 - email") until the next basepull. Inserts are now re-read in the write's transaction with display values and possible-value labels, as updates are; a failed re-read is recorded in `quickSearchFailedEvent` for basepull and never fails the insert (#28).
|
||||
|
||||
## [0.2.0] - 2026-03-30
|
||||
- Config-driven table indexing (`SearchableTableConfig`).
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
- Real-time indexing happens after commit. Code that searched for a record inside the same transaction that inserted it will not find it until commit.
|
||||
- Real-time failures no longer vanish: they land in `quickSearchFailedEvent` and are replayed by the next basepull. Review that table after an OpenSearch outage.
|
||||
- Inserted and updated records are re-read inside the write's transaction, with display values and possible-value labels, before they are indexed. That is one extra query per write batch on an indexed table, and it runs the host's possible-value providers. If the re-read fails, the write still succeeds and the records are replayed by basepull.
|
||||
- A table without the change-detection field (`modifyDate` by default) is indexed on the first basepull and thereafter only by reconcile.
|
||||
- `limit` is capped (default 100), `offset + limit` at 10,000, and search terms must be 2 to 100 characters.
|
||||
- Query semantics changed from OR over n-grams to AND over whole terms with prefix matching: "blue widget" now requires both words.
|
||||
|
||||
+18
-6
@@ -1,19 +1,31 @@
|
||||
# Session State
|
||||
|
||||
Updated: 2026-10-05
|
||||
Updated: 2026-10-06
|
||||
|
||||
## Context
|
||||
|
||||
`1.0.0-RC.1` is released (2026-10-05). PR #10 merged as `0cd892d` and PR #8 as `36bc0fb`, both on `develop`. `release/1.0` was cut from `36bc0fb`; the orb's `publish_release_candidate` workflow succeeded, pushed `0b7bc98` ("Bump version to 1.0.0-RC.1 [skip ci]") to `release/1.0`, and published to Maven Central (jar, pom, sources, javadoc, signatures). A clean consumer resolves it with qqq-backend-core 4.1.0-RC.1 and opensearch-java 3.10.0.
|
||||
`1.0.0-RC.2` is released (2026-10-06). It fixes the five pre-GA findings from the PR #10 review, each on its own branch and PR into `develop`:
|
||||
|
||||
| Issue | PR | Merge commit | Fix |
|
||||
|---|---|---|---|
|
||||
| #15 | #18 | `2398e62` | Transport: warn on `hostnameVerification=false`, blank `${env.X}` treated as missing, userinfo in `opensearchUrl` rejected |
|
||||
| #17 | #21 | `e9ec54a` | `safeCount` logs at warn and keeps the previous `documentCount` |
|
||||
| #16 | #19 | `9941a70` | Search `offset` counts readable results under record locks; bounded scan; 10,000 window cap |
|
||||
| #14 | #20 | `dce0f26` | Deletes during a full reindex captured (`REBUILDING`, `AWAITING_REINDEX`) and applied after the alias swap |
|
||||
| #13 | #22 | `c5c41d4` | Listener re-read failure and missing AWS SDK (`LinkageError`) recorded as failed events |
|
||||
|
||||
`develop` (`c5c41d4`) was merged into `release/1.0` as `b7d366c` (only difference from develop: `<revision>1.0.0-RC.1</revision>`). The orb's `publish_release_candidate` workflow succeeded on `b7d366c` (CircleCI build 82) and pushed `c11b2c2` ("Bump version to 1.0.0-RC.2 [skip ci]"). Maven Central serves the jar, pom, sources, javadoc and signatures for `com.kingsrook.qbits:qbit-quick-search:1.0.0-RC.2`.
|
||||
|
||||
## Release mechanics (qqq-orb 0.6.8, confirmed in its source)
|
||||
|
||||
- RCs publish artifacts only: no git tag and no GitHub release. The GitHub release comes when the release branch merges to `main` and a `vX.Y.Z` tag is pushed.
|
||||
- Never push a `vX.Y.Z-RC.n` tag: it matches the GA job's `v*` filter, maps back to `release/X.Y.Z`, and would publish a stray `RC.n+1`.
|
||||
- The next push to `release/1.0` publishes `1.0.0-RC.2`.
|
||||
- The agent cannot run `gh pr merge` (auto-mode classifier); James merges. GitHub SSH times out from this machine; push over HTTPS with `git -c credential.helper='!gh auth git-credential' push https://github.com/QRun-IO/qbit-quick-search.git <ref>`.
|
||||
- The next push to `release/1.0` publishes `1.0.0-RC.3`. Keep the release branch's `<revision>` on merge.
|
||||
- `develop` requires one approving review and the PR author cannot approve their own PR; James merges with `gh pr merge <n> --squash --admin`. The agent cannot run `gh pr merge`, and the auto-mode classifier also refused a background loop that polled PR merge state.
|
||||
- GitHub SSH times out from this machine; push over HTTPS with `git -c credential.helper='!gh auth git-credential' push https://github.com/QRun-IO/qbit-quick-search.git <ref>`.
|
||||
|
||||
## Next
|
||||
|
||||
1. Pin `1.0.0-RC.1` in qqq-all and Voyage (see `docs/TODO.md`); their feedback decides RC.2 or GA.
|
||||
2. Work the "Before 1.0.0 GA" list in `docs/TODO.md` on feature branches into `develop`, then merge `develop` into `release/1.0` for RC.2.
|
||||
1. qqq-all and Voyage pin `1.0.0-RC.2` and report back; their feedback decides RC.3 or GA.
|
||||
2. Remaining "Before 1.0.0 GA" items in `docs/TODO.md`: stuck `REBUILDING` after a killed or overlapping full reindex (from the PR #20 review), and the `qbit-build-parent` 2.1.0 re-pin.
|
||||
3. GA: merge `release/1.0` to `main` and tag `v1.0.0`.
|
||||
+9
-6
@@ -2,7 +2,7 @@
|
||||
|
||||
## Before tagging 1.0.0
|
||||
|
||||
- [ ] qqq-all and Voyage adopt `1.0.0-RC.1` (items below) and report back.
|
||||
- [ ] qqq-all and Voyage adopt `1.0.0-RC.2` (items below) and report back.
|
||||
|
||||
- [x] PR #10 (`feature/GH-1-0-release-readiness`) opened and reviewed 2026-10-05; `AGENTS.md` kept.
|
||||
- [ ] Re-pin to `qbit-build-parent` 2.1.0 when qbit-bom publishes it, and remove the `activeByDefault` on the `qqq-snapshot` profile (ADR-0007).
|
||||
@@ -17,11 +17,14 @@
|
||||
|
||||
## Before 1.0.0 GA (from the PR #10 review, 2026-10-05; filed as #13 to #17 under #12)
|
||||
|
||||
- [ ] Listener: if the in-transaction re-read in `fetchCurrentRecords` throws, record a `quickSearchFailedEvent` row from the event's primary keys instead of dropping the update (`QuickSearchRecordChangeListener`).
|
||||
- [ ] Full reindex: deletes that arrive during a rebuild are not replayed into the new index; capture them (failed-event or a delete log) and apply after the swap.
|
||||
- [ ] Transport: warn when `tls.hostnameVerification=false`; treat an empty `${env.X}` value as missing and name the field; reject userinfo in `opensearchUrl`.
|
||||
- [ ] Search: with record locks on, `offset` counts raw hits, so pages can overlap; per-table mode ignores `offset`; cap `fetchSize` at the 10,000 window.
|
||||
- [ ] `safeCount` in `BasepullIndexStep` swallows exceptions silently; log at warn.
|
||||
- [x] #13 Listener: a failed in-transaction re-read or a missing AWS SDK (`LinkageError`) records `quickSearchFailedEvent` rows (PR #22).
|
||||
- [x] #14 Full reindex: deletes made during a rebuild are captured (`REBUILDING`, `AWAITING_REINDEX`) and applied after the alias swap (PR #20).
|
||||
- [x] #15 Transport: warn on `tls.hostnameVerification=false`, empty `${env.X}` treated as missing, userinfo in `opensearchUrl` rejected (PR #18).
|
||||
- [x] #16 Search: `offset` counts readable results under record locks, per-table `offset`, bounded scan and 10,000 window cap (PR #19).
|
||||
- [x] #17 `safeCount` logs at warn and keeps the previous `documentCount` (PR #21).
|
||||
- [x] `1.0.0-RC.2` published to Maven Central from `release/1.0` (2026-10-06).
|
||||
- [ ] Stuck `REBUILDING` after a killed or overlapping full reindex (from the PR #20 review): only another all-tables reindex clears it, `AWAITING_REINDEX` rows accumulate and drift detection stays off for those tables. No data is lost. Fix needs a liveness check on the `FULL_REINDEX` run record before reconcile may reset the status, plus refusing a second concurrent full reindex.
|
||||
- [ ] Drift detection still writes `NEEDS_REINDEX` into `status`, so a few-millisecond window can overwrite `REBUILDING`; the durable fix is a separate drift field (schema change, so 1.1 at the earliest).
|
||||
|
||||
## Later (1.x)
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<properties>
|
||||
<revision>1.0.0-RC.2</revision>
|
||||
<revision>1.0.0-SNAPSHOT</revision>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<opensearch.version>3.10.0</opensearch.version>
|
||||
|
||||
+17
-10
@@ -51,14 +51,20 @@ import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
** Real-time indexing hook. Registered once per QInstance; applies to the
|
||||
** tables this QBit indexes.
|
||||
**
|
||||
** Events are built while the write's transaction is open (so updates can be
|
||||
** re-read in full), but published only after that transaction commits, via
|
||||
** Events are built while the write's transaction is open, but published only
|
||||
** after that transaction commits, via
|
||||
** QBackendTransaction.addAfterCommitCallback. A write without a transaction
|
||||
** is already committed, so it publishes immediately. A rolled-back write never
|
||||
** reaches the index.
|
||||
**
|
||||
** This listener never fails the host's write. When the re-read of updated
|
||||
** records or publishing fails (including a LinkageError from a missing
|
||||
** Inserted and updated records are re-read inside that transaction, with
|
||||
** display values and possible-value labels, so a real-time document matches
|
||||
** the one basepull, reconcile and full reindex build. The event's own records
|
||||
** are not enough: for an update they are the sparse input, and for an insert
|
||||
** they carry only raw values.
|
||||
**
|
||||
** This listener never fails the host's write. When the re-read of inserted or
|
||||
** updated records or publishing fails (including a LinkageError from a missing
|
||||
** optional library while the client is built), each affected (table, record,
|
||||
** action) is written to quickSearchFailedEvent for the scheduled basepull to
|
||||
** replay. A failed re-read is recorded after the commit too, so a rolled-back
|
||||
@@ -115,8 +121,7 @@ public class QuickSearchRecordChangeListener implements RecordChangeListenerInte
|
||||
|
||||
switch(event.getType())
|
||||
{
|
||||
case INSERT -> indexEvents.addAll(toIndexEvents(tableName, primaryKeyField, event.getRecords()));
|
||||
case UPDATE ->
|
||||
case INSERT, UPDATE ->
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -127,10 +132,11 @@ public class QuickSearchRecordChangeListener implements RecordChangeListenerInte
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// log now: with a transaction, nothing else is logged until commit, //
|
||||
// and a rollback (a transaction the failed re-read aborted, too) //
|
||||
// would leave no trace. Then record the updated keys after commit, //
|
||||
// would leave no trace. Then record the written keys after commit, //
|
||||
// so basepull re-reads and indexes them. //
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
LOG.warn("Could not re-read updated records for indexing; recording them for replay once the write commits", e,
|
||||
LOG.warn("Could not re-read " + (event.getType() == RecordChangeType.INSERT ? "inserted" : "updated")
|
||||
+ " records for indexing; recording them for replay once the write commits", e,
|
||||
logPair("tableName", tableName), logPair("recordCount", event.getRecords().size()));
|
||||
List<IndexEvent> failedEvents = toIndexEvents(tableName, primaryKeyField, event.getRecords());
|
||||
runAfterCommit(event, () -> recordFailures(runtime, failedEvents, e));
|
||||
@@ -444,8 +450,9 @@ public class QuickSearchRecordChangeListener implements RecordChangeListenerInte
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Updated records are "as the backend returned them", which for RDBMS is the
|
||||
** sparse input. Re-read the full rows inside the same transaction, with
|
||||
** The event's records are "as the backend returned them": for an update on
|
||||
** RDBMS that is the sparse input, and for an insert the raw values with no
|
||||
** display values. Re-read the full rows inside the same transaction, with
|
||||
** display values so possible-value labels are indexed (which can run host
|
||||
** possible-value providers).
|
||||
*******************************************************************************/
|
||||
|
||||
@@ -39,6 +39,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule;
|
||||
@@ -64,7 +65,8 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/*******************************************************************************
|
||||
** Integration tests, against a real OpenSearch, that real-time indexing keeps
|
||||
** the index in step with the source table through updates and deletes, and
|
||||
** the index in step with the source table through inserts (with display
|
||||
** values), updates and deletes, and
|
||||
** that the reconcile process repairs an index that has drifted.
|
||||
**
|
||||
** Each test uses its own record IDs and search terms, so tests are independent.
|
||||
@@ -75,6 +77,7 @@ class IndexDriftIntegrationTest
|
||||
{
|
||||
private static final String BACKEND_NAME = "driftMemoryBackend";
|
||||
private static final String TABLE_NAME = "driftCustomer";
|
||||
private static final String REGION_TABLE = "driftRegion";
|
||||
private static final String INDEX_NAME = "quick_search_drift_test";
|
||||
|
||||
@Container
|
||||
@@ -98,6 +101,9 @@ class IndexDriftIntegrationTest
|
||||
|
||||
@QuickSearchField
|
||||
private String city;
|
||||
|
||||
@QuickSearchField
|
||||
private Integer regionId;
|
||||
}
|
||||
|
||||
|
||||
@@ -124,7 +130,19 @@ class IndexDriftIntegrationTest
|
||||
.withPrimaryKeyField("id")
|
||||
.withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
|
||||
.withField(new QFieldMetaData("name", QFieldType.STRING))
|
||||
.withField(new QFieldMetaData("city", QFieldType.STRING)));
|
||||
.withField(new QFieldMetaData("city", QFieldType.STRING))
|
||||
.withField(new QFieldMetaData("regionId", QFieldType.INTEGER).withPossibleValueSourceName(REGION_TABLE))
|
||||
.withRecordLabelFormat("%s - %s")
|
||||
.withRecordLabelFields("name", "regionId"));
|
||||
qInstance.addTable(new QTableMetaData()
|
||||
.withName(REGION_TABLE)
|
||||
.withBackendName(BACKEND_NAME)
|
||||
.withPrimaryKeyField("id")
|
||||
.withRecordLabelFormat("%s")
|
||||
.withRecordLabelFields("name")
|
||||
.withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
|
||||
.withField(new QFieldMetaData("name", QFieldType.STRING)));
|
||||
qInstance.addPossibleValueSource(QPossibleValueSource.newForTable(REGION_TABLE));
|
||||
|
||||
QuickSearchQBitConfig config = new QuickSearchQBitConfig()
|
||||
.withBackendName(BACKEND_NAME)
|
||||
@@ -154,6 +172,33 @@ class IndexDriftIntegrationTest
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test: a just-inserted record's document carries display values: the
|
||||
** possible-value label of a foreign key is searchable and is in the record
|
||||
** label, with no basepull in between (#28).
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testInsert_documentCarriesDisplayValuesAndPossibleValueLabel() throws Exception
|
||||
{
|
||||
InsertInput regionInsert = new InsertInput();
|
||||
regionInsert.setTableName(REGION_TABLE);
|
||||
regionInsert.setRecords(List.of(new QRecord().withValue("id", 7).withValue("name", "Northwind Territory")));
|
||||
new InsertAction().execute(regionInsert);
|
||||
|
||||
insert(new QRecord().withValue("id", 31).withValue("name", "Rhoda").withValue("city", "Labelburg").withValue("regionId", 7));
|
||||
|
||||
client().refreshIndex();
|
||||
List<QuickSearchResult> results = new QuickSearchAction().execute(new QuickSearchInput().withSearchTerm("northwind").withTableName(TABLE_NAME)).getResults();
|
||||
assertThat(results).as("the region's label, not its key, is indexed").extracting(QuickSearchResult::getRecordId).containsExactly("31");
|
||||
assertThat(results.get(0).getRecordLabel()).isEqualTo("Rhoda - Northwind Territory");
|
||||
|
||||
Map<String, Object> fieldValues = client().search("labelburg", List.of(TABLE_NAME), 10, 0, QuickSearchQBitContext.getDiscoveredTables())
|
||||
.hits().hits().get(0).source().getFieldValues();
|
||||
assertThat(fieldValues).containsEntry("regionId", "Northwind Territory");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test: a partial update (one field) keeps the other fields searchable.
|
||||
*******************************************************************************/
|
||||
|
||||
+106
-1
@@ -37,7 +37,11 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qbits.quicksearch.BaseQuickSearchTest;
|
||||
import com.kingsrook.qbits.quicksearch.MissingLibraryTransportCustomizer;
|
||||
import com.kingsrook.qbits.quicksearch.QuickSearchQBitConfig;
|
||||
@@ -74,6 +78,8 @@ import static org.mockito.Mockito.when;
|
||||
*******************************************************************************/
|
||||
class QuickSearchRecordChangeListenerTest extends BaseQuickSearchTest
|
||||
{
|
||||
private static final String COMPANY_TABLE = "company";
|
||||
|
||||
private IndexEventPublisher publisher;
|
||||
private QuickSearchRuntime runtime;
|
||||
|
||||
@@ -100,6 +106,25 @@ class QuickSearchRecordChangeListenerTest extends BaseQuickSearchTest
|
||||
config.setRuntime(runtime);
|
||||
|
||||
QInstance qInstance = QContext.getQInstance();
|
||||
|
||||
///////////////////////////////////////////////////////////////////
|
||||
// a foreign key to a company table, through a table PVS, and a //
|
||||
// record label over it, so display values can be asserted //
|
||||
///////////////////////////////////////////////////////////////////
|
||||
qInstance.addTable(new QTableMetaData()
|
||||
.withName(COMPANY_TABLE)
|
||||
.withBackendName(TEST_BACKEND_NAME)
|
||||
.withPrimaryKeyField("id")
|
||||
.withRecordLabelFormat("%s")
|
||||
.withRecordLabelFields("name")
|
||||
.withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
|
||||
.withField(new QFieldMetaData("name", QFieldType.STRING)));
|
||||
qInstance.addPossibleValueSource(QPossibleValueSource.newForTable(COMPANY_TABLE));
|
||||
qInstance.getTable(TEST_ENTITY_TABLE)
|
||||
.withField(new QFieldMetaData("companyId", QFieldType.INTEGER).withPossibleValueSourceName(COMPANY_TABLE))
|
||||
.withRecordLabelFormat("%s - %s")
|
||||
.withRecordLabelFields("name", "companyId");
|
||||
|
||||
qInstance.addQBit(new QBitMetaData().withGroupId("com.kingsrook.qbits").withArtifactId("quick-search").withVersion("test").withConfig(config));
|
||||
qInstance.withRecordChangeListener(new QCodeReference(QuickSearchRecordChangeListener.class));
|
||||
}
|
||||
@@ -145,6 +170,79 @@ class QuickSearchRecordChangeListenerTest extends BaseQuickSearchTest
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
void testInsert_indexesDisplayValuesAndPossibleValueLabels() throws QException
|
||||
{
|
||||
InsertInput companyInsert = new InsertInput();
|
||||
companyInsert.setTableName(COMPANY_TABLE);
|
||||
companyInsert.setRecords(List.of(new QRecord().withValue("name", "Acme Corp")));
|
||||
Integer companyId = new InsertAction().execute(companyInsert).getRecords().get(0).getValueInteger("id");
|
||||
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName(TEST_ENTITY_TABLE);
|
||||
insertInput.setRecords(List.of(new QRecord().withValue("name", "widget").withValue("description", "d").withValue("companyId", companyId)));
|
||||
new InsertAction().execute(insertInput);
|
||||
|
||||
ArgumentCaptor<List<IndexEvent>> captor = ArgumentCaptor.forClass(List.class);
|
||||
verify(publisher, times(1)).publishIndexEvents(captor.capture());
|
||||
QRecord indexed = captor.getValue().get(0).getRecord();
|
||||
assertThat(indexed.getValueInteger("companyId")).isEqualTo(companyId);
|
||||
assertThat(indexed.getDisplayValue("companyId")).as("the PVS label, not the raw key").isEqualTo("Acme Corp");
|
||||
assertThat(indexed.getRecordLabel()).isEqualTo("widget - Acme Corp");
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
void testInsert_reReadFails_recordsFailedEventsAfterCommit() throws QException
|
||||
{
|
||||
QBackendTransaction transaction = new QBackendTransaction();
|
||||
try(MockedConstruction<QueryAction> ignored = failingQueries(new QException("possible-value backend unavailable")))
|
||||
{
|
||||
assertThatCode(() -> new QuickSearchRecordChangeListener().onRecordsChanged(changeEvent(RecordChangeType.INSERT, transaction, 7, 8))).doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
assertThat(queryFailedEvents()).isEmpty();
|
||||
transaction.commit();
|
||||
|
||||
List<QRecord> failed = queryFailedEvents();
|
||||
assertThat(failed).extracting(r -> r.getValueString("recordId")).containsExactlyInAnyOrder("7", "8");
|
||||
assertThat(failed).allSatisfy(r ->
|
||||
{
|
||||
assertThat(r.getValueString("action")).isEqualTo("INDEX");
|
||||
assertThat(r.getValueString("status")).isEqualTo(QuickSearchFailedEvent.STATUS_PENDING);
|
||||
assertThat(r.getValueString("errorMessage")).contains("possible-value backend unavailable");
|
||||
});
|
||||
verify(publisher, never()).publishIndexEvents(anyList());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
void testInsert_reReadFailsThenRollback_logsCauseAndRecordsNothing() throws QException
|
||||
{
|
||||
QCollectingLogger collectingLogger = QLogger.activateCollectingLoggerForClass(QuickSearchRecordChangeListener.class);
|
||||
QBackendTransaction transaction = new QBackendTransaction();
|
||||
try(MockedConstruction<QueryAction> ignored = failingQueries(new NoClassDefFoundError("com/example/pvs/Provider")))
|
||||
{
|
||||
new QuickSearchRecordChangeListener().onRecordsChanged(changeEvent(RecordChangeType.INSERT, transaction, 7));
|
||||
}
|
||||
finally
|
||||
{
|
||||
QLogger.deactivateCollectingLoggerForClass(QuickSearchRecordChangeListener.class);
|
||||
}
|
||||
transaction.rollback();
|
||||
|
||||
assertThat(queryFailedEvents()).isEmpty();
|
||||
assertThat(collectingLogger.getCollectedMessages()).anySatisfy(m ->
|
||||
{
|
||||
assertThat(m.getLevel()).isEqualTo(Level.WARN);
|
||||
assertThat(m.getMessage()).contains("Could not re-read inserted records").contains(TEST_ENTITY_TABLE).contains("com/example/pvs/Provider");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
void testInsert_withTransaction_publishesOnlyAfterCommit() throws QException
|
||||
{
|
||||
@@ -470,13 +568,20 @@ class QuickSearchRecordChangeListenerTest extends BaseQuickSearchTest
|
||||
|
||||
|
||||
private RecordChangeEvent updateEvent(QBackendTransaction transaction, Integer... ids)
|
||||
{
|
||||
return (changeEvent(RecordChangeType.UPDATE, transaction, ids));
|
||||
}
|
||||
|
||||
|
||||
|
||||
private RecordChangeEvent changeEvent(RecordChangeType type, QBackendTransaction transaction, Integer... ids)
|
||||
{
|
||||
List<QRecord> records = new ArrayList<>();
|
||||
for(Integer id : ids)
|
||||
{
|
||||
records.add(new QRecord().withValue("id", id).withValue("description", "changed"));
|
||||
}
|
||||
return (new RecordChangeEvent().withTableName(TEST_ENTITY_TABLE).withType(RecordChangeType.UPDATE).withRecords(records).withTransaction(transaction));
|
||||
return (new RecordChangeEvent().withTableName(TEST_ENTITY_TABLE).withType(type).withRecords(records).withTransaction(transaction));
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user