@livestore/livestore 0.0.38 → 0.0.39-dev.2

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 (117) hide show
  1. package/README.md +3 -4
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/__tests__/react/fixture.d.ts +97 -3
  4. package/dist/__tests__/react/fixture.d.ts.map +1 -1
  5. package/dist/__tests__/react/fixture.js +10 -7
  6. package/dist/__tests__/react/fixture.js.map +1 -1
  7. package/dist/__tests__/react/useQuery.test.js +12 -23
  8. package/dist/__tests__/react/useQuery.test.js.map +1 -1
  9. package/dist/__tests__/react/useRow.test.js +4 -4
  10. package/dist/__tests__/react/useRow.test.js.map +1 -1
  11. package/dist/__tests__/reactiveQueries/sql.test.js +32 -35
  12. package/dist/__tests__/reactiveQueries/sql.test.js.map +1 -1
  13. package/dist/effect/LiveStore.d.ts +3 -2
  14. package/dist/effect/LiveStore.d.ts.map +1 -1
  15. package/dist/effect/LiveStore.js.map +1 -1
  16. package/dist/index.d.ts +6 -6
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +3 -3
  19. package/dist/index.js.map +1 -1
  20. package/dist/migrations.js +2 -2
  21. package/dist/migrations.js.map +1 -1
  22. package/dist/mutations.d.ts +3 -1
  23. package/dist/mutations.d.ts.map +1 -1
  24. package/dist/mutations.js +2 -2
  25. package/dist/mutations.js.map +1 -1
  26. package/dist/react/LiveStoreContext.d.ts +2 -2
  27. package/dist/react/LiveStoreContext.d.ts.map +1 -1
  28. package/dist/react/LiveStoreContext.js.map +1 -1
  29. package/dist/react/index.d.ts +1 -0
  30. package/dist/react/index.d.ts.map +1 -1
  31. package/dist/react/index.js +1 -0
  32. package/dist/react/index.js.map +1 -1
  33. package/dist/react/useAtom.d.ts +5 -0
  34. package/dist/react/useAtom.d.ts.map +1 -0
  35. package/dist/react/useAtom.js +16 -0
  36. package/dist/react/useAtom.js.map +1 -0
  37. package/dist/react/useQuery.d.ts +3 -3
  38. package/dist/react/useQuery.d.ts.map +1 -1
  39. package/dist/react/useQuery.js.map +1 -1
  40. package/dist/react/useRow.d.ts +5 -5
  41. package/dist/react/useRow.d.ts.map +1 -1
  42. package/dist/react/useRow.js +16 -30
  43. package/dist/react/useRow.js.map +1 -1
  44. package/dist/react/useTemporaryQuery.d.ts +3 -3
  45. package/dist/react/useTemporaryQuery.d.ts.map +1 -1
  46. package/dist/react/useTemporaryQuery.js.map +1 -1
  47. package/dist/reactiveQueries/base-class.d.ts +12 -4
  48. package/dist/reactiveQueries/base-class.d.ts.map +1 -1
  49. package/dist/reactiveQueries/base-class.js +1 -0
  50. package/dist/reactiveQueries/base-class.js.map +1 -1
  51. package/dist/reactiveQueries/graphql.d.ts +5 -5
  52. package/dist/reactiveQueries/graphql.d.ts.map +1 -1
  53. package/dist/reactiveQueries/graphql.js +11 -10
  54. package/dist/reactiveQueries/graphql.js.map +1 -1
  55. package/dist/reactiveQueries/js.d.ts +10 -7
  56. package/dist/reactiveQueries/js.d.ts.map +1 -1
  57. package/dist/reactiveQueries/js.js +19 -11
  58. package/dist/reactiveQueries/js.js.map +1 -1
  59. package/dist/reactiveQueries/sql.d.ts +21 -15
  60. package/dist/reactiveQueries/sql.d.ts.map +1 -1
  61. package/dist/reactiveQueries/sql.js +50 -28
  62. package/dist/reactiveQueries/sql.js.map +1 -1
  63. package/dist/row-query.d.ts +22 -21
  64. package/dist/row-query.d.ts.map +1 -1
  65. package/dist/row-query.js +62 -47
  66. package/dist/row-query.js.map +1 -1
  67. package/dist/schema/index.d.ts +3 -2
  68. package/dist/schema/index.d.ts.map +1 -1
  69. package/dist/schema/index.js +3 -2
  70. package/dist/schema/index.js.map +1 -1
  71. package/dist/schema/parse-utils.d.ts +6 -0
  72. package/dist/schema/parse-utils.d.ts.map +1 -0
  73. package/dist/schema/parse-utils.js +59 -0
  74. package/dist/schema/parse-utils.js.map +1 -0
  75. package/dist/schema/system-tables.d.ts +24 -8
  76. package/dist/schema/system-tables.d.ts.map +1 -1
  77. package/dist/schema/table-def.d.ts +32 -7
  78. package/dist/schema/table-def.d.ts.map +1 -1
  79. package/dist/schema/table-def.js +18 -6
  80. package/dist/schema/table-def.js.map +1 -1
  81. package/dist/store.d.ts +4 -8
  82. package/dist/store.d.ts.map +1 -1
  83. package/dist/store.js +7 -8
  84. package/dist/store.js.map +1 -1
  85. package/dist/update-path.d.ts +52 -0
  86. package/dist/update-path.d.ts.map +1 -0
  87. package/dist/update-path.js +33 -0
  88. package/dist/update-path.js.map +1 -0
  89. package/dist/utils/util.d.ts +1 -0
  90. package/dist/utils/util.d.ts.map +1 -1
  91. package/dist/utils/util.js.map +1 -1
  92. package/package.json +4 -4
  93. package/src/__tests__/react/fixture.tsx +13 -7
  94. package/src/__tests__/react/useQuery.test.tsx +12 -29
  95. package/src/__tests__/react/useRow.test.tsx +5 -7
  96. package/src/__tests__/reactiveQueries/sql.test.ts +33 -35
  97. package/src/effect/LiveStore.ts +3 -2
  98. package/src/index.ts +6 -6
  99. package/src/migrations.ts +2 -2
  100. package/src/mutations.ts +8 -3
  101. package/src/react/LiveStoreContext.ts +3 -2
  102. package/src/react/index.ts +1 -0
  103. package/src/react/useAtom.ts +25 -0
  104. package/src/react/useQuery.ts +7 -7
  105. package/src/react/useRow.ts +27 -47
  106. package/src/react/useTemporaryQuery.ts +4 -6
  107. package/src/reactiveQueries/base-class.ts +18 -4
  108. package/src/reactiveQueries/graphql.ts +16 -14
  109. package/src/reactiveQueries/js.ts +36 -15
  110. package/src/reactiveQueries/sql.ts +77 -37
  111. package/src/row-query.ts +155 -113
  112. package/src/schema/index.ts +5 -4
  113. package/src/schema/parse-utils.ts +84 -0
  114. package/src/schema/table-def.ts +80 -12
  115. package/src/store.ts +14 -29
  116. package/src/update-path.ts +102 -0
  117. package/src/utils/util.ts +2 -0
