muya 2.3.5 → 2.4.1
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/esm/sqlite/__tests__/table.test.js +1 -1
- package/esm/sqlite/create-sqlite.js +1 -1
- package/esm/sqlite/table/table.js +12 -12
- package/package.json +1 -1
- package/src/sqlite/__tests__/table.test.ts +31 -0
- package/src/sqlite/__tests__/use-sqlite.test.tsx +49 -0
- package/src/sqlite/create-sqlite.ts +4 -4
- package/src/sqlite/table/table.ts +19 -10
- package/src/sqlite/table/table.types.ts +1 -1
- package/types/sqlite/table/table.types.d.ts +1 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
import{bunMemoryBackend as l}from"../table/bun-backend";import{createTable as c}from"../table/table";describe("table",()=>{let i=l(),t,r;beforeEach(async()=>{i=l(),t=await c({backend:i,tableName:"TestTable",key:"name"}),r=await c({backend:i,tableName:"TestTableNested",key:"info.name",indexes:["info.age","info.city"]})}),it("should set and get items",async()=>{const e=await t.set({name:"Alice",age:30,city:"Paris"});expect(e.key).toBe("Alice"),expect(e.op).toBe("insert");const o=await t.get("Alice");expect(o).toEqual({name:"Alice",age:30,city:"Paris"});const a=await t.set({name:"Alice",age:31,city:"Paris"});expect(a.key).toBe("Alice"),expect(a.op).toBe("update");const n=await t.get("Alice");expect(n).toEqual({name:"Alice",age:31,city:"Paris"})}),it("should set and get nested key",async()=>{const e=await r.set({info:{name:"Bob",age:25,city:"London"}});expect(e.key).toBe("Bob"),expect(e.op).toBe("insert");const o=await r.get("Bob");expect(o).toEqual({info:{name:"Bob",age:25,city:"London"}});const a=await r.set({info:{name:"Bob",age:26,city:"London"}});expect(a.key).toBe("Bob"),expect(a.op).toBe("update");const n=await r.get("Bob");expect(n).toEqual({info:{name:"Bob",age:26,city:"London"}});const s=[];for await(const d of r.search({where:{info:{city:{like:"London"}}}}))s.push(d);expect(s.length).toBe(1)}),it("should count items and count with where",async()=>{await t.set({name:"Alice",age:30,city:"Paris"}),await t.set({name:"Bob",age:25,city:"London"}),expect(await t.count()).toBe(2),expect(await t.count({where:{city:"Paris"}})).toBe(1)}),it("should search with ordering, limit and offset",async()=>{const e=[{name:"Alice",age:30,city:"Paris"},{name:"Bob",age:25,city:"London"},{name:"Carol",age:35,city:"Berlin"}];for(const s of e)await t.set(s);const o=[];for await(const s of t.search({sortBy:"age",order:"asc"}))o.push(s);expect(o.map(s=>s.name)).toEqual(["Bob","Alice","Carol"]);const a=[];for await(const s of t.search({sortBy:"age",order:"asc",limit:2}))a.push(s);expect(a.map(s=>s.name)).toEqual(["Bob","Alice"]);const n=[];for await(const s of t.search({sortBy:"age",order:"asc",offset:1,limit:2}))n.push(s);expect(n.map(s=>s.name)).toEqual(["Alice","Carol"])}),it("should deleteBy where clause",async()=>{await t.set({name:"Dave",age:40,city:"NY"}),await t.set({name:"Eve",age:45,city:"NY"}),await t.set({name:"Frank",age:50,city:"LA"}),expect(await t.count()).toBe(3),await t.deleteBy({city:"NY"}),expect(await t.count()).toBe(1),expect(await t.get("Frank")).toEqual({name:"Frank",age:50,city:"LA"}),expect(await t.get("Dave")).toBeUndefined()}),it("should use selector in get and search",async()=>{await t.set({name:"Gary",age:60,city:"SF"});const e=await t.get("Gary",({age:a})=>a);expect(e).toBe(60);const o=[];for await(const a of t.search({select:({city:n})=>n}))o.push(a);expect(o).toEqual(["SF"])}),it("should delete items by key",async()=>{await t.set({name:"Helen",age:28,city:"Rome"}),expect(await t.get("Helen")).toBeDefined(),await t.delete("Helen"),expect(await t.get("Helen")).toBeUndefined()}),it("should test search with 1000 items",async()=>{const e=[];for(let a=0;a<1e3;a++)e.push({name:`Person${a}`,age:Math.floor(Math.random()*100),city:"City"+a%10});for(const a of e)await t.set(a);const o=[];for await(const a of t.search({sortBy:"age",order:"asc",limit:100}))o.push(a);expect(o.length).toBe(100)}),it("should handle operations on an empty table",async()=>{expect(await t.count()).toBe(0),expect(await t.get("NonExistent")).toBeUndefined();const e=[];for await(const o of t.search({sortBy:"age",order:"asc"}))e.push(o);expect(e.length).toBe(0)}),it("should handle duplicate keys gracefully",async()=>{await t.set({name:"Alice",age:30,city:"Paris"}),await t.set({name:"Alice",age:35,city:"Berlin"});const e=await t.get("Alice");expect(e).toEqual({name:"Alice",age:35,city:"Berlin"})}),it("should handle edge cases in selectors",async()=>{await t.set({name:"Charlie",age:40,city:"NY"});const e=await t.get("Charlie",()=>null);expect(e).toBeNull();const o=await t.get("Charlie",()=>{});expect(o).toBeUndefined()}),it("should clear the table",async()=>{await t.set({name:"Alice",age:30,city:"Paris"}),await t.set({name:"Bob",age:25,city:"London"}),expect(await t.count()).toBe(2),await t.clear(),expect(await t.count()).toBe(0)}),it("should use fts index",async()=>{const e=await c({backend:i,tableName:"TestTableFTS",key:"id",indexes:["fts:content"]});await e.set({id:"1",content:"The \u010Coho brown fox"}),await e.set({id:"2",content:"jumps over the lazy dog"}),await e.set({id:"3",content:"hello world"});const o=[];for await(const n of e.search({where:{content:{fts:["coho","fox"]}}}))o.push(n);expect(o.length).toBe(1),expect(o[0].id).toBe("1");const a=[];for await(const n of e.search({where:{content:{fts:["the"]}}}))a.push(n);expect(a.length).toBe(2)}),it("should use fts index with custom tokenizer options",async()=>{const e=await c({backend:i,tableName:"TestTableFTS2",key:"id",indexes:[{type:"fts",path:"content",tokenizer:{removeDiacritics:0,tokenChars:"abc",separators:"xyz"}}]});await e.set({id:"1",content:"abc xyz"}),await e.set({id:"2",content:"abc"}),await e.set({id:"3",content:"other"});const o=[];for await(const n of e.search({where:{content:{fts:["abc"]}}}))o.push(n);expect(o.length).toBe(2);const a=[];for await(const n of e.search({where:{content:{fts:["xyz"]}}}))a.push(n);expect(a.length).toBe(1),expect(a[0].id).toBe("1")}),it("should support multiple fts fields",async()=>{const e=await c({backend:i,tableName:"TestTableFTSMulti",key:"id",indexes:[{type:"fts",path:"title"},{type:"fts",path:"body"}]});await e.set({id:"1",title:"Hello",body:"World"}),await e.set({id:"2",title:"Foo",body:"Bar"}),await e.set({id:"3",title:"Hello",body:"Bar"});const o=[];for await(const n of e.search({where:{title:{fts:["Hello"]}}}))o.push(n);expect(o.length).toBe(2);const a=[];for await(const n of e.search({where:{body:{fts:["Bar"]}}}))a.push(n);expect(a.length).toBe(2)}),it("should handle fts search with no results",async()=>{const e=await c({backend:i,tableName:"TestTableFTSNone",key:"id",indexes:["fts:content"]});await e.set({id:"1",content:"foo bar"});const o=[];for await(const a of e.search({where:{content:{fts:["notfound"]}}}))o.push(a);expect(o.length).toBe(0)}),it("should custom fn fts index",async()=>{const e=await c({backend:i,tableName:"TestTableFTS",key:"id",indexes:[{type:"fts",path:"content",tokenizer:{removeDiacritics:1}}]});await e.set({id:"1",content:"The \u010Coho brown fox"}),await e.set({id:"2",content:"jumps over the lazy dog"}),await e.set({id:"3",content:"hello world"});const o=[];for await(const n of e.search({where:{content:{fts:["coho","fox"]}}}))o.push(n);expect(o.length).toBe(1),expect(o[0].id).toBe("1");const a=[];for await(const n of e.search({where:{content:{fts:["the"]}}}))a.push(n);expect(a.length).toBe(2)})});
|
|
1
|
+
import{bunMemoryBackend as l}from"../table/bun-backend";import{createTable as c}from"../table/table";describe("table",()=>{let i=l(),t,r;beforeEach(async()=>{i=l(),t=await c({backend:i,tableName:"TestTable",key:"name"}),r=await c({backend:i,tableName:"TestTableNested",key:"info.name",indexes:["info.age","info.city"]})}),it("should set and get items",async()=>{const e=await t.set({name:"Alice",age:30,city:"Paris"});expect(e.key).toBe("Alice"),expect(e.op).toBe("insert");const o=await t.get("Alice");expect(o).toEqual({name:"Alice",age:30,city:"Paris"});const a=await t.set({name:"Alice",age:31,city:"Paris"});expect(a.key).toBe("Alice"),expect(a.op).toBe("update");const n=await t.get("Alice");expect(n).toEqual({name:"Alice",age:31,city:"Paris"})}),it("should set and get nested key",async()=>{const e=await r.set({info:{name:"Bob",age:25,city:"London"}});expect(e.key).toBe("Bob"),expect(e.op).toBe("insert");const o=await r.get("Bob");expect(o).toEqual({info:{name:"Bob",age:25,city:"London"}});const a=await r.set({info:{name:"Bob",age:26,city:"London"}});expect(a.key).toBe("Bob"),expect(a.op).toBe("update");const n=await r.get("Bob");expect(n).toEqual({info:{name:"Bob",age:26,city:"London"}});const s=[];for await(const d of r.search({where:{info:{city:{like:"London"}}}}))s.push(d);expect(s.length).toBe(1)}),it("should count items and count with where",async()=>{await t.set({name:"Alice",age:30,city:"Paris"}),await t.set({name:"Bob",age:25,city:"London"}),expect(await t.count()).toBe(2),expect(await t.count({where:{city:"Paris"}})).toBe(1)}),it("should search with ordering, limit and offset",async()=>{const e=[{name:"Alice",age:30,city:"Paris"},{name:"Bob",age:25,city:"London"},{name:"Carol",age:35,city:"Berlin"}];for(const s of e)await t.set(s);const o=[];for await(const s of t.search({sortBy:"age",order:"asc"}))o.push(s);expect(o.map(s=>s.name)).toEqual(["Bob","Alice","Carol"]);const a=[];for await(const s of t.search({sortBy:"age",order:"asc",limit:2}))a.push(s);expect(a.map(s=>s.name)).toEqual(["Bob","Alice"]);const n=[];for await(const s of t.search({sortBy:"age",order:"asc",offset:1,limit:2}))n.push(s);expect(n.map(s=>s.name)).toEqual(["Alice","Carol"])}),it("should deleteBy where clause",async()=>{await t.set({name:"Dave",age:40,city:"NY"}),await t.set({name:"Eve",age:45,city:"NY"}),await t.set({name:"Frank",age:50,city:"LA"}),expect(await t.count()).toBe(3),await t.deleteBy({city:"NY"}),expect(await t.count()).toBe(1),expect(await t.get("Frank")).toEqual({name:"Frank",age:50,city:"LA"}),expect(await t.get("Dave")).toBeUndefined()}),it("should use selector in get and search",async()=>{await t.set({name:"Gary",age:60,city:"SF"});const e=await t.get("Gary",({age:a})=>a);expect(e).toBe(60);const o=[];for await(const a of t.search({select:({city:n})=>n}))o.push(a);expect(o).toEqual(["SF"])}),it("should delete items by key",async()=>{await t.set({name:"Helen",age:28,city:"Rome"}),expect(await t.get("Helen")).toBeDefined(),await t.delete("Helen"),expect(await t.get("Helen")).toBeUndefined()}),it("should test search with 1000 items",async()=>{const e=[];for(let a=0;a<1e3;a++)e.push({name:`Person${a}`,age:Math.floor(Math.random()*100),city:"City"+a%10});for(const a of e)await t.set(a);const o=[];for await(const a of t.search({sortBy:"age",order:"asc",limit:100}))o.push(a);expect(o.length).toBe(100)}),it("should handle operations on an empty table",async()=>{expect(await t.count()).toBe(0),expect(await t.get("NonExistent")).toBeUndefined();const e=[];for await(const o of t.search({sortBy:"age",order:"asc"}))e.push(o);expect(e.length).toBe(0)}),it("should handle duplicate keys gracefully",async()=>{await t.set({name:"Alice",age:30,city:"Paris"}),await t.set({name:"Alice",age:35,city:"Berlin"});const e=await t.get("Alice");expect(e).toEqual({name:"Alice",age:35,city:"Berlin"})}),it("should handle edge cases in selectors",async()=>{await t.set({name:"Charlie",age:40,city:"NY"});const e=await t.get("Charlie",()=>null);expect(e).toBeNull();const o=await t.get("Charlie",()=>{});expect(o).toBeUndefined()}),it("should clear the table",async()=>{await t.set({name:"Alice",age:30,city:"Paris"}),await t.set({name:"Bob",age:25,city:"London"}),expect(await t.count()).toBe(2),await t.clear(),expect(await t.count()).toBe(0)}),it("should use fts index",async()=>{const e=await c({backend:i,tableName:"TestTableFTS",key:"id",indexes:["fts:content"]});await e.set({id:"1",content:"The \u010Coho brown fox"}),await e.set({id:"2",content:"jumps over the lazy dog"}),await e.set({id:"3",content:"hello world"});const o=[];for await(const n of e.search({where:{content:{fts:["coho","fox"]}}}))o.push(n);expect(o.length).toBe(1),expect(o[0].id).toBe("1");const a=[];for await(const n of e.search({where:{content:{fts:["the"]}}}))a.push(n);expect(a.length).toBe(2)}),it("should use fts index with custom tokenizer options",async()=>{const e=await c({backend:i,tableName:"TestTableFTS2",key:"id",indexes:[{type:"fts",path:"content",tokenizer:{removeDiacritics:0,tokenChars:"abc",separators:"xyz"}}]});await e.set({id:"1",content:"abc xyz"}),await e.set({id:"2",content:"abc"}),await e.set({id:"3",content:"other"});const o=[];for await(const n of e.search({where:{content:{fts:["abc"]}}}))o.push(n);expect(o.length).toBe(2);const a=[];for await(const n of e.search({where:{content:{fts:["xyz"]}}}))a.push(n);expect(a.length).toBe(1),expect(a[0].id).toBe("1")}),it("should support multiple fts fields",async()=>{const e=await c({backend:i,tableName:"TestTableFTSMulti",key:"id",indexes:[{type:"fts",path:"title"},{type:"fts",path:"body"}]});await e.set({id:"1",title:"Hello",body:"World"}),await e.set({id:"2",title:"Foo",body:"Bar"}),await e.set({id:"3",title:"Hello",body:"Bar"});const o=[];for await(const n of e.search({where:{title:{fts:["Hello"]}}}))o.push(n);expect(o.length).toBe(2);const a=[];for await(const n of e.search({where:{body:{fts:["Bar"]}}}))a.push(n);expect(a.length).toBe(2)}),it("should handle fts search with no results",async()=>{const e=await c({backend:i,tableName:"TestTableFTSNone",key:"id",indexes:["fts:content"]});await e.set({id:"1",content:"foo bar"});const o=[];for await(const a of e.search({where:{content:{fts:["notfound"]}}}))o.push(a);expect(o.length).toBe(0)}),it("should custom fn fts index",async()=>{const e=await c({backend:i,tableName:"TestTableFTS",key:"id",indexes:[{type:"fts",path:"content",tokenizer:{removeDiacritics:1}}]});await e.set({id:"1",content:"The \u010Coho brown fox"}),await e.set({id:"2",content:"jumps over the lazy dog"}),await e.set({id:"3",content:"hello world"});const o=[];for await(const n of e.search({where:{content:{fts:["coho","fox"]}}}))o.push(n);expect(o.length).toBe(1),expect(o[0].id).toBe("1");const a=[];for await(const n of e.search({where:{content:{fts:["the"]}}}))a.push(n);expect(a.length).toBe(2)}),it("should test fts index with nested fields",async()=>{const e=await c({backend:i,tableName:"TestTableFTSNested",key:"id",indexes:["fts:info.content"]});await e.set({id:"1",info:{content:"The quick brown fox"}}),await e.set({id:"2",info:{content:"jumps over the lazy dog"}}),await e.set({id:"3",info:{content:"hello world"}});const o=[];for await(const n of e.search({where:{info:{content:{fts:["quick","fox"]}}}}))o.push(n);expect(o.length).toBe(1),expect(o[0].id).toBe("1");const a=[];for await(const n of e.search({where:{info:{content:{fts:["the"]}}}}))a.push(n);expect(a.length).toBe(2)})});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{STATE_SCHEDULER as f}from"../create";import{getId as P}from"../utils/id";import{shallow as v}from"../utils/shallow";import{selectSql as O}from"./select-sql";import{createTable as R,DEFAULT_STEP_SIZE as M}from"./table/table";function
|
|
1
|
+
import{STATE_SCHEDULER as f}from"../create";import{getId as P}from"../utils/id";import{shallow as v}from"../utils/shallow";import{selectSql as O}from"./select-sql";import{createTable as R,DEFAULT_STEP_SIZE as M}from"./table/table";function A(k){const g=P();function m(e){return`state-${g}-search-${e}`}let h;async function o(){if(!h){const{backend:e,...n}=k,t=e instanceof Promise?await e:e;h=await R({backend:t,...n})}return h}const c=new Map,d=new Map,y=new Map;async function p(e,n){const t=y.get(e),{options:a={}}=n,{stepSize:s=M}=a;if(!t)return!1;const r=[];for(let u=0;u<s;u++){const i=await t.next();if(i.done){y.delete(e);break}n.keys.has(String(i.value.key))||(r.push(i.value.document),n.keys.add(String(i.value.key)))}return r.length===0||v(n.items,r)?!1:(n.items=[...n.items,...r],!0)}function b(e){const n=d.get(e);n&&n()}async function x(e){const n=await o(),t=c.get(e);if(!t)return;const{options:a}=t,s=n.search({...a,select:(r,{rowId:u,key:i})=>({document:r,rowId:u,key:i})});y.set(e,s),t.keys=new Set,t.items=[],await p(e,t)}async function S(e){await x(e),b(e)}function T(e){const{key:n,op:t}=e,a=new Set;for(const[s,{keys:r}]of c)switch(t){case"delete":case"update":{r.has(String(n))&&a.add(s);break}case"insert":{a.add(s);break}}return a}async function l(e){const n=new Set;for(const t of e){const a=T(t);for(const s of a)n.add(s)}for(const t of n){const a=m(t);f.schedule(a,{searchId:t})}}const D=new Set;function w(e,n){c.has(e)||(c.set(e,{items:[],options:n,keys:new Set}),n&&S(e));const t=c.get(e);return n&&(t.options=n),t}const I={clear(e){c.delete(e)},async set(e){const t=await(await o()).set(e);return await l([t]),t},async batchSet(e){const t=await(await o()).batchSet(e);return await l(t),t},async delete(e){const t=await(await o()).delete(e);return t&&await l([t]),t},async deleteBy(e){const t=await(await o()).deleteBy(e);return await l(t),t},async get(e,n){return(await o()).get(e,n)},async*search(e={}){const n=await o();for await(const t of n.search(e))yield t},async count(e){return await(await o()).count(e)},updateSearchOptions(e,n){const t=w(e,n);t.options=n;const a=m(e);f.schedule(a,{searchId:e})},subscribe(e,n){const t=m(e),a=f.add(t,{onScheduleDone(){S(e)}});return D.add(a),d.has(e)||d.set(e,n),()=>{d.delete(e),a()}},getSnapshot(e){return w(e).items},refresh:S,destroy(){for(const e of D)e();c.clear(),d.clear()},async next(e){const n=c.get(e);if(n){const t=await p(e,n);return t&&b(e),t}return!1},select(e){return O(I,e)}};return I}export{A as createSqliteState};
|
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
import{unicodeTokenizer as C}from"./tokenizer";import{getWhereQuery as
|
|
1
|
+
import{unicodeTokenizer as C}from"./tokenizer";import{getWhereQuery as k}from"./where";const L=500,M=100;function R(l){return"$."+l}function U(l,a){if(!(!l||!a))return a.split(".").reduce((t,y)=>{if(typeof t=="object"&&t!==null&&y in t)return t[y]},l)}async function z(l){const{backend:a,tableName:t,indexes:y,key:$,disablePragmaOptimization:x}=l,u=$!==void 0;x||(await a.execute("PRAGMA journal_mode=WAL;"),await a.execute("PRAGMA synchronous=NORMAL;"),await a.execute("PRAGMA temp_store=MEMORY;"),await a.execute("PRAGMA cache_size=-20000;")),u?await a.execute(`
|
|
2
2
|
CREATE TABLE IF NOT EXISTS ${t} (
|
|
3
3
|
key TEXT PRIMARY KEY,
|
|
4
4
|
data TEXT NOT NULL
|
|
5
5
|
);
|
|
6
|
-
`):await
|
|
6
|
+
`):await a.execute(`
|
|
7
7
|
CREATE TABLE IF NOT EXISTS ${t} (
|
|
8
8
|
data TEXT NOT NULL
|
|
9
9
|
);
|
|
10
|
-
`);let
|
|
11
|
-
ON ${t} (json_extract(data, '${
|
|
10
|
+
`);let d;const T=[],f={};for(const e of y??[])if(typeof e=="string"&&e.startsWith("fts:")){const n=e.slice(4),r=n.replaceAll(".","_");T.push(n),f[n]=r}else if(typeof e=="object"&&e.type==="fts"){const n=e.path,r=n.replaceAll(".","_");if(T.push(n),f[n]=r,e.tokenizer){if(!d)d=e.tokenizer;else if(d!==e.tokenizer)throw new Error(`Conflicting FTS tokenizers: already using "${d}", got "${e.tokenizer}"`)}}else{const n=String(e);await a.execute(`CREATE INDEX IF NOT EXISTS idx_${t}_${n.replaceAll(/\W/g,"_")}
|
|
11
|
+
ON ${t} (json_extract(data, '${R(n)}'));`)}if(T.length>0){let e;typeof d=="object"?e=C(d):d===void 0?e='"unicode61", "remove_diacritics=1"':e=d;const n=T.map(o=>f[o]).join(", "),r=`
|
|
12
12
|
CREATE VIRTUAL TABLE IF NOT EXISTS ${t}_fts
|
|
13
|
-
USING fts5(${
|
|
14
|
-
`;await
|
|
13
|
+
USING fts5(${n}, tokenize=${e});
|
|
14
|
+
`;await a.execute(r),await a.execute(`
|
|
15
15
|
CREATE TRIGGER IF NOT EXISTS ${t}_ai
|
|
16
16
|
AFTER INSERT ON ${t}
|
|
17
17
|
BEGIN
|
|
18
|
-
INSERT INTO ${t}_fts(rowid, ${
|
|
18
|
+
INSERT INTO ${t}_fts(rowid, ${n})
|
|
19
19
|
VALUES (
|
|
20
20
|
new.rowid,
|
|
21
|
-
${T.map(
|
|
21
|
+
${T.map(o=>`json_extract(new.data, '${R(o)}')`).join(", ")}
|
|
22
22
|
);
|
|
23
23
|
END;
|
|
24
|
-
`),await
|
|
24
|
+
`),await a.execute(`
|
|
25
25
|
CREATE TRIGGER IF NOT EXISTS ${t}_ad
|
|
26
26
|
AFTER DELETE ON ${t}
|
|
27
27
|
BEGIN
|
|
28
28
|
DELETE FROM ${t}_fts WHERE rowid = old.rowid;
|
|
29
29
|
END;
|
|
30
|
-
`),await
|
|
30
|
+
`),await a.execute(`
|
|
31
31
|
CREATE TRIGGER IF NOT EXISTS ${t}_au
|
|
32
32
|
AFTER UPDATE ON ${t}
|
|
33
33
|
BEGIN
|
|
34
34
|
UPDATE ${t}_fts
|
|
35
|
-
SET ${T.map(
|
|
35
|
+
SET ${T.map(o=>`${f[o]}=json_extract(new.data, '${R(o)}')`).join(", ")}
|
|
36
36
|
WHERE rowid = old.rowid;
|
|
37
37
|
END;
|
|
38
|
-
`)}function
|
|
38
|
+
`)}function A(e){if(u)return U(e,String($))}async function b(e){return(await e.select("SELECT changes() AS c"))[0]?.c??0}const g={backend:a,async set(e,n){const r=n??a,o=JSON.stringify(e);if(u){const s=A(e);if(s==null)throw new Error(`Document is missing the configured key "${String($)}".`);if(await r.execute(`UPDATE ${t} SET data = ? WHERE key = ?`,[o,s]),await b(r)===1)return{key:s,op:"update"};try{return await r.execute(`INSERT INTO ${t} (key, data) VALUES (?, ?)`,[s,o]),{key:s,op:"insert"}}catch{return await r.execute(`UPDATE ${t} SET data = ? WHERE key = ?`,[o,s]),{key:s,op:"update"}}}await r.execute(`INSERT INTO ${t} (data) VALUES (?)`,[o]);const c=(await r.select("SELECT last_insert_rowid() AS id"))[0]?.id;if(typeof c!="number")throw new Error("Failed to retrieve last_insert_rowid()");return{key:c,op:"insert"}},async get(e,n=r=>r){const r=u?"key = ?":"rowid = ?",o=await a.select(`SELECT rowid, data FROM ${t} WHERE ${r}`,[e]);if(o.length===0)return;const{data:E,rowid:c}=o[0],s=JSON.parse(E),i=u?A(s)??c:c;return n(s,{rowId:c,key:i})},async delete(e){const n=u?"key = ?":"rowid = ?";if(await a.execute(`DELETE FROM ${t} WHERE ${n}`,[e]),((await a.select("SELECT changes() AS c"))[0]?.c??0)>0)return{key:e,op:"delete"}},async*search(e={}){const{sortBy:n,order:r="asc",limit:o,offset:E=0,where:c,select:s=w=>w,stepSize:i=M}=e,p=k(c,t),h=`SELECT rowid, data FROM ${t} ${p}`;let S=0,D=E;for(;;){let w=h;n?w+=` ORDER BY json_extract(data, '${R(String(n))}') COLLATE NOCASE ${r.toUpperCase()}`:w+=u?` ORDER BY key COLLATE NOCASE ${r.toUpperCase()}`:` ORDER BY rowid ${r.toUpperCase()}`;const I=o?Math.min(i,o-S):i;w+=` LIMIT ${I} OFFSET ${D}`;const m=await a.select(w);if(m.length===0)break;for(const{rowid:O,data:F}of m){if(o&&S>=o)return;const N=JSON.parse(F),_=u?A(N)??O:O;yield s(N,{rowId:O,key:_}),S++}if(m.length<I||o&&S>=o)break;D+=m.length}},async count(e={}){const n=k(e.where,t),r=`SELECT COUNT(*) as count FROM ${t} ${n}`;return(await a.select(r))[0]?.count??0},async deleteBy(e){const n=k(e,t),r=u?"key":"rowid",o=[];return await a.transaction(async E=>{const c=await E.select(`SELECT ${r} AS k FROM ${t} ${n}`);if(c.length===0)return;const s=c.map(i=>i.k);for(let i=0;i<s.length;i+=L){const p=s.slice(i,i+L),h=p.map(()=>"?").join(",");await E.execute(`DELETE FROM ${t} WHERE ${r} IN (${h})`,p)}for(const i of s)o.push({key:i,op:"delete"})}),o},async clear(){await a.execute(`DELETE FROM ${t}`)},async batchSet(e){const n=[];return await a.transaction(async r=>{for(const o of e){const E=await g.set(o,r);n.push(E)}}),n}};return g}export{M as DEFAULT_STEP_SIZE,z as createTable,U as getByPath,R as toJsonPath};
|
package/package.json
CHANGED
|
@@ -317,4 +317,35 @@ describe('table', () => {
|
|
|
317
317
|
}
|
|
318
318
|
expect(results2.length).toBe(2)
|
|
319
319
|
})
|
|
320
|
+
|
|
321
|
+
it('should test fts index with nested fields', async () => {
|
|
322
|
+
const tableFts = await createTable<{ id: string; info: { content: string } }>({
|
|
323
|
+
backend,
|
|
324
|
+
tableName: 'TestTableFTSNested',
|
|
325
|
+
key: 'id',
|
|
326
|
+
indexes: ['fts:info.content'],
|
|
327
|
+
})
|
|
328
|
+
await tableFts.set({ id: '1', info: { content: 'The quick brown fox' } })
|
|
329
|
+
await tableFts.set({ id: '2', info: { content: 'jumps over the lazy dog' } })
|
|
330
|
+
await tableFts.set({ id: '3', info: { content: 'hello world' } })
|
|
331
|
+
|
|
332
|
+
const results: { id: string; info: { content: string } }[] = []
|
|
333
|
+
for await (const doc of tableFts.search({
|
|
334
|
+
where: {
|
|
335
|
+
info: {
|
|
336
|
+
content: { fts: ['quick', 'fox'] },
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
})) {
|
|
340
|
+
results.push(doc)
|
|
341
|
+
}
|
|
342
|
+
expect(results.length).toBe(1)
|
|
343
|
+
expect(results[0].id).toBe('1')
|
|
344
|
+
|
|
345
|
+
const results2: { id: string; info: { content: string } }[] = []
|
|
346
|
+
for await (const doc of tableFts.search({ where: { info: { content: { fts: ['the'] } } } })) {
|
|
347
|
+
results2.push(doc)
|
|
348
|
+
}
|
|
349
|
+
expect(results2.length).toBe(2)
|
|
350
|
+
})
|
|
320
351
|
})
|
|
@@ -324,4 +324,53 @@ describe('use-sqlite-state', () => {
|
|
|
324
324
|
expect(result2.current[0].length).toBe(50)
|
|
325
325
|
})
|
|
326
326
|
})
|
|
327
|
+
|
|
328
|
+
it('should handle update of deep fields with deep id', async () => {
|
|
329
|
+
interface DeepItem {
|
|
330
|
+
person: {
|
|
331
|
+
id: string
|
|
332
|
+
name: string
|
|
333
|
+
age: number
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
const sql = createSqliteState<DeepItem>({ backend, tableName: 'State10', key: 'person.id' })
|
|
337
|
+
let reRenders = 0
|
|
338
|
+
const { result } = renderHook(() => {
|
|
339
|
+
reRenders++
|
|
340
|
+
return useSqliteValue(sql, { sortBy: 'person.age' }, [])
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
await waitFor(() => {
|
|
344
|
+
expect(reRenders).toBe(2)
|
|
345
|
+
expect(result.current[0].length).toBe(0)
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
act(() => {
|
|
349
|
+
sql.set({ person: { id: 'some_id', name: 'Alice', age: 30 } })
|
|
350
|
+
})
|
|
351
|
+
await waitFor(() => {
|
|
352
|
+
expect(reRenders).toBe(3)
|
|
353
|
+
expect(result.current[0]).toEqual([{ person: { id: 'some_id', name: 'Alice', age: 30 } }])
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
// update deep field
|
|
357
|
+
act(() => {
|
|
358
|
+
sql.set({ person: { id: 'some_id', name: 'Alice', age: 31 } })
|
|
359
|
+
})
|
|
360
|
+
await waitFor(() => {
|
|
361
|
+
// expect(reRenders).toBe(4)
|
|
362
|
+
expect(result.current[0]).toEqual([{ person: { id: 'some_id', name: 'Alice', age: 31 } }])
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
// update same field
|
|
366
|
+
act(() => {
|
|
367
|
+
sql.set({ person: { id: 'some_id', name: 'Alice', age: 31 } })
|
|
368
|
+
})
|
|
369
|
+
// should not re-render
|
|
370
|
+
await waitFor(() => {
|
|
371
|
+
expect(result.current[0]).toEqual([{ person: { id: 'some_id', name: 'Alice', age: 31 } }])
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
// add another item
|
|
375
|
+
})
|
|
327
376
|
})
|
|
@@ -77,7 +77,7 @@ export function createSqliteState<Document extends DocType>(options: CreateSqlit
|
|
|
77
77
|
|
|
78
78
|
interface NextResult {
|
|
79
79
|
document: Document
|
|
80
|
-
|
|
80
|
+
key: Key
|
|
81
81
|
}
|
|
82
82
|
// const emitter = createEmitter<Table<Document>>()
|
|
83
83
|
const cachedData = new Map<SearchId, DataItems<Document>>()
|
|
@@ -104,9 +104,9 @@ export function createSqliteState<Document extends DocType>(options: CreateSqlit
|
|
|
104
104
|
break
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
if (!data.keys.has(String(result.value.
|
|
107
|
+
if (!data.keys.has(String(result.value.key))) {
|
|
108
108
|
newItems.push(result.value.document)
|
|
109
|
-
data.keys.add(String(result.value.
|
|
109
|
+
data.keys.add(String(result.value.key))
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
|
|
@@ -136,7 +136,7 @@ export function createSqliteState<Document extends DocType>(options: CreateSqlit
|
|
|
136
136
|
const data = cachedData.get(searchId)
|
|
137
137
|
if (!data) return
|
|
138
138
|
const { options: refreshOptions } = data
|
|
139
|
-
const iterator = table.search({ ...refreshOptions, select: (document, { rowId }) => ({ document, rowId }) })
|
|
139
|
+
const iterator = table.search({ ...refreshOptions, select: (document, { rowId, key }) => ({ document, rowId, key }) })
|
|
140
140
|
iterators.set(searchId, iterator)
|
|
141
141
|
data.keys = new Set()
|
|
142
142
|
data.items = []
|
|
@@ -69,15 +69,22 @@ export async function createTable<Document extends DocType>(options: DbOptions<D
|
|
|
69
69
|
`)
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
// Track FTS fields
|
|
72
|
+
// Track FTS fields and map dot paths to valid SQLite column names
|
|
73
73
|
let ftsTokenizer: string | undefined | FtsTokenizerOptions
|
|
74
74
|
const ftsFields: string[] = []
|
|
75
|
+
const ftsFieldMap: Record<string, string> = {} // dot path -> column name
|
|
75
76
|
|
|
76
77
|
for (const index of indexes ?? []) {
|
|
77
78
|
if (typeof index === 'string' && index.startsWith('fts:')) {
|
|
78
|
-
|
|
79
|
+
const path = index.slice(4)
|
|
80
|
+
const col = path.replaceAll('.', '_')
|
|
81
|
+
ftsFields.push(path)
|
|
82
|
+
ftsFieldMap[path] = col
|
|
79
83
|
} else if (typeof index === 'object' && index.type === 'fts') {
|
|
80
|
-
|
|
84
|
+
const path = index.path
|
|
85
|
+
const col = path.replaceAll('.', '_')
|
|
86
|
+
ftsFields.push(path)
|
|
87
|
+
ftsFieldMap[path] = col
|
|
81
88
|
if (index.tokenizer) {
|
|
82
89
|
if (!ftsTokenizer) {
|
|
83
90
|
ftsTokenizer = index.tokenizer
|
|
@@ -96,7 +103,6 @@ export async function createTable<Document extends DocType>(options: DbOptions<D
|
|
|
96
103
|
|
|
97
104
|
// Create FTS table + triggers
|
|
98
105
|
if (ftsFields.length > 0) {
|
|
99
|
-
// const tokenizerSpec = ftsTokenizer ?? '"unicode61", "remove_diacritics=1"'
|
|
100
106
|
let tokenizerSpec: string
|
|
101
107
|
if (typeof ftsTokenizer === 'object') {
|
|
102
108
|
tokenizerSpec = unicodeTokenizer(ftsTokenizer)
|
|
@@ -105,8 +111,8 @@ export async function createTable<Document extends DocType>(options: DbOptions<D
|
|
|
105
111
|
} else {
|
|
106
112
|
tokenizerSpec = ftsTokenizer
|
|
107
113
|
}
|
|
108
|
-
// Use
|
|
109
|
-
const ftsColumns = ftsFields.map((f) => f).join(', ')
|
|
114
|
+
// Use mapped column names for FTS columns
|
|
115
|
+
const ftsColumns = ftsFields.map((f) => ftsFieldMap[f]).join(', ')
|
|
110
116
|
const query = `
|
|
111
117
|
CREATE VIRTUAL TABLE IF NOT EXISTS ${tableName}_fts
|
|
112
118
|
USING fts5(${ftsColumns}, tokenize=${tokenizerSpec});
|
|
@@ -142,7 +148,7 @@ export async function createTable<Document extends DocType>(options: DbOptions<D
|
|
|
142
148
|
AFTER UPDATE ON ${tableName}
|
|
143
149
|
BEGIN
|
|
144
150
|
UPDATE ${tableName}_fts
|
|
145
|
-
SET ${ftsFields.map((f) => `${f}=json_extract(new.data, '${toJsonPath(f)}')`).join(', ')}
|
|
151
|
+
SET ${ftsFields.map((f) => `${ftsFieldMap[f]}=json_extract(new.data, '${toJsonPath(f)}')`).join(', ')}
|
|
146
152
|
WHERE rowid = old.rowid;
|
|
147
153
|
END;
|
|
148
154
|
`)
|
|
@@ -203,7 +209,7 @@ export async function createTable<Document extends DocType>(options: DbOptions<D
|
|
|
203
209
|
|
|
204
210
|
async get<Selected = Document>(
|
|
205
211
|
keyValue: Key,
|
|
206
|
-
selector: (document: Document, meta: { rowId: number }) => Selected = (d) => d as unknown as Selected,
|
|
212
|
+
selector: (document: Document, meta: { rowId: number; key: Key }) => Selected = (d) => d as unknown as Selected,
|
|
207
213
|
) {
|
|
208
214
|
const whereKey = hasUserKey ? `key = ?` : `rowid = ?`
|
|
209
215
|
const result = await backend.select<Array<{ data: string; rowid: number }>>(
|
|
@@ -213,7 +219,8 @@ export async function createTable<Document extends DocType>(options: DbOptions<D
|
|
|
213
219
|
if (result.length === 0) return
|
|
214
220
|
const { data, rowid } = result[0]
|
|
215
221
|
const document = JSON.parse(data) as Document
|
|
216
|
-
|
|
222
|
+
const logicalKey = hasUserKey ? (getKeyFromDocument(document) ?? rowid) : rowid
|
|
223
|
+
return selector(document, { rowId: rowid, key: logicalKey }) as Selected
|
|
217
224
|
},
|
|
218
225
|
|
|
219
226
|
async delete(keyValue: Key) {
|
|
@@ -258,7 +265,9 @@ export async function createTable<Document extends DocType>(options: DbOptions<D
|
|
|
258
265
|
for (const { rowid, data } of results) {
|
|
259
266
|
if (limit && yielded >= limit) return
|
|
260
267
|
const document = JSON.parse(data) as Document
|
|
261
|
-
|
|
268
|
+
const logicalKey = hasUserKey ? (getKeyFromDocument(document) ?? rowid) : rowid
|
|
269
|
+
// Pass both rowId and logicalKey
|
|
270
|
+
yield select(document, { rowId: rowid, key: logicalKey }) as Selected
|
|
262
271
|
yielded++
|
|
263
272
|
}
|
|
264
273
|
|
|
@@ -47,7 +47,7 @@ export interface DbOptions<Document extends DocType> {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
export interface SearchOptions<Document extends DocType, Selected = Document> extends SqlSeachOptions<Document> {
|
|
50
|
-
readonly select?: (document: Document, meta: { rowId: number }) => Selected
|
|
50
|
+
readonly select?: (document: Document, meta: { rowId: number; key: Key }) => Selected
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
interface DbNotGeneric {
|
|
@@ -28,6 +28,7 @@ export interface DbOptions<Document extends DocType> {
|
|
|
28
28
|
export interface SearchOptions<Document extends DocType, Selected = Document> extends SqlSeachOptions<Document> {
|
|
29
29
|
readonly select?: (document: Document, meta: {
|
|
30
30
|
rowId: number;
|
|
31
|
+
key: Key;
|
|
31
32
|
}) => Selected;
|
|
32
33
|
}
|
|
33
34
|
interface DbNotGeneric {
|