Skip to content
Open
Show file tree
Hide file tree
Changes from 62 commits
Commits
Show all changes
74 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
841f92e
Merge branch 'main' into feat/vector-simplified
vkozyura Jul 21, 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
12 changes: 11 additions & 1 deletion package-lock.json

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

10 changes: 10 additions & 0 deletions postgres/lib/PostgresService.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const cds = require('@sap/cds')
const crypto = require('crypto')
const { Writable, Readable } = require('stream')
const sessionVariableMap = require('./session.json')
const pgvector = require('pgvector/pg');
Comment thread
vkozyura marked this conversation as resolved.
Outdated

const LOG = cds.log('sql|db')

Expand Down Expand Up @@ -49,6 +50,14 @@ class PostgresService extends SQLService {
try {
const dbc = new Client({ ...credentials, ...clientOptions })
await dbc.connect()
// cds.Vector support for PG
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)
}

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.

This should not be ran every create. This might not even need to run at all depending on how the postgres build task is deploying the model.

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.

commented out with a revisit containing possible options - pg tests are currently disabled

dbc.open = true
dbc.on('end', () => { dbc.open = false })
return dbc
Expand Down Expand Up @@ -535,6 +544,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: 2 additions & 1 deletion postgres/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
},
"dependencies": {
"@cap-js/db-service": "^3.0.1",
"pg": "^8"
"pg": "^8",
"pgvector": "^0.2.1"
Comment thread
vkozyura marked this conversation as resolved.
Outdated
},
"peerDependencies": {
"@sap/cds": "^10",
Expand Down
5 changes: 4 additions & 1 deletion postgres/pg-stack.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ version: '3.1'

services:
db:
image: postgres:16-alpine
image: pgvector/pgvector:pg16
restart: always
environment:
POSTGRES_PASSWORD: postgres
LANG: en_US.utf8
LC_COLLATE: en_US.utf8
LC_CTYPE: en_US.utf8
Comment thread
vkozyura marked this conversation as resolved.
Outdated
ports:
- '5432:5432'
command: ['postgres', '-c', 'log_statement=all']
Expand Down
5 changes: 5 additions & 0 deletions sqlite/lib/SQLiteService.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const $session = Symbol('dbc.session')
const sessionVariableMap = require('./session.json') // Adjust the path as necessary for your project
const convStrm = require('stream/consumers')
const { Readable } = require('stream')
const { cosineSimilarity, l2Distance, l2Normalize, hashEmbedding, toFloatArray, fromFloatArray } = require('./vector_handling')

const keywords = cds.compiler.to.sql.sqlite.keywords
// keywords come as array
Expand Down Expand Up @@ -46,6 +47,10 @@ class SQLiteService extends SQLService {
dbc.function('hour', deterministic, d => d === null ? null : toDate(d, true).getUTCHours())
dbc.function('minute', deterministic, d => d === null ? null : toDate(d, true).getUTCMinutes())
dbc.function('second', deterministic, d => d === null ? null : toDate(d, true).getUTCSeconds())
dbc.function('COSINE_SIMILARITY', deterministic, (a, b) => cosineSimilarity(toFloatArray(a), toFloatArray(b)))
dbc.function('L2DISTANCE', deterministic, (a, b) => l2Distance(toFloatArray(a), toFloatArray(b)))
dbc.function('L2NORMALIZE', deterministic, v => v == null ? null : fromFloatArray(l2Normalize(toFloatArray(v)), v))
dbc.function('VECTOR_EMBEDDING', deterministic, (model, text) => text == null ? null : JSON.stringify(hashEmbedding(String(text))))
if (database !== ':memory:') dbc.pragma?.('journal_mode = WAL') || dbc.exec('PRAGMA journal_mode = WAL')
return dbc
} catch (err) {
Expand Down
92 changes: 92 additions & 0 deletions sqlite/lib/vector_handling/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
function cosineSimilarity(a, b) {
if (a == null || b == null) return null
let dot = 0, normA = 0, normB = 0
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i]
normA += a[i] * a[i]
normB += b[i] * b[i]
}
const denom = Math.sqrt(normA) * Math.sqrt(normB)
return denom === 0 ? 0 : dot / denom
}

function l2Distance(a, b) {
if (a == null || b == null) return null
let sum = 0
for (let i = 0; i < a.length; i++) {
const diff = a[i] - b[i]
sum += diff * diff
}
return Math.sqrt(sum)
}