@@ -1,19 +1,25 @@
1
1
  import { shouldNeverHappen } from '@livestore/utils'
2
+ import { Schema } from '@livestore/utils/effect'
2
3
  import * as otel from '@opentelemetry/api'
3
4
 
4
5
  import { globalDbGraph } from '../global-state.js'
5
6
  import type { Thunk } from '../reactive.js'
6
7
  import type { RefreshReason } from '../store.js'
8
+ import type { UpdatePathDesc, UpdatePathDescNone } from '../update-path.js'
7
9
  import { getDurationMsFromSpan } from '../utils/otel.js'
8
10
  import type { Bindable } from '../utils/util.js'
9
11
  import { prepareBindValues } from '../utils/util.js'
10
- import type { DbContext, DbGraph, GetAtomResult } from './base-class.js'
12
+ import type { DbContext, DbGraph, GetAtomResult, LiveQuery } from './base-class.js'
11
13
  import { LiveStoreQueryBase, makeGetAtomResult } from './base-class.js'
12
- import { LiveStoreJSQuery } from './js.js'
13
14
 
14
- export const querySQL = <Row>(
15
+ export type MapRows<TResult, TRaw = any> =
16
+ | ((rows: ReadonlyArray<TRaw>) => TResult)
17
+ | Schema.Schema<ReadonlyArray<TRaw>, TResult>
18
+
19
+ export const querySQL = <Result, TRaw = any>(
15
20
  query: string | ((get: GetAtomResult) => string),
16
21
  options?: {
22
+ map?: MapRows<Result, TRaw>
17
23
  /**
18
24
  * Can be provided explicitly to slightly speed up initial query performance
19
25
  *
@@ -24,28 +30,40 @@ export const querySQL = <Row>(
24
30
  label?: string
25
31
  dbGraph?: DbGraph
26
32
  },
27
- ) =>
28
- new LiveStoreSQLQuery<Row>({
33
+ ): LiveQuery<Result, UpdatePathDescNone> =>
34
+ new LiveStoreSQLQuery<Result, UpdatePathDescNone>({
29
35
  label: options?.label,
30
36
  genQueryString: query,
31
37
  queriedTables: options?.queriedTables,
32
38
  bindValues: options?.bindValues,
33
39
  dbGraph: options?.dbGraph,
40
+ map: options?.map,
41
+ updatePathDesc: { _tag: 'None' },
34
42
  })
35
43
 
36
44
  /* An object encapsulating a reactive SQL query */
37
- export class LiveStoreSQLQuery<Row> extends LiveStoreQueryBase<ReadonlyArray<Row>> {
45
+ export class LiveStoreSQLQuery<
46
+ Result,
47
+ TUpdatePath extends UpdatePathDesc = UpdatePathDescNone,
48
+ > extends LiveStoreQueryBase<Result, TUpdatePath> {
38
49
  _tag: 'sql' = 'sql'
39
50
 
40
51
  /** A reactive thunk representing the query text */
41
52
  queryString$: Thunk<string, DbContext, RefreshReason>
42
53
 
43
54
  /** A reactive thunk representing the query results */
44
- results$: Thunk<ReadonlyArray<Row>, DbContext, RefreshReason>
55
+ results$: Thunk<Result, DbContext, RefreshReason>
45
56
 
46
57
  label: string
47
58
 
48
- protected dbGraph: DbGraph
59
+ protected dbGraph
60
+
61
+ /** Currently only used by `rowQuery` for lazy table migrations and eager default row insertion */
62
+ private execBeforeFirstRun
63
+
64
+ private mapRows
65
+
66
+ updatePathDesc: TUpdatePath
49
67
 
50
68
  constructor({
51
69
  genQueryString,
@@ -53,18 +71,32 @@ export class LiveStoreSQLQuery<Row> extends LiveStoreQueryBase<ReadonlyArray<Row
53
71
  bindValues,
54
72
  label: label_,
55
73
  dbGraph,
74
+ map,
75
+ execBeforeFirstRun,
76
+ updatePathDesc,
56
77
  }: {
57
78
  label?: string
58
79
  genQueryString: string | ((get: GetAtomResult) => string)
59
80
  queriedTables?: Set<string>
60
81
  bindValues?: Bindable
61
82
  dbGraph?: DbGraph
83
+ map?: MapRows<Result>
84
+ execBeforeFirstRun?: (ctx: DbContext) => void
85
+ updatePathDesc?: TUpdatePath
62
86
  }) {
63
87
  super()
64
88
 
65
89
  const label = label_ ?? genQueryString.toString()
66
90
  this.label = `sql(${label})`
67
91
  this.dbGraph = dbGraph ?? globalDbGraph
92
+ this.execBeforeFirstRun = execBeforeFirstRun
93
+ this.updatePathDesc = updatePathDesc ?? ({ _tag: 'None' } as TUpdatePath)
94
+ this.mapRows =
95
+ map === undefined
96
+ ? (rows: any) => rows as Result
97
+ : typeof map === 'function'
98
+ ? map
99
+ : (rows: any) => Schema.parseSync(map)(rows)
68
100
 
69
101
  // TODO don't even create a thunk if query string is static
70
102
  const queryString$ = this.dbGraph.makeThunk(
@@ -88,7 +120,7 @@ export class LiveStoreSQLQuery<Row> extends LiveStoreQueryBase<ReadonlyArray<Row
88
120
 
89
121
  const queriedTablesRef = { current: queriedTables }
90
122
 
91
- const results$ = this.dbGraph.makeThunk<ReadonlyArray<Row>>(
123
+ const results$ = this.dbGraph.makeThunk<Result>(
92
124
  (get, setDebugInfo, { store, otelTracer, rootOtelContext }, otelContext) =>
93
125
  otelTracer.startActiveSpan(
94
126
  'sql:...', // NOTE span name will be overridden further down
@@ -97,6 +129,11 @@ export class LiveStoreSQLQuery<Row> extends LiveStoreQueryBase<ReadonlyArray<Row
97
129
  (span) => {
98
130
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
99
131
 
132
+ if (this.execBeforeFirstRun !== undefined) {
133
+ this.execBeforeFirstRun({ store, otelTracer, rootOtelContext })
134
+ this.execBeforeFirstRun = undefined
135
+ }
136
+
100
137
  const sqlString = get(queryString$, otelContext)
101
138
 
102
139
  if (queriedTablesRef.current === undefined) {
@@ -112,13 +149,15 @@ export class LiveStoreSQLQuery<Row> extends LiveStoreQueryBase<ReadonlyArray<Row
112
149
  span.setAttribute('sql.query', sqlString)
113
150
  span.updateName(`sql:${sqlString.slice(0, 50)}`)
114
151
 
115
- const results = store.inMemoryDB.select<Row>(sqlString, {
152
+ const rawResults = store.inMemoryDB.select<any>(sqlString, {
116
153
  queriedTables,
117
154
  bindValues: bindValues ? prepareBindValues(bindValues, sqlString) : undefined,
118
155
  otelContext,
119
156
  })
120
157
 
121
- span.setAttribute('sql.rowsCount', results.length)
158
+ span.setAttribute('sql.rowsCount', rawResults.length)
159
+
160
+ const result = this.mapRows(rawResults)
122
161
 
123
162
  span.end()
124
163
 
@@ -126,7 +165,7 @@ export class LiveStoreSQLQuery<Row> extends LiveStoreQueryBase<ReadonlyArray<Row
126
165
 
127
166
  setDebugInfo({ _tag: 'sql', label, query: sqlString, durationMs })
128
167
 
129
- return results
168
+ return result
130
169
  },
131
170
  ),
132
171
  { label: queryLabel },
@@ -139,33 +178,34 @@ export class LiveStoreSQLQuery<Row> extends LiveStoreQueryBase<ReadonlyArray<Row
139
178
  * Returns a new reactive query that contains the result of
140
179
  * running an arbitrary JS computation on the results of this SQL query.
141
180
  */
142
- pipe = <U>(fn: (result: ReadonlyArray<Row>, get: GetAtomResult) => U): LiveStoreJSQuery<U> =>
143
- new LiveStoreJSQuery({
144
- fn: (get) => {
145
- const results = get(this.results$!)
146
- return fn(results, get)
147
- },
148
- label: `${this.label}:js`,
149
- onDestroy: () => this.destroy(),
150
- dbGraph: this.dbGraph,
151
- })
181
+ // pipe = <U>(fn: (result: Result, get: GetAtomResult) => U): LiveStoreJSQuery<U> =>
182
+ // new LiveStoreJSQuery({
183
+ // fn: (get) => {
184
+ // const results = get(this.results$!)
185
+ // return fn(results, get)
186
+ // },
187
+ // label: `${this.label}:js`,
188
+ // onDestroy: () => this.destroy(),
189
+ // dbGraph: this.dbGraph,
190
+ // updatePathDesc: undefined,
191
+ // })
152
192
 
153
193
  /** Returns a reactive query */
154
- getFirstRow = (args?: { defaultValue?: Row }) =>
155
- new LiveStoreJSQuery({
156
- fn: (get) => {
157
- const results = get(this.results$!)
158
- if (results.length === 0 && args?.defaultValue === undefined) {
159
- // const queryLabel = this._tag === 'sql' ? this.queryString$!.computeResult(otelContext) : this.label
160
- const queryLabel = this.label
161
- return shouldNeverHappen(`Expected query ${queryLabel} to return at least one result`)
162
- }
163
- return results[0] ?? args!.defaultValue!
164
- },
165
- label: `${this.label}:first`,
166
- onDestroy: () => this.destroy(),
167
- dbGraph: this.dbGraph,
168
- })
194
+ // getFirstRow = (args?: { defaultValue?: Result }) =>
195
+ // new LiveStoreJSQuery({
196
+ // fn: (get) => {
197
+ // const results = get(this.results$!)
198
+ // if (results.length === 0 && args?.defaultValue === undefined) {
199
+ // // const queryLabel = this._tag === 'sql' ? this.queryString$!.computeResult(otelContext) : this.label
200
+ // const queryLabel = this.label
201
+ // return shouldNeverHappen(`Expected query ${queryLabel} to return at least one result`)
202
+ // }
203
+ // return results[0] ?? args!.defaultValue!
204
+ // },
205
+ // label: `${this.label}:first`,
206
+ // onDestroy: () => this.destroy(),
207
+ // dbGraph: this.dbGraph,
208
+ // })
169
209
 
170
210
  destroy = () => {
171
211
  this.dbGraph.destroyNode(this.queryString$)
package/src/row-query.ts CHANGED
@@ -3,167 +3,209 @@ import { pipe, ReadonlyRecord, Schema, TreeFormatter } from '@livestore/utils/ef
3
3
  import type * as otel from '@opentelemetry/api'
4
4
  import { SqliteAst, SqliteDsl } from 'effect-db-schema'
5
5
 
6
+ import { computed } from './index.js'
6
7
  import type { InMemoryDatabase } from './inMemoryDatabase.js'
7
8
  import { migrateTable } from './migrations.js'
8
9
  import type { Ref } from './reactive.js'
9
- import type { DbContext, DbGraph } from './reactiveQueries/base-class.js'
10
- import type { LiveStoreJSQuery } from './reactiveQueries/js.js'
10
+ import type { DbContext, DbGraph, GetResult, LiveQuery, LiveQueryAny } from './reactiveQueries/base-class.js'
11
+ // import type { LiveStoreJSQuery } from './reactiveQueries/js.js'
11
12
  import { LiveStoreSQLQuery } from './reactiveQueries/sql.js'
12
13
  import { SCHEMA_META_TABLE } from './schema/index.js'
13
- import type { TableDef } from './schema/table-def.js'
14
- import type { RefreshReason, Store } from './store.js'
14
+ import {
15
+ type DefaultSqliteTableDef,
16
+ getDefaultValuesEncoded,
17
+ type TableDef,
18
+ type TableOptions,
19
+ } from './schema/table-def.js'
20
+ import type { RefreshReason } from './store.js'
21
+ import type { UpdatePathDesc, UpdatePathDescCol, UpdatePathDescNone, UpdatePathDescRow } from './update-path.js'
22
+ import type { GetValForKey } from './utils/util.js'
15
23
  import { prepareBindValues, sql } from './utils/util.js'
16
24
 
17
- export type RowQueryArgs<TTableDef extends TableDef> = TTableDef['options']['isSingleton'] extends true
18
- ? {
19
- table: TTableDef
20
- store: Store
21
- otelContext?: otel.Context
22
- defaultValues: Partial<RowResult<TTableDef>>
23
- skipInsertDefaultRow?: boolean
24
- dbGraph?: DbGraph
25
- }
26
- : {
27
- table: TTableDef
28
- store: Store
29
- otelContext?: otel.Context
30
- id: string
31
- defaultValues: Partial<RowResult<TTableDef>>
32
- skipInsertDefaultRow?: boolean
33
- dbGraph?: DbGraph
34
- }
25
+ export type RowQueryOptions = {
26
+ otelContext?: otel.Context
27
+ skipInsertDefaultRow?: boolean
28
+ dbGraph?: DbGraph
29
+ }
30
+
31
+ export type RowQueryOptionsDefaulValues<TTableDef extends TableDef> = {
32
+ defaultValues: Partial<RowResult<TTableDef>>
33
+ }
34
+
35
+ export type MakeRowQuery = {
36
+ <TTableDef extends TableDef<DefaultSqliteTableDef, boolean, TableOptions & { isSingleton: true }>>(
37
+ table: TTableDef,
38
+ options?: RowQueryOptions,
39
+ ): LiveQuery<RowResult<TTableDef>, UpdatePathDescRow<TTableDef>>
40
+ <TTableDef extends TableDef<DefaultSqliteTableDef, boolean, TableOptions & { isSingleton: false }>>(
41
+ table: TTableDef,
42
+ // TODO adjust so it works with arbitrary primary keys or unique constraints
43
+ id: string,
44
+ options?: RowQueryOptions & RowQueryOptionsDefaulValues<TTableDef>,
45
+ ): LiveQuery<RowResult<TTableDef>, UpdatePathDescRow<TTableDef>>
46
+ }
35
47
 
36
48
  // TODO also allow other where clauses and multiple rows
37
- export const rowQuery = <TTableDef extends TableDef>(
38
- args: RowQueryArgs<TTableDef>,
39
- ): LiveStoreJSQuery<RowResult<TTableDef>> => {
40
- const { table, store, defaultValues, skipInsertDefaultRow, dbGraph } = args
41
- const otelContext = args.otelContext ?? store.otel.queriesSpanContext
42
- const id: string | undefined = (args as any).id
49
+ export const rowQuery: MakeRowQuery = <TTableDef extends TableDef>(
50
+ table: TTableDef,
51
+ idOrOptions?: string | RowQueryOptions,
52
+ options_?: RowQueryOptions & RowQueryOptionsDefaulValues<TTableDef>,
53
+ ) => {
54
+ const id = typeof idOrOptions === 'string' ? idOrOptions : undefined
55
+ const options = typeof idOrOptions === 'string' ? options_ : idOrOptions
56
+ const defaultValues: Partial<RowResult<TTableDef>> | undefined = (options as any)?.defaultValues ?? {}
43
57
 
44
58
  // Validate query args
45
59
  if (table.options.isSingleton === true && id !== undefined) {
46
- shouldNeverHappen(`Cannot query state table ${table.schema.name} with id "${id}" as it is a singleton`)
60
+ shouldNeverHappen(`Cannot query state table ${table.sqliteDef.name} with id "${id}" as it is a singleton`)
47
61
  } else if (table.options.isSingleton !== true && id === undefined) {
48
- shouldNeverHappen(`Cannot query state table ${table.schema.name} without id`)
62
+ shouldNeverHappen(`Cannot query state table ${table.sqliteDef.name} without id`)
49
63
  }
50
64
 
51
- const stateSchema = table.schema
65
+ const stateSchema = table.sqliteDef
52
66
  const componentTableName = stateSchema.name
53
67
 
54
- type TComponentState = SqliteDsl.FromColumns.RowDecoded<TTableDef['schema']['columns']>
55
-
56
- // TODO find a better solution for this
57
- if (store.tableRefs[componentTableName] === undefined) {
58
- const schemaHash = SqliteAst.hash(stateSchema.ast)
59
- const res = store.inMemoryDB.select<{ schemaHash: number }>(
60
- sql`SELECT schemaHash FROM ${SCHEMA_META_TABLE} WHERE tableName = '${componentTableName}'`,
61
- )
62
- if (res.length === 0 || res[0]!.schemaHash !== schemaHash) {
63
- migrateTable({
64
- db: store._proxyDb,
65
- tableAst: stateSchema.ast,
66
- otelContext,
67
- schemaHash,
68
- })
69
- }
70
-
71
- const label = `tableRef:${componentTableName}`
72
-
73
- const existingTableRefFromGraph = Array.from(store.graph.atoms.values()).find(
74
- (_) => _._tag === 'ref' && _.label === label,
75
- ) as Ref<null, DbContext, RefreshReason> | undefined
76
-
77
- store.tableRefs[componentTableName] =
78
- existingTableRefFromGraph ??
79
- store.graph.makeRef(null, {
80
- equal: () => false,
81
- label,
82
- meta: { liveStoreRefType: 'table' },
83
- })
84
- }
85
-
86
- if (skipInsertDefaultRow !== true) {
87
- // TODO find a way to only do this if necessary
88
- insertRowWithDefaultValuesOrIgnore({
89
- db: store._proxyDb,
90
- id: id ?? 'singleton',
91
- stateSchema,
92
- otelContext,
93
- defaultValues,
94
- })
95
- }
96
-
97
68
  const whereClause = id === undefined ? '' : `where id = '${id}'`
98
69
  const queryStr = sql`select * from ${componentTableName} ${whereClause} limit 1`
99
70
 
100
71
  return new LiveStoreSQLQuery({
101
- label: `localState:query:${stateSchema.name}${id === undefined ? '' : `:${id}`}`,
72
+ label: `rowQuery:query:${stateSchema.name}${id === undefined ? '' : `:${id}`}`,
102
73
  genQueryString: queryStr,
103
74
  queriedTables: new Set([componentTableName]),
104
- dbGraph,
105
- }).pipe<TComponentState>((results) => {
106
- if (results.length === 0) return shouldNeverHappen(`No results for query ${queryStr}`)
107
-
108
- const componentStateEffectSchema = SqliteDsl.structSchemaForTable(stateSchema)
109
- const parseResult = Schema.parseEither(componentStateEffectSchema)(results[0]!)
110
-
111
- if (parseResult._tag === 'Left') {
112
- console.error('decode error', TreeFormatter.formatErrors(parseResult.left.errors), 'results', results)
113
- return shouldNeverHappen(`Error decoding query result for ${queryStr}`)
114
- }
115
-
116
- return table.isSingleColumn === true ? parseResult.right.value : parseResult.right
117
- }) as unknown as LiveStoreJSQuery<RowResult<TTableDef>>
75
+ dbGraph: options?.dbGraph,
76
+ execBeforeFirstRun: makeExecBeforeFirstRun({
77
+ otelContext: options?.otelContext,
78
+ table,
79
+ componentTableName,
80
+ defaultValues,
81
+ id,
82
+ skipInsertDefaultRow: options?.skipInsertDefaultRow,
83
+ }),
84
+ map: (results): RowResult<TTableDef> => {
85
+ if (results.length === 0) return shouldNeverHappen(`No results for query ${queryStr}`)
86
+
87
+ const componentStateEffectSchema = SqliteDsl.structSchemaForTable(stateSchema)
88
+ const parseResult = Schema.parseEither(componentStateEffectSchema)(results[0]!)
89
+
90
+ if (parseResult._tag === 'Left') {
91
+ console.error('decode error', TreeFormatter.formatError(parseResult.left), 'results', results)
92
+ return shouldNeverHappen(`Error decoding query result for ${queryStr}`)
93
+ }
94
+
95
+ return table.isSingleColumn === true ? parseResult.right.value : parseResult.right
96
+ },
97
+ updatePathDesc: { _tag: 'Row', table, id: id ?? 'singleton' },
98
+ })
118
99
  }
119
100
 
120
- type GetValForKey<T, K> = K extends keyof T ? T[K] : never
121
-
122
101
  export type RowResult<TTableDef extends TableDef> = TTableDef['isSingleColumn'] extends true
123
- ? GetValForKey<SqliteDsl.FromColumns.RowDecoded<TTableDef['schema']['columns']>, 'value'>
124
- : SqliteDsl.FromColumns.RowDecoded<TTableDef['schema']['columns']>
102
+ ? GetValForKey<SqliteDsl.FromColumns.RowDecoded<TTableDef['sqliteDef']['columns']>, 'value'>
103
+ : SqliteDsl.FromColumns.RowDecoded<TTableDef['sqliteDef']['columns']>
125
104
 
126
105
  export type RowResultEncoded<TTableDef extends TableDef> = TTableDef['isSingleColumn'] extends true
127
- ? GetValForKey<SqliteDsl.FromColumns.RowEncoded<TTableDef['schema']['columns']>, 'value'>
128
- : SqliteDsl.FromColumns.RowEncoded<TTableDef['schema']['columns']>
129
-
130
- export type RowInsert<TTableDef extends TableDef> = TTableDef['isSingleColumn'] extends true
131
- ? GetValForKey<SqliteDsl.FromColumns.InsertRowDecoded<TTableDef['schema']['columns']>, 'value'>
132
- : SqliteDsl.FromColumns.InsertRowDecoded<TTableDef['schema']['columns']>
106
+ ? GetValForKey<SqliteDsl.FromColumns.RowEncoded<TTableDef['sqliteDef']['columns']>, 'value'>
107
+ : SqliteDsl.FromColumns.RowEncoded<TTableDef['sqliteDef']['columns']>
108
+
109
+ export const deriveColQuery: {
110
+ <TQuery extends LiveQuery<any, UpdatePathDescNone>, TCol extends keyof TQuery['result!'] & string>(
111
+ query$: TQuery,
112
+ colName: TCol,
113
+ ): LiveQuery<TQuery['result!'][TCol], UpdatePathDescNone>
114
+ <TQuery extends LiveQuery<any, UpdatePathDescRow<any>>, TCol extends keyof TQuery['result!'] & string>(
115
+ query$: TQuery,
116
+ colName: TCol,
117
+ ): LiveQuery<TQuery['result!'][TCol], UpdatePathDescCol<TQuery['updatePathDesc']['table'], TCol>>
118
+ } = (query$: LiveQueryAny, colName: string) => {
119
+ return computed((get) => get(query$)[colName], {
120
+ label: `deriveColQuery:${query$.label}:${colName}`,
121
+ updatePathDesc:
122
+ query$.updatePathDesc._tag === 'Row'
123
+ ? { _tag: 'Col', table: query$.updatePathDesc.table, column: colName, id: query$.updatePathDesc.id }
124
+ : undefined,
125
+ }) as any
126
+ }
133
127
 
134
128
  const insertRowWithDefaultValuesOrIgnore = ({
135
129
  db,
136
130
  id,
137
- stateSchema,
131
+ table,
138
132
  otelContext,
139
133
  defaultValues: explicitDefaultValues,
140
134
  }: {
141
135
  db: InMemoryDatabase
142
136
  id: string
143
- stateSchema: SqliteDsl.TableDefinition<string, SqliteDsl.Columns>
137
+ table: TableDef
144
138
  otelContext: otel.Context
145
139
  defaultValues: Partial<RowResult<TableDef>> | undefined
146
140
  }) => {
147
- const columnNames = Object.keys(stateSchema.columns)
141
+ const columnNames = Object.keys(table.sqliteDef.columns)
148
142
  const columnValues = columnNames.map((name) => `$${name}`).join(', ')
149
143
 
150
- const tableName = stateSchema.name
144
+ const tableName = table.sqliteDef.name
151
145
  const insertQuery = sql`insert into ${tableName} (${columnNames.join(
152
146
  ', ',
153
147
  )}) select ${columnValues} where not exists(select 1 from ${tableName} where id = '${id}')`
154
148
 
155
149
  const defaultValues = pipe(
156
- stateSchema.columns,
157
- ReadonlyRecord.filter((_, key) => key !== 'id'),
158
- ReadonlyRecord.map((column, columnName) =>
159
- column.default._tag === 'None'
160
- ? column.nullable === true
161
- ? null
162
- : shouldNeverHappen(`Column ${columnName} has no default value and is not nullable`)
163
- : Schema.encodeSync(column.schema)(column.default.value),
164
- ),
150
+ getDefaultValuesEncoded(table),
165
151
  ReadonlyRecord.map((val, columnName) => explicitDefaultValues?.[columnName] ?? val),
166
152
  )
167
153
 
168
- void db.execute(insertQuery, prepareBindValues({ ...defaultValues, id }, insertQuery), [tableName], { otelContext })
154
+ db.execute(insertQuery, prepareBindValues({ ...defaultValues, id }, insertQuery), [tableName], { otelContext })
169
155
  }
156
+
157
+ const makeExecBeforeFirstRun =
158
+ ({
159
+ id,
160
+ defaultValues,
161
+ skipInsertDefaultRow,
162
+ otelContext: otelContext_,
163
+ table,
164
+ componentTableName,
165
+ }: {
166
+ id?: string
167
+ defaultValues?: any
168
+ skipInsertDefaultRow?: boolean
169
+ otelContext?: otel.Context
170
+ componentTableName: string
171
+ table: TableDef
172
+ }) =>
173
+ ({ store }: DbContext) => {
174
+ const otelContext = otelContext_ ?? store.otel.queriesSpanContext
175
+
176
+ // TODO find a better solution for this
177
+ if (store.tableRefs[componentTableName] === undefined) {
178
+ const schemaHash = SqliteAst.hash(table.sqliteDef.ast)
179
+ const res = store.inMemoryDB.select<{ schemaHash: number }>(
180
+ sql`SELECT schemaHash FROM ${SCHEMA_META_TABLE} WHERE tableName = '${componentTableName}'`,
181
+ )
182
+ if (res.length === 0 || res[0]!.schemaHash !== schemaHash) {
183
+ migrateTable({
184
+ db: store._proxyDb,
185
+ tableAst: table.sqliteDef.ast,
186
+ otelContext,
187
+ schemaHash,
188
+ })
189
+ }
190
+
191
+ const label = `tableRef:${componentTableName}`
192
+
193
+ // TODO find a better implementation for this
194
+ const existingTableRefFromGraph = Array.from(store.graph.atoms.values()).find(
195
+ (_) => _._tag === 'ref' && _.label === label,
196
+ ) as Ref<null, DbContext, RefreshReason> | undefined
197
+
198
+ store.tableRefs[componentTableName] = existingTableRefFromGraph ?? store.makeTableRef(componentTableName)
199
+ }
200
+
201
+ if (skipInsertDefaultRow !== true) {
202
+ // TODO find a way to only do this if necessary
203
+ insertRowWithDefaultValuesOrIgnore({
204
+ db: store._proxyDb,
205
+ id: id ?? 'singleton',
206
+ table,
207
+ otelContext,
208
+ defaultValues,
209
+ })
210
+ }
211
+ }
@@ -7,6 +7,7 @@ import type { TableDef } from './table-def.js'
7
7
  export * from './action.js'
8
8
  export * from './system-tables.js'
9
9
  export * as DbSchema from './table-def.js'
10
+ export * as ParseUtils from './parse-utils.js'
10
11
 
11
12
  // export { SqliteDsl as DbSchema } from 'effect-db-schema'
12
13
 
@@ -36,11 +37,11 @@ export const makeSchema = <TInputSchema extends InputSchema>(
36
37
 
37
38
  for (const tableDef of inputTables) {
38
39
  // TODO validate tables (e.g. index names are unique)
39
- tables.set(tableDef.schema.ast.name, tableDef)
40
+ tables.set(tableDef.sqliteDef.ast.name, tableDef)
40
41
  }
41
42
 
42
43
  for (const tableDef of systemTables) {
43
- tables.set(tableDef.schema.name, tableDef)
44
+ tables.set(tableDef.sqliteDef.name, tableDef)
44
45
  }
45
46
 
46
47
  return {
@@ -57,7 +58,7 @@ export const makeSchema = <TInputSchema extends InputSchema>(
57
58
  */
58
59
  export type DbSchemaFromInputSchemaTables<TTables extends InputSchema['tables']> =
59
60
  TTables extends ReadonlyArray<TableDef>
60
- ? { [K in TTables[number] as K['schema']['name']]: K['schema'] }
61
+ ? { [K in TTables[number] as K['sqliteDef']['name']]: K['sqliteDef'] }
61
62
  : TTables extends Record<string, TableDef>
62
- ? { [K in keyof TTables as TTables[K]['schema']['name']]: TTables[K]['schema'] }
63
+ ? { [K in keyof TTables as TTables[K]['sqliteDef']['name']]: TTables[K]['sqliteDef'] }
63
64
  : never
@@ -0,0 +1,84 @@
1
+ import { shouldNeverHappen } from '@livestore/utils'
2
+ import type { ReadonlyArray } from '@livestore/utils/effect'
3
+ import { pipe, ReadonlyRecord, Schema, TreeFormatter } from '@livestore/utils/effect'
4
+ import { SqliteDsl as __SqliteDsl } from 'effect-db-schema'
5
+
6
+ import { type FromColumns, type FromTable, getDefaultValuesDecoded, type TableDef } from './table-def.js'
7
+
8
+ // export const headUnsafe = <From, To>(schema: Schema.Schema<ReadonlyArray<From>, ReadonlyArray<To>>) =>
9
+ // Schema.transform(
10
+ // schema,
11
+ // Schema.xxx(schema),
12
+ // (rows) => rows[0]!,
13
+ // (row) => [row],
14
+ // )
15
+
16
+ // export const head = <From, To>(schema: Schema.Schema<From, To>) =>
17
+ // Schema.transform(
18
+ // Schema.array(schema),
19
+ // Schema.optionFromSelf(Schema.to(schema)),
20
+ // (rows) => Option.fromNullable(rows[0]),
21
+ // (row) => (row._tag === 'None' ? [] : [row.value]),
22
+ // )
23
+
24
+ // export const headOr = <From, To>(schema: Schema.Schema<From, To>, fallback: To) =>
25
+ // Schema.transform(
26
+ // Schema.array(schema),
27
+ // Schema.to(schema),
28
+ // (rows) => rows[0] ?? fallback,
29
+ // (row) => [row],
30
+ // )
31
+
32
+ // export const pluck = <From extends {}, To, K extends keyof From & keyof To & string>(
33
+ // schema: Schema.Schema<From, To>,
34
+ // prop: K,
35
+ // ): Schema.Schema<From, To[K]> => {
36
+ // const toSchema = Schema.make(SchemaAST.getPropertySignatures(schema.ast).find((s) => s.name === prop)!.type) as any
37
+ // return Schema.transform(
38
+ // schema,
39
+ // toSchema,
40
+ // (row) => (row as any)[prop],
41
+ // (val) => ({ [prop]: val }) as any,
42
+ // )
43
+ // }
44
+
45
+ // export const schemaFor = <TTableDef extends TableDef>(
46
+ // table: TTableDef,
47
+ // ): Schema.Schema<FromTable.RowEncoded<TTableDef>, FromTable.RowDecoded<TTableDef>> =>
48
+ // SqliteDsl.structSchemaForTable(table.sqliteDef) as any
49
+
50
+ export const many = <TTableDef extends TableDef>(
51
+ table: TTableDef,
52
+ ): ((rawRows: ReadonlyArray<any>) => ReadonlyArray<FromTable.RowDecoded<TTableDef>>) => {
53
+ return Schema.parseSync(Schema.array(table.schema)) as TODO
54
+ }
55
+
56
+ export const first =
57
+ <TTableDef extends TableDef>(
58
+ table: TTableDef,
59
+ fallback?: FromColumns.InsertRowDecoded<TTableDef['sqliteDef']['columns']>,
60
+ ) =>
61
+ (rawRows: ReadonlyArray<any>) => {
62
+ const rows = Schema.parseSync(Schema.array(table.schema))(rawRows)
63
+
64
+ if (rows.length === 0) {
65
+ const schemaDefaultValues = getDefaultValuesDecoded(table)
66
+
67
+ const defaultValuesResult = pipe(
68
+ table.sqliteDef.columns,
69
+ ReadonlyRecord.map((_column, columnName) => (fallback as any)?.[columnName] ?? schemaDefaultValues[columnName]),
70
+ Schema.validateEither(table.schema),
71
+ )
72
+
73
+ if (defaultValuesResult._tag === 'Right') {
74
+ return defaultValuesResult.right
75
+ } else {
76
+ console.error('decode error', TreeFormatter.formatError(defaultValuesResult.left))
77
+ return shouldNeverHappen(
78
+ `Expected query (for table ${table.sqliteDef.name}) to return at least one result but found none. Also can't fallback to default values as some were not provided.`,
79
+ )
80
+ }
81
+ }
82
+
83
+ return rows[0]!
84
+ }