Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
c957c30
Init
schiwekM Mar 2, 2026
f5b18f0
ESM -> CJS
schiwekM Mar 2, 2026
77eaffd
Merge branch 'main' into feat/vector-parity
schiwekM Mar 2, 2026
4d577ba
Get rid of unused code
schiwekM Mar 2, 2026
fad7e12
Replace synckit with own impl
schiwekM Mar 2, 2026
8b636c5
Merge branch 'main' into feat/vector-parity
schiwekM Mar 3, 2026
937d410
Simplify embeddings logic
schiwekM Mar 3, 2026
415a5d8
Add tests
schiwekM Mar 3, 2026
1fc2404
Merge branch 'main' into feat/vector-parity
schiwekM Mar 3, 2026
dd6f20f
Update cql-functions.js
schiwekM Mar 3, 2026
c7a0624
Update sqlite-vector-worker.js
schiwekM Mar 3, 2026
7828bcb
Update index.cds
schiwekM Mar 3, 2026
faebed1
Merge branch 'main' into feat/vector-parity
schiwekM Mar 3, 2026
86d0603
Generating vectors without WorkerThreads
schiwekM Mar 4, 2026
b4e58f1
Update embedding.js
schiwekM Mar 4, 2026
530aa86
Merge branch 'main' into feat/vector-parity
schiwekM Mar 4, 2026
2034e6d
Add PG compatibility
schiwekM Mar 4, 2026
1efa2e5
Update index.js
schiwekM Mar 19, 2026
6609105
Own parity impl to avoid dependency
schiwekM Mar 19, 2026
cf7e643
Merge branch 'main' into feat/vector-parity
schiwekM Mar 20, 2026
76ecc98
Update index.js
schiwekM Apr 1, 2026
f5dfb08
Remove unsupported functions
schiwekM Apr 15, 2026
61700e7
Merge branch 'main' into feat/vector-parity
schiwekM Apr 15, 2026
3550dfb
Update cql-functions.js
schiwekM Apr 15, 2026
cf78fa5
Update functions.test.js
schiwekM Apr 15, 2026
a20fa99
Update index.js
schiwekM Apr 15, 2026
636f612
Fix faulty test setup
schiwekM Apr 16, 2026
9b802e4
Merge branch 'main' into feat/vector-parity
schiwekM Apr 16, 2026
7bbebe0
Update pg-stack.yml
schiwekM Apr 16, 2026
29e93ca
Fix
schiwekM Apr 16, 2026
1e119cf
Update vectors.cds
schiwekM Apr 16, 2026
99b8b47
manual cleanup of sqlite vector implementation
BobdenOs Apr 20, 2026
3576232
optional onnx runtime dependency
BobdenOs Apr 20, 2026
88ffcc9
Attempt to convince eslint that TextDecoder is an esm global variable
BobdenOs Apr 20, 2026
b30d66b
allow onnx runtime dependency to be not installed
BobdenOs Apr 20, 2026
a525ab1
still do nothing in the catch block
BobdenOs Apr 20, 2026
6e74619
refactor: simplify vector embedding implementation
vkozyura Jul 2, 2026
a76461b
refactor: extract shared vector math functions
vkozyura Jul 2, 2026
364c20b
refactor: deduplicate VECTOR_EMBEDDING error message
vkozyura Jul 2, 2026
cea1aed
refactor: reduce comments and simplify code
vkozyura Jul 2, 2026
7fd0133
test: add vector embedding tests
vkozyura Jul 2, 2026
365d8a3
fix: update vector tests to work without chai closeTo
vkozyura Jul 2, 2026
f2c0c1b
simplify: use synchronous hash-based VECTOR_EMBEDDING
vkozyura Jul 3, 2026
e0d3a1b
docs: update hash embedding comment
vkozyura Jul 3, 2026
cd98416
docs: move NOTE to hashEmbedding function
vkozyura Jul 3, 2026
b98e412
refactor: inline vector function definitions in SQLiteService
vkozyura Jul 3, 2026
e746d9a
test: remove redundant hash-based embedding tests
vkozyura Jul 3, 2026
36fd908
test: update VECTOR_EMBEDDING test to verify it works
vkozyura Jul 3, 2026
c3ba20c
test: consolidate vector tests into functions.test.js
vkozyura Jul 3, 2026
2796628
Merge branch 'main' into feat/vector-simplified
vkozyura Jul 3, 2026
b1b73d3
fix: reset package-lock.json to main (remove onnxruntime-node)
vkozyura Jul 3, 2026
4f49b5b
fix: update package-lock.json with pgvector
vkozyura Jul 3, 2026
998cef1
fix: use Buffer instead of TextDecoder for eslint
vkozyura Jul 3, 2026
32e5583
fix: skip vector tests on HANA Express (no REAL_VECTOR support)
vkozyura Jul 3, 2026
cdbefdc
fix: rename describeIf to describeSkipIf for clarity
vkozyura Jul 3, 2026
ff73234
fix: remove unused vectors test data
vkozyura Jul 3, 2026
4a82f1a
fix: use early return pattern for HANA skip (cds.db not available at …
vkozyura Jul 3, 2026
93512f8
fix: run vector tests only for SQLite (postgres cast not working)
vkozyura Jul 3, 2026
a3ae0bb
fix: add Vector type to postgres TypeMap, enable vector tests for pos…
vkozyura Jul 3, 2026
c61a964
fix: run vector tests only for SQLite (postgres pgvector not availabl…
vkozyura Jul 3, 2026
0940db2
fix: set locale in pgvector docker to match collation
vkozyura Jul 3, 2026
43149fa
fix: enable vector tests for postgres (skip only HANA)
vkozyura Jul 3, 2026
b4921e0
fix: skip vector tests for non-SQLite (HANA/Postgres issues)
vkozyura Jul 3, 2026
d69bd65
fix: revert to postgres:16-alpine (pgvector image has different colla…
vkozyura Jul 3, 2026
cf73757
docs: add comment about pgvector image for vector support
vkozyura Jul 3, 2026
bae2150
fix: remove pg vector dependency and init
vkozyura Jul 3, 2026
a1de2e3
refactor: delete index.js
vkozyura Jul 6, 2026
ac24478
move tests to sqlite/test/vector.test.js
vkozyura Jul 6, 2026
0ec572e
Merge branch 'main' into feat/vector-simplified
vkozyura Jul 6, 2026
5e8b463
add pg support
vkozyura Jul 6, 2026
701f065
fix tests
vkozyura Jul 6, 2026
21098dd
fix tests
vkozyura Jul 6, 2026
004c277
move sqlite function to file end
vkozyura Jul 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions hana/lib/cql-functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,15 @@ const HANAFunctions = {
HIERARCHY: undefined,
HIERARCHY_DESCENDANTS: undefined,
HIERARCHY_ANCESTORS: undefined,

// Ease of use for stakeholders for auto-filling the remote source
vector_embedding(text, text_type, model_and_version, remote_source = cds.env.ai?.embeddings?.remoteSource) {

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.

What is the source of cds.env.ai?.embeddings?.remoteSource ? If this comes from the previous PR or the AI plugin we should probably communicate this with java as well as they are looking into a global option as well.

I would expect also an global setting for model_and_version so that it doesn't need to be hardcoded into models.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The cds.env.ai.embeddings.remoteSource configuration is introduced by the @cap-js/ai.
Java stack has no ai property in CdsProperties.java. The Java test hardcodes the model.

The Parameter model_and_version is called embeddingModel in Java.

Suggestion: Sync with Java as follow-up

if (!remote_source || (model_and_version.val ?? model_and_version).startswith('SAP')) {
return `vector_embedding(${text},${text_type},${model_and_version})`
} else {
return `vector_embedding(${text},${text_type},${model_and_version},${remote_source})`
}
}
}

for (let each in HANAFunctions) HANAFunctions[each.toUpperCase()] = HANAFunctions[each]
Expand Down
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 18 additions & 1 deletion postgres/lib/PostgresService.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,22 @@ class PostgresService extends SQLService {
try {
const dbc = new Client({ ...credentials, ...clientOptions })
await dbc.connect()
// cds.Vector support for PG
// REVISIT: Check if module 'pgvector/pg' is installed
// More ideas:
// 1. Move CREATE EXTENSION to deployment (migration/deployment scripts). The CDS build/deploy could handle this
// 2. Only pgvector.registerTypes(dbc) - This registers type parsers with the pg client and may need to run per connection
// 3. Make it conditional - Only run if a config flag is set, e.g., cds.env.requires.db.vector: true
// 4. Lazy initialization - Only initialize vector support when a vector operation is first attempted
// 5. Run once per process - Use a module-level flag
/*
try {
await dbc.query('CREATE EXTENSION IF NOT EXISTS vector')
await pgvector.registerTypes(dbc)
} catch (e) {
const LOG = cds.log('postgres')
LOG.debug('pgvector extension not available, skipping vector support:', e.message)
}*/
dbc.open = true
dbc.on('end', () => { dbc.open = false })
return dbc
Expand Down Expand Up @@ -89,7 +105,7 @@ class PostgresService extends SQLService {
JSON.stringify(env),
]),
...(this.options?.credentials?.schema
? [this.exec(`SET search_path TO "${this.options?.credentials?.schema}";`)]
? [this.exec(`SET search_path TO "${this.options?.credentials?.schema}", public;`)] // include public for extensions like pgvector
: []),

...(!this._initalCollateCheck ? [this._checkCollation()] : []),
Expand Down Expand Up @@ -535,6 +551,7 @@ GROUP BY k
DateTime: () => 'TIMESTAMP',
Timestamp: () => 'TIMESTAMP',
Map: () => 'JSONB',
Vector: () => 'vector',

// HANA Types
'cds.hana.CLOB': () => 'BYTEA',
Expand Down
27 changes: 27 additions & 0 deletions postgres/lib/cql-functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,33 @@ const HANAFunctions = {
years_between(x, y) {
return `TRUNC(${this.expr({ func: 'months_between', args: [x, y] })} / 12,0)`
},

/**
* Computes the cosine similarity of two vectors
* @param {*} v1 - Vector 1
* @param {*} v2 - Vector 2
* @returns {string} - SQL statement
*/
cosine_similarity(v1, v2) {
return `1 - cosine_distance(${this.expr(v1)},${this.expr(v2)})`
},
/**
* Computes the L2 distance of two vectors.
* @param {*} v1 - Vector 1
* @param {*} v2 - Vector 2
* @returns {string} - SQL statement
*/
l2distance(v1, v2) {
return `l2_distance(${this.expr(v1)},${this.expr(v2)})`
},
/**
* L2 normalizes the vector
* @param {*} v - Vector
* @returns {string} - SQL statement
*/
l2normalize(v) {
return `l2_normalize(${this.expr(v)})`
},
}

for (let each in HANAFunctions) HANAFunctions[each.toUpperCase()] = HANAFunctions[each]
Expand Down
3 changes: 3 additions & 0 deletions postgres/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
"@cap-js/db-service": "^3.0.1",
"pg": "^8.22.0"
},
"devDependencies": {
"pgvector": "^0.3.0"
},
"peerDependencies": {
"@sap/cds": "^10",
"@sap/cds-dk": "^10"
Expand Down
4 changes: 1 addition & 3 deletions postgres/pg-stack.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
# Use postgres/example user/password credentials
version: '3.1'

services:
db:
image: postgres:16-alpine
image: pgvector/pgvector:pg16
restart: always
environment:
POSTGRES_PASSWORD: postgres
Expand Down
132 changes: 132 additions & 0 deletions postgres/test/vector.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
const cds = require('../../test/cds.js')
const { Client } = require('pg')
const os = require('os')

describe('vector functions', () => {
const { expect } = cds.test(__dirname + '/../../test/compliance/resources')

// Setup pgvector extension and fake vector_embedding function
beforeAll(async () => {
const testDb = process.env.TRAVIS_JOB_ID || process.env.GITHUB_RUN_ID || os.userInfo().username || 'test_db'

// First connect to postgres db to create extension in template1 and ensure test db exists
const adminClient = new Client({
host: 'localhost',
port: 5432,
user: 'postgres',
password: 'postgres',
database: 'postgres'
})
await adminClient.connect()
await adminClient.query('CREATE EXTENSION IF NOT EXISTS vector')

// Create test database if it doesn't exist
try {
await adminClient.query(`CREATE DATABASE "${testDb}"`)
} catch (e) {
// Database might already exist
}
await adminClient.end()

// Now connect to the test database and set up extension + function
const client = new Client({
host: 'localhost',
port: 5432,
user: 'postgres',
password: 'postgres',
database: testDb
})
await client.connect()
await client.query('CREATE EXTENSION IF NOT EXISTS vector')
await client.query(`
CREATE OR REPLACE FUNCTION public.vector_embedding(model text, input text)
RETURNS text AS $$
SELECT CASE WHEN input IS NULL THEN NULL
ELSE (SELECT json_agg(sin(i * hashtext(input)::float8 / 1000))::text
FROM generate_series(1, 384) i)
END;
$$ LANGUAGE SQL IMMUTABLE;
`)
await client.end()
})

describe('COSINE_SIMILARITY', () => {
test('identical vectors return 1', async () => {
const res = await SELECT.from('complex.associations.Books')
.columns`cosine_similarity(cast('[1, 0, 0]' as cds.Vector), cast('[1, 0, 0]' as cds.Vector)) as similarity`
expect(res[0].similarity).to.eq(1)
})
test('orthogonal vectors return 0', async () => {
const res = await SELECT.from('complex.associations.Books')
.columns`cosine_similarity(cast('[1, 0, 0]' as cds.Vector), cast('[0, 1, 0]' as cds.Vector)) as similarity`
expect(res[0].similarity).to.eq(0)
})
test('opposite vectors return -1', async () => {
const res = await SELECT.from('complex.associations.Books')
.columns`cosine_similarity(cast('[1, 0, 0]' as cds.Vector), cast('[-1, 0, 0]' as cds.Vector)) as similarity`
expect(res[0].similarity).to.eq(-1)
})
})

describe('L2DISTANCE', () => {
test('identical vectors return 0', async () => {
const res = await SELECT.from('complex.associations.Books')
.columns`l2distance(cast('[1, 0, 0]' as cds.Vector), cast('[1, 0, 0]' as cds.Vector)) as distance`
expect(res[0].distance).to.eq(0)
})
test('unit vectors distance', async () => {
const res = await SELECT.from('complex.associations.Books')
.columns`l2distance(cast('[1, 0, 0]' as cds.Vector), cast('[0, 1, 0]' as cds.Vector)) as distance`
expect(Math.abs(res[0].distance - Math.SQRT2) < 0.0001).to.eq(true)
})
test('known distance', async () => {
const res = await SELECT.from('complex.associations.Books')
.columns`l2distance(cast('[0, 0, 0]' as cds.Vector), cast('[3, 4, 0]' as cds.Vector)) as distance`
expect(res[0].distance).to.eq(5)
})
})

describe('L2NORMALIZE', () => {
test('normalizes to unit length', async () => {
const res = await SELECT.from('complex.associations.Books')
.columns`l2normalize(cast('[3, 4, 0]' as cds.Vector)) as normalized`
const normalized = JSON.parse(res[0].normalized)
expect(Math.abs(normalized[0] - 0.6) < 0.0001).to.eq(true)
expect(Math.abs(normalized[1] - 0.8) < 0.0001).to.eq(true)
expect(normalized[2]).to.eq(0)
})
test('already normalized unchanged', async () => {
const res = await SELECT.from('complex.associations.Books')
.columns`l2normalize(cast('[1, 0, 0]' as cds.Vector)) as normalized`
const normalized = JSON.parse(res[0].normalized)
expect(normalized[0]).to.eq(1)
expect(normalized[1]).to.eq(0)
})
})

describe('VECTOR_EMBEDDING', () => {
test('computes embedding', async () => {
const res = await SELECT.from('complex.associations.Books')
.columns`VECTOR_EMBEDDING('model', title) as embedding`
.limit(1)
const embedding = JSON.parse(res[0].embedding)
expect(Array.isArray(embedding)).to.eq(true)
expect(embedding.length).to.eq(384)
})
test('deterministic', async () => {
const res = await SELECT.from('complex.associations.Books')
.columns`vector_embedding('model', 'test') as e1, vector_embedding('model', 'test') as e2`
expect(res[0].e1).to.eq(res[0].e2)
})
test('different inputs different outputs', async () => {
const res = await SELECT.from('complex.associations.Books')
.columns`vector_embedding('model', 'hello') as e1, vector_embedding('model', 'world') as e2`
expect(res[0].e1).to.not.eq(res[0].e2)
})
test('null returns null', async () => {
const res = await SELECT.from('complex.associations.Books')
.columns`vector_embedding('model', null) as embedding`
expect(res[0].embedding).to.eq(null)
})
})
})
Loading