zeed 1.4.0 → 1.5.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.
Files changed (187) hide show
  1. package/dist/{args-FLoL3OKJ.d.cts → args-CEjib9V9.d.mts} +1 -1
  2. package/dist/{args-WC9q5kz2.d.mts → args-DEig-jw4.d.cts} +1 -1
  3. package/dist/{clipboard-BkUO-syY.d.mts → clipboard-BusqmLLY.d.cts} +1 -1
  4. package/dist/{clipboard-Cfpr331X.d.cts → clipboard-DcuuFRwa.d.mts} +1 -1
  5. package/dist/common/exec/index.d.cts +2 -2
  6. package/dist/common/exec/index.d.mts +2 -2
  7. package/dist/common/exec/promise.d.cts +1 -1
  8. package/dist/common/exec/promise.d.mts +1 -1
  9. package/dist/common/exec/throttle-debounce.d.cts +1 -1
  10. package/dist/common/exec/throttle-debounce.d.mts +1 -1
  11. package/dist/common/index.cjs +21 -0
  12. package/dist/common/index.d.cts +14 -11
  13. package/dist/common/index.d.mts +15 -12
  14. package/dist/common/index.mjs +4 -1
  15. package/dist/common/msg/rpc.cjs +8 -8
  16. package/dist/common/msg/rpc.cjs.map +1 -1
  17. package/dist/common/msg/rpc.mjs +8 -8
  18. package/dist/common/msg/rpc.mjs.map +1 -1
  19. package/dist/common/schema/export-json-schema.cjs +40 -31
  20. package/dist/common/schema/export-json-schema.cjs.map +1 -1
  21. package/dist/common/schema/export-json-schema.mjs +40 -31
  22. package/dist/common/schema/export-json-schema.mjs.map +1 -1
  23. package/dist/common/schema/index.cjs +21 -0
  24. package/dist/common/schema/index.d.cts +6 -3
  25. package/dist/common/schema/index.d.mts +7 -4
  26. package/dist/common/schema/index.mjs +4 -1
  27. package/dist/common/schema/sql/expr.cjs +128 -0
  28. package/dist/common/schema/sql/expr.cjs.map +1 -0
  29. package/dist/common/schema/sql/expr.d.cts +2 -0
  30. package/dist/common/schema/sql/expr.d.mts +2 -0
  31. package/dist/common/schema/sql/expr.mjs +115 -0
  32. package/dist/common/schema/sql/expr.mjs.map +1 -0
  33. package/dist/common/schema/sql/index.cjs +23 -0
  34. package/dist/common/schema/sql/index.d.cts +4 -0
  35. package/dist/common/schema/sql/index.d.mts +4 -0
  36. package/dist/common/schema/sql/index.mjs +5 -0
  37. package/dist/common/schema/sql/select.cjs +143 -0
  38. package/dist/common/schema/sql/select.cjs.map +1 -0
  39. package/dist/common/schema/sql/select.d.cts +2 -0
  40. package/dist/common/schema/sql/select.d.mts +2 -0
  41. package/dist/common/schema/sql/select.mjs +139 -0
  42. package/dist/common/schema/sql/select.mjs.map +1 -0
  43. package/dist/common/schema/sql/table.cjs +23 -0
  44. package/dist/common/schema/sql/table.cjs.map +1 -0
  45. package/dist/common/schema/sql/table.d.cts +2 -0
  46. package/dist/common/schema/sql/table.d.mts +2 -0
  47. package/dist/common/schema/sql/table.mjs +20 -0
  48. package/dist/common/schema/sql/table.mjs.map +1 -0
  49. package/dist/common/schema/type-test.d.cts +1 -1
  50. package/dist/common/schema/type-test.d.mts +1 -1
  51. package/dist/common/schema/utils.d.cts +1 -1
  52. package/dist/common/schema/utils.d.mts +1 -1
  53. package/dist/common/schema/z.d.mts +1 -1
  54. package/dist/common/storage/index.d.cts +1 -1
  55. package/dist/common/storage/index.d.mts +1 -1
  56. package/dist/common/storage/memstorage.d.cts +1 -1
  57. package/dist/common/storage/memstorage.d.mts +1 -1
  58. package/dist/common/test.d.cts +1 -1
  59. package/dist/common/test.d.mts +1 -1
  60. package/dist/common/time.d.cts +1 -1
  61. package/dist/common/time.d.mts +1 -1
  62. package/dist/common/timeout.d.cts +1 -1
  63. package/dist/common/timeout.d.mts +1 -1
  64. package/dist/common/utils.d.cts +1 -1
  65. package/dist/common/utils.d.mts +1 -1
  66. package/dist/common/uuid.d.cts +1 -1
  67. package/dist/common/uuid.d.mts +1 -1
  68. package/dist/{crypto-CyTV7Qce.d.cts → crypto-D68rVmvU.d.mts} +1 -1
  69. package/dist/{crypto-LT7EC5_d.d.mts → crypto-KzGHoCJE.d.cts} +1 -1
  70. package/dist/{env-B3vOiVY8.d.cts → env-BJXdwBKq.d.mts} +1 -1
  71. package/dist/{env-C3npYe8w.d.mts → env-HsOnA_yK.d.cts} +1 -1
  72. package/dist/expr-CCKrqOw1.d.mts +25 -0
  73. package/dist/expr-yYgSeBZ3.d.cts +25 -0
  74. package/dist/{files-CDNKX9VI.d.mts → files-4O-PxnAC.d.cts} +1 -1
  75. package/dist/{files-DdI9UZvg.d.cts → files-BlpxqSTT.d.mts} +1 -1
  76. package/dist/{files-async-1V0bu_ca.d.cts → files-async-DFLC-Nkd.d.cts} +1 -1
  77. package/dist/{files-async-cBMkRwsu.d.mts → files-async-DfuEEDjH.d.mts} +1 -1
  78. package/dist/{filestorage-CXQ9MzeW.d.cts → filestorage-BjeBZEAs.d.cts} +1 -1
  79. package/dist/{filestorage-YzM2z9sU.d.mts → filestorage-CmfztpWm.d.mts} +1 -1
  80. package/dist/{fs-DHJ9AqUk.d.cts → fs-D837bjRT.d.cts} +1 -1
  81. package/dist/{fs-DgjZdpuF.d.mts → fs-DlYLapik.d.mts} +1 -1
  82. package/dist/{glob-Bfs7ZS_i.d.mts → glob-5yW09dkR.d.mts} +1 -1
  83. package/dist/{glob-Bt150jOY.d.cts → glob-CZaZPqiy.d.cts} +1 -1
  84. package/dist/index.all.cjs +21 -0
  85. package/dist/index.all.d.cts +28 -25
  86. package/dist/index.all.d.mts +29 -26
  87. package/dist/index.all.mjs +4 -1
  88. package/dist/index.browser.cjs +21 -0
  89. package/dist/index.browser.d.cts +14 -11
  90. package/dist/index.browser.d.mts +15 -12
  91. package/dist/index.browser.mjs +4 -1
  92. package/dist/index.jsr.d.cts +4 -4
  93. package/dist/index.jsr.d.mts +4 -4
  94. package/dist/index.node.cjs +21 -0
  95. package/dist/index.node.d.cts +28 -25
  96. package/dist/index.node.d.mts +29 -26
  97. package/dist/index.node.mjs +4 -1
  98. package/dist/{log-file-bsTsc9KM.d.cts → log-file-DwEDms1F.d.cts} +2 -2
  99. package/dist/{log-file-DTuImomJ.d.mts → log-file-QV1unm3z.d.mts} +2 -2
  100. package/dist/{log-file-rotation-_YruAcNc.d.cts → log-file-rotation-BpZxXYlU.d.cts} +2 -2
  101. package/dist/{log-file-rotation-FBmtp_Uz.d.mts → log-file-rotation-DanrO_2y.d.mts} +2 -2
  102. package/dist/{log-node-DlrXl3QO.d.mts → log-node-BSn7RqAc.d.mts} +1 -1
  103. package/dist/{log-node-Dk948mHX.d.cts → log-node-D_fiJL6x.d.cts} +1 -1
  104. package/dist/{log-rotation-CkyjZbK5.d.mts → log-rotation-BdGakFya.d.cts} +1 -1
  105. package/dist/{log-rotation-_d7iRm9s.d.cts → log-rotation-Ce4e-8LN.d.mts} +1 -1
  106. package/dist/{log-util-2Ls76P-0.d.cts → log-util-C0U3zCjw.d.cts} +1 -1
  107. package/dist/{log-util-Da_d19f8.d.mts → log-util-Da_UCcmt.d.mts} +1 -1
  108. package/dist/{memstorage-D5A9FwiP.d.mts → memstorage-BhWXthO8.d.mts} +1 -1
  109. package/dist/{memstorage-BcjQLdaQ.d.cts → memstorage-tvlWDYgS.d.cts} +1 -1
  110. package/dist/node/args.d.cts +1 -1
  111. package/dist/node/args.d.mts +1 -1
  112. package/dist/node/clipboard.d.cts +1 -1
  113. package/dist/node/clipboard.d.mts +1 -1
  114. package/dist/node/crypto.d.cts +1 -1
  115. package/dist/node/crypto.d.mts +1 -1
  116. package/dist/node/env.d.cts +1 -1
  117. package/dist/node/env.d.mts +1 -1
  118. package/dist/node/files-async.d.cts +1 -1
  119. package/dist/node/files-async.d.mts +1 -1
  120. package/dist/node/files.d.cts +1 -1
  121. package/dist/node/files.d.mts +1 -1
  122. package/dist/node/filestorage.d.cts +1 -1
  123. package/dist/node/filestorage.d.mts +1 -1
  124. package/dist/node/fs.d.cts +1 -1
  125. package/dist/node/fs.d.mts +1 -1
  126. package/dist/node/glob.d.cts +1 -1
  127. package/dist/node/glob.d.mts +1 -1
  128. package/dist/node/index.d.cts +14 -14
  129. package/dist/node/index.d.mts +14 -14
  130. package/dist/node/log/index.d.cts +5 -5
  131. package/dist/node/log/index.d.mts +5 -5
  132. package/dist/node/log/log-file-rotation.d.cts +1 -1
  133. package/dist/node/log/log-file-rotation.d.mts +1 -1
  134. package/dist/node/log/log-file.d.cts +1 -1
  135. package/dist/node/log/log-file.d.mts +1 -1
  136. package/dist/node/log/log-node.cjs +4 -13
  137. package/dist/node/log/log-node.cjs.map +1 -1
  138. package/dist/node/log/log-node.d.cts +1 -1
  139. package/dist/node/log/log-node.d.mts +1 -1
  140. package/dist/node/log/log-node.mjs +4 -13
  141. package/dist/node/log/log-node.mjs.map +1 -1
  142. package/dist/node/log/log-rotation.d.cts +1 -1
  143. package/dist/node/log/log-rotation.d.mts +1 -1
  144. package/dist/node/log/log-util.d.cts +1 -1
  145. package/dist/node/log/log-util.d.mts +1 -1
  146. package/dist/{promise-DGgiRckN.d.cts → promise-CU_CENbU.d.cts} +1 -1
  147. package/dist/{promise-MH3xAy4S.d.mts → promise-CoWXgo4w.d.mts} +1 -1
  148. package/dist/select-DrciHdk_.d.cts +52 -0
  149. package/dist/select-F2KpP6mo.d.mts +52 -0
  150. package/dist/table-Cr8tjDIL.d.mts +19 -0
  151. package/dist/table-IkLXirT-.d.cts +19 -0
  152. package/dist/{test-CAhm15f4.d.mts → test-DcXa0MeX.d.cts} +1 -1
  153. package/dist/{test-D2plOVHF.d.cts → test-jZsc7P2c.d.mts} +1 -1
  154. package/dist/{throttle-debounce-BLFxAZ8W.d.mts → throttle-debounce-CCh0F100.d.mts} +1 -1
  155. package/dist/{throttle-debounce-Psb0ay1r.d.cts → throttle-debounce-DyFiyoAk.d.cts} +1 -1
  156. package/dist/{time-BfKJBbym.d.cts → time-BgFZe9ys.d.cts} +1 -1
  157. package/dist/{time-DxE-vjjw.d.mts → time-DSV_k3mG.d.mts} +1 -1
  158. package/dist/{timeout-CnUk6Ruj.d.mts → timeout-DDSSNZY8.d.mts} +1 -1
  159. package/dist/{timeout-CpFcK8MD.d.cts → timeout-E3ZQbJgK.d.cts} +1 -1
  160. package/dist/{type-test-BiKyEZkc.d.mts → type-test-BvzWDJz3.d.mts} +1 -1
  161. package/dist/{type-test-sM7QpfQU.d.cts → type-test-CBK-iJ9d.d.cts} +1 -1
  162. package/dist/{utils-B8DsVgFr.d.mts → utils-1RyCGkpQ.d.mts} +1 -1
  163. package/dist/{utils-BfZkD2Pt.d.mts → utils-6Culwiaf.d.cts} +1 -1
  164. package/dist/{utils-DHQBNh-Z.d.cts → utils-CDJihcg3.d.mts} +1 -1
  165. package/dist/{utils-Bctk_WhH.d.cts → utils-nCQklGHV.d.cts} +1 -1
  166. package/dist/{uuid-Cusm2nIK.d.cts → uuid-CKFZfSff.d.mts} +1 -1
  167. package/dist/{uuid-ININPGKB.d.mts → uuid-D42A8UdP.d.cts} +1 -1
  168. package/dist/z-C0fpNWZg.d.cts +1 -0
  169. package/dist/z-D_jezYmm.d.mts +1 -0
  170. package/dist/{z-collection-BmuBin--.d.mts → z-collection-BSfgRU0Q.d.mts} +1 -1
  171. package/package.json +7 -8
  172. package/src/common/schema/export-json-schema.spec.ts +11 -7
  173. package/src/common/schema/export-json-schema.ts +59 -52
  174. package/src/common/schema/index.ts +1 -0
  175. package/src/common/schema/sql/README.md +254 -0
  176. package/src/common/schema/sql/expr.ts +99 -0
  177. package/src/common/schema/sql/index.ts +3 -0
  178. package/src/common/schema/sql/select.spec.ts +144 -0
  179. package/src/common/schema/sql/select.ts +207 -0
  180. package/src/common/schema/sql/table.ts +36 -0
  181. /package/dist/{index-BH1nuHdZ.d.cts → index-CIABef8t.d.mts} +0 -0
  182. /package/dist/{index-BL7o4fG9.d.cts → index-CliqZ9rj.d.mts} +0 -0
  183. /package/dist/{index-CP2eJYlK.d.mts → index-D6xqj1Qx.d.cts} +0 -0
  184. /package/dist/{index-DjOaHFU3.d.mts → index-DHFfG4yr.d.cts} +0 -0
  185. /package/dist/{index-sViox9YW.d.mts → index-N-OgGgfF.d.mts} +0 -0
  186. /package/dist/{z-ClMox7qS.d.mts → index-WOw4GVZo.d.cts} +0 -0
  187. /package/dist/{z-dtM4F8Lo.d.cts → index-luywJTzJ.d.mts} +0 -0
