uql-orm 0.4.4 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,9 +1,33 @@
1
+ # Change Log
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
+
6
+ ## [0.4.5](https://github.com/rogerpadilla/uql/compare/uql-orm@0.4.0...uql-orm@0.4.5) (2026-03-14)
7
+
8
+
9
+ ### Features
10
+
11
+ * add vector search integration tests for `findMany` with `$sort: { $vector }` and update test infrastructure. ([c920cde](https://github.com/rogerpadilla/uql/commit/c920cde46dbb4985d07a1926dfc3ac845862244c))
12
+ * implement cursor-based stream with across all queriers and deprecate in favor of ([2d6c2ea](https://github.com/rogerpadilla/uql/commit/2d6c2ea0851d9c20ca9d092717087eaa65e5f937))
13
+ * upgrade to Vite 8 and TypeScript 6, removing redundant compiler options and `vite-tsconfig-paths` plugin. ([f81563c](https://github.com/rogerpadilla/uql/commit/f81563cfe5246bfa59966f08493fd9786a817e02))
14
+
15
+
16
+
17
+
18
+
1
19
  # Changelog
2
20
 
3
21
  All notable changes to this project will be documented in this file. Please add new changes to the top.
4
22
 
5
23
  date format is [yyyy-mm-dd]
6
24
 
25
+ ## [0.4.5] - 2026-03-14
26
+ ### Testing
27
+ - **Vector search integration tests**: Added 7 end-to-end tests for `findMany` with `$sort: { $vector }` against a real Postgres+pgvector database — covers cosine/L2 similarity ordering, `$project` distance projection, filter+sort combo, `$limit`, and empty-table edge case.
28
+ - Docker Postgres image switched to `pgvector/pgvector:pg18` for pgvector extension support.
29
+ - Test DDL generator now handles `vector`, `halfvec`, and `sparsevec` column types.
30
+
7
31
  ## [0.4.4] - 2026-03-14
8
32
  ### Dependencies
9
33
  - **Vite 7 → 8**: Upgraded to Vite 8 (powered by Rolldown), replacing the `vite-tsconfig-paths` plugin with Vite's built-in `resolve.tsconfigPaths` option.
@@ -1,181 +1,207 @@
1
- import { _ as _apply_decs_2203_r } from './cc-BEf4wTUm.js';
2
- import { randomUUID } from 'node:crypto';
3
- import { Id, Field, ManyToOne, Entity, OneToOne, OneToMany, ManyToMany } from '../entity/index.js';
4
- import '../type/index.js';
5
- import { raw, lowerFirst, upperFirst, getKeys } from '../util/index.js';
6
- import 'reflect-metadata';
1
+ import sqlstring from 'sqlstring-sqlite';
2
+ import { AbstractSqlDialect } from '../dialect/index.js';
3
+ import { getMeta } from '../entity/index.js';
4
+ import { hasKeys } from '../util/index.js';
5
+ import { AbstractSqlQuerier, AbstractQuerierPool } from '../querier/index.js';
7
6
 
8
- var _dec, _dec1, _dec2, _dec3, _dec4, _dec5, _dec6, /**
9
- * auto-generated primary-key (when the `onInsert` property is omitted).
10
- */ _init_id, /**
11
- * foreign-keys are really simple to specify with the `reference` property.
12
- */ _init_companyId, /**
13
- * The `Relation` wrapper type can be used in ESM projects for the relations to
14
- * avoid circular dependency issues.
15
- */ _init_company, _init_creatorId, _init_creator, /**
16
- * 'onInsert' property can be used to specify a custom mechanism for
17
- * obtaining the value of a field when inserting:
18
- */ _init_createdAt, /**
19
- * 'onUpdate' property can be used to specify a custom mechanism for
20
- * obtaining the value of a field when updating:
21
- */ _init_updatedAt, _initProto, _dec7, _initClass, _BaseEntity, _dec8, _dec9, _dec10, _init_name, _init_description, _init_kind, _initProto1, _dec11, _initClass1, _BaseEntity1, _dec12, _dec13, /**
22
- * an entity can specify its own ID Field and still inherit the others
23
- * columns/relations from its parent entity.
24
- */ _init_pk, _init_picture, _initProto2, _dec14, _initClass2, _BaseEntity2, _dec15, _dec16, _dec17, _dec18, _dec19, _init_name1, _init_email, _init_password, /**
25
- * `mappedBy` property can be a callback or a string (callback is useful for auto-refactoring).
26
- */ _init_profile, _init_users, _initProto3, _dec20, _initClass3, _dec21, _dec22, _init_id1, _init_name2, _initProto4, _dec23, _initClass4, _BaseEntity3, _dec24, _dec25, _dec26, _dec27, _init_name3, _init_description1, _init_parentLedgerId, _init_parentLedger, _initProto5, _dec28, _initClass5, _BaseEntity4, _dec29, _dec30, _dec31, /**
27
- * an entity can override the ID Field and still inherit the others
28
- * columns/relations from its parent entity.
29
- * 'onInsert' property can be used to specify a custom mechanism for
30
- * auto-generating the primary-key's value when inserting.
31
- */ _init_pk1, _init_name4, _init_description2, _initProto6, _dec32, _initClass6, _BaseEntity5, _dec33, _dec34, _dec35, _dec36, _dec37, _init_name5, _init_percentage, _init_categoryId, _init_category, _init_description3, _initProto7, _dec38, _initClass7, _BaseEntity6, _dec39, _dec40, _dec41, _init_name6, _init_measureUnits, /**
32
- * `onDelete` callback allows to specify which field will be used when deleting/querying this entity.
33
- */ _init_deletedAt, _initProto8, _dec42, _initClass8, _BaseEntity7, _dec43, _dec44, _dec45, _dec46, _init_name7, _init_categoryId1, _init_category1, _init_deletedAt1, _initProto9, _dec47, _initClass9, _BaseEntity8, _dec48, _dec49, _dec50, _init_name8, _init_address, _init_description4, _initProto10, _dec51, _initClass10, _BaseEntity9, _dec52, _dec53, _dec54, _dec55, _dec56, _dec57, _dec58, _dec59, _dec60, _dec61, _dec62, _dec63, _dec64, _dec65, _dec66, _init_name9, _init_description5, _init_code, _init_buyLedgerAccountId, _init_buyLedgerAccount, _init_saleLedgerAccountId, _init_saleLedgerAccount, _init_taxId, _init_tax, _init_measureUnitId, _init_measureUnit, _init_salePrice, _init_inventoryable, _init_tags, _init_tagsCount, _initProto11, _dec67, _initClass11, _BaseEntity10, _dec68, _dec69, _dec70, _init_name10, _init_items, _init_itemsCount, _initProto12, _dec71, _initClass12, _dec72, _dec73, _dec74, _init_id2, _init_itemId, _init_tagId, _initProto13, _dec75, _initClass13, _BaseEntity11, _dec76, _dec77, _dec78, _init_itemAdjustments, _init_date, _init_description6, _initProto14, _dec79, _initClass14, _BaseEntity12, _dec80, _dec81, _dec82, _dec83, _dec84, _dec85, _dec86, _dec87, _init_itemId1, _init_item, _init_number, _init_buyPrice, _init_storehouseId, _init_storehouse, _init_inventoryAdjustmentId, _init_inventoryAdjustment, _initProto15;
34
- _dec = Id(), _dec1 = Field({
35
- references: ()=>_Company
36
- }), _dec2 = ManyToOne({
37
- entity: ()=>_Company
38
- }), _dec3 = Field({
39
- references: ()=>_User
40
- }), _dec4 = ManyToOne({
41
- entity: ()=>_User
42
- }), _dec5 = Field({
43
- onInsert: Date.now
44
- }), _dec6 = Field({
45
- onUpdate: Date.now
46
- });
47
- /**
48
- * an `abstract` class can (optionally) be used as the base "template" for the entities
49
- * (so common fields' declaration is easily reused).
50
- */ class BaseEntity {
51
- static{
52
- ({ e: [_init_id, _init_companyId, _init_company, _init_creatorId, _init_creator, _init_createdAt, _init_updatedAt, _initProto] } = _apply_decs_2203_r(this, [
53
- [
54
- _dec,
55
- 0,
56
- "id"
57
- ],
58
- [
59
- _dec1,
60
- 0,
61
- "companyId"
62
- ],
63
- [
64
- _dec2,
65
- 0,
66
- "company"
67
- ],
68
- [
69
- _dec3,
70
- 0,
71
- "creatorId"
72
- ],
73
- [
74
- _dec4,
75
- 0,
76
- "creator"
77
- ],
78
- [
79
- _dec5,
80
- 0,
81
- "createdAt"
82
- ],
83
- [
84
- _dec6,
85
- 0,
86
- "updatedAt"
87
- ]
88
- ], []));
7
+ class SqliteDialect extends AbstractSqlDialect {
8
+ constructor(namingStrategy){
9
+ super('sqlite', namingStrategy);
10
+ }
11
+ addValue(values, value) {
12
+ if (value instanceof Date) {
13
+ value = value.getTime();
14
+ } else if (typeof value === 'boolean') {
15
+ value = value ? 1 : 0;
16
+ }
17
+ return super.addValue(values, value);
18
+ }
19
+ compare(ctx, entity, key, val, opts) {
20
+ if (key === '$text') {
21
+ const meta = getMeta(entity);
22
+ const search = val;
23
+ const fields = search.$fields.map((fKey)=>{
24
+ const field = meta.fields[fKey];
25
+ const columnName = this.resolveColumnName(fKey, field);
26
+ return this.escapeId(columnName);
27
+ });
28
+ const tableName = this.resolveTableName(entity, meta);
29
+ ctx.append(`${this.escapeId(tableName)} MATCH {${fields.join(' ')}} : `);
30
+ ctx.addValue(search.$value);
31
+ return;
32
+ }
33
+ super.compare(ctx, entity, key, val, opts);
89
34
  }
90
- constructor(){
91
- this.id = (_initProto(this), _init_id(this));
92
- this.companyId = _init_companyId(this);
93
- this.company = _init_company(this);
94
- this.creatorId = _init_creatorId(this);
95
- this.creator = _init_creator(this);
96
- this.createdAt = _init_createdAt(this);
97
- this.updatedAt = _init_updatedAt(this);
98
- }
99
- }
100
- let _Company;
101
- _dec7 = Entity(), _dec8 = Field(), _dec9 = Field(), _dec10 = Field({
102
- type: 'jsonb'
103
- });
104
- class Company extends (_BaseEntity = BaseEntity) {
105
- static{
106
- ({ e: [_init_name, _init_description, _init_kind, _initProto1], c: [_Company, _initClass] } = _apply_decs_2203_r(this, [
107
- [
108
- _dec8,
109
- 0,
110
- "name"
111
- ],
112
- [
113
- _dec9,
114
- 0,
115
- "description"
116
- ],
117
- [
118
- _dec10,
119
- 0,
120
- "kind"
121
- ]
122
- ], [
123
- _dec7
124
- ], _BaseEntity));
35
+ compareFieldOperator(ctx, entity, key, op, val, opts = {}) {
36
+ switch(op){
37
+ case '$elemMatch':
38
+ this.buildElemMatchCondition(ctx, entity, key, val, opts);
39
+ break;
40
+ case '$all':
41
+ {
42
+ // SQLite: Check JSON array contains all values using multiple json_each subqueries
43
+ const values = val;
44
+ const conditions = values.map((v)=>{
45
+ ctx.pushValue(JSON.stringify(v));
46
+ return `EXISTS (SELECT 1 FROM json_each(${this.escapeId(key)}) WHERE value = json(?))`;
47
+ }).join(' AND ');
48
+ ctx.append(`(${conditions})`);
49
+ break;
50
+ }
51
+ case '$size':
52
+ // SQLite: Check JSON array length
53
+ // e.g., json_array_length(roles) = 3, or json_array_length(roles) >= 2
54
+ this.buildSizeComparison(ctx, ()=>{
55
+ ctx.append('json_array_length(');
56
+ this.getComparisonKey(ctx, entity, key, opts);
57
+ ctx.append(')');
58
+ }, val);
59
+ break;
60
+ default:
61
+ super.compareFieldOperator(ctx, entity, key, op, val, opts);
62
+ }
63
+ }
64
+ /**
65
+ * Build $elemMatch condition for SQLite JSON arrays.
66
+ * Uses EXISTS with json_each and supports nested operators.
67
+ */ buildElemMatchCondition(ctx, _entity, key, match, opts) {
68
+ ctx.append('EXISTS (SELECT 1 FROM json_each(');
69
+ this.getComparisonKey(ctx, _entity, key, opts);
70
+ ctx.append(') WHERE ');
71
+ const conditions = [];
72
+ for (const [field, value] of Object.entries(match)){
73
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
74
+ // Value is an operator object
75
+ const ops = value;
76
+ for (const [op, opVal] of Object.entries(ops)){
77
+ conditions.push(this.buildJsonFieldOperator(ctx, field, op, opVal));
78
+ }
79
+ } else {
80
+ // Simple equality
81
+ ctx.pushValue(value);
82
+ conditions.push(`json_extract(value, '$.${field}') = ?`);
83
+ }
84
+ }
85
+ ctx.append(conditions.join(' AND '));
86
+ ctx.append(')');
87
+ }
88
+ /**
89
+ * Build a comparison condition for a JSON field with an operator.
90
+ */ buildJsonFieldOperator(ctx, field, op, value) {
91
+ return this.buildJsonFieldCondition(ctx, {
92
+ ...this.getBaseJsonConfig(),
93
+ fieldAccessor: (f)=>`json_extract(value, '$.${f}')`
94
+ }, field, op, value);
95
+ }
96
+ getJsonFieldConfig(escapedColumn, jsonPath) {
97
+ return {
98
+ ...this.getBaseJsonConfig(),
99
+ fieldAccessor: ()=>`json_extract(${escapedColumn}, '$.${jsonPath}')`
100
+ };
101
+ }
102
+ getBaseJsonConfig() {
103
+ return {
104
+ ...super.getBaseJsonConfig(),
105
+ numericCast: (expr)=>`CAST(${expr} AS REAL)`,
106
+ neExpr: (f, ph)=>`${f} IS NOT ${ph}`
107
+ };
108
+ }
109
+ upsert(ctx, entity, conflictPaths, payload) {
110
+ this.onConflictUpsert(ctx, entity, conflictPaths, payload, this.insert.bind(this));
111
+ }
112
+ formatJsonMerge(ctx, escapedCol, value) {
113
+ let expr = escapedCol;
114
+ if (hasKeys(value.$merge)) {
115
+ ctx.pushValue(JSON.stringify(value.$merge));
116
+ expr = `json_patch(COALESCE(${escapedCol}, '{}'), ?)`;
117
+ }
118
+ if (value.$unset?.length) {
119
+ const paths = value.$unset.map((k)=>`'$.${this.escapeJsonKey(k)}'`).join(', ');
120
+ expr = `json_remove(${expr}, ${paths})`;
121
+ }
122
+ ctx.append(`${escapedCol} = ${expr}`);
125
123
  }
126
124
  static{
127
- _initClass();
125
+ /** sqlite-vec distance functions. */ this.VECTOR_FNS = {
126
+ cosine: 'vec_distance_cosine',
127
+ l2: 'vec_distance_L2',
128
+ hamming: 'vec_distance_hamming'
129
+ };
128
130
  }
129
- constructor(...args){
130
- super(...args), this.name = (_initProto1(this), _init_name(this)), this.description = _init_description(this), this.kind = _init_kind(this);
131
+ /** Emit a sqlite-vec distance function: `vec_distance_<metric>(col, ?)`. */ appendVectorSort(ctx, meta, key, search) {
132
+ this.appendFunctionVectorSort(ctx, meta, key, search, SqliteDialect.VECTOR_FNS, 'SQLite');
133
+ }
134
+ escape(value) {
135
+ return sqlstring.escape(value);
131
136
  }
132
137
  }
133
- let _Profile;
134
- _dec11 = Entity({
135
- name: 'user_profile'
136
- }), _dec12 = Id(), _dec13 = Field({
137
- name: 'image'
138
- });
139
- class Profile extends (_BaseEntity1 = BaseEntity) {
140
- static{
141
- ({ e: [_init_pk, _init_picture, _initProto2], c: [_Profile, _initClass1] } = _apply_decs_2203_r(this, [
142
- [
143
- _dec12,
144
- 0,
145
- "pk"
146
- ],
147
- [
148
- _dec13,
149
- 0,
150
- "picture"
151
- ]
152
- ], [
153
- _dec11
154
- ], _BaseEntity1));
138
+
139
+ class AbstractSqliteQuerier extends AbstractSqlQuerier {
140
+ buildUpdateResult(changes, lastInsertRowid) {
141
+ const firstId = lastInsertRowid ? Number(lastInsertRowid) - (changes - 1) : undefined;
142
+ const ids = firstId ? Array(changes).fill(firstId).map((i, index)=>i + index) : [];
143
+ return {
144
+ changes,
145
+ ids,
146
+ firstId
147
+ };
155
148
  }
156
- static{
157
- _initClass1();
149
+ }
150
+
151
+ class SqliteQuerier extends AbstractSqliteQuerier {
152
+ constructor(db, extra){
153
+ super(new SqliteDialect(extra?.namingStrategy), extra), this.db = db, this.extra = extra;
158
154
  }
159
- constructor(...args){
160
- super(...args), this.pk = (_initProto2(this), _init_pk(this)), this.picture = _init_picture(this);
155
+ async internalAll(query, values) {
156
+ return this.db.prepare(query).all(values || []);
157
+ }
158
+ async *internalStream(query, values) {
159
+ for (const row of this.db.prepare(query).iterate(values || [])){
160
+ yield row;
161
+ }
162
+ }
163
+ async internalRun(query, values) {
164
+ const { changes, lastInsertRowid } = this.db.prepare(query).run(values || []);
165
+ return this.buildUpdateResult(changes, lastInsertRowid);
166
+ }
167
+ async internalRelease() {
168
+ if (this.hasOpenTransaction) {
169
+ throw TypeError('pending transaction');
170
+ }
171
+ // no-op
161
172
  }
162
173
  }
163
- let _User;
164
- _dec14 = Entity(), _dec15 = Field(), _dec16 = Field({
165
- updatable: false
166
- }), _dec17 = Field({
167
- eager: false
168
- }), _dec18 = OneToOne({
169
- entity: ()=>_Profile,
170
- mappedBy: (profile)=>profile.creator,
171
- cascade: true
172
- }), _dec19 = OneToMany({
173
- entity: ()=>_User,
174
- mappedBy: 'creator'
175
- });
176
- class User extends (_BaseEntity2 = BaseEntity) {
177
- static{
178
- ({ e: [_init_name1, _init_email, _init_password, _init_profile, _init_users, _initProto3], c: [_User, _initClass2] } = _apply_decs_2203_r(this, [
174
+
175
+ class Sqlite3QuerierPool extends AbstractQuerierPool {
176
+ constructor(filename, opts, extra){
177
+ super('sqlite', extra), this.filename = filename, this.opts = opts;
178
+ }
179
+ async getQuerier() {
180
+ if (!this.querier) {
181
+ let db;
182
+ if (typeof Bun !== 'undefined') {
183
+ const { Database: BunDatabase } = await import('bun:sqlite');
184
+ const bunDb = new BunDatabase(this.filename, this.opts);
185
+ bunDb.run('PRAGMA journal_mode = WAL');
186
+ db = bunDb;
187
+ } else {
188
+ const { default: BetterSqlite3 } = await import('better-sqlite3');
189
+ db = new BetterSqlite3(this.filename, this.opts);
190
+ db.pragma('journal_mode = WAL');
191
+ }
192
+ this.querier = new SqliteQuerier(db, this.extra);
193
+ }
194
+ return this.querier;
195
+ }
196
+ async end() {
197
+ await this.querier?.db.close();
198
+ this.querier = undefined;
199
+ }
200
+ }
201
+
202
+ export { Sqlite3QuerierPool, SqliteDialect, SqliteQuerier };
203
+ //# sourceMappingURL=uql-browser.min.js.map
204
+ 1, _init_email, _init_password, _init_profile, _init_users, _initProto3], c: [_User, _initClass2] } = _apply_decs_2203_r(this, [
179
205
  [
180
206
  _dec15,
181
207
  0,
@@ -772,6 +798,42 @@ class ItemAdjustment extends (_BaseEntity12 = BaseEntity) {
772
798
  super(...args), this.itemId = (_initProto15(this), _init_itemId1(this)), this.item = _init_item(this), this.number = _init_number(this), this.buyPrice = _init_buyPrice(this), this.storehouseId = _init_storehouseId(this), this.storehouse = _init_storehouse(this), this.inventoryAdjustmentId = _init_inventoryAdjustmentId(this), this.inventoryAdjustment = _init_inventoryAdjustment(this);
773
799
  }
774
800
  }
801
+ let _VectorItem;
802
+ _dec88 = Entity(), _dec89 = Id(), _dec90 = Field(), _dec91 = Field({
803
+ type: 'vector',
804
+ dimensions: 3
805
+ });
806
+ class VectorItem {
807
+ static{
808
+ ({ e: [_init_id3, _init_name11, _init_vec, _initProto16], c: [_VectorItem, _initClass15] } = _apply_decs_2203_r(this, [
809
+ [
810
+ _dec89,
811
+ 0,
812
+ "id"
813
+ ],
814
+ [
815
+ _dec90,
816
+ 0,
817
+ "name"
818
+ ],
819
+ [
820
+ _dec91,
821
+ 0,
822
+ "vec"
823
+ ]
824
+ ], [
825
+ _dec88
826
+ ]));
827
+ }
828
+ static{
829
+ _initClass15();
830
+ }
831
+ constructor(){
832
+ this.id = (_initProto16(this), _init_id3(this));
833
+ this.name = _init_name11(this);
834
+ this.vec = _init_vec(this);
835
+ }
836
+ }
775
837
 
776
838
  const holder = globalThis;
777
839
  const metaKey = Symbol.for('uql-orm/entity/decorator');
@@ -960,14 +1022,14 @@ function getDdlForTable(entity, querier, primaryKeyType) {
960
1022
  let propSql = querier.dialect.escapeId(field.name) + ' ';
961
1023
  if (field.isId) {
962
1024
  propSql += field.onInsert ? `${insertableIdType} PRIMARY KEY` : primaryKeyType;
1025
+ } else if (field.type === 'vector' || field.type === 'halfvec' || field.type === 'sparsevec') {
1026
+ propSql += field.dimensions ? `${field.type}(${field.dimensions})` : field.type;
1027
+ } else if (field.type === Number) {
1028
+ propSql += 'BIGINT';
1029
+ } else if (field.type === Date) {
1030
+ propSql += 'TIMESTAMP';
963
1031
  } else {
964
- if (field.type === Number) {
965
- propSql += 'BIGINT';
966
- } else if (field.type === Date) {
967
- propSql += 'TIMESTAMP';
968
- } else {
969
- propSql += defaultType;
970
- }
1032
+ propSql += defaultType;
971
1033
  }
972
1034
  return propSql;
973
1035
  });
@@ -5380,5 +5442,5 @@ const hooks = {
5380
5442
  afterAll
5381
5443
  };
5382
5444
 
5383
- export { BaseEntity, _Company as Company, _InventoryAdjustment as InventoryAdjustment, _Item as Item, _ItemAdjustment as ItemAdjustment, _ItemTag as ItemTag, _LedgerAccount as LedgerAccount, _MeasureUnit as MeasureUnit, _MeasureUnitCategory as MeasureUnitCategory, _Profile as Profile, _Storehouse as Storehouse, _Tag as Tag, _Tax as Tax, _TaxCategory as TaxCategory, _User as User, _UserWithNonUpdatableId as UserWithNonUpdatableId, clearTables, createSpec, createTables, dropTables };
5445
+ export { BaseEntity, _Company as Company, _InventoryAdjustment as InventoryAdjustment, _Item as Item, _ItemAdjustment as ItemAdjustment, _ItemTag as ItemTag, _LedgerAccount as LedgerAccount, _MeasureUnit as MeasureUnit, _MeasureUnitCategory as MeasureUnitCategory, _Profile as Profile, _Storehouse as Storehouse, _Tag as Tag, _Tax as Tax, _TaxCategory as TaxCategory, _User as User, _UserWithNonUpdatableId as UserWithNonUpdatableId, _VectorItem as VectorItem, clearTables, createSpec, createTables, dropTables };
5384
5446
  //# sourceMappingURL=uql-browser.min.js.map