muya 2.3.5 → 2.4.0

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.
@@ -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,38 +1,38 @@
1
- import{unicodeTokenizer as C}from"./tokenizer";import{getWhereQuery as A}from"./where";const D=500,F=100;function m(l){return"$."+l}function _(l,r){if(!(!l||!r))return r.split(".").reduce((t,y)=>{if(typeof t=="object"&&t!==null&&y in t)return t[y]},l)}async function P(l){const{backend:r,tableName:t,indexes:y,key:R,disablePragmaOptimization:I}=l,d=R!==void 0;I||(await r.execute("PRAGMA journal_mode=WAL;"),await r.execute("PRAGMA synchronous=NORMAL;"),await r.execute("PRAGMA temp_store=MEMORY;"),await r.execute("PRAGMA cache_size=-20000;")),d?await r.execute(`
1
+ import{unicodeTokenizer as _}from"./tokenizer";import{getWhereQuery as h}from"./where";const D=500,C=100;function R(l){return"$."+l}function M(l,a){if(!(!l||!a))return a.split(".").reduce((t,f)=>{if(typeof t=="object"&&t!==null&&f in t)return t[f]},l)}async function z(l){const{backend:a,tableName:t,indexes:f,key:$,disablePragmaOptimization:I}=l,d=$!==void 0;I||(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;")),d?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 r.execute(`
6
+ `):await a.execute(`
7
7
  CREATE TABLE IF NOT EXISTS ${t} (
8
8
  data TEXT NOT NULL
9
9
  );
10
- `);let E;const T=[];for(const e of y??[])if(typeof e=="string"&&e.startsWith("fts:"))T.push(e.slice(4));else if(typeof e=="object"&&e.type==="fts"){if(T.push(e.path),e.tokenizer){if(!E)E=e.tokenizer;else if(E!==e.tokenizer)throw new Error(`Conflicting FTS tokenizers: already using "${E}", got "${e.tokenizer}"`)}}else{const a=String(e);await r.execute(`CREATE INDEX IF NOT EXISTS idx_${t}_${a.replaceAll(/\W/g,"_")}
11
- ON ${t} (json_extract(data, '${m(a)}'));`)}if(T.length>0){let e;typeof E=="object"?e=C(E):E===void 0?e='"unicode61", "remove_diacritics=1"':e=E;const a=T.map(n=>n).join(", "),o=`
10
+ `);let E;const T=[],y={};for(const e of f??[])if(typeof e=="string"&&e.startsWith("fts:")){const n=e.slice(4),r=n.replaceAll(".","_");T.push(n),y[n]=r}else if(typeof e=="object"&&e.type==="fts"){const n=e.path,r=n.replaceAll(".","_");if(T.push(n),y[n]=r,e.tokenizer){if(!E)E=e.tokenizer;else if(E!==e.tokenizer)throw new Error(`Conflicting FTS tokenizers: already using "${E}", 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 E=="object"?e=_(E):E===void 0?e='"unicode61", "remove_diacritics=1"':e=E;const n=T.map(o=>y[o]).join(", "),r=`
12
12
  CREATE VIRTUAL TABLE IF NOT EXISTS ${t}_fts
13
- USING fts5(${a}, tokenize=${e});
14
- `;await r.execute(o),await r.execute(`
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, ${a})
18
+ INSERT INTO ${t}_fts(rowid, ${n})
19
19
  VALUES (
20
20
  new.rowid,
21
- ${T.map(n=>`json_extract(new.data, '${m(n)}')`).join(", ")}
21
+ ${T.map(o=>`json_extract(new.data, '${R(o)}')`).join(", ")}
22
22
  );
23
23
  END;
24
- `),await r.execute(`
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 r.execute(`
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(n=>`${n}=json_extract(new.data, '${m(n)}')`).join(", ")}
35
+ SET ${T.map(o=>`${y[o]}=json_extract(new.data, '${R(o)}')`).join(", ")}
36
36
  WHERE rowid = old.rowid;
37
37
  END;
38
- `)}function N(e){if(d)return _(e,String(R))}async function g(e){return(await e.select("SELECT changes() AS c"))[0]?.c??0}const h={backend:r,async set(e,a){const o=a??r,n=JSON.stringify(e);if(d){const s=N(e);if(s==null)throw new Error(`Document is missing the configured key "${String(R)}".`);if(await o.execute(`UPDATE ${t} SET data = ? WHERE key = ?`,[n,s]),await g(o)===1)return{key:s,op:"update"};try{return await o.execute(`INSERT INTO ${t} (key, data) VALUES (?, ?)`,[s,n]),{key:s,op:"insert"}}catch{return await o.execute(`UPDATE ${t} SET data = ? WHERE key = ?`,[n,s]),{key:s,op:"update"}}}await o.execute(`INSERT INTO ${t} (data) VALUES (?)`,[n]);const u=(await o.select("SELECT last_insert_rowid() AS id"))[0]?.id;if(typeof u!="number")throw new Error("Failed to retrieve last_insert_rowid()");return{key:u,op:"insert"}},async get(e,a=o=>o){const o=d?"key = ?":"rowid = ?",n=await r.select(`SELECT rowid, data FROM ${t} WHERE ${o}`,[e]);if(n.length===0)return;const{data:c,rowid:u}=n[0],s=JSON.parse(c);return a(s,{rowId:u})},async delete(e){const a=d?"key = ?":"rowid = ?";if(await r.execute(`DELETE FROM ${t} WHERE ${a}`,[e]),((await r.select("SELECT changes() AS c"))[0]?.c??0)>0)return{key:e,op:"delete"}},async*search(e={}){const{sortBy:a,order:o="asc",limit:n,offset:c=0,where:u,select:s=w=>w,stepSize:i=F}=e,f=A(u,t),$=`SELECT rowid, data FROM ${t} ${f}`;let S=0,O=c;for(;;){let w=$;a?w+=` ORDER BY json_extract(data, '${m(String(a))}') COLLATE NOCASE ${o.toUpperCase()}`:w+=d?` ORDER BY key COLLATE NOCASE ${o.toUpperCase()}`:` ORDER BY rowid ${o.toUpperCase()}`;const k=n?Math.min(i,n-S):i;w+=` LIMIT ${k} OFFSET ${O}`;const p=await r.select(w);if(p.length===0)break;for(const{rowid:L,data:x}of p){if(n&&S>=n)return;const b=JSON.parse(x);yield s(b,{rowId:L}),S++}if(p.length<k||n&&S>=n)break;O+=p.length}},async count(e={}){const a=A(e.where,t),o=`SELECT COUNT(*) as count FROM ${t} ${a}`;return(await r.select(o))[0]?.count??0},async deleteBy(e){const a=A(e,t),o=d?"key":"rowid",n=[];return await r.transaction(async c=>{const u=await c.select(`SELECT ${o} AS k FROM ${t} ${a}`);if(u.length===0)return;const s=u.map(i=>i.k);for(let i=0;i<s.length;i+=D){const f=s.slice(i,i+D),$=f.map(()=>"?").join(",");await c.execute(`DELETE FROM ${t} WHERE ${o} IN (${$})`,f)}for(const i of s)n.push({key:i,op:"delete"})}),n},async clear(){await r.execute(`DELETE FROM ${t}`)},async batchSet(e){const a=[];return await r.transaction(async o=>{for(const n of e){const c=await h.set(n,o);a.push(c)}}),a}};return h}export{F as DEFAULT_STEP_SIZE,P as createTable,_ as getByPath,m as toJsonPath};
38
+ `)}function N(e){if(d)return M(e,String($))}async function L(e){return(await e.select("SELECT changes() AS c"))[0]?.c??0}const O={backend:a,async set(e,n){const r=n??a,o=JSON.stringify(e);if(d){const s=N(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 L(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 u=(await r.select("SELECT last_insert_rowid() AS id"))[0]?.id;if(typeof u!="number")throw new Error("Failed to retrieve last_insert_rowid()");return{key:u,op:"insert"}},async get(e,n=r=>r){const r=d?"key = ?":"rowid = ?",o=await a.select(`SELECT rowid, data FROM ${t} WHERE ${r}`,[e]);if(o.length===0)return;const{data:c,rowid:u}=o[0],s=JSON.parse(c);return n(s,{rowId:u})},async delete(e){const n=d?"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:c=0,where:u,select:s=w=>w,stepSize:i=C}=e,p=h(u,t),A=`SELECT rowid, data FROM ${t} ${p}`;let S=0,k=c;for(;;){let w=A;n?w+=` ORDER BY json_extract(data, '${R(String(n))}') COLLATE NOCASE ${r.toUpperCase()}`:w+=d?` ORDER BY key COLLATE NOCASE ${r.toUpperCase()}`:` ORDER BY rowid ${r.toUpperCase()}`;const g=o?Math.min(i,o-S):i;w+=` LIMIT ${g} OFFSET ${k}`;const m=await a.select(w);if(m.length===0)break;for(const{rowid:x,data:b}of m){if(o&&S>=o)return;const F=JSON.parse(b);yield s(F,{rowId:x}),S++}if(m.length<g||o&&S>=o)break;k+=m.length}},async count(e={}){const n=h(e.where,t),r=`SELECT COUNT(*) as count FROM ${t} ${n}`;return(await a.select(r))[0]?.count??0},async deleteBy(e){const n=h(e,t),r=d?"key":"rowid",o=[];return await a.transaction(async c=>{const u=await c.select(`SELECT ${r} AS k FROM ${t} ${n}`);if(u.length===0)return;const s=u.map(i=>i.k);for(let i=0;i<s.length;i+=D){const p=s.slice(i,i+D),A=p.map(()=>"?").join(",");await c.execute(`DELETE FROM ${t} WHERE ${r} IN (${A})`,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 c=await O.set(o,r);n.push(c)}}),n}};return O}export{C as DEFAULT_STEP_SIZE,z as createTable,M as getByPath,R as toJsonPath};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muya",
3
- "version": "2.3.5",
3
+ "version": "2.4.0",
4
4
  "author": "samuel.gjabel@gmail.com",
5
5
  "repository": "https://github.com/samuelgjabel/muya",
6
6
  "main": "cjs/index.js",
@@ -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
  })
@@ -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
- ftsFields.push(index.slice(4))
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
- ftsFields.push(index.path)
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 actual field names for FTS columns
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
  `)