Skip to content

MVA and Float Vector UDF support#3865

Open
sanikolaev wants to merge 10 commits intomasterfrom
udf_mva
Open

MVA and Float Vector UDF support#3865
sanikolaev wants to merge 10 commits intomasterfrom
udf_mva

Conversation

@sanikolaev
Copy link
Copy Markdown
Collaborator

No description provided.

@sanikolaev sanikolaev added pack To trigger CI to build packages for PR and removed pack To trigger CI to build packages for PR labels Oct 29, 2025
@sanikolaev sanikolaev marked this pull request as ready for review October 29, 2025 14:54
@manticoresoftware manticoresoftware deleted a comment from github-actions Bot Oct 29, 2025
@manticoresoftware manticoresoftware deleted a comment from github-actions Bot Oct 29, 2025
@manticoresoftware manticoresoftware deleted a comment from github-actions Bot Oct 29, 2025
@manticoresoftware manticoresoftware deleted a comment from github-actions Bot Oct 29, 2025
@manticoresoftware manticoresoftware deleted a comment from github-actions Bot Oct 29, 2025
@manticoresoftware manticoresoftware deleted a comment from github-actions Bot Oct 29, 2025
@manticoresoftware manticoresoftware deleted a comment from github-actions Bot Oct 29, 2025
@manticoresoftware manticoresoftware deleted a comment from github-actions Bot Oct 29, 2025
@manticoresoftware manticoresoftware deleted a comment from github-actions Bot Oct 29, 2025
@manticoresoftware manticoresoftware deleted a comment from github-actions Bot Oct 29, 2025
@manticoresoftware manticoresoftware deleted a comment from github-actions Bot Oct 29, 2025
@manticoresoftware manticoresoftware deleted a comment from github-actions Bot Oct 29, 2025
@manticoresoftware manticoresoftware deleted a comment from github-actions Bot Oct 29, 2025
@manticoresoftware manticoresoftware deleted a comment from github-actions Bot Oct 29, 2025
@manticoresoftware manticoresoftware deleted a comment from github-actions Bot Oct 29, 2025
@manticoresoftware manticoresoftware deleted a comment from github-actions Bot Oct 29, 2025
@manticoresoftware manticoresoftware deleted a comment from github-actions Bot Oct 29, 2025
@manticoresoftware manticoresoftware deleted a comment from github-actions Bot Oct 29, 2025
@manticoresoftware manticoresoftware deleted a comment from github-actions Bot Oct 29, 2025
@manticoresoftware manticoresoftware deleted a comment from github-actions Bot Oct 29, 2025
@manticoresoftware manticoresoftware deleted a comment from github-actions Bot Oct 29, 2025
@manticoresoftware manticoresoftware deleted a comment from github-actions Bot Oct 29, 2025
@manticoresoftware manticoresoftware deleted a comment from github-actions Bot Oct 29, 2025
@sanikolaev
Copy link
Copy Markdown
Collaborator Author

Heres's an example of how the new functionality works:

git checkout udf_mva
docker run -v $(pwd):/manticore -it --rm ghcr.io/manticoresoftware/manticoresearch:test-kit-udf_mva bash
apt -y install gcc
cd /manticore/src/
gcc -fPIC -shared -o udfexample.so udfexample.c
cp udfexample.so /usr/local/lib/manticore/
searchd

mysql -v -P9306 -h0 -e "CREATE FUNCTION makemva RETURNS MULTI SONAME 'udfexample.so'"
mysql -v -P9306 -h0 -e "CREATE FUNCTION makemva64 RETURNS MULTI64 SONAME 'udfexample.so'"
mysql -v -P9306 -h0 -e "CREATE FUNCTION makefloatvec RETURNS FLOAT_VECTOR SONAME 'udfexample.so'"

manticore-load \
--batch-size=1000 \
--threads=5 \
--total=1000000 \
--init="CREATE TABLE test(id bigint, name text, type int)" \
--load="INSERT INTO test(id,name,type) VALUES(<increment>,'<text/10/100>',<int/1/100>)"

Result:

root@28d6cf82d3e8:/manticore/src# mysql -v -P9306 -h0 -e "SELECT id, makemva64() as mva64_values FROM test"
mysql -v -P9306 -h0 -e "SELECT id, makefloatvec() as float_vec FROM test limit 3\G"
mysql -v -P9306 -h0 -e "SELECT id, type FROM test facet makemva()"
--------------
SELECT id, makemva64() as mva64_values FROM test
--------------

+--------+-------------------------------------------------------------------------------------------+
| id     | mva64_values                                                                              |
+--------+-------------------------------------------------------------------------------------------+
| 983001 | 6879,6201,2735,9183,9587,6641,1279,6224,3610,8417,8739,9100                               |
| 720001 | 7437,7501,5864,2819,6472,8889,3192,1540,5297,1395,5730,1659,8895,6124,2967                |
| 920001 | 8669,6130,6919,1129                                                                       |
| 342001 | 7452,3033,1293                                                                            |
|  67001 | 9689,6074,5482,5165,5971,3540,6329,2183,405,8971,3129,2484                                |
| 279001 | 3131                                                                                      |
| 669001 | 3345,2273,4364,7882,4972,8391,796,6855,4672,5712,8179,3722,1362                           |
| 405001 | 9854                                                                                      |
| 572001 | 9219,7974,5574,9491,890,1402,3087,8027,9818,593,7396,9976,4479,6834,2641,5,3835           |
|  36001 | 2717,2910,1188,1462,6568,766,3330,338,6546,1865,2642,2628,150,222,740,9738,7222,9858,8283 |
| 279002 | 9026,3669                                                                                 |
|  67002 | 5251,1953,3453,6536,5232,7720,9913,7312,7738,8875,3941,3903,7857                          |
| 983002 | 1806,5751,5971,3589,9164,2305,3387,7555,2096,286,618,7139,2515                            |
| 720002 | 7641,2311,1198,9703,7615,8692,4120,4028,7560,5636,1854,6092                               |
|  36002 | 9295,4405,3006,3833,6740,5490,8758,9844,7208,3828,7983,3261,4585,6631,1434,1896,4029      |
| 405002 | 4485,1308,3582                                                                            |
| 342002 | 4366,9573,7280,1485,3501,3352,4666,7925,232,4756,8948,4506,6369,2703,4976,1989            |
| 572002 | 7253,2741,6571,7264,3924,724,7209                                                         |
| 669002 | 9790,7935                                                                                 |
| 920002 | 9822,5892,392,6070,1296,7420,3988,6254,3310,7145,662,6306                                 |
+--------+-------------------------------------------------------------------------------------------+
--------------
SELECT id, makefloatvec() as float_vec FROM test limit 3
--------------

*************************** 1. row ***************************
       id: 983001
float_vec: 99.500000,34.299999,5.700000,26.700001,19.600000,27.799999,67.099998,36.500000,91.699997,19.799999,7.200000,84.300003,28.600000,42.799999,52.099998,18.799999,30.500000,75.500000,45.500000,4.300000,97.699997,52.200001,33.299999,54.500000,11.300000,9.500000,26.000000,98.599998,62.700001,36.099998,86.900002,97.400002,5.700000,92.599998,24.100000,25.299999,55.700001,26.400000,61.900002,47.400002,81.400002,4.300000,31.799999,45.200001,82.400002,19.100000,64.099998,12.900000,29.799999,44.799999,17.200001,27.500000,97.099998,85.699997,82.099998,43.599998,95.300003,43.299999,42.200001,93.199997,79.400002,29.200001,90.699997,20.299999,57.000000,50.000000,45.700001,12.700000,11.700000,42.799999,95.400002,93.099998,82.300003,62.400002,38.400002,64.699997,81.500000,37.700001,77.699997,11.300000,82.500000,30.100000,74.000000,14.800000,15.900000,56.099998,58.500000,46.400002,34.599998,35.900002,39.599998,49.299999,0.300000,65.500000,69.599998,57.400002,15.600000,15.300000,5.300000,27.299999,93.300003,35.900002,20.400000,75.699997,98.300003,94.000000,40.400002,15.000000,31.700001,18.100000,61.500000,14.300000,48.299999,35.599998,64.300003,99.400002,26.900000,22.799999,45.799999,61.599998,94.000000,20.600000,46.099998,94.300003,86.199997,15.700000,86.900002,37.000000
*************************** 2. row ***************************
       id: 920001
