@capgo/capacitor-fast-sql 8.0.37 → 8.0.38

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.
@@ -401,6 +401,7 @@ class FastSQL {
401
401
  // Check if already connected
402
402
  const existing = this.connections.get(options.database);
403
403
  if (existing) {
404
+ this.sharedConnections.add(options.database);
404
405
  return existing;
405
406
  }
406
407
  // Connect via native plugin
@@ -424,14 +425,22 @@ class FastSQL {
424
425
  * @param database - Database name to close
425
426
  */
426
427
  static async disconnect(database) {
428
+ var _a;
427
429
  const connection = this.connections.get(database);
428
430
  if (!connection) {
429
431
  throw new Error(`Database '${database}' is not connected`);
430
432
  }
433
+ if (((_a = this.connectionRetainers.get(database)) !== null && _a !== void 0 ? _a : 0) > 0) {
434
+ this.pendingDisconnects.add(database);
435
+ return;
436
+ }
431
437
  // Disconnect via native plugin
432
438
  await CapgoCapacitorFastSql.disconnect({ database });
433
439
  // Remove connection
434
440
  this.connections.delete(database);
441
+ this.sharedConnections.delete(database);
442
+ this.connectionRetainers.delete(database);
443
+ this.pendingDisconnects.delete(database);
435
444
  }
436
445
  /**
437
446
  * Get an existing connection
@@ -442,12 +451,50 @@ class FastSQL {
442
451
  static getConnection(database) {
443
452
  return this.connections.get(database) || null;
444
453
  }
454
+ /**
455
+ * Check whether a connection has been handed out more than once.
456
+ */
457
+ static isConnectionShared(database) {
458
+ return this.sharedConnections.has(database);
459
+ }
460
+ /**
461
+ * Temporarily retain a connection for helpers that share cached connections.
462
+ */
463
+ static retainConnection(database) {
464
+ var _a;
465
+ if (!this.connections.has(database)) {
466
+ throw new Error(`Database '${database}' is not connected`);
467
+ }
468
+ this.connectionRetainers.set(database, ((_a = this.connectionRetainers.get(database)) !== null && _a !== void 0 ? _a : 0) + 1);
469
+ }
470
+ /**
471
+ * Release a retained connection. Returns true when a pending disconnect closed it.
472
+ */
473
+ static async releaseConnection(database) {
474
+ var _a;
475
+ const retainers = (_a = this.connectionRetainers.get(database)) !== null && _a !== void 0 ? _a : 0;
476
+ if (retainers > 1) {
477
+ this.connectionRetainers.set(database, retainers - 1);
478
+ return false;
479
+ }
480
+ this.connectionRetainers.delete(database);
481
+ if (this.pendingDisconnects.has(database)) {
482
+ this.pendingDisconnects.delete(database);
483
+ await this.disconnect(database);
484
+ return true;
485
+ }
486
+ return false;
487
+ }
445
488
  /**
446
489
  * Close all open connections
447
490
  */
448
491
  static async disconnectAll() {
449
492
  const databases = Array.from(this.connections.keys());
450
- await Promise.all(databases.map((db) => this.disconnect(db)));
493
+ await Promise.all(databases.map((db) => {
494
+ this.connectionRetainers.delete(db);
495
+ this.pendingDisconnects.delete(db);
496
+ return this.disconnect(db);
497
+ }));
451
498
  }
452
499
  /**
453
500
  * Get list of all open database connections
@@ -459,6 +506,9 @@ class FastSQL {
459
506
  }
460
507
  }
461
508
  FastSQL.connections = new Map();
509
+ FastSQL.sharedConnections = new Set();
510
+ FastSQL.connectionRetainers = new Map();
511
+ FastSQL.pendingDisconnects = new Set();
462
512
 
463
513
  /**
464
514
  * Key-value storage backed by the FastSQL database connection.
@@ -643,6 +693,383 @@ class KeyValueStore {
643
693
  }
644
694
  }
645
695
 
696
+ const DEFAULT_STORE = 'default';
697
+ const VECTOR_TABLE = '__fast_sql_vector_store';
698
+ const VECTOR_META_TABLE = '__fast_sql_vector_store_meta';
699
+ /**
700
+ * SQLite-backed VectorStore for local RAG usage.
701
+ *
702
+ * The helper is optional and dependency-free: pass your own embeddings provider,
703
+ * or use precomputed embeddings with `embeddingDimension`. Multiple stores can
704
+ * share one database by choosing different `store` names.
705
+ */
706
+ class FastSQLVectorStore {
707
+ constructor(connection, options = {}) {
708
+ var _a;
709
+ this.initialized = false;
710
+ this.ownsConnection = false;
711
+ this.shouldDisconnectConnection = false;
712
+ this.closed = false;
713
+ this.connection = connection;
714
+ this.store = (_a = options.store) !== null && _a !== void 0 ? _a : DEFAULT_STORE;
715
+ this.embeddings = options.embeddings;
716
+ this.configuredEmbeddingDim = options.embeddingDimension;
717
+ this.embeddingDim = options.embeddingDimension;
718
+ }
719
+ static retainConnection(database) {
720
+ var _a;
721
+ this.connectionRefs.set(database, ((_a = this.connectionRefs.get(database)) !== null && _a !== void 0 ? _a : 0) + 1);
722
+ }
723
+ static releaseConnection(database) {
724
+ var _a;
725
+ const refs = (_a = this.connectionRefs.get(database)) !== null && _a !== void 0 ? _a : 0;
726
+ if (refs > 1) {
727
+ this.connectionRefs.set(database, refs - 1);
728
+ return false;
729
+ }
730
+ this.connectionRefs.delete(database);
731
+ this.ownedConnections.delete(database);
732
+ return true;
733
+ }
734
+ /**
735
+ * Open a vector store from database connection options.
736
+ */
737
+ static async open(options) {
738
+ const existing = FastSQL.getConnection(options.database);
739
+ const ownsExistingVectorConnection = existing && FastSQLVectorStore.connectionRefs.has(options.database);
740
+ const connection = ownsExistingVectorConnection ? existing : await FastSQL.connect(options);
741
+ const vectorStore = new FastSQLVectorStore(connection, options);
742
+ vectorStore.ownsConnection = true;
743
+ vectorStore.shouldDisconnectConnection = !existing || FastSQLVectorStore.ownedConnections.has(options.database);
744
+ FastSQLVectorStore.retainConnection(connection.getDatabaseName());
745
+ FastSQL.retainConnection(connection.getDatabaseName());
746
+ if (vectorStore.shouldDisconnectConnection) {
747
+ FastSQLVectorStore.ownedConnections.add(connection.getDatabaseName());
748
+ }
749
+ try {
750
+ await vectorStore.load();
751
+ return vectorStore;
752
+ }
753
+ catch (error) {
754
+ await vectorStore.unload();
755
+ throw error;
756
+ }
757
+ }
758
+ /**
759
+ * Create a vector store from an existing SQL connection.
760
+ * The caller keeps ownership of the connection.
761
+ */
762
+ static async fromConnection(connection, options = {}) {
763
+ const vectorStore = new FastSQLVectorStore(connection, options);
764
+ await vectorStore.load();
765
+ return vectorStore;
766
+ }
767
+ getStoreName() {
768
+ return this.store;
769
+ }
770
+ getDatabaseName() {
771
+ return this.connection.getDatabaseName();
772
+ }
773
+ async load() {
774
+ var _a, _b;
775
+ this.assertOpen();
776
+ if (this.initialized) {
777
+ return this;
778
+ }
779
+ await ((_b = (_a = this.embeddings) === null || _a === void 0 ? void 0 : _a.load) === null || _b === void 0 ? void 0 : _b.call(_a));
780
+ await this.ensureSchema();
781
+ await this.ensureStoreDimension();
782
+ this.initialized = true;
783
+ return this;
784
+ }
785
+ async unload() {
786
+ var _a, _b;
787
+ if (this.closed) {
788
+ return;
789
+ }
790
+ await ((_b = (_a = this.embeddings) === null || _a === void 0 ? void 0 : _a.unload) === null || _b === void 0 ? void 0 : _b.call(_a));
791
+ if (this.ownsConnection) {
792
+ const database = this.connection.getDatabaseName();
793
+ const releasedLastVectorStore = FastSQLVectorStore.releaseConnection(database);
794
+ const closedFromPendingDisconnect = await FastSQL.releaseConnection(database);
795
+ if (releasedLastVectorStore &&
796
+ this.shouldDisconnectConnection &&
797
+ !FastSQL.isConnectionShared(database) &&
798
+ !closedFromPendingDisconnect) {
799
+ await FastSQL.disconnect(database);
800
+ }
801
+ }
802
+ this.initialized = false;
803
+ this.closed = true;
804
+ }
805
+ async close() {
806
+ await this.unload();
807
+ }
808
+ async add(params) {
809
+ await this.ensureReady();
810
+ const { id = this.createId(), document, embedding, metadata } = params;
811
+ if (document === undefined && embedding === undefined) {
812
+ throw new Error('document and embedding cannot both be undefined');
813
+ }
814
+ const existing = await this.connection.query(`SELECT 1 FROM ${VECTOR_TABLE} WHERE store = ? AND id = ? LIMIT 1`, [
815
+ this.store,
816
+ id,
817
+ ]);
818
+ if (existing.length > 0) {
819
+ throw new Error(`id already exists: ${id}`);
820
+ }
821
+ const resolvedEmbedding = embedding !== null && embedding !== void 0 ? embedding : (await this.embedDocument(document));
822
+ await this.ensureEmbeddingDimension(resolvedEmbedding);
823
+ const now = Date.now();
824
+ await this.connection.run(`INSERT INTO ${VECTOR_TABLE} (store, id, document, embedding, metadata, created_at, updated_at)
825
+ VALUES (?, ?, ?, ?, ?, ?, ?)`, [
826
+ this.store,
827
+ id,
828
+ document !== null && document !== void 0 ? document : '',
829
+ this.encodeEmbedding(resolvedEmbedding),
830
+ this.encodeMetadata(metadata),
831
+ now,
832
+ now,
833
+ ]);
834
+ return id;
835
+ }
836
+ async update(params) {
837
+ var _a, _b;
838
+ await this.ensureReady();
839
+ const rows = await this.connection.query(`SELECT id, document, embedding, metadata FROM ${VECTOR_TABLE} WHERE store = ? AND id = ? LIMIT 1`, [this.store, params.id]);
840
+ if (rows.length === 0) {
841
+ throw new Error(`id not found: ${params.id}`);
842
+ }
843
+ const current = this.rowToGetResult(rows[0]);
844
+ const document = (_a = params.document) !== null && _a !== void 0 ? _a : current.document;
845
+ const embedding = (_b = params.embedding) !== null && _b !== void 0 ? _b : (params.document !== undefined ? await this.embedDocument(params.document) : current.embedding);
846
+ const metadata = params.metadata === undefined ? current.metadata : params.metadata;
847
+ await this.ensureEmbeddingDimension(embedding);
848
+ await this.connection.run(`UPDATE ${VECTOR_TABLE}
849
+ SET document = ?, embedding = ?, metadata = ?, updated_at = ?
850
+ WHERE store = ? AND id = ?`, [document, this.encodeEmbedding(embedding), this.encodeMetadata(metadata), Date.now(), this.store, params.id]);
851
+ }
852
+ async delete(params) {
853
+ await this.ensureReady();
854
+ const rows = await this.getStoreRows();
855
+ const ids = rows
856
+ .map((row) => this.rowToGetResult(row))
857
+ .filter(params.predicate)
858
+ .map((row) => row.id);
859
+ for (const id of ids) {
860
+ await this.connection.run(`DELETE FROM ${VECTOR_TABLE} WHERE store = ? AND id = ?`, [this.store, id]);
861
+ }
862
+ }
863
+ async query(params) {
864
+ await this.ensureReady();
865
+ const { queryText, queryEmbedding, nResults, predicate } = params;
866
+ if (queryText === undefined && queryEmbedding === undefined) {
867
+ throw new Error('queryText and queryEmbedding cannot both be undefined');
868
+ }
869
+ if (nResults !== undefined && nResults < 0) {
870
+ throw new Error('nResults cannot be negative');
871
+ }
872
+ const searchEmbedding = queryEmbedding !== null && queryEmbedding !== void 0 ? queryEmbedding : (await this.embedDocument(queryText));
873
+ await this.ensureEmbeddingDimension(searchEmbedding);
874
+ const results = (await this.getStoreRows())
875
+ .map((row) => {
876
+ const item = this.rowToGetResult(row);
877
+ return Object.assign(Object.assign({}, item), { similarity: this.cosineSimilarity(item.embedding, searchEmbedding) });
878
+ })
879
+ .filter(predicate !== null && predicate !== void 0 ? predicate : (() => true))
880
+ .sort((a, b) => b.similarity - a.similarity);
881
+ return nResults === undefined ? results : results.slice(0, nResults);
882
+ }
883
+ /**
884
+ * Delete only this logical vector store. Other stores in the same database stay intact.
885
+ */
886
+ async deleteVectorStore() {
887
+ await this.ensureReady();
888
+ await this.connection.run(`DELETE FROM ${VECTOR_TABLE} WHERE store = ?`, [this.store]);
889
+ await this.connection.run(`DELETE FROM ${VECTOR_META_TABLE} WHERE store = ?`, [this.store]);
890
+ this.embeddingDim = this.configuredEmbeddingDim;
891
+ if (this.embeddingDim !== undefined) {
892
+ await this.saveEmbeddingDimension();
893
+ }
894
+ }
895
+ async ensureReady() {
896
+ this.assertOpen();
897
+ if (!this.initialized) {
898
+ await this.load();
899
+ }
900
+ }
901
+ assertOpen() {
902
+ if (this.closed) {
903
+ throw new Error('Vector store is closed');
904
+ }
905
+ }
906
+ async ensureSchema() {
907
+ await this.connection.run(`CREATE TABLE IF NOT EXISTS ${VECTOR_TABLE} (
908
+ store TEXT NOT NULL,
909
+ id TEXT NOT NULL,
910
+ document TEXT NOT NULL,
911
+ embedding BLOB NOT NULL,
912
+ metadata TEXT DEFAULT NULL,
913
+ created_at INTEGER NOT NULL,
914
+ updated_at INTEGER NOT NULL,
915
+ PRIMARY KEY (store, id)
916
+ )`);
917
+ await this.connection.run(`CREATE INDEX IF NOT EXISTS ${VECTOR_TABLE}_store_idx ON ${VECTOR_TABLE} (store)`);
918
+ await this.connection.run(`CREATE TABLE IF NOT EXISTS ${VECTOR_META_TABLE} (
919
+ store TEXT PRIMARY KEY,
920
+ embedding_dim INTEGER NOT NULL
921
+ )`);
922
+ }
923
+ async ensureStoreDimension() {
924
+ const rows = await this.connection.query(`SELECT embedding_dim FROM ${VECTOR_META_TABLE} WHERE store = ? LIMIT 1`, [
925
+ this.store,
926
+ ]);
927
+ const storedDim = this.parseStoredDimension(rows[0]);
928
+ if (storedDim !== undefined && this.embeddingDim !== undefined && storedDim !== this.embeddingDim) {
929
+ throw new Error(`embedding dimension ${this.embeddingDim} does not match existing store dimension ${storedDim}`);
930
+ }
931
+ if (this.embeddingDim === undefined) {
932
+ this.embeddingDim = storedDim !== null && storedDim !== void 0 ? storedDim : (this.embeddings ? (await this.embedDocument('dummy')).length : undefined);
933
+ }
934
+ if (this.embeddingDim !== undefined) {
935
+ await this.saveEmbeddingDimension();
936
+ }
937
+ }
938
+ parseStoredDimension(row) {
939
+ if (!row) {
940
+ return undefined;
941
+ }
942
+ const value = row.embedding_dim;
943
+ if (typeof value === 'number') {
944
+ return value;
945
+ }
946
+ if (typeof value === 'string') {
947
+ const parsed = Number(value);
948
+ return Number.isFinite(parsed) ? parsed : undefined;
949
+ }
950
+ return undefined;
951
+ }
952
+ async saveEmbeddingDimension() {
953
+ if (this.embeddingDim === undefined) {
954
+ return;
955
+ }
956
+ await this.connection.run(`INSERT OR REPLACE INTO ${VECTOR_META_TABLE} (store, embedding_dim) VALUES (?, ?)`, [
957
+ this.store,
958
+ this.embeddingDim,
959
+ ]);
960
+ }
961
+ async ensureEmbeddingDimension(embedding) {
962
+ this.validateEmbedding(embedding);
963
+ if (this.embeddingDim === undefined) {
964
+ this.embeddingDim = embedding.length;
965
+ await this.saveEmbeddingDimension();
966
+ return;
967
+ }
968
+ if (embedding.length !== this.embeddingDim) {
969
+ throw new Error(`embedding dimension ${embedding.length} does not match vector store dimension ${this.embeddingDim}`);
970
+ }
971
+ }
972
+ async embedDocument(document) {
973
+ if (document === undefined) {
974
+ throw new Error('document is required when embedding is not provided');
975
+ }
976
+ if (!this.embeddings) {
977
+ throw new Error('embeddings provider is required for text operations');
978
+ }
979
+ return this.embeddings.embed(document);
980
+ }
981
+ validateEmbedding(embedding) {
982
+ if (!Array.isArray(embedding) || embedding.length === 0) {
983
+ throw new Error('embedding must be a non-empty number array');
984
+ }
985
+ for (const value of embedding) {
986
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
987
+ throw new Error('embedding values must be finite numbers');
988
+ }
989
+ }
990
+ }
991
+ async getStoreRows() {
992
+ return this.connection.query(`SELECT id, document, embedding, metadata FROM ${VECTOR_TABLE} WHERE store = ? ORDER BY updated_at DESC`, [this.store]);
993
+ }
994
+ rowToGetResult(row) {
995
+ const result = {
996
+ id: String(row.id),
997
+ document: row.document === null || row.document === undefined ? '' : String(row.document),
998
+ embedding: this.decodeEmbedding(row.embedding),
999
+ };
1000
+ const metadata = this.decodeMetadata(row.metadata);
1001
+ if (metadata !== undefined) {
1002
+ result.metadata = metadata;
1003
+ }
1004
+ return result;
1005
+ }
1006
+ encodeEmbedding(embedding) {
1007
+ const bytes = new Uint8Array(embedding.length * Float32Array.BYTES_PER_ELEMENT);
1008
+ const view = new DataView(bytes.buffer);
1009
+ embedding.forEach((value, index) => view.setFloat32(index * Float32Array.BYTES_PER_ELEMENT, value, true));
1010
+ return bytes;
1011
+ }
1012
+ decodeEmbedding(value) {
1013
+ if (!(value instanceof Uint8Array)) {
1014
+ throw new Error('stored embedding is not binary data');
1015
+ }
1016
+ if (value.byteLength % Float32Array.BYTES_PER_ELEMENT !== 0) {
1017
+ throw new Error('stored embedding has invalid byte length');
1018
+ }
1019
+ const view = new DataView(value.buffer, value.byteOffset, value.byteLength);
1020
+ const embedding = [];
1021
+ for (let offset = 0; offset < value.byteLength; offset += Float32Array.BYTES_PER_ELEMENT) {
1022
+ embedding.push(view.getFloat32(offset, true));
1023
+ }
1024
+ return embedding;
1025
+ }
1026
+ encodeMetadata(metadata) {
1027
+ return metadata ? JSON.stringify(metadata) : null;
1028
+ }
1029
+ decodeMetadata(value) {
1030
+ if (typeof value !== 'string' || value.length === 0) {
1031
+ return undefined;
1032
+ }
1033
+ try {
1034
+ const parsed = JSON.parse(value);
1035
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : undefined;
1036
+ }
1037
+ catch (_a) {
1038
+ return undefined;
1039
+ }
1040
+ }
1041
+ cosineSimilarity(a, b) {
1042
+ if (a.length !== b.length) {
1043
+ throw new Error(`embedding dimension ${b.length} does not match stored embedding dimension ${a.length}`);
1044
+ }
1045
+ let dot = 0;
1046
+ let normA = 0;
1047
+ let normB = 0;
1048
+ for (let i = 0; i < a.length; i++) {
1049
+ dot += a[i] * b[i];
1050
+ normA += a[i] * a[i];
1051
+ normB += b[i] * b[i];
1052
+ }
1053
+ if (normA === 0 || normB === 0) {
1054
+ return 0;
1055
+ }
1056
+ return dot / (Math.sqrt(normA) * Math.sqrt(normB));
1057
+ }
1058
+ createId() {
1059
+ const cryptoApi = globalThis.crypto;
1060
+ if (typeof (cryptoApi === null || cryptoApi === void 0 ? void 0 : cryptoApi.randomUUID) === 'function') {
1061
+ return cryptoApi.randomUUID();
1062
+ }
1063
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (char) => {
1064
+ const value = Math.floor(Math.random() * 16);
1065
+ const nibble = char === 'x' ? value : (value & 0x3) | 0x8;
1066
+ return nibble.toString(16);
1067
+ });
1068
+ }
1069
+ }
1070
+ FastSQLVectorStore.connectionRefs = new Map();
1071
+ FastSQLVectorStore.ownedConnections = new Set();
1072
+
646
1073
  const DEFAULT_SQLJS_URL = 'https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.8.0/sql-wasm.js';
647
1074
  const DEFAULT_WASM_URL = 'https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.8.0/sql-wasm.wasm';
648
1075
  /**
@@ -880,6 +1307,7 @@ var web = /*#__PURE__*/Object.freeze({
880
1307
 
881
1308
  exports.CapgoCapacitorFastSql = CapgoCapacitorFastSql;
882
1309
  exports.FastSQL = FastSQL;
1310
+ exports.FastSQLVectorStore = FastSQLVectorStore;
883
1311
  exports.KeyValueStore = KeyValueStore;
884
1312
  exports.NativeSQLConnection = NativeSQLConnection;
885
1313
  exports.WebSQLConnection = WebSQLConnection;