function l2Normalize(v) {
if (v == null) return null
let norm = 0
for (let i = 0; i < v.length; i++) norm += v[i] * v[i]
if (norm === 0) return v
norm = Math.sqrt(norm)
for (let i = 0; i < v.length; i++) v[i] /= norm
return v
}

/**
* Deterministic synchronous hash-based embedding function for SQLite.
* NOTE: If a synchronous embedding library becomes available for Node.js,
* it can be integrated here to replace the hash-based implementation.
*/
function hashEmbedding(text, dimensions = 384) {
if (text == null) return null
const vector = new Float32Array(dimensions)
const normalized = text.toLowerCase()
const ngramSize = 3

if (normalized.length >= ngramSize) {
for (let i = 0; i <= normalized.length - ngramSize; i++)
project(ngramHash(normalized, i, ngramSize), vector, dimensions)
} else {
for (let i = 0; i < normalized.length; i++)
project(normalized.charCodeAt(i), vector, dimensions)
}
return Array.from(l2Normalize(vector))
}

function ngramHash(text, start, len) {
let hash = 0x811c9dc5
for (let i = start; i < start + len; i++) {
hash ^= text.charCodeAt(i)
hash = Math.imul(hash, 0x01000193)
}
return hash
}

function project(hash, vector, dimensions) {
for (let band = 0; band < 4; band++) {
const h = rehash(hash, band)
vector[Math.abs(h % dimensions)] += ((h >>> 16) & 1) === 0 ? 1.0 : -1.0
}
}

function rehash(hash, band) {
let h = hash ^ Math.imul(band, 0x9e3779b9)
h ^= h >>> 16
h = Math.imul(h, 0x45d9f3b)
h ^= h >>> 16
return h
}

function toFloatArray(vector) {
if (vector == null) return null
if (vector instanceof Float32Array) return Array.from(vector)
if (Buffer.isBuffer(vector)) return JSON.parse(vector.toString('utf8'))
if (vector instanceof Uint8Array) return JSON.parse(Buffer.from(vector).toString('utf8'))
if (typeof vector === 'string') return JSON.parse(vector)
if (Array.isArray(vector)) return vector
throw new Error(`Unsupported vector type: ${typeof vector}`)
}

function fromFloatArray(arr, original) {
return original instanceof Float32Array ? new Float32Array(arr) : JSON.stringify(arr)
}

module.exports = { cosineSimilarity, l2Distance, l2Normalize, hashEmbedding, toFloatArray, fromFloatArray }
Comment thread
vkozyura marked this conversation as resolved.
Outdated
90 changes: 90 additions & 0 deletions test/compliance/functions.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
const cds = require('../cds.js')

const isHana = () => cds.db?.options?.impl === '@cap-js/hana'
Comment thread
vkozyura marked this conversation as resolved.
Outdated

describe('functions', () => {
const { expect, data } = cds.test(__dirname + '/resources')
data.autoIsolation(true)
Expand Down Expand Up @@ -229,6 +231,26 @@ describe('functions', () => {
throw new Error('not supported')
})
})
describe('COSINE_SIMILARITY', () => {
test('identical vectors return 1', async () => {
if (isHana()) return // HANA Express doesn't support REAL_VECTOR
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 () => {
if (isHana()) return
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 () => {
if (isHana()) return
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('COSH', () => {
test.skip('missing', () => {
throw new Error('not supported')
Expand Down Expand Up @@ -535,6 +557,45 @@ describe('functions', () => {
throw new Error('not supported')
})
})
describe('L2DISTANCE', () => {
test('identical vectors return 0', async () => {
if (isHana()) return
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 () => {
if (isHana()) return
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 () => {
if (isHana()) return
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 () => {
if (isHana()) return
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 () => {
if (isHana()) return
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('LAG', () => {
test.skip('missing', () => {
throw new Error('not supported')
Expand Down Expand Up @@ -1213,6 +1274,35 @@ describe('functions', () => {
throw new Error('not supported')
})
})
describe('VECTOR_EMBEDDING', () => {
test('computes embedding', async () => {
if (isHana()) return
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 () => {
if (isHana()) return
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 () => {
if (isHana()) return
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 () => {
if (isHana()) return
const res = await SELECT.from('complex.associations.Books')
.columns`vector_embedding('model', null) as embedding`
expect(res[0].embedding).to.eq(null)
})
})
describe('WEEK', () => {
test.skip('missing', () => {
throw new Error('not supported')
Expand Down
Loading