@@ -1,72 +1,79 @@
1
1
  import type { Type } from './schema'
2
2
  import { isEmpty } from '../data'
3
- import { objectMap } from '../data/object'
4
3
 
5
- const _mapJsonSchemaType: Record<string, string> = {
4
+ const _primitiveMap: Record<string, string> = {
6
5
  string: 'string',
7
6
  number: 'number',
8
- boolean: 'boolean',
9
7
  int: 'integer',
8
+ boolean: 'boolean',
10
9
  }
11
10
 
12
- export function schemaExportJsonSchema<T>(schema: Type<T>): Record<string, any> {
13
- // assert(isSchemaObjectFlat(schema), 'schema should be a flat object')
11
+ function transformSchema(schema: Type<any>): Record<string, any> {
12
+ const out: Record<string, any> = {}
14
13
 
15
- function transformSchema(schema: Type<any>): any {
16
- const type = _mapJsonSchemaType[schema.type] ?? schema.type ?? 'object'
14
+ if (schema._enumValues) {
15
+ out.type = _primitiveMap[schema.type] ?? schema.type
16
+ out.enum = [...schema._enumValues]
17
+ }
18
+ else if (schema.type === 'literal') {
19
+ out.const = schema._default
20
+ }
21
+ else if (schema.type === 'union' && Array.isArray(schema._union)) {
22
+ out.anyOf = schema._union.map((s: Type<any>) => transformSchema(s))
23
+ }
24
+ else if (schema.type === 'object' && schema._object) {
25
+ out.type = 'object'
17
26
  const properties: Record<string, any> = {}
18
27
  const required: string[] = []
19
-
20
- objectMap(schema._object!, (key, schema: any) => {
21
- const type = _mapJsonSchemaType[schema.type] ?? schema.type
22
- properties[key] = { type }
23
- const enumValues = (schema as any)._enumValues
24
- if (enumValues) {
25
- properties[key].enum = enumValues
26
- }
27
- if (schema._default !== undefined) {
28
- properties[key].default = schema._default
29
- }
30
- if (schema._meta?.desc) {
31
- properties[key].description = schema._meta.desc
32
- }
33
- if (schema._optional !== true) {
28
+ for (const key of Object.keys(schema._object)) {
29
+ const propSchema = schema._object[key] as Type<any>
30
+ properties[key] = transformSchema(propSchema)
31
+ if (propSchema._optional !== true)
34
32
  required.push(key)
35
- }
36
- if (schema.type === 'array' && schema._type) {
37
- properties[key].items = transformSchema(schema._type)
38
- }
39
- else if (schema.type === 'object' && schema._object) {
40
- Object.assign(properties[key], transformSchema(schema))
41
- properties[key].additionalProperties = false
42
- }
43
- else if (schema.type === 'record' && schema._type) {
44
- properties[key].type = 'object'
45
- properties[key].additionalProperties = transformSchema(schema._type)
46
- }
47
- // Handle union types (e.g., z.union)
48
- else if (schema.type === 'union' && Array.isArray(schema._union)) {
49
- properties[key].type = schema._union.map((s: any) => _mapJsonSchemaType[s.type] ?? s.type) // todo complex types
50
- }
51
- })
52
-
53
- if (!isEmpty(properties)) {
54
- return {
55
- type,
56
- properties,
57
- additionalProperties: false,
58
- ...(required.length > 0 ? { required } : {}),
59
- }
60
- }
61
-
62
- return {
63
- type,
64
33
  }
34
+ if (!isEmpty(properties))
35
+ out.properties = properties
36
+ out.additionalProperties = false
37
+ if (required.length > 0)
38
+ out.required = required
65
39
  }
40
+ else if (schema.type === 'record' && schema._type) {
41
+ out.type = 'object'
42
+ out.additionalProperties = transformSchema(schema._type)
43
+ }
44
+ else if (schema.type === 'array' && schema._type) {
45
+ out.type = 'array'
46
+ out.items = transformSchema(schema._type)
47
+ }
48
+ else if (schema.type === 'tuple' && Array.isArray(schema._type)) {
49
+ out.type = 'array'
50
+ out.items = (schema._type as Type<any>[]).map(s => transformSchema(s))
51
+ out.minItems = schema._type.length
52
+ out.maxItems = schema._type.length
53
+ }
54
+ else if (schema.type === 'none') {
55
+ out.type = 'null'
56
+ }
57
+ else if (schema.type === 'any') {
58
+ // no constraint
59
+ }
60
+ else {
61
+ const t = _primitiveMap[schema.type]
62
+ if (t)
63
+ out.type = t
64
+ }
65
+
66
+ if (schema._default !== undefined && schema.type !== 'literal')
67
+ out.default = schema._default
68
+ if (schema._meta?.desc)
69
+ out.description = schema._meta.desc
66
70
 
71
+ return out
72
+ }
73
+
74
+ export function schemaExportJsonSchema<T>(schema: Type<T>): Record<string, any> {
67
75
  return {
68
76
  $schema: 'http://json-schema.org/draft-07/schema#',
69
- additionalProperties: false,
70
77
  ...transformSchema(schema),
71
78
  }
72
79
  }
@@ -7,6 +7,7 @@ export * from './parse-object'
7
7
  export * from './schema'
8
8
  export * from './schema-standard'
9
9
  export * from './serialize'
10
+ export * from './sql'
10
11
  export * from './type-test'
11
12
  export * from './utils'
12
13
  export * from './z'
@@ -0,0 +1,254 @@
1
+ # SQL Select Builder
2
+
3
+ A minimal, type-safe SQL `SELECT` builder built on top of zeed's `z` schema system. Inspired by Drizzle ORM and Kysely, but deliberately small: single-table queries, no joins, no inserts/updates/deletes.
4
+
5
+ The goal is to get full TypeScript inference, editor autocompletion, and - uniquely - per-query dependency tracking for reactive use cases.
6
+
7
+ ## Features
8
+
9
+ - Type-safe table definitions backed by `z` schemas
10
+ - Full row type inference for results, including narrowed `pick`/`select` forms
11
+ - Editor autocompletion on column keys
12
+ - Parameterized SQL output (`?` placeholders, values returned separately)
13
+ - Per-query dependency tracking split by role: `select`, `where`, `orderBy`
14
+ - Zero runtime dependencies, tree-shakable
15
+
16
+ ## Quick Start
17
+
18
+ ```ts
19
+ import { and, boolean, eq, from, gt, inArray, int, like, or, string, table } from 'zeed'
20
+
21
+ const users = table('users', {
22
+ id: int(),
23
+ name: string(),
24
+ email: string(),
25
+ age: int(),
26
+ active: boolean(),
27
+ })
28
+
29
+ const query = from(users)
30
+ .pick('id', 'name')
31
+ .where(and(eq(users.active, true), gt(users.age, 18)))
32
+ .orderBy(users.name)
33
+ .limit(20)
34
+
35
+ const { sql, params } = query.toSQL()
36
+ // sql: SELECT "users"."id", "users"."name" FROM "users"
37
+ // WHERE ("users"."active" = ?) AND ("users"."age" > ?)
38
+ // ORDER BY "users"."name" ASC LIMIT ?
39
+ // params: [true, 18, 20]
40
+ ```
41
+
42
+ ## Defining Tables
43
+
44
+ A table pairs a name with a `z`-schema shape. Each column becomes a typed reference usable in expressions.
45
+
46
+ ```ts
47
+ const posts = table('posts', {
48
+ id: int(),
49
+ title: string(),
50
+ body: string(),
51
+ authorId: int(),
52
+ published: boolean(),
53
+ })
54
+
55
+ posts.id // Column<number>
56
+ posts.title // Column<string>
57
+ ```
58
+
59
+ The shape is a plain `Record<string, Type>`, so any `z` type works (`string`, `int`, `boolean`, `stringLiterals`, ...).
60
+
61
+ ## Selecting Columns
62
+
63
+ Three forms, depending on how much control you need.
64
+
65
+ ### 1. Full row (default)
66
+
67
+ ```ts
68
+ from(users)
69
+ // Row: { id: number, name: string, email: string, age: number, active: boolean }
70
+ ```
71
+
72
+ ### 2. `pick(...keys)` - compact and typed
73
+
74
+ ```ts
75
+ from(users).pick('id', 'email')
76
+ // Row: { id: number, email: string }
77
+ ```
78
+
79
+ Keys are typed against the table shape, so the editor autocompletes valid column names and rejects unknown ones at compile time.
80
+
81
+ ### 3. `select({ alias: column })` - for aliases or mixed expressions
82
+
83
+ ```ts
84
+ import { select } from 'zeed'
85
+
86
+ select({ uid: users.id, n: users.name }).from(users)
87
+ // Row: { uid: number, n: string }
88
+ // SQL: SELECT "users"."id" AS "uid", "users"."name" AS "n" FROM "users"
89
+ ```
90
+
91
+ ## Where Clauses
92
+
93
+ Expressions are built from operator helpers. Columns and literal values can be mixed freely - literals are automatically bound as parameters.
94
+
95
+ ```ts
96
+ import { and, eq, gt, gte, inArray, like, lt, ne, or, sqlIsNotNull, sqlIsNull } from 'zeed'
97
+
98
+ from(users).where(
99
+ and(
100
+ eq(users.active, true),
101
+ or(
102
+ like(users.name, 'A%'),
103
+ inArray(users.id, [1, 2, 3]),
104
+ ),
105
+ sqlIsNotNull(users.email),
106
+ ),
107
+ )
108
+ ```
109
+
110
+ Available operators:
111
+
112
+ | Helper | SQL |
113
+ | --------------- | ------------ |
114
+ | `eq(a, b)` | `a = b` |
115
+ | `ne(a, b)` | `a <> b` |
116
+ | `gt(a, b)` | `a > b` |
117
+ | `gte(a, b)` | `a >= b` |
118
+ | `lt(a, b)` | `a < b` |
119
+ | `lte(a, b)` | `a <= b` |
120
+ | `like(a, b)` | `a LIKE b` |
121
+ | `inArray(a, [])`| `a IN (...)` |
122
+ | `sqlIsNull(a)` | `a IS NULL` |
123
+ | `sqlIsNotNull(a)` | `a IS NOT NULL` |
124
+ | `and(...)` | `(..) AND (..)` |
125
+ | `or(...)` | `(..) OR (..)` |
126
+ | `not(e)` | `NOT (..)` |
127
+
128
+ `and` and `or` accept `undefined`, `false`, and `null` entries and drop them, which is handy for conditional filters.
129
+
130
+ Note: `sqlIsNull` and `sqlIsNotNull` are prefixed to avoid collision with zeed's existing `isNull`/`isNotNull` data helpers.
131
+
132
+ ## Ordering, Limit, Offset
133
+
134
+ ```ts
135
+ from(users)
136
+ .orderBy(users.age, 'DESC')
137
+ .orderBy(users.name) // ASC by default
138
+ .limit(10)
139
+ .offset(20)
140
+ ```
141
+
142
+ ## Compiling a Query
143
+
144
+ ```ts
145
+ const { sql, params, dependencies } = query.toSQL()
146
+ ```
147
+
148
+ - `sql` - the query string with `?` placeholders
149
+ - `params` - array of bound values in order
150
+ - `dependencies` - see below
151
+
152
+ Pass `sql` and `params` to whichever SQLite/Postgres/MySQL driver you use.
153
+
154
+ ## Dependency Tracking
155
+
156
+ Every compiled query reports exactly which table columns it touches, split by role. This is the key building block for reactive queries: when the database changes, you can decide whether a given query needs to re-run based on which columns were affected.
157
+
158
+ ```ts
159
+ const q = from(users)
160
+ .pick('id', 'name')
161
+ .where(eq(users.email, 'a@b.c'))
162
+ .orderBy(users.age, 'DESC')
163
+
164
+ q.toSQL().dependencies
165
+ // [
166
+ // {
167
+ // table: 'users',
168
+ // select: ['id', 'name'], // fields returned to the caller
169
+ // where: ['email'], // fields used in filters
170
+ // orderBy: ['age'], // fields used for ordering
171
+ // all: ['age', 'email', 'id', 'name'], // union of the above
172
+ // }
173
+ // ]
174
+ ```
175
+
176
+ ### Example: reactive invalidation
177
+
178
+ ```ts
179
+ function isAffected(
180
+ deps: readonly QueryDependencies[],
181
+ change: { table: string, columns: string[] },
182
+ ): boolean {
183
+ return deps.some(d =>
184
+ d.table === change.table
185
+ && d.all.some(c => change.columns.includes(c)),
186
+ )
187
+ }
188
+
189
+ // On a write notification from the database:
190
+ if (isAffected(query.toSQL().dependencies, { table: 'users', columns: ['email'] })) {
191
+ // re-run the query, push new rows to subscribers
192
+ }
193
+ ```
194
+
195
+ Splitting by role lets you optimize further:
196
+
197
+ - A change to a `select` column means the result *shape* changes - re-render.
198
+ - A change to a `where` or `orderBy` column means the result *set* may change - re-query.
199
+ - A change to a column not in `all` is irrelevant - skip.
200
+
201
+ ## Type Inference
202
+
203
+ Row types flow through the builder. Use `InferRow` to extract the result type for downstream code:
204
+
205
+ ```ts
206
+ import type { InferRow } from 'zeed'
207
+
208
+ const query = from(users).pick('id', 'email')
209
+ type Row = InferRow<typeof query> // { id: number, email: string }
210
+
211
+ function render(rows: Row[]) { /* ... */ }
212
+ render(await run(query))
213
+ ```
214
+
215
+ ## Scope and Limitations
216
+
217
+ By design, this builder is minimal:
218
+
219
+ - `SELECT` only. No `INSERT`, `UPDATE`, `DELETE`, `CREATE TABLE`.
220
+ - Single table per query. No `JOIN`, no subqueries, no CTEs.
221
+ - No aggregate functions (`COUNT`, `SUM`, `GROUP BY`, `HAVING`).
222
+ - One SQL dialect (`?` placeholders, double-quoted identifiers). Works with SQLite and, with most drivers, Postgres.
223
+
224
+ The existing `select({...}).from(...)` form remains available for cases that need column aliases. If you need joins or mutations, use a dedicated ORM - this module is intended for read-only queries in reactive contexts where dependency tracking is the main value-add.
225
+
226
+ ## API Reference
227
+
228
+ ### `table(name, shape)`
229
+
230
+ Creates a table handle. `shape` is a `Record<string, Type>` from `z`. Returns an object where each key is a typed `Column` plus `_name` and `_shape` metadata.
231
+
232
+ ### `from(table)`
233
+
234
+ Starts a query. Returns a `SelectBuilder` with the full row type as its default result.
235
+
236
+ ### `select(selection?)`
237
+
238
+ Alternative entry point. With no argument, behaves like a full-row select (requires a subsequent `.from(...)`). With a `{ alias: column }` map, creates an aliased selection.
239
+
240
+ ### `SelectBuilder`
241
+
242
+ Chainable methods:
243
+
244
+ - `.from(table)` - set the source table (only needed after `select()`)
245
+ - `.pick(...keys)` - narrow to specific columns by name
246
+ - `.where(expr)` - set the WHERE clause
247
+ - `.orderBy(column, 'ASC' | 'DESC')` - append an ORDER BY entry
248
+ - `.limit(n)` / `.offset(n)`
249
+ - `.toSQL()` - compile to `{ sql, params, dependencies }`
250
+ - `.dependencies()` - shortcut that returns just the dependencies
251
+
252
+ ### `InferRow<Query>`
253
+
254
+ Type helper that extracts the row type from a builder or compiled query.
@@ -0,0 +1,99 @@
1
+ import type { Column } from './table'
2
+ import { isColumn } from './table'
3
+
4
+ export interface Expr {
5
+ readonly kind: 'expr'
6
+ readonly sql: string
7
+ readonly params: unknown[]
8
+ readonly refs: Column[]
9
+ }
10
+
11
+ function operand(v: any): Expr {
12
+ if (isColumn(v)) {
13
+ return {
14
+ kind: 'expr',
15
+ sql: `"${v._table}"."${v._name}"`,
16
+ params: [],
17
+ refs: [v],
18
+ }
19
+ }
20
+ if (v && v.kind === 'expr')
21
+ return v as Expr
22
+ return { kind: 'expr', sql: '?', params: [v], refs: [] }
23
+ }
24
+
25
+ function binop(op: string) {
26
+ return (a: Column | Expr | unknown, b: Column | Expr | unknown): Expr => {
27
+ const x = operand(a)
28
+ const y = operand(b)
29
+ return {
30
+ kind: 'expr',
31
+ sql: `${x.sql} ${op} ${y.sql}`,
32
+ params: [...x.params, ...y.params],
33
+ refs: [...x.refs, ...y.refs],
34
+ }
35
+ }
36
+ }
37
+
38
+ export const eq = binop('=')
39
+ export const ne = binop('<>')
40
+ export const gt = binop('>')
41
+ export const gte = binop('>=')
42
+ export const lt = binop('<')
43
+ export const lte = binop('<=')
44
+ export const like = binop('LIKE')
45
+
46
+ export function and(...parts: (Expr | undefined | false | null)[]): Expr {
47
+ const list = parts.filter((p): p is Expr => !!p)
48
+ if (list.length === 0)
49
+ return { kind: 'expr', sql: '1=1', params: [], refs: [] }
50
+ if (list.length === 1)
51
+ return list[0]
52
+ return {
53
+ kind: 'expr',
54
+ sql: list.map(p => `(${p.sql})`).join(' AND '),
55
+ params: list.flatMap(p => p.params),
56
+ refs: list.flatMap(p => p.refs),
57
+ }
58
+ }
59
+
60
+ export function or(...parts: (Expr | undefined | false | null)[]): Expr {
61
+ const list = parts.filter((p): p is Expr => !!p)
62
+ if (list.length === 0)
63
+ return { kind: 'expr', sql: '1=0', params: [], refs: [] }
64
+ if (list.length === 1)
65
+ return list[0]
66
+ return {
67
+ kind: 'expr',
68
+ sql: list.map(p => `(${p.sql})`).join(' OR '),
69
+ params: list.flatMap(p => p.params),
70
+ refs: list.flatMap(p => p.refs),
71
+ }
72
+ }
73
+
74
+ export function not(e: Expr): Expr {
75
+ return { kind: 'expr', sql: `NOT (${e.sql})`, params: e.params, refs: e.refs }
76
+ }
77
+
78
+ export function sqlIsNull(col: Column | Expr): Expr {
79
+ const x = operand(col)
80
+ return { kind: 'expr', sql: `${x.sql} IS NULL`, params: x.params, refs: x.refs }
81
+ }
82
+
83
+ export function sqlIsNotNull(col: Column | Expr): Expr {
84
+ const x = operand(col)
85
+ return { kind: 'expr', sql: `${x.sql} IS NOT NULL`, params: x.params, refs: x.refs }
86
+ }
87
+
88
+ export function inArray(col: Column | Expr, values: unknown[]): Expr {
89
+ const x = operand(col)
90
+ if (values.length === 0)
91
+ return { kind: 'expr', sql: '1=0', params: [], refs: x.refs }
92
+ const placeholders = values.map(() => '?').join(', ')
93
+ return {
94
+ kind: 'expr',
95
+ sql: `${x.sql} IN (${placeholders})`,
96
+ params: [...x.params, ...values],
97
+ refs: x.refs,
98
+ }
99
+ }
@@ -0,0 +1,3 @@
1
+ export * from './expr'
2
+ export * from './select'
3
+ export * from './table'
@@ -0,0 +1,144 @@
1
+ import type { Expect, IsEqual } from '../type-test'
2
+ import type { InferRow } from './select'
3
+ import { boolean, int, string } from '../schema'
4
+ import { and, eq, gt, inArray, like, or } from './expr'
5
+ import { from, select } from './select'
6
+ import { table } from './table'
7
+
8
+ const users = table('users', {
9
+ id: int(),
10
+ name: string(),
11
+ email: string(),
12
+ age: int(),
13
+ active: boolean(),
14
+ })
15
+
16
+ describe('sql select', () => {
17
+ it('selects all columns of a table', () => {
18
+ const q = select().from(users).toSQL()
19
+ expect(q.sql).toBe(
20
+ 'SELECT "users"."id", "users"."name", "users"."email", "users"."age", "users"."active" FROM "users"',
21
+ )
22
+ expect(q.params).toEqual([])
23
+ expect(q.dependencies).toEqual([
24
+ {
25
+ table: 'users',
26
+ select: ['active', 'age', 'email', 'id', 'name'],
27
+ where: [],
28
+ orderBy: [],
29
+ all: ['active', 'age', 'email', 'id', 'name'],
30
+ },
31
+ ])
32
+ })
33
+
34
+ it('selects specific columns with type inference', () => {
35
+ const q = select({ id: users.id, name: users.name }).from(users)
36
+ type Row = InferRow<typeof q>
37
+ type _T = Expect<IsEqual<Row, { id: number, name: string }>>
38
+
39
+ const c = q.toSQL()
40
+ expect(c.sql).toBe('SELECT "users"."id", "users"."name" FROM "users"')
41
+ expect(c.dependencies).toEqual([
42
+ {
43
+ table: 'users',
44
+ select: ['id', 'name'],
45
+ where: [],
46
+ orderBy: [],
47
+ all: ['id', 'name'],
48
+ },
49
+ ])
50
+ })
51
+
52
+ it('from() + pick() is a compact form with typed keys', () => {
53
+ const q = from(users).pick('id', 'name').where(eq(users.email, 'x'))
54
+ type Row = InferRow<typeof q>
55
+ type _T = Expect<IsEqual<Row, { id: number, name: string }>>
56
+
57
+ const c = q.toSQL()
58
+ expect(c.sql).toBe(
59
+ 'SELECT "users"."id", "users"."name" FROM "users" WHERE "users"."email" = ?',
60
+ )
61
+ expect(c.params).toEqual(['x'])
62
+ expect(c.dependencies[0].select).toEqual(['id', 'name'])
63
+ expect(c.dependencies[0].where).toEqual(['email'])
64
+ })
65
+
66
+ it('from() without pick returns the full row', () => {
67
+ const q = from(users)
68
+ type Row = InferRow<typeof q>
69
+ type _T = Expect<IsEqual<Row, {
70
+ id: number
71
+ name: string
72
+ email: string
73
+ age: number
74
+ active: boolean
75
+ }>>
76
+ expect(q.toSQL().sql).toBe(
77
+ 'SELECT "users"."id", "users"."name", "users"."email", "users"."age", "users"."active" FROM "users"',
78
+ )
79
+ })
80
+
81
+ it('supports aliases in selection', () => {
82
+ const q = select({ uid: users.id }).from(users).toSQL()
83
+ expect(q.sql).toBe('SELECT "users"."id" AS "uid" FROM "users"')
84
+ })
85
+
86
+ it('builds where with parameters', () => {
87
+ const q = select()
88
+ .from(users)
89
+ .where(and(eq(users.active, true), gt(users.age, 18)))
90
+ .toSQL()
91
+ expect(q.sql).toContain('WHERE ("users"."active" = ?) AND ("users"."age" > ?)')
92
+ expect(q.params).toEqual([true, 18])
93
+ })
94
+
95
+ it('separates select, where and orderBy dependencies', () => {
96
+ const q = select({ id: users.id })
97
+ .from(users)
98
+ .where(eq(users.email, 'a@b.c'))
99
+ .orderBy(users.age, 'DESC')
100
+ .toSQL()
101
+ const dep = q.dependencies[0]
102
+ expect(dep.table).toBe('users')
103
+ expect(dep.select).toEqual(['id'])
104
+ expect(dep.where).toEqual(['email'])
105
+ expect(dep.orderBy).toEqual(['age'])
106
+ expect(dep.all).toEqual(['age', 'email', 'id'])
107
+ })
108
+
109
+ it('supports or, like, inArray', () => {
110
+ const q = select()
111
+ .from(users)
112
+ .where(or(like(users.name, 'A%'), inArray(users.id, [1, 2, 3])))
113
+ .toSQL()
114
+ expect(q.sql).toContain('WHERE ("users"."name" LIKE ?) OR ("users"."id" IN (?, ?, ?))')
115
+ expect(q.params).toEqual(['A%', 1, 2, 3])
116
+ })
117
+
118
+ it('handles orderBy, limit, offset and tracks orderBy deps', () => {
119
+ const q = select({ id: users.id })
120
+ .from(users)
121
+ .orderBy(users.age, 'DESC')
122
+ .limit(10)
123
+ .offset(20)
124
+ .toSQL()
125
+ expect(q.sql).toBe(
126
+ 'SELECT "users"."id" FROM "users" ORDER BY "users"."age" DESC LIMIT ? OFFSET ?',
127
+ )
128
+ expect(q.params).toEqual([10, 20])
129
+ expect(q.dependencies[0].select).toEqual(['id'])
130
+ expect(q.dependencies[0].orderBy).toEqual(['age'])
131
+ expect(q.dependencies[0].all).toEqual(['age', 'id'])
132
+ })
133
+
134
+ it('dependencies() invalidation check helper', () => {
135
+ const q = select({ id: users.id }).from(users).where(eq(users.email, 'x'))
136
+ const deps = q.dependencies()
137
+ const changedTable = 'users'
138
+ const changedColumns = ['email']
139
+ const affected = deps.some(d =>
140
+ d.table === changedTable && d.all.some(c => changedColumns.includes(c)),
141
+ )
142
+ expect(affected).toBe(true)
143
+ })
144
+ })