float_vec: 57.500000,56.599998,47.900002,21.900000,79.599998,66.400002,82.199997,84.800003,99.599998,97.500000,18.100000,18.100000,57.599998,33.799999,54.400002,3.400000,65.900002,68.599998,74.099998,97.099998,63.200001,59.000000,76.599998,75.599998,58.200001,43.799999,39.400002,24.700001,15.600000,89.800003,46.099998,8.300000,46.400002,29.200001,30.299999,61.299999,95.599998,47.700001,81.300003,95.199997,45.200001,34.700001,13.400000,2.800000,68.500000,3.000000,41.400002,34.400002,71.599998,50.799999,66.699997,34.900002,9.800000,43.299999,10.500000,3.200000,22.299999,49.900002,27.900000,38.000000,74.900002,74.000000,46.299999,56.599998,3.200000,11.800000,53.099998,34.000000,59.500000,34.400002,29.299999,40.000000,69.099998,22.700001,63.200001,76.699997,49.299999,55.799999,11.600000,17.600000,59.400002,61.400002,92.800003,22.600000,95.300003,98.500000,78.199997,93.400002,12.500000,1.700000,73.000000,57.200001,91.699997,49.700001,14.300000,17.500000,50.400002,5.100000,28.700001,32.400002,66.500000,4.500000,18.600000,43.099998,29.299999,26.000000,48.599998,30.400000,48.500000,32.599998,72.800003,36.799999,72.699997,61.599998,67.199997,36.400002,17.799999,50.599998,57.900002,3.200000,18.200001,6.500000,42.200001,83.599998,46.900002,90.300003,67.800003,57.599998
*************************** 3. row ***************************
       id: 720001
float_vec: 32.500000,77.000000,78.599998,20.700001,53.900002,38.299999,7.000000,52.000000,85.400002,29.600000,13.400000,21.100000,44.799999,81.300003,57.900002,34.799999,8.800000,68.000000,68.699997,22.400000,24.400000,49.599998,2.100000,47.700001,97.099998,95.699997,72.300003,73.300003,44.599998,82.300003,10.100000,77.199997,59.400002,23.900000,97.900002,48.500000,62.200001,40.099998,0.500000,82.800003,5.000000,49.099998,4.000000,49.799999,65.699997,61.900002,84.599998,9.700000,29.900000,88.599998,32.099998,78.800003,5.600000,59.900002,5.500000,98.699997,65.599998,4.700000,51.200001,10.800000,79.199997,69.199997,86.199997,31.400000,98.699997,85.599998,48.000000,86.099998,43.900002,27.900000,8.200000,8.000000,33.099998,58.900002,78.699997,23.299999,32.299999,8.500000,4.800000,23.900000,56.900002,25.700001,45.299999,1.500000,15.300000,4.600000,1.700000,7.600000,93.900002,52.099998,41.400002,67.699997,62.099998,71.900002,61.700001,62.099998,71.300003,70.300003,22.200001,28.700001,86.500000,46.900002,62.099998,8.700000,13.400000,93.400002,66.199997,53.400002,78.800003,70.500000,17.400000,94.300003,20.700001,38.900002,60.299999,53.900002,31.100000,60.700001,73.400002,52.400002,12.900000,24.100000,24.799999,26.200001,67.000000,60.500000,14.200000,13.000000
--------------
SELECT id, type FROM test facet makemva()
--------------

+--------+------+
| id     | type |
+--------+------+
| 983001 |    7 |
| 279001 |    9 |
| 920001 |   80 |
| 405001 |   85 |
| 572001 |   14 |
| 669001 |   93 |
| 342001 |   51 |
|  36001 |   23 |
|  67001 |   60 |
| 720001 |   10 |
| 572002 |   64 |
| 983002 |   33 |
| 669002 |   84 |
| 405002 |   16 |
| 279002 |   48 |
| 342002 |    9 |
| 920002 |   80 |
| 720002 |   77 |
|  36002 |   18 |
|  67002 |   55 |
+--------+------+
+-----------+----------+
| makemva() | count(*) |
+-----------+----------+
|       100 |    10600 |
|       207 |    10512 |
|       699 |    10506 |
|       827 |    10658 |
|       356 |    10441 |
|        54 |    10263 |
|       553 |    10539 |
|       754 |    10501 |
|       153 |    10451 |
|       655 |    10454 |
|       193 |    10535 |
|        97 |    10508 |
|       422 |    10578 |
|       145 |    10571 |
|       206 |    10551 |
|       781 |    10394 |
|        47 |    10362 |
|       238 |    10561 |
|       460 |    10460 |
|       226 |    10468 |
+-----------+----------+

@sanikolaev sanikolaev requested a review from tomatolog October 29, 2025 15:32
Copy link
Copy Markdown
Contributor

@tomatolog tomatolog left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it could be better to add ubertest or CLT test there to check that

  • master could receive and handle that data at result sets from agents
  • master could merge result sets from local indexes

to make sure the new functionality works for distributed cases

ie
the result sets from agents

create table d_agent2 type='distributed' agent='agent0:t1' agent='agent1:t2';
SELECT id, makemva64() as mva64_values FROM d_agent2;

merge of the result sets from local

create table d_loc2 type='distributed' local='t1' local='t2';
SELECT id, makemva64() as mva64_values FROM d_loc2;

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Nov 5, 2025

Linux debug test results

1 135 tests   1 081 ✅  47m 8s ⏱️
    1 suites     54 💤
    1 files        0 ❌

Results for commit 9dc1640.

♻️ This comment has been updated with latest results.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Nov 5, 2025

Windows test results

    3 files      3 suites   26m 9s ⏱️
1 112 tests 1 058 ✅ 54 💤 0 ❌
1 114 runs  1 060 ✅ 54 💤 0 ❌

Results for commit 1a5edff.

♻️ This comment has been updated with latest results.

@sanikolaev
Copy link
Copy Markdown
Collaborator Author

it could be better to add ubertest or CLT test there to check that
master could receive and handle that data at result sets from agents
master could merge result sets from local indexes

Done. There was also a bug — GREATEST() and LEAST() didn't work correctly with multi or multi64 sets returned from UDFs. That's fixed now.

@sanikolaev sanikolaev requested a review from tomatolog November 5, 2025 14:03
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Nov 5, 2025

clt

❌ CLT tests in test/clt-tests/bugs/
✅ OK: 8
❌ Failed: 1
⏳ Duration: 559s
👉 Check Action Results for commit a5f1838

Failed tests:

🔧 Edit failed tests in UI:

test/clt-tests/bugs/3847-conflict-handling-verification.rec
––– input –––
set -b +m
––– output –––
OK
––– input –––
grep -q 'threads = 4' test/clt-tests/base/searchd-with-flexible-ports.conf || sed -i '/searchd {/a\	threads = 4' test/clt-tests/base/searchd-with-flexible-ports.conf
––– output –––
OK
––– input –––
export INSTANCE=1
––– output –––
OK
––– input –––
mkdir -p /var/{run,lib,log}/manticore-${INSTANCE}
––– output –––
OK
––– input –––
stdbuf -oL searchd -c test/clt-tests/base/searchd-with-flexible-ports.conf > /dev/null
––– output –––
OK
––– input –––
if timeout 10 grep -qm1 '\[BUDDY\] started' <(tail -n 1000 -f /var/log/manticore-${INSTANCE}/searchd.log); then echo 'Buddy started!'; else echo 'Timeout or failed!'; cat /var/log/manticore-${INSTANCE}/searchd.log; fi
––– output –––
OK
––– input –––
export INSTANCE=2
––– output –––
OK
––– input –––
mkdir -p /var/{run,lib,log}/manticore-${INSTANCE}
––– output –––
OK
––– input –––
stdbuf -oL searchd -c test/clt-tests/base/searchd-with-flexible-ports.conf > /dev/null
––– output –––
OK
––– input –––
if timeout 10 grep -qm1 '\[BUDDY\] started' <(tail -n 1000 -f /var/log/manticore-${INSTANCE}/searchd.log); then echo 'Buddy started!'; else echo 'Timeout or failed!'; cat /var/log/manticore-${INSTANCE}/searchd.log; fi
––– output –––
OK
––– input –––
export INSTANCE=3
––– output –––
OK
––– input –––
mkdir -p /var/{run,lib,log}/manticore-${INSTANCE}
––– output –––
OK
––– input –––
stdbuf -oL searchd -c test/clt-tests/base/searchd-with-flexible-ports.conf > /dev/null
––– output –––
OK
––– input –––
if timeout 10 grep -qm1 '\[BUDDY\] started' <(tail -n 1000 -f /var/log/manticore-${INSTANCE}/searchd.log); then echo 'Buddy started!'; else echo 'Timeout or failed!'; cat /var/log/manticore-${INSTANCE}/searchd.log; fi
––– output –––
OK
––– input –––
mkdir /var/{lib,log}/manticore-{1,2,3}/test
––– output –––
OK
––– input –––
mysql -h0 -P1306 -e "CREATE CLUSTER test 'test' as path"; echo $?
––– output –––
OK
––– input –––
mysql -h0 -P2306 -e "JOIN CLUSTER test at '127.0.0.1:1312' 'test' as path"; echo $?
––– output –––
OK
––– input –––
mysql -h0 -P3306 -e "JOIN CLUSTER test at '127.0.0.1:1312' 'test' as path"; echo $?
––– output –––
OK
––– input –––
mysql -h0 -P1306 -e "CREATE TABLE tbl1 (id bigint, attr1 int)"; echo $?
––– output –––
OK
––– input –––
mysql -h0 -P1306 -e "ALTER CLUSTER test ADD tbl1"; echo $?
––– output –––
OK
––– input –––
mysql -h0 -P1306 -e "INSERT INTO test:tbl1 (id, attr1) VALUES (1,1), (3,2), (10,3), (11,4), (12,5), (13,6), (14,7), (15,8), (20,9)"; echo $?
––– output –––
OK
––– input –––
mysql -h0 -P1306 -NB -e "SELECT COUNT(*) FROM test:tbl1\G"
––– output –––
OK
––– input –––
mysql -h0 -P2306 -NB -e "SELECT COUNT(*) FROM test:tbl1\G"
––– output –––
OK
––– input –––
mysql -h0 -P3306 -NB -e "SELECT COUNT(*) FROM test:tbl1\G"
––– output –––
OK
––– input –––
manticore-load --host=127.0.0.1 --threads=4 --port=1306 --query="REPLACE INTO test:tbl1 (id, attr1) VALUES (%RAND, %RAND)" --host=127.0.0.1 --port=2306 --together > /dev/null 2>&1 & LOAD_PID=$!; sleep 1; echo "Load started: $LOAD_PID"
––– output –––
OK
––– input –––
mysql -h0 -P2306 -e "UPDATE test:tbl1 SET attr1=1 WHERE id=13" & mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (10, 999)" & wait
––– output –––
OK
––– input –––
mysql -h0 -P2306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (11, 111)" & mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (10, 101)" & wait
––– output –––
OK
––– input –––
mysql -h0 -P2306 -e "DELETE FROM test:tbl1 WHERE id=3" & mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (10, 102)" & wait
––– output –––
OK
––– input –––
mysql -h0 -P2306 -e "INSERT INTO test:tbl1 (id, attr1) VALUES (100, 1)" & mysql -h0 -P1306 -e "INSERT INTO test:tbl1 (id, attr1) VALUES (200, 2)" & wait
––– output –––
OK
––– input –––
mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (13, 6)"; sleep 2
––– output –––
OK
––– input –––
conflicts=0; for i in {1..30}; do result=$( (mysql -h0 -P2306 -e "UPDATE test:tbl1 SET attr1=1 WHERE id=13" & mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (13, 999)" & wait) 2>&1 ); if echo "$result" | grep -q "error at PostRollback"; then ((conflicts++)); fi; done; echo "Conflicts: $conflicts/30"; test $conflicts -ge 1 && echo "PASS" || echo "FAIL"
––– output –––
OK
––– input –––
conflicts=0; for i in {1..30}; do result=$( (mysql -h0 -P2306 -e "UPDATE test:tbl1 SET attr1=1 WHERE id>13" & mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (14, 888)" & wait) 2>&1 ); if echo "$result" | grep -q "error at PostRollback"; then ((conflicts++)); fi; done; echo "Conflicts: $conflicts/30"; test $conflicts -ge 1 && echo "PASS" || echo "FAIL"
––– output –––
OK
––– input –––
conflicts=0; for i in {1..30}; do mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (3, 2)" > /dev/null 2>&1; sleep 3; result=$( (mysql -h0 -P2306 -e "UPDATE test:tbl1 SET attr1=1 WHERE attr1=2" & mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (3, 333)" & wait) 2>&1 ); if echo "$result" | grep -q "error at PostRollback"; then ((conflicts++)); fi; done; echo "Conflicts: $conflicts/30"; test $conflicts -ge 1 && echo "PASS" || echo "FAIL"
––– output –––
OK
––– input –––
conflicts=0; for i in {1..30}; do mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (3, 2)" > /dev/null 2>&1; sleep 3; result=$( (mysql -h0 -P2306 -e "UPDATE test:tbl1 SET attr1=1 WHERE attr1=2" & mysql -h0 -P1306 -e "DELETE FROM test:tbl1 WHERE id=3" & wait) 2>&1 ); if echo "$result" | grep -q "error at PostRollback"; then ((conflicts++)); fi; done; echo "Conflicts: $conflicts/30"; test $conflicts -ge 1 && echo "PASS" || echo "FAIL"
––– output –––
Conflicts: %{NUMBER}/30
- PASS
+ FAIL
––– input –––
conflicts=0; for i in {1..30}; do mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (3, 2)" > /dev/null 2>&1; sleep 3; result=$( (mysql -h0 -P2306 -e "DELETE FROM test:tbl1 WHERE id=3" & mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (3, 303)" & wait) 2>&1 ); if echo "$result" | grep -q "error at PostRollback"; then ((conflicts++)); fi; done; echo "Conflicts: $conflicts/30"; test $conflicts -ge 1 && echo "PASS" || echo "FAIL"
––– output –––
OK
––– input –––
conflicts=0; for i in {1..30}; do mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (1, 1)" > /dev/null 2>&1; sleep 3; result=$( (mysql -h0 -P2306 -e "DELETE FROM test:tbl1 WHERE id=1" & mysql -h0 -P1306 -e "DELETE FROM test:tbl1 WHERE id=1" & wait) 2>&1 ); if echo "$result" | grep -q "error at PostRollback"; then ((conflicts++)); fi; done; echo "Conflicts: $conflicts/30"; test $conflicts -ge 1 && echo "PASS" || echo "FAIL"
––– output –––
OK
––– input –––
conflicts=0; for i in {1..30}; do result=$( (mysql -h0 -P2306 -e "UPDATE test:tbl1 SET attr1=111 WHERE id=15" & mysql -h0 -P1306 -e "UPDATE test:tbl1 SET attr1=222 WHERE id=15" & wait) 2>&1 ); if echo "$result" | grep -q "error at PostRollback"; then ((conflicts++)); fi; done; echo "Conflicts: $conflicts/30"; test $conflicts -ge 1 && echo "PASS" || echo "FAIL"
––– output –––
OK
––– input –––
mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (1, 1001)" & mysql -h0 -P2306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (10, 1010)" & mysql -h0 -P3306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (20, 1020)" & wait
––– output –––
OK
––– input –––
conflicts=0; for i in {1..30}; do result=$( (mysql -h0 -P1306 -e "UPDATE test:tbl1 SET attr1=100 WHERE id=12" & mysql -h0 -P2306 -e "UPDATE test:tbl1 SET attr1=200 WHERE id=12" & mysql -h0 -P3306 -e "UPDATE test:tbl1 SET attr1=300 WHERE id=12" & wait) 2>&1 ); if echo "$result" | grep -q "error at PostRollback"; then ((conflicts++)); fi; done; echo "Conflicts: $conflicts/30"; test $conflicts -ge 1 && echo "PASS" || echo "FAIL"
––– output –––
OK
––– input –––
conflicts=0; for i in {1..30}; do mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (14, 14)" > /dev/null 2>&1; sleep 3; result=$( (mysql -h0 -P1306 -e "DELETE FROM test:tbl1 WHERE id=14" & mysql -h0 -P2306 -e "DELETE FROM test:tbl1 WHERE id=14" & mysql -h0 -P3306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (15, 1500)" & wait) 2>&1 ); if echo "$result" | grep -q "error at PostRollback"; then ((conflicts++)); fi; done; echo "Conflicts: $conflicts/30"; test $conflicts -ge 1 && echo "PASS" || echo "FAIL"
––– output –––
OK
––– input –––
kill $LOAD_PID 2>/dev/null; wait $LOAD_PID 2>/dev/null; echo "Load stopped"
––– output –––
OK
––– input –––
mysql -h0 -P1306 -NB -e "SELECT COUNT(*) FROM test:tbl1\G"
––– output –––
OK
––– input –––
mysql -h0 -P2306 -NB -e "SELECT COUNT(*) FROM test:tbl1\G"
––– output –––
OK
––– input –––
mysql -h0 -P3306 -NB -e "SELECT COUNT(*) FROM test:tbl1\G"
––– output –––
OK
––– input –––
mysql -h0 -P1306 -e "SELECT * FROM test:tbl1" > /tmp/node1.txt; mysql -h0 -P2306 -e "SELECT * FROM test:tbl1" > /tmp/node2.txt; mysql -h0 -P3306 -e "SELECT * FROM test:tbl1" > /tmp/node3.txt; diff /tmp/node1.txt /tmp/node2.txt && diff /tmp/node2.txt /tmp/node3.txt && echo "All nodes synchronized" || echo "Discrepancies detected"
––– output –––
OK
––– input –––
for i in 1 2 3; do grep -q 'FATAL:' /var/log/manticore-${i}/searchd.log && echo "Node #$i has FATAL" || echo "Node #$i OK"; done
––– output –––
OK

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Nov 5, 2025

clt

❌ CLT tests in test/clt-tests/bugs/
✅ OK: 8
❌ Failed: 1
⏳ Duration: 557s
👉 Check Action Results for commit a5f1838

Failed tests:

🔧 Edit failed tests in UI:

test/clt-tests/bugs/3847-conflict-handling-verification.rec
––– input –––
set -b +m
––– output –––
OK
––– input –––
grep -q 'threads = 4' test/clt-tests/base/searchd-with-flexible-ports.conf || sed -i '/searchd {/a\	threads = 4' test/clt-tests/base/searchd-with-flexible-ports.conf
––– output –––
OK
––– input –––
export INSTANCE=1
––– output –––
OK
––– input –––
mkdir -p /var/{run,lib,log}/manticore-${INSTANCE}
––– output –––
OK
––– input –––
stdbuf -oL searchd -c test/clt-tests/base/searchd-with-flexible-ports.conf > /dev/null
––– output –––
OK
––– input –––
if timeout 10 grep -qm1 '\[BUDDY\] started' <(tail -n 1000 -f /var/log/manticore-${INSTANCE}/searchd.log); then echo 'Buddy started!'; else echo 'Timeout or failed!'; cat /var/log/manticore-${INSTANCE}/searchd.log; fi
––– output –––
OK
––– input –––
export INSTANCE=2
––– output –––
OK
––– input –––
mkdir -p /var/{run,lib,log}/manticore-${INSTANCE}
––– output –––
OK
––– input –––
stdbuf -oL searchd -c test/clt-tests/base/searchd-with-flexible-ports.conf > /dev/null
––– output –––
OK
––– input –––
if timeout 10 grep -qm1 '\[BUDDY\] started' <(tail -n 1000 -f /var/log/manticore-${INSTANCE}/searchd.log); then echo 'Buddy started!'; else echo 'Timeout or failed!'; cat /var/log/manticore-${INSTANCE}/searchd.log; fi
––– output –––
OK
––– input –––
export INSTANCE=3
––– output –––
OK
––– input –––
mkdir -p /var/{run,lib,log}/manticore-${INSTANCE}
––– output –––
OK
––– input –––
stdbuf -oL searchd -c test/clt-tests/base/searchd-with-flexible-ports.conf > /dev/null
––– output –––
OK
––– input –––
if timeout 10 grep -qm1 '\[BUDDY\] started' <(tail -n 1000 -f /var/log/manticore-${INSTANCE}/searchd.log); then echo 'Buddy started!'; else echo 'Timeout or failed!'; cat /var/log/manticore-${INSTANCE}/searchd.log; fi
––– output –––
OK
––– input –––
mkdir /var/{lib,log}/manticore-{1,2,3}/test
––– output –––
OK
––– input –––
mysql -h0 -P1306 -e "CREATE CLUSTER test 'test' as path"; echo $?
––– output –––
OK
––– input –––
mysql -h0 -P2306 -e "JOIN CLUSTER test at '127.0.0.1:1312' 'test' as path"; echo $?
––– output –––
OK
––– input –––
mysql -h0 -P3306 -e "JOIN CLUSTER test at '127.0.0.1:1312' 'test' as path"; echo $?
––– output –––
OK
––– input –––
mysql -h0 -P1306 -e "CREATE TABLE tbl1 (id bigint, attr1 int)"; echo $?
––– output –––
OK
––– input –––
mysql -h0 -P1306 -e "ALTER CLUSTER test ADD tbl1"; echo $?
––– output –––
OK
––– input –––
mysql -h0 -P1306 -e "INSERT INTO test:tbl1 (id, attr1) VALUES (1,1), (3,2), (10,3), (11,4), (12,5), (13,6), (14,7), (15,8), (20,9)"; echo $?
––– output –––
OK
––– input –––
mysql -h0 -P1306 -NB -e "SELECT COUNT(*) FROM test:tbl1\G"
––– output –––
OK
––– input –––
mysql -h0 -P2306 -NB -e "SELECT COUNT(*) FROM test:tbl1\G"
––– output –––
OK
––– input –––
mysql -h0 -P3306 -NB -e "SELECT COUNT(*) FROM test:tbl1\G"
––– output –––
OK
––– input –––
manticore-load --host=127.0.0.1 --threads=4 --port=1306 --query="REPLACE INTO test:tbl1 (id, attr1) VALUES (%RAND, %RAND)" --host=127.0.0.1 --port=2306 --together > /dev/null 2>&1 & LOAD_PID=$!; sleep 1; echo "Load started: $LOAD_PID"
––– output –––
OK
––– input –––
mysql -h0 -P2306 -e "UPDATE test:tbl1 SET attr1=1 WHERE id=13" & mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (10, 999)" & wait
––– output –––
OK
––– input –––
mysql -h0 -P2306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (11, 111)" & mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (10, 101)" & wait
––– output –––
OK
––– input –––
mysql -h0 -P2306 -e "DELETE FROM test:tbl1 WHERE id=3" & mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (10, 102)" & wait
––– output –––
OK
––– input –––
mysql -h0 -P2306 -e "INSERT INTO test:tbl1 (id, attr1) VALUES (100, 1)" & mysql -h0 -P1306 -e "INSERT INTO test:tbl1 (id, attr1) VALUES (200, 2)" & wait
––– output –––
OK
––– input –––
mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (13, 6)"; sleep 2
––– output –––
OK
––– input –––
conflicts=0; for i in {1..30}; do result=$( (mysql -h0 -P2306 -e "UPDATE test:tbl1 SET attr1=1 WHERE id=13" & mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (13, 999)" & wait) 2>&1 ); if echo "$result" | grep -q "error at PostRollback"; then ((conflicts++)); fi; done; echo "Conflicts: $conflicts/30"; test $conflicts -ge 1 && echo "PASS" || echo "FAIL"
––– output –––
OK
––– input –––
conflicts=0; for i in {1..30}; do result=$( (mysql -h0 -P2306 -e "UPDATE test:tbl1 SET attr1=1 WHERE id>13" & mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (14, 888)" & wait) 2>&1 ); if echo "$result" | grep -q "error at PostRollback"; then ((conflicts++)); fi; done; echo "Conflicts: $conflicts/30"; test $conflicts -ge 1 && echo "PASS" || echo "FAIL"
––– output –––
OK
––– input –––
conflicts=0; for i in {1..30}; do mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (3, 2)" > /dev/null 2>&1; sleep 3; result=$( (mysql -h0 -P2306 -e "UPDATE test:tbl1 SET attr1=1 WHERE attr1=2" & mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (3, 333)" & wait) 2>&1 ); if echo "$result" | grep -q "error at PostRollback"; then ((conflicts++)); fi; done; echo "Conflicts: $conflicts/30"; test $conflicts -ge 1 && echo "PASS" || echo "FAIL"
––– output –––
Conflicts: %{NUMBER}/30
- PASS
+ FAIL
––– input –––
conflicts=0; for i in {1..30}; do mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (3, 2)" > /dev/null 2>&1; sleep 3; result=$( (mysql -h0 -P2306 -e "UPDATE test:tbl1 SET attr1=1 WHERE attr1=2" & mysql -h0 -P1306 -e "DELETE FROM test:tbl1 WHERE id=3" & wait) 2>&1 ); if echo "$result" | grep -q "error at PostRollback"; then ((conflicts++)); fi; done; echo "Conflicts: $conflicts/30"; test $conflicts -ge 1 && echo "PASS" || echo "FAIL"
––– output –––
OK
––– input –––
conflicts=0; for i in {1..30}; do mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (3, 2)" > /dev/null 2>&1; sleep 3; result=$( (mysql -h0 -P2306 -e "DELETE FROM test:tbl1 WHERE id=3" & mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (3, 303)" & wait) 2>&1 ); if echo "$result" | grep -q "error at PostRollback"; then ((conflicts++)); fi; done; echo "Conflicts: $conflicts/30"; test $conflicts -ge 1 && echo "PASS" || echo "FAIL"
––– output –––
OK
––– input –––
conflicts=0; for i in {1..30}; do mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (1, 1)" > /dev/null 2>&1; sleep 3; result=$( (mysql -h0 -P2306 -e "DELETE FROM test:tbl1 WHERE id=1" & mysql -h0 -P1306 -e "DELETE FROM test:tbl1 WHERE id=1" & wait) 2>&1 ); if echo "$result" | grep -q "error at PostRollback"; then ((conflicts++)); fi; done; echo "Conflicts: $conflicts/30"; test $conflicts -ge 1 && echo "PASS" || echo "FAIL"
––– output –––
OK
––– input –––
conflicts=0; for i in {1..30}; do result=$( (mysql -h0 -P2306 -e "UPDATE test:tbl1 SET attr1=111 WHERE id=15" & mysql -h0 -P1306 -e "UPDATE test:tbl1 SET attr1=222 WHERE id=15" & wait) 2>&1 ); if echo "$result" | grep -q "error at PostRollback"; then ((conflicts++)); fi; done; echo "Conflicts: $conflicts/30"; test $conflicts -ge 1 && echo "PASS" || echo "FAIL"
––– output –––
OK
––– input –––
mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (1, 1001)" & mysql -h0 -P2306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (10, 1010)" & mysql -h0 -P3306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (20, 1020)" & wait
––– output –––
OK
––– input –––
conflicts=0; for i in {1..30}; do result=$( (mysql -h0 -P1306 -e "UPDATE test:tbl1 SET attr1=100 WHERE id=12" & mysql -h0 -P2306 -e "UPDATE test:tbl1 SET attr1=200 WHERE id=12" & mysql -h0 -P3306 -e "UPDATE test:tbl1 SET attr1=300 WHERE id=12" & wait) 2>&1 ); if echo "$result" | grep -q "error at PostRollback"; then ((conflicts++)); fi; done; echo "Conflicts: $conflicts/30"; test $conflicts -ge 1 && echo "PASS" || echo "FAIL"
––– output –––
OK
––– input –––
conflicts=0; for i in {1..30}; do mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (14, 14)" > /dev/null 2>&1; sleep 3; result=$( (mysql -h0 -P1306 -e "DELETE FROM test:tbl1 WHERE id=14" & mysql -h0 -P2306 -e "DELETE FROM test:tbl1 WHERE id=14" & mysql -h0 -P3306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (15, 1500)" & wait) 2>&1 ); if echo "$result" | grep -q "error at PostRollback"; then ((conflicts++)); fi; done; echo "Conflicts: $conflicts/30"; test $conflicts -ge 1 && echo "PASS" || echo "FAIL"
––– output –––
OK
––– input –––
kill $LOAD_PID 2>/dev/null; wait $LOAD_PID 2>/dev/null; echo "Load stopped"
––– output –––
OK
––– input –––
mysql -h0 -P1306 -NB -e "SELECT COUNT(*) FROM test:tbl1\G"
––– output –––
OK
––– input –––
mysql -h0 -P2306 -NB -e "SELECT COUNT(*) FROM test:tbl1\G"
––– output –––
OK
––– input –––
mysql -h0 -P3306 -NB -e "SELECT COUNT(*) FROM test:tbl1\G"
––– output –––
OK
––– input –––
mysql -h0 -P1306 -e "SELECT * FROM test:tbl1" > /tmp/node1.txt; mysql -h0 -P2306 -e "SELECT * FROM test:tbl1" > /tmp/node2.txt; mysql -h0 -P3306 -e "SELECT * FROM test:tbl1" > /tmp/node3.txt; diff /tmp/node1.txt /tmp/node2.txt && diff /tmp/node2.txt /tmp/node3.txt && echo "All nodes synchronized" || echo "Discrepancies detected"
––– output –––
OK
––– input –––
for i in 1 2 3; do grep -q 'FATAL:' /var/log/manticore-${i}/searchd.log && echo "Node #$i has FATAL" || echo "Node #$i OK"; done
––– output –––
OK

Copy link
Copy Markdown
Contributor

@tomatolog tomatolog left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there are some code duplicates these should be eliminated to reduce the patch size and allow to read the commit easier

Comment thread src/sphinxexpr.cpp Outdated
pRes = new Expr_UdfStringptr_c ( m_dUdfCalls[iCall], m_pProfiler );
break;
case SPH_ATTR_UINT32SET_PTR:
pRes = new Expr_UdfMva32_c ( m_dUdfCalls[iCall], m_pProfiler );
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expr_UdfMva32_c and Expr_UdfMva64_c is the full clone of each other. The only difference are multiple asserts messages

- assert ( 0 && "internal error: mva64 udf evaluated as float" );
+ assert ( 0 && "internal error: mva32 udf evaluated as float" );

and also this assert

- assert ( pCall->m_pUdf->m_eRetType==SPH_ATTR_UINT32SET_PTR );
+ assert ( pCall->m_pUdf->m_eRetType==SPH_ATTR_INT64SET_PTR );

I sure it could be the single Expr_UdfMva_c class that could has bool m_bMva64 set at the constructor then that bool could control the all asserts messages or behavior for assert for the type check like

assert ( ( !m_bMva64 && pCall->m_pUdf->m_eRetType==SPH_ATTR_UINT32SET_PTR ) || ( m_bMva64 && pCall->m_pUdf->m_eRetType==SPH_ATTR_INT64SET_PTR ) );

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merged both classes (and Expr_UdfFloatVector_c) into Expr_UdfMva_c as suggested. The constructor validates the return type using a combined assert:

assert ( ( !m_bMva64 && !m_bFloatVector && pCall->m_pUdf->m_eRetType==SPH_ATTR_UINT32SET_PTR ) ||    ( m_bMva64 && !m_bFloatVector && pCall->m_pUdf->m_eRetType==SPH_ATTR_INT64SET_PTR ) ||    ( !m_bMva64 && m_bFloatVector && pCall->m_pUdf->m_eRetType==SPH_ATTR_FLOAT_VECTOR_PTR ) );

The assert messages in Eval(), IntEval(), and Int64Eval() are conditionally set based on the flags, providing type-specific error messages.

Comment thread src/queuecreator.cpp Outdated
// but that ideal route seems somewhat more complicated in the current architecture
// For columnar attributes, use the index schema to create proper columnar expressions.
// For computed expressions (like UDFs), use the sorter schema.
const ISphSchema * pSchemaToUse = bColumnar ? &m_tSettings.m_tSchema : m_pSorterSchema.get();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that is very strange change as it is not clear how SPH_AGGR_xxx and bColumnar could be affected by that MVA from UDF feature. CLT test also show no any regression related to columnar engine.

I'd rolled back this change (all src/queuecreator.cpp code changes) if all tests passed without this change.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the conditional schema selection logic that used bColumnar to choose between m_tSettings.m_tSchema and m_pSorterSchema. The code now always uses m_pSorterSchema, as it did before this change.

Comment thread src/sphinxexpr.cpp
case SPH_ATTR_INT64SET_PTR:
pRes = new Expr_UdfMva64_c ( m_dUdfCalls[iCall], m_pProfiler );
break;
case SPH_ATTR_FLOAT_VECTOR_PTR:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expr_UdfFloatVector_c is the also full clone of the Expr_UdfMva32_c and Expr_UdfMva64_c - no need to keep these 3 classes - should be only one class that could be configured via constructor

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expr_UdfFloatVector_c was merged into the unified Expr_UdfMva_c class along with the MVA32 and MVA64 classes. All three types are now handled by a single class with boolean flags (m_bMva64 and m_bFloatVector) to distinguish between them. The CreateUdfNode() function now uses a single case for all three return types:

case SPH_ATTR_UINT32SET_PTR:
case SPH_ATTR_INT64SET_PTR:
case SPH_ATTR_FLOAT_VECTOR_PTR:
    pRes = new Expr_UdfMva_c ( m_dUdfCalls[iCall], m_pProfiler );
    break;

Comment thread src/sphinxexpr.cpp Outdated
case TOK_ATTR_MVA32:
{
// Check if attribute is from storage (sorted) or computed expression (unsorted)
bool bIsSorted = true;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it could be better to move all code that set bIsSorted into static function and call that function at the case TOK_ATTR_MVA32: and at the case TOK_ATTR_MVA64: like this

		case TOK_ATTR_MVA32:
		{
			bool bIsSorted = CheckIsStored ( m_pSchema, tLeft );
			return new Expr_MVAAggr_c<DWORD> ( tLeft.m_tLocator, GetNameByLocator(tLeft), eFunc, bIsSorted );
		}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extracted the bIsSorted logic into a static function CheckIsStored() that encapsulates the logic for determining whether an MVA attribute is from storage (sorted) or computed (unsorted). This function is now reused in both TOK_ATTR_MVA32 and TOK_ATTR_MVA64 cases.

- Renamed `Expr_UdfMva32_c` to `Expr_UdfMva_c` and consolidated logic for MVA32 and MVA64 handling.
- Enhanced error assertions in evaluation methods to differentiate between MVA types.
- Introduced a new helper function `CheckIsStored` to streamline attribute storage checks in aggregate node creation.
- Updated references in `CreateAggregateNode` to utilize the new helper function for improved clarity and maintainability.
@sanikolaev sanikolaev requested a review from tomatolog November 11, 2025 06:48
Copy link
Copy Markdown
Contributor

@tomatolog tomatolog left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all previous comments are now implemented and the final changes looks more clear - seems good to merge

@github-actions
Copy link
Copy Markdown
Contributor

clt

❌ CLT tests in test/clt-tests/mcl/auto-embeddings-hnsw-configs test/clt-tests/mcl/auto-embeddings-jina-remote test/clt-tests/mcl/auto-embeddings-json-api test/clt-tests/mcl/auto-embeddings-knn-select-scenarios test/clt-tests/mcl/auto-embeddings-openai-remote test/clt-tests/mcl/auto-embeddings-qwen test/clt-tests/mcl/auto-embeddings-syntax-check test/clt-tests/mcl/auto-embeddings-voyage-remote test/clt-tests/http-interface/cli-endpoint test/clt-tests/http-interface/cli-json-endpoint
✅ OK: 9
❌ Failed: 1
⏳ Duration: 65s
👉 Check Action Results for commit 5a702b5

Failed tests:

🔧 Edit failed tests in UI:

test/clt-tests/mcl/auto-embeddings-openai-remote.rec
––– input –––
rm -f /var/log/manticore/searchd.log; stdbuf -oL searchd --stopwait > /dev/null; stdbuf -oL searchd ${SEARCHD_ARGS:-} > /dev/null
––– output –––
OK
––– input –––
if timeout 10 grep -qm1 'accepting connections' <(tail -n 1000 -f /var/log/manticore/searchd.log); then echo 'Accepting connections!'; else echo 'Timeout or failed!'; fi
––– output –––
OK
––– input –––
cosine_similarity() {
    local file1="$1" file2="$2"

    awk '
    NR==FNR { a[NR]=$1; suma2+=$1*$1; next }
    {
        dot += a[FNR]*$1
        sumb2 += $1*$1
    }
    END {
        print dot / (sqrt(suma2) * sqrt(sumb2))
    }' "$file1" "$file2"
}
––– output –––
OK
––– input –––
export -f cosine_similarity
––– output –––
OK
––– input –––
mysql -h0 -P9306 -e "CREATE TABLE test_invalid_model (title TEXT, embedding FLOAT_VECTOR KNN_TYPE='hnsw' HNSW_SIMILARITY='l2' MODEL_NAME = 'openai/invalid-model-name-12345' FROM = 'title') " 2>&1
––– output –––
OK
––– input –––
mysql -h0 -P9306 -e "CREATE TABLE test_valid_model_no_api_key (title TEXT, embedding FLOAT_VECTOR KNN_TYPE='hnsw' HNSW_SIMILARITY='l2' MODEL_NAME = 'openai/text-embedding-ada-002' FROM = 'title') " 2>&1
––– output –––
OK
––– input –––
mysql -h0 -P9306 -e "CREATE TABLE test_openai_remote (title TEXT, content TEXT, description TEXT, embedding FLOAT_VECTOR KNN_TYPE='hnsw' HNSW_SIMILARITY='l2' MODEL_NAME = 'openai/text-embedding-ada-002' FROM = 'title, content' API_KEY='${OPENAI_API_KEY}') "; echo $?
––– output –––
OK
––– input –––
mysql -h0 -P9306 -E -e "SHOW CREATE TABLE test_openai_remote"
––– output –––
OK
––– input –––
mysql -h0 -P9306 -e "INSERT INTO test_openai_remote (id, title, content, description) VALUES(1, 'machine learning algorithms', 'deep neural networks and artificial intelligence', 'advanced AI research')"; echo $?
––– output –––
OK
––– input –––
mysql -h0 -P9306 -e "SELECT COUNT(*) as record_count FROM test_openai_remote WHERE id=1"
––– output –––
OK
––– input –––
mysql -h0 -P9306 -e "INSERT INTO test_openai_remote (id, title, content, description) VALUES(2, 'machine learning algorithms', 'deep neural networks and artificial intelligence', 'different description')"

mysql -h0 -P9306 -e "SELECT embedding FROM test_openai_remote WHERE id=1" | \
    grep -v embedding | \
    sed 's/[0-9]\+\(\.[0-9]\+\)\?/\n&\n/g' | \
    grep -E '^[0-9]+(\.[0-9]+)?$' | \
    awk '{printf "%.5f\n", $1}' > /tmp/vector1.txt

mysql -h0 -P9306 -e "SELECT embedding FROM test_openai_remote WHERE id=2" | \
    grep -v embedding | \
    sed 's/[0-9]\+\(\.[0-9]\+\)\?/\n&\n/g' | \
    grep -E '^[0-9]+(\.[0-9]+)?$' | \
    awk '{printf "%.5f\n", $1}' > /tmp/vector2.txt

SIMILARITY=$(cosine_similarity /tmp/vector1.txt /tmp/vector2.txt)

echo "Cosine similarity: $SIMILARITY"

RESULT=$(awk -v sim="$SIMILARITY" 'BEGIN {
    if (sim > 0.99)
        print "SUCCESS: Same FROM fields produce similar vectors (similarity: " sim ")"
    else
        print "FAIL: Different vectors (FROM does not include description field and should not change generated vector value) (similarity: " sim ")"
}')

echo "$RESULT"

rm -f /tmp/vector1.txt /tmp/vector2.txt
––– output –––
OK
––– input –––
mysql -h0 -P9306 -e "CREATE TABLE test_openai_title_only (title TEXT, content TEXT, embedding FLOAT_VECTOR KNN_TYPE='hnsw' HNSW_SIMILARITY='l2' MODEL_NAME = 'openai/text-embedding-ada-002' FROM = 'title' API_KEY='${OPENAI_API_KEY}') "; mysql -h0 -P9306 -e "INSERT INTO test_openai_title_only (id, title, content) VALUES(1, 'machine learning algorithms', 'completely different content here')"; MD5_MULTI=$(mysql -h0 -P9306 -e "SELECT embedding FROM test_openai_remote WHERE id=1" | grep -v embedding | md5sum | awk '{print $1}'); MD5_SINGLE=$(mysql -h0 -P9306 -e "SELECT embedding FROM test_openai_title_only WHERE id=1" | grep -v embedding | md5sum | awk '{print $1}'); echo "multi_field_md5: $MD5_MULTI"; echo "single_field_md5: $MD5_SINGLE"; if [ "$MD5_MULTI" != "$MD5_SINGLE" ]; then echo "SUCCESS: Different FROM specifications produce different vectors"; else echo "INFO: FROM field comparison result"; fi
––– output –––
OK
––– input –––
mysql -h0 -P9306 -e "CREATE TABLE test_openai_invalid_field (title TEXT, embedding FLOAT_VECTOR KNN_TYPE='hnsw' HNSW_SIMILARITY='l2' MODEL_NAME = 'openai/text-embedding-ada-002' FROM = 'nonexistent_field') " 2>&1
––– output –––
OK
––– input –––
if mysql -h0 -P9306 -e "SHOW TABLES LIKE 'test_openai_no_from'" | grep -q test_openai_no_from; then mysql -h0 -P9306 -e "INSERT INTO test_openai_no_from (id, title, embedding) VALUES(1, 'test title', '(0.1, 0.2, 0.3, 0.4, 0.5)')"; echo "insert_result: $?"; else echo "insert_result: skipped (table not created)"; fi
––– output –––
OK
––– input –––
if mysql -h0 -P9306 -e "SHOW TABLES LIKE 'test_openai_no_from'" | grep -q test_openai_no_from; then mysql -h0 -P9306 -e "SHOW CREATE TABLE test_openai_no_from"; else echo "table_structure: skipped (table not created)"; fi
––– output –––
OK
––– input –––
if [ -n "$OPENAI_API_KEY" ] && [ "$OPENAI_API_KEY" != "dummy_key_for_testing" ]; then echo "API key is available for testing"; else echo "API key not available - using dummy for error testing"; fi
––– output –––
OK
––– input –––
mysql -h0 -P9306 -e "SELECT id, knn_dist() FROM test_openai_remote WHERE knn(embedding, 3, 'machine learning and artificial intelligence')\G"
––– output –––
- *************************** 1. row ***************************
+ ERROR 1064 (42000) at line 1: table test_openai_remote: Error generating embeddings for attribute 'embedding' : Failed to send request to remote model
-         id: %{NUMBER}
- knn_dist(): #!/[0-9]+\.[0-9]+/!#
- *************************** 2. row ***************************
-         id: %{NUMBER}
- knn_dist(): #!/[0-9]+\.[0-9]+/!#
––– input –––
mysql -h0 -P9306 -e "SELECT COUNT(*) as count FROM test_openai_remote WHERE knn(embedding, 5, 'technology and AI') AND id > 0"
––– output –––
OK
––– input –––
API_KEY_VAL="${OPENAI_API_KEY}"; cat > /etc/manticoresearch/manticore.conf << CONFEOF
searchd {
    listen = 127.0.0.1:9306:mysql41
    listen = 127.0.0.1:9308:http
    log = /var/log/manticore/searchd.log
    pid_file = /var/run/manticore/searchd.pid
}

table test_openai_plain {
    type = rt
    path = /var/lib/manticore/test_openai_plain
    rt_field = title
    rt_field = content
    rt_attr_float_vector = embedding
    knn = {"attrs":[{"name":"embedding","type":"hnsw","hnsw_similarity":"L2","hnsw_m":16,"hnsw_ef_construction":200,"model_name":"openai/text-embedding-ada-002","from":"title,content","api_key":"${API_KEY_VAL}"}]}
}
CONFEOF
––– output –––
OK
––– input –––
searchd --stopwait --quiet
––– output –––
OK
––– input –––
rm -f /var/log/manticore/searchd.log; stdbuf -oL searchd --stopwait > /dev/null; stdbuf -oL searchd ${SEARCHD_ARGS:-} > /dev/null
––– output –––
OK
––– input –––
if timeout 10 grep -qm1 'accepting connections' <(tail -n 1000 -f /var/log/manticore/searchd.log); then echo 'Accepting connections!'; else echo 'Timeout or failed!'; fi
––– output –––
OK
––– input –––
mysql -h0 -P9306 -e "SHOW TABLES"
––– output –––
OK
––– input –––
mysql -h0 -P9306 -e "INSERT INTO test_openai_plain (id, title, content) VALUES(1, 'bread', 'food item'), (2, 'cat', 'animal pet')"; echo $?
––– output –––
OK
––– input –––
mysql -h0 -P9306 -e "SELECT COUNT(*) as count FROM test_openai_plain"
––– output –––
OK
––– input –––
mysql -h0 -P9306 -E -e "SELECT id, title FROM test_openai_plain WHERE knn(embedding, 2, 'dog')"
––– output –––
OK
––– input –––
cat > /etc/manticoresearch/manticore.conf << 'EOF'
searchd {
    listen = 127.0.0.1:9306:mysql41
    listen = 127.0.0.1:9308:http
    log = /var/log/manticore/searchd.log
    pid_file = /var/run/manticore/searchd.pid
}

table test_openai_no_key {
    type = rt
    path = /var/lib/manticore/test_openai_no_key
    rt_field = title
    rt_attr_float_vector = embedding
    knn = {"attrs":[{"name":"embedding","type":"hnsw","hnsw_similarity":"L2","model_name":"openai/text-embedding-ada-002","from":"title"}]}
}
EOF
––– output –––
OK
––– input –––
searchd --stopwait --quiet
––– output –––
OK
––– input –––
searchd 2>&1|grep WARNING
––– output –––
OK

@github-actions
Copy link
Copy Markdown
Contributor

clt

❌ CLT tests in test/clt-tests/bugs/3489-fuzzy-option-alias-bug test/clt-tests/bugs/3602-knn-dist-attribute-error test/clt-tests/bugs/3832-fuzzy-cli-json-multiquery test/clt-tests/bugs/3844-max-ft-field-crash test/clt-tests/bugs/3847-conflict-handling-verification test/clt-tests/bugs/4009-qcache-secondaryindex-bug test/clt-tests/bugs/4176-attach-rt-externals test/clt-tests/bugs/4176-rename-rt-externals test/clt-tests/bugs/4375-max-query-time test/clt-tests/bugs/567-fuzzy-search-skips-numeric-words
✅ OK: 9
❌ Failed: 1
⏳ Duration: 267s
👉 Check Action Results for commit 5a702b5

Failed tests:

🔧 Edit failed tests in UI:

test/clt-tests/bugs/3847-conflict-handling-verification.rec
––– input –––
set -b +m
––– output –––
OK
––– input –––
grep -q 'threads = 4' test/clt-tests/base/searchd-with-flexible-ports.conf || sed -i '/searchd {/a\	threads = 4' test/clt-tests/base/searchd-with-flexible-ports.conf
––– output –––
OK
––– input –––
export INSTANCE=1
––– output –––
OK
––– input –––
mkdir -p /var/{run,lib,log}/manticore-${INSTANCE}
––– output –––
OK
––– input –––
stdbuf -oL searchd -c test/clt-tests/base/searchd-with-flexible-ports.conf > /dev/null
––– output –––
OK
––– input –––
if timeout 10 grep -qm1 '\[BUDDY\] started' <(tail -n 1000 -f /var/log/manticore-${INSTANCE}/searchd.log); then echo 'Buddy started!'; else echo 'Timeout or failed!'; cat /var/log/manticore-${INSTANCE}/searchd.log; fi
––– output –––
OK
––– input –––
export INSTANCE=2
––– output –––
OK
––– input –––
mkdir -p /var/{run,lib,log}/manticore-${INSTANCE}
––– output –––
OK
––– input –––
stdbuf -oL searchd -c test/clt-tests/base/searchd-with-flexible-ports.conf > /dev/null
––– output –––
OK
––– input –––
if timeout 10 grep -qm1 '\[BUDDY\] started' <(tail -n 1000 -f /var/log/manticore-${INSTANCE}/searchd.log); then echo 'Buddy started!'; else echo 'Timeout or failed!'; cat /var/log/manticore-${INSTANCE}/searchd.log; fi
––– output –––
OK
––– input –––
export INSTANCE=3
––– output –––
OK
––– input –––
mkdir -p /var/{run,lib,log}/manticore-${INSTANCE}
––– output –––
OK
––– input –––
stdbuf -oL searchd -c test/clt-tests/base/searchd-with-flexible-ports.conf > /dev/null
––– output –––
OK
––– input –––
if timeout 10 grep -qm1 '\[BUDDY\] started' <(tail -n 1000 -f /var/log/manticore-${INSTANCE}/searchd.log); then echo 'Buddy started!'; else echo 'Timeout or failed!'; cat /var/log/manticore-${INSTANCE}/searchd.log; fi
––– output –––
OK
––– input –––
wait_for_sync() { sleep 0.5; for i in {1..10}; do c1=$(mysql -h0 -P1306 -sN -e "SELECT COUNT(*) FROM test:tbl1" 2>/dev/null | grep -oE '[0-9]+' | head -1); c2=$(mysql -h0 -P2306 -sN -e "SELECT COUNT(*) FROM test:tbl1" 2>/dev/null | grep -oE '[0-9]+' | head -1); c3=$(mysql -h0 -P3306 -sN -e "SELECT COUNT(*) FROM test:tbl1" 2>/dev/null | grep -oE '[0-9]+' | head -1); if [ "$c1" = "$c2" ] && [ "$c2" = "$c3" ] && [ -n "$c1" ]; then return 0; fi; sleep 0.5; done; return 1; }
––– output –––
OK
––– input –––
mkdir /var/{lib,log}/manticore-{1,2,3}/test
––– output –––
OK
––– input –––
mysql -h0 -P1306 -e "CREATE CLUSTER test 'test' as path"; echo $?
––– output –––
OK
––– input –––
mysql -h0 -P2306 -e "JOIN CLUSTER test at '127.0.0.1:1312' 'test' as path"; echo $?
––– output –––
OK
––– input –––
mysql -h0 -P3306 -e "JOIN CLUSTER test at '127.0.0.1:1312' 'test' as path"; echo $?
––– output –––
OK
––– input –––
mysql -h0 -P1306 -e "CREATE TABLE tbl1 (id bigint, attr1 int)"; echo $?
––– output –––
OK
––– input –––
mysql -h0 -P1306 -e "ALTER CLUSTER test ADD tbl1"; echo $?
––– output –––
OK
––– input –––
mysql -h0 -P1306 -e "INSERT INTO test:tbl1 (id, attr1) VALUES (1,1), (3,2), (10,3), (11,4), (12,5), (13,6), (14,7), (15,8), (20,9)"; echo $?
––– output –––
OK
––– input –––
wait_for_sync && echo "Cluster synchronized" || echo "Sync timeout"
––– output –––
OK
––– input –––
mysql -h0 -P1306 -NB -e "SELECT COUNT(*) FROM test:tbl1\G"
––– output –––
OK
––– input –––
mysql -h0 -P2306 -NB -e "SELECT COUNT(*) FROM test:tbl1\G"
––– output –––
OK
––– input –––
mysql -h0 -P3306 -NB -e "SELECT COUNT(*) FROM test:tbl1\G"
––– output –––
OK
––– input –––
manticore-load --host=127.0.0.1 --threads=4 --port=1306 --total=1000000 --query="REPLACE INTO test:tbl1 (id, attr1) VALUES (%RAND, %RAND)" --together --host=127.0.0.1 --threads=4 --port=2306 --total=1000000 --query="REPLACE INTO test:tbl1 (id, attr1) VALUES (%RAND, %RAND)" > /dev/null 2>&1 & LOAD_PID=$!; sleep 1; echo "Load started: $LOAD_PID"
––– output –––
OK
––– input –––
mysql -h0 -P2306 -e "UPDATE test:tbl1 SET attr1=1 WHERE id=13" & sleep 0.05; mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (10, 999)" & wait
––– output –––
OK
––– input –––
mysql -h0 -P2306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (11, 111)" & sleep 0.05; mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (10, 101)" & wait
––– output –––
OK
––– input –––
mysql -h0 -P2306 -e "DELETE FROM test:tbl1 WHERE id=3" & sleep 0.05; mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (10, 102)" & wait
––– output –––
OK
––– input –––
mysql -h0 -P2306 -e "INSERT INTO test:tbl1 (id, attr1) VALUES (100, 1)" & sleep 0.05; mysql -h0 -P1306 -e "INSERT INTO test:tbl1 (id, attr1) VALUES (200, 2)" & wait
––– output –––
OK
––– input –––
conflicts=0; for i in {1..50}; do result=$( (mysql -h0 -P2306 -e "UPDATE test:tbl1 SET attr1=1 WHERE id=13" 2>&1 & mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (13, 999)" 2>&1 & wait) ); if echo "$result" | grep -q "error at PostRollback"; then ((conflicts++)); fi; done; echo "Conflicts: $conflicts/50"; test $conflicts -ge 1 && echo "PASS" || echo "FAIL"
––– output –––
OK
––– input –––
conflicts=0; for i in {1..50}; do result=$( (mysql -h0 -P2306 -e "UPDATE test:tbl1 SET attr1=1 WHERE id>13" 2>&1 & mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (14, 888)" 2>&1 & wait) ); if echo "$result" | grep -q "error at PostRollback"; then ((conflicts++)); fi; done; echo "Conflicts: $conflicts/50"; test $conflicts -ge 1 && echo "PASS" || echo "FAIL"
––– output –––
OK
––– input –––
conflicts=0; for i in {1..100}; do mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (3, 2)" > /dev/null 2>&1; sleep 0.5; result=$( (mysql -h0 -P2306 -e "UPDATE test:tbl1 SET attr1=1 WHERE attr1=2" 2>&1 & mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (3, 333)" 2>&1 & wait) ); if echo "$result" | grep -q "error at PostRollback"; then ((conflicts++)); fi; done; echo "Conflicts: $conflicts/100"; test $conflicts -ge 1 && echo "PASS" || echo "FAIL"
––– output –––
OK
––– input –––
conflicts=0; for i in {1..100}; do mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (3, 2)" > /dev/null 2>&1; sleep 0.5; result=$( (mysql -h0 -P2306 -e "UPDATE test:tbl1 SET attr1=1 WHERE attr1=2" 2>&1 & mysql -h0 -P1306 -e "DELETE FROM test:tbl1 WHERE id=3" 2>&1 & wait) ); if echo "$result" | grep -q "error at PostRollback"; then ((conflicts++)); fi; done; echo "Conflicts: $conflicts/100"; test $conflicts -ge 1 && echo "PASS" || echo "FAIL"
––– output –––
Conflicts: %{NUMBER}/100
- PASS
+ FAIL
––– input –––
mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (3, 2)" > /dev/null 2>&1; sleep 2
––– output –––
OK
––– input –––
conflicts=0; for i in {1..50}; do result=$( (mysql -h0 -P2306 -e "DELETE FROM test:tbl1 WHERE id=3" 2>&1 & mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (3, 303)" 2>&1 & wait) ); if echo "$result" | grep -q "error at PostRollback"; then ((conflicts++)); fi; done; echo "Conflicts: $conflicts/50"; test $conflicts -ge 1 && echo "PASS" || echo "FAIL"
––– output –––
OK
––– input –––
mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (1, 1)" > /dev/null 2>&1; sleep 2
––– output –––
OK
––– input –––
conflicts=0; for i in {1..50}; do result=$( (mysql -h0 -P2306 -e "DELETE FROM test:tbl1 WHERE id=1" 2>&1 & mysql -h0 -P1306 -e "DELETE FROM test:tbl1 WHERE id=1" 2>&1 & wait) ); if echo "$result" | grep -q "error at PostRollback"; then ((conflicts++)); fi; done; echo "Conflicts: $conflicts/50"; test $conflicts -ge 1 && echo "PASS" || echo "FAIL"
––– output –––
OK
––– input –––
conflicts=0; for i in {1..50}; do result=$( (mysql -h0 -P2306 -e "UPDATE test:tbl1 SET attr1=111 WHERE id=15" 2>&1 & mysql -h0 -P1306 -e "UPDATE test:tbl1 SET attr1=222 WHERE id=15" 2>&1 & wait) ); if echo "$result" | grep -q "error at PostRollback"; then ((conflicts++)); fi; done; echo "Conflicts: $conflicts/50"; test $conflicts -ge 1 && echo "PASS" || echo "FAIL"
––– output –––
OK
––– input –––
mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (1, 1001)" & sleep 0.05; mysql -h0 -P2306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (10, 1010)" & sleep 0.05; mysql -h0 -P3306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (20, 1020)" & wait
––– output –––
OK
––– input –––
conflicts=0; for i in {1..50}; do result=$( (mysql -h0 -P1306 -e "UPDATE test:tbl1 SET attr1=100 WHERE id=12" 2>&1 & mysql -h0 -P2306 -e "UPDATE test:tbl1 SET attr1=200 WHERE id=12" 2>&1 & mysql -h0 -P3306 -e "UPDATE test:tbl1 SET attr1=300 WHERE id=12" 2>&1 & wait) ); if echo "$result" | grep -q "error at PostRollback"; then ((conflicts++)); fi; done; echo "Conflicts: $conflicts/50"; test $conflicts -ge 1 && echo "PASS" || echo "FAIL"
––– output –––
OK
––– input –––
mysql -h0 -P1306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (14, 14)" > /dev/null 2>&1; sleep 2
––– output –––
OK
––– input –––
conflicts=0; for i in {1..50}; do result=$( (mysql -h0 -P1306 -e "DELETE FROM test:tbl1 WHERE id=14" 2>&1 & mysql -h0 -P2306 -e "DELETE FROM test:tbl1 WHERE id=14" 2>&1 & mysql -h0 -P3306 -e "REPLACE INTO test:tbl1 (id, attr1) VALUES (14, 1500)" 2>&1 & wait) ); if echo "$result" | grep -q "error at PostRollback"; then ((conflicts++)); fi; done; echo "Conflicts: $conflicts/50"; test $conflicts -ge 1 && echo "PASS" || echo "FAIL"
––– output –––
OK
––– input –––
kill $LOAD_PID 2>/dev/null; wait $LOAD_PID 2>/dev/null; echo "Load stopped"
––– output –––
OK
––– input –––
wait_for_sync && echo "Final sync successful" || echo "Final sync failed"
––– output –––
OK
––– input –––
mysql -h0 -P1306 -NB -e "SELECT COUNT(*) FROM test:tbl1\G"
––– output –––
OK
––– input –––
mysql -h0 -P2306 -NB -e "SELECT COUNT(*) FROM test:tbl1\G"
––– output –––
OK
––– input –––
mysql -h0 -P3306 -NB -e "SELECT COUNT(*) FROM test:tbl1\G"
––– output –––
OK
––– input –––
c1=$(mysql -h0 -P1306 -sN -e "SELECT COUNT(*) FROM test:tbl1" | grep -oE '[0-9]+' | head -1); c2=$(mysql -h0 -P2306 -sN -e "SELECT COUNT(*) FROM test:tbl1" | grep -oE '[0-9]+' | head -1); c3=$(mysql -h0 -P3306 -sN -e "SELECT COUNT(*) FROM test:tbl1" | grep -oE '[0-9]+' | head -1); if [ "$c1" = "$c2" ] && [ "$c2" = "$c3" ]; then echo "All nodes synchronized ($c1 rows)"; else echo "Discrepancies: node1=$c1, node2=$c2, node3=$c3"; fi
––– output –––
OK
––– input –––
for i in 1 2 3; do grep -q 'FATAL:' /var/log/manticore-${i}/searchd.log && echo "Node #$i has FATAL" || echo "Node #$i OK"; done
––– output –––
OK

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants