@livestore/common 0.3.2-dev.9 → 0.4.0-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/ClientSessionLeaderThreadProxy.d.ts +2 -2
- package/dist/ClientSessionLeaderThreadProxy.d.ts.map +1 -1
- package/dist/adapter-types.d.ts +4 -4
- package/dist/adapter-types.d.ts.map +1 -1
- package/dist/debug-info.d.ts +17 -17
- package/dist/devtools/devtools-messages-client-session.d.ts +38 -38
- package/dist/devtools/devtools-messages-common.d.ts +6 -6
- package/dist/devtools/devtools-messages-leader.d.ts +28 -28
- package/dist/devtools/devtools-messages-leader.d.ts.map +1 -1
- package/dist/devtools/devtools-messages-leader.js.map +1 -1
- package/dist/leader-thread/LeaderSyncProcessor.js +3 -1
- package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.js +21 -4
- package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
- package/dist/leader-thread/shutdown-channel.d.ts +2 -2
- package/dist/leader-thread/shutdown-channel.d.ts.map +1 -1
- package/dist/leader-thread/shutdown-channel.js +2 -2
- package/dist/leader-thread/shutdown-channel.js.map +1 -1
- package/dist/leader-thread/types.d.ts +1 -1
- package/dist/leader-thread/types.d.ts.map +1 -1
- package/dist/materializer-helper.d.ts +3 -3
- package/dist/materializer-helper.d.ts.map +1 -1
- package/dist/materializer-helper.js +2 -2
- package/dist/materializer-helper.js.map +1 -1
- package/dist/rematerialize-from-eventlog.js +1 -1
- package/dist/rematerialize-from-eventlog.js.map +1 -1
- package/dist/schema/EventDef.d.ts +104 -178
- package/dist/schema/EventSequenceNumber.d.ts +5 -0
- package/dist/schema/EventSequenceNumber.d.ts.map +1 -1
- package/dist/schema/EventSequenceNumber.js +7 -2
- package/dist/schema/EventSequenceNumber.js.map +1 -1
- package/dist/schema/EventSequenceNumber.test.js +2 -2
- package/dist/schema/LiveStoreEvent.d.ts +6 -5
- package/dist/schema/LiveStoreEvent.d.ts.map +1 -1
- package/dist/schema/LiveStoreEvent.js +5 -0
- package/dist/schema/LiveStoreEvent.js.map +1 -1
- package/dist/schema/schema.d.ts +3 -0
- package/dist/schema/schema.d.ts.map +1 -1
- package/dist/schema/schema.js.map +1 -1
- package/dist/schema/state/sqlite/client-document-def.d.ts +3 -2
- package/dist/schema/state/sqlite/client-document-def.d.ts.map +1 -1
- package/dist/schema/state/sqlite/client-document-def.js +6 -4
- package/dist/schema/state/sqlite/client-document-def.js.map +1 -1
- package/dist/schema/state/sqlite/client-document-def.test.js +76 -1
- package/dist/schema/state/sqlite/client-document-def.test.js.map +1 -1
- package/dist/schema/state/sqlite/column-annotations.d.ts +34 -0
- package/dist/schema/state/sqlite/column-annotations.d.ts.map +1 -0
- package/dist/schema/state/sqlite/column-annotations.js +50 -0
- package/dist/schema/state/sqlite/column-annotations.js.map +1 -0
- package/dist/schema/state/sqlite/column-annotations.test.d.ts +2 -0
- package/dist/schema/state/sqlite/column-annotations.test.d.ts.map +1 -0
- package/dist/schema/state/sqlite/column-annotations.test.js +179 -0
- package/dist/schema/state/sqlite/column-annotations.test.js.map +1 -0
- package/dist/schema/state/sqlite/column-def.d.ts +15 -0
- package/dist/schema/state/sqlite/column-def.d.ts.map +1 -0
- package/dist/schema/state/sqlite/column-def.js +242 -0
- package/dist/schema/state/sqlite/column-def.js.map +1 -0
- package/dist/schema/state/sqlite/column-def.test.d.ts +2 -0
- package/dist/schema/state/sqlite/column-def.test.d.ts.map +1 -0
- package/dist/schema/state/sqlite/column-def.test.js +529 -0
- package/dist/schema/state/sqlite/column-def.test.js.map +1 -0
- package/dist/schema/state/sqlite/column-spec.d.ts +11 -0
- package/dist/schema/state/sqlite/column-spec.d.ts.map +1 -0
- package/dist/schema/state/sqlite/column-spec.js +39 -0
- package/dist/schema/state/sqlite/column-spec.js.map +1 -0
- package/dist/schema/state/sqlite/column-spec.test.d.ts +2 -0
- package/dist/schema/state/sqlite/column-spec.test.d.ts.map +1 -0
- package/dist/schema/state/sqlite/column-spec.test.js +146 -0
- package/dist/schema/state/sqlite/column-spec.test.js.map +1 -0
- package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts +1 -0
- package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts.map +1 -1
- package/dist/schema/state/sqlite/db-schema/ast/sqlite.js +1 -0
- package/dist/schema/state/sqlite/db-schema/ast/sqlite.js.map +1 -1
- package/dist/schema/state/sqlite/db-schema/dsl/field-defs.d.ts +17 -4
- package/dist/schema/state/sqlite/db-schema/dsl/field-defs.d.ts.map +1 -1
- package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js +2 -0
- package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js.map +1 -1
- package/dist/schema/state/sqlite/db-schema/dsl/mod.d.ts +65 -165
- package/dist/schema/state/sqlite/db-schema/dsl/mod.d.ts.map +1 -1
- package/dist/schema/state/sqlite/db-schema/dsl/mod.js +1 -0
- package/dist/schema/state/sqlite/db-schema/dsl/mod.js.map +1 -1
- package/dist/schema/state/sqlite/mod.d.ts +2 -0
- package/dist/schema/state/sqlite/mod.d.ts.map +1 -1
- package/dist/schema/state/sqlite/mod.js +2 -0
- package/dist/schema/state/sqlite/mod.js.map +1 -1
- package/dist/schema/state/sqlite/query-builder/api.d.ts +309 -560
- package/dist/schema/state/sqlite/query-builder/api.d.ts.map +1 -1
- package/dist/schema/state/sqlite/query-builder/astToSql.d.ts +1 -0
- package/dist/schema/state/sqlite/query-builder/astToSql.d.ts.map +1 -1
- package/dist/schema/state/sqlite/query-builder/astToSql.js +8 -6
- package/dist/schema/state/sqlite/query-builder/astToSql.js.map +1 -1
- package/dist/schema/state/sqlite/system-tables.d.ts +464 -46
- package/dist/schema/state/sqlite/system-tables.d.ts.map +1 -1
- package/dist/schema/state/sqlite/table-def.d.ts +159 -152
- package/dist/schema/state/sqlite/table-def.d.ts.map +1 -1
- package/dist/schema/state/sqlite/table-def.js +45 -6
- package/dist/schema/state/sqlite/table-def.js.map +1 -1
- package/dist/schema/state/sqlite/table-def.test.d.ts +2 -0
- package/dist/schema/state/sqlite/table-def.test.d.ts.map +1 -0
- package/dist/schema/state/sqlite/table-def.test.js +192 -0
- package/dist/schema/state/sqlite/table-def.test.js.map +1 -0
- package/dist/schema-management/common.d.ts +1 -1
- package/dist/schema-management/common.d.ts.map +1 -1
- package/dist/schema-management/common.js +11 -2
- package/dist/schema-management/common.js.map +1 -1
- package/dist/schema-management/migrations.d.ts +0 -1
- package/dist/schema-management/migrations.d.ts.map +1 -1
- package/dist/schema-management/migrations.js +4 -30
- package/dist/schema-management/migrations.js.map +1 -1
- package/dist/schema-management/migrations.test.d.ts +2 -0
- package/dist/schema-management/migrations.test.d.ts.map +1 -0
- package/dist/schema-management/migrations.test.js +52 -0
- package/dist/schema-management/migrations.test.js.map +1 -0
- package/dist/sql-queries/types.d.ts +37 -133
- package/dist/sqlite-db-helper.d.ts +3 -1
- package/dist/sqlite-db-helper.d.ts.map +1 -1
- package/dist/sqlite-db-helper.js +16 -0
- package/dist/sqlite-db-helper.js.map +1 -1
- package/dist/sqlite-types.d.ts +4 -4
- package/dist/sqlite-types.d.ts.map +1 -1
- package/dist/sync/ClientSessionSyncProcessor.d.ts +2 -2
- package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
- package/dist/sync/ClientSessionSyncProcessor.js +8 -7
- package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
- package/dist/sync/sync.d.ts.map +1 -1
- package/dist/sync/sync.js.map +1 -1
- package/dist/util.d.ts +3 -3
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -4
- package/src/ClientSessionLeaderThreadProxy.ts +2 -2
- package/src/adapter-types.ts +6 -4
- package/src/devtools/devtools-messages-leader.ts +3 -3
- package/src/leader-thread/LeaderSyncProcessor.ts +3 -1
- package/src/leader-thread/make-leader-thread-layer.ts +26 -7
- package/src/leader-thread/shutdown-channel.ts +2 -2
- package/src/leader-thread/types.ts +1 -1
- package/src/materializer-helper.ts +5 -11
- package/src/rematerialize-from-eventlog.ts +2 -2
- package/src/schema/EventSequenceNumber.test.ts +2 -2
- package/src/schema/EventSequenceNumber.ts +8 -2
- package/src/schema/LiveStoreEvent.ts +7 -1
- package/src/schema/schema.ts +4 -0
- package/src/schema/state/sqlite/client-document-def.test.ts +89 -1
- package/src/schema/state/sqlite/client-document-def.ts +7 -4
- package/src/schema/state/sqlite/column-annotations.test.ts +212 -0
- package/src/schema/state/sqlite/column-annotations.ts +77 -0
- package/src/schema/state/sqlite/column-def.test.ts +665 -0
- package/src/schema/state/sqlite/column-def.ts +290 -0
- package/src/schema/state/sqlite/column-spec.test.ts +223 -0
- package/src/schema/state/sqlite/column-spec.ts +42 -0
- package/src/schema/state/sqlite/db-schema/ast/sqlite.ts +2 -0
- package/src/schema/state/sqlite/db-schema/dsl/__snapshots__/field-defs.test.ts.snap +15 -0
- package/src/schema/state/sqlite/db-schema/dsl/field-defs.ts +20 -2
- package/src/schema/state/sqlite/db-schema/dsl/mod.ts +1 -0
- package/src/schema/state/sqlite/mod.ts +2 -0
- package/src/schema/state/sqlite/query-builder/api.ts +4 -3
- package/src/schema/state/sqlite/query-builder/astToSql.ts +9 -7
- package/src/schema/state/sqlite/table-def.test.ts +241 -0
- package/src/schema/state/sqlite/table-def.ts +222 -16
- package/src/schema-management/common.ts +10 -3
- package/src/schema-management/migrations.ts +4 -33
- package/src/sqlite-db-helper.ts +19 -1
- package/src/sqlite-types.ts +4 -4
- package/src/sync/ClientSessionSyncProcessor.ts +13 -8
- package/src/sync/sync.ts +2 -0
- package/src/util.ts +7 -2
- package/src/version.ts +1 -1
@@ -0,0 +1,665 @@
|
|
1
|
+
import { Schema } from '@livestore/utils/effect'
|
2
|
+
import { describe, expect, it } from 'vitest'
|
3
|
+
|
4
|
+
import * as State from '../mod.ts'
|
5
|
+
import { withAutoIncrement, withColumnType, withDefault, withPrimaryKey, withUnique } from './column-annotations.ts'
|
6
|
+
|
7
|
+
describe('getColumnDefForSchema', () => {
|
8
|
+
describe('basic types', () => {
|
9
|
+
it('should map Schema.String to text column', () => {
|
10
|
+
const columnDef = State.SQLite.getColumnDefForSchema(Schema.String)
|
11
|
+
expect(columnDef.columnType).toBe('text')
|
12
|
+
})
|
13
|
+
|
14
|
+
it('should map Schema.Number to real column', () => {
|
15
|
+
const columnDef = State.SQLite.getColumnDefForSchema(Schema.Number)
|
16
|
+
expect(columnDef.columnType).toBe('real')
|
17
|
+
})
|
18
|
+
|
19
|
+
it('should map Schema.Boolean to integer column', () => {
|
20
|
+
const columnDef = State.SQLite.getColumnDefForSchema(Schema.Boolean)
|
21
|
+
expect(columnDef.columnType).toBe('integer')
|
22
|
+
})
|
23
|
+
|
24
|
+
it('should map Schema.Date to text column', () => {
|
25
|
+
const columnDef = State.SQLite.getColumnDefForSchema(Schema.Date)
|
26
|
+
expect(columnDef.columnType).toBe('text')
|
27
|
+
})
|
28
|
+
|
29
|
+
it('should map Schema.BigInt to text column', () => {
|
30
|
+
const columnDef = State.SQLite.getColumnDefForSchema(Schema.BigInt)
|
31
|
+
expect(columnDef.columnType).toBe('text')
|
32
|
+
})
|
33
|
+
})
|
34
|
+
|
35
|
+
describe('refinements', () => {
|
36
|
+
it('should map Schema.Int to integer column', () => {
|
37
|
+
const columnDef = State.SQLite.getColumnDefForSchema(Schema.Int)
|
38
|
+
expect(columnDef.columnType).toBe('integer')
|
39
|
+
})
|
40
|
+
|
41
|
+
it('should map string refinements to text column', () => {
|
42
|
+
const refinements = [
|
43
|
+
{ schema: Schema.NonEmptyString, name: 'NonEmptyString' },
|
44
|
+
{ schema: Schema.Trim, name: 'Trim' },
|
45
|
+
{ schema: Schema.UUID, name: 'UUID' },
|
46
|
+
{ schema: Schema.ULID, name: 'ULID' },
|
47
|
+
{ schema: Schema.String.pipe(Schema.minLength(5)), name: 'minLength' },
|
48
|
+
{
|
49
|
+
schema: Schema.String.pipe(Schema.pattern(/^[A-Z]+$/)),
|
50
|
+
name: 'pattern',
|
51
|
+
},
|
52
|
+
]
|
53
|
+
|
54
|
+
for (const { schema, name } of refinements) {
|
55
|
+
const columnDef = State.SQLite.getColumnDefForSchema(schema)
|
56
|
+
expect(columnDef.columnType, `${name} should map to text`).toBe('text')
|
57
|
+
}
|
58
|
+
})
|
59
|
+
|
60
|
+
it('should map number refinements to real column', () => {
|
61
|
+
const refinements = [
|
62
|
+
{ schema: Schema.Finite, name: 'Finite' },
|
63
|
+
{ schema: Schema.Number.pipe(Schema.positive()), name: 'positive' },
|
64
|
+
{ schema: Schema.Number.pipe(Schema.between(0, 100)), name: 'between' },
|
65
|
+
]
|
66
|
+
|
67
|
+
for (const { schema, name } of refinements) {
|
68
|
+
const columnDef = State.SQLite.getColumnDefForSchema(schema)
|
69
|
+
expect(columnDef.columnType, `${name} should map to real`).toBe('real')
|
70
|
+
}
|
71
|
+
})
|
72
|
+
})
|
73
|
+
|
74
|
+
describe('literal types', () => {
|
75
|
+
it('should map string literals to text column', () => {
|
76
|
+
const columnDef = State.SQLite.getColumnDefForSchema(Schema.Literal('active'))
|
77
|
+
expect(columnDef.columnType).toBe('text')
|
78
|
+
})
|
79
|
+
|
80
|
+
it('should map number literals to real column', () => {
|
81
|
+
const columnDef = State.SQLite.getColumnDefForSchema(Schema.Literal(42))
|
82
|
+
expect(columnDef.columnType).toBe('real')
|
83
|
+
})
|
84
|
+
|
85
|
+
it('should map boolean literals to integer column', () => {
|
86
|
+
const columnDef = State.SQLite.getColumnDefForSchema(Schema.Literal(true))
|
87
|
+
expect(columnDef.columnType).toBe('integer')
|
88
|
+
})
|
89
|
+
})
|
90
|
+
|
91
|
+
describe('transformations', () => {
|
92
|
+
it('should map transformations based on target type', () => {
|
93
|
+
const StringToNumber = Schema.String.pipe(
|
94
|
+
Schema.transform(Schema.Number, {
|
95
|
+
decode: Number.parseFloat,
|
96
|
+
encode: String,
|
97
|
+
}),
|
98
|
+
)
|
99
|
+
|
100
|
+
const columnDef = State.SQLite.getColumnDefForSchema(StringToNumber)
|
101
|
+
expect(columnDef.columnType).toBe('real') // Based on the target type (Number)
|
102
|
+
})
|
103
|
+
|
104
|
+
it('should handle Date transformations', () => {
|
105
|
+
const columnDef = State.SQLite.getColumnDefForSchema(Schema.Date)
|
106
|
+
expect(columnDef.columnType).toBe('text')
|
107
|
+
})
|
108
|
+
})
|
109
|
+
|
110
|
+
describe('complex types', () => {
|
111
|
+
it('should map structs to json column', () => {
|
112
|
+
const UserSchema = Schema.Struct({
|
113
|
+
name: Schema.String,
|
114
|
+
age: Schema.Number,
|
115
|
+
})
|
116
|
+
|
117
|
+
const columnDef = State.SQLite.getColumnDefForSchema(UserSchema)
|
118
|
+
expect(columnDef.columnType).toBe('text')
|
119
|
+
})
|
120
|
+
|
121
|
+
it('should map arrays to json column', () => {
|
122
|
+
const columnDef = State.SQLite.getColumnDefForSchema(Schema.Array(Schema.String))
|
123
|
+
expect(columnDef.columnType).toBe('text')
|
124
|
+
})
|
125
|
+
|
126
|
+
it('should map records to json column', () => {
|
127
|
+
const columnDef = State.SQLite.getColumnDefForSchema(Schema.Record({ key: Schema.String, value: Schema.Number }))
|
128
|
+
expect(columnDef.columnType).toBe('text')
|
129
|
+
})
|
130
|
+
|
131
|
+
it('should map tuples to json column', () => {
|
132
|
+
const columnDef = State.SQLite.getColumnDefForSchema(Schema.Tuple(Schema.String, Schema.Number))
|
133
|
+
expect(columnDef.columnType).toBe('text')
|
134
|
+
})
|
135
|
+
|
136
|
+
it('should map tagged unions to json column', () => {
|
137
|
+
const ResultSchema = Schema.Union(
|
138
|
+
Schema.Struct({
|
139
|
+
_tag: Schema.Literal('success'),
|
140
|
+
value: Schema.String,
|
141
|
+
}),
|
142
|
+
Schema.Struct({ _tag: Schema.Literal('error'), error: Schema.String }),
|
143
|
+
)
|
144
|
+
|
145
|
+
const columnDef = State.SQLite.getColumnDefForSchema(ResultSchema)
|
146
|
+
expect(columnDef.columnType).toBe('text')
|
147
|
+
})
|
148
|
+
})
|
149
|
+
|
150
|
+
describe('nested schemas', () => {
|
151
|
+
it('should handle deeply nested schemas', () => {
|
152
|
+
const NestedSchema = Schema.Struct({
|
153
|
+
level1: Schema.Struct({
|
154
|
+
level2: Schema.Struct({
|
155
|
+
value: Schema.String,
|
156
|
+
}),
|
157
|
+
}),
|
158
|
+
})
|
159
|
+
|
160
|
+
const columnDef = State.SQLite.getColumnDefForSchema(NestedSchema)
|
161
|
+
expect(columnDef.columnType).toBe('text')
|
162
|
+
})
|
163
|
+
|
164
|
+
it('should handle optional nested schemas', () => {
|
165
|
+
const columnDef = State.SQLite.getColumnDefForSchema(
|
166
|
+
Schema.Union(Schema.Struct({ name: Schema.String }), Schema.Undefined),
|
167
|
+
)
|
168
|
+
expect(columnDef.columnType).toBe('text')
|
169
|
+
})
|
170
|
+
})
|
171
|
+
|
172
|
+
describe('edge cases', () => {
|
173
|
+
it('should default to json column for unhandled types', () => {
|
174
|
+
// Test various edge cases that all result in JSON columns
|
175
|
+
const edgeCases = [
|
176
|
+
{ schema: Schema.Unknown, name: 'Unknown' },
|
177
|
+
{ schema: Schema.Any, name: 'Any' },
|
178
|
+
{ schema: Schema.Null, name: 'Null' },
|
179
|
+
{ schema: Schema.Undefined, name: 'Undefined' },
|
180
|
+
{ schema: Schema.Void, name: 'Void' },
|
181
|
+
]
|
182
|
+
|
183
|
+
for (const { schema, name } of edgeCases) {
|
184
|
+
const columnDef = State.SQLite.getColumnDefForSchema(schema)
|
185
|
+
expect(columnDef.columnType, `${name} should map to text (JSON storage)`).toBe('text')
|
186
|
+
}
|
187
|
+
})
|
188
|
+
|
189
|
+
it('should handle never schema', () => {
|
190
|
+
// Create a schema that should never validate
|
191
|
+
const neverSchema = Schema.String.pipe(Schema.filter(() => false, { message: () => 'Always fails' }))
|
192
|
+
|
193
|
+
const columnDef = State.SQLite.getColumnDefForSchema(neverSchema)
|
194
|
+
expect(columnDef.columnType).toBe('text')
|
195
|
+
})
|
196
|
+
|
197
|
+
it('should handle symbol schema', () => {
|
198
|
+
const columnDef = State.SQLite.getColumnDefForSchema(Schema.Symbol)
|
199
|
+
expect(columnDef.columnType).toBe('text')
|
200
|
+
})
|
201
|
+
})
|
202
|
+
|
203
|
+
describe('custom schemas', () => {
|
204
|
+
it('should handle Schema.extend', () => {
|
205
|
+
const BaseSchema = Schema.Struct({
|
206
|
+
id: Schema.String,
|
207
|
+
createdAt: Schema.Date,
|
208
|
+
})
|
209
|
+
|
210
|
+
const ExtendedSchema = Schema.Struct({
|
211
|
+
...BaseSchema.fields,
|
212
|
+
name: Schema.String,
|
213
|
+
updatedAt: Schema.Date,
|
214
|
+
})
|
215
|
+
|
216
|
+
const columnDef = State.SQLite.getColumnDefForSchema(ExtendedSchema)
|
217
|
+
expect(columnDef.columnType).toBe('text')
|
218
|
+
})
|
219
|
+
|
220
|
+
it('should handle Schema.pick', () => {
|
221
|
+
const UserSchema = Schema.Struct({
|
222
|
+
id: Schema.String,
|
223
|
+
name: Schema.String,
|
224
|
+
email: Schema.String,
|
225
|
+
})
|
226
|
+
|
227
|
+
const PickedSchema = UserSchema.pipe(Schema.pick('id', 'name'))
|
228
|
+
|
229
|
+
const columnDef = State.SQLite.getColumnDefForSchema(PickedSchema)
|
230
|
+
expect(columnDef.columnType).toBe('text')
|
231
|
+
})
|
232
|
+
|
233
|
+
it('should handle Schema.omit', () => {
|
234
|
+
const UserSchema = Schema.Struct({
|
235
|
+
id: Schema.String,
|
236
|
+
name: Schema.String,
|
237
|
+
password: Schema.String,
|
238
|
+
})
|
239
|
+
|
240
|
+
const PublicUserSchema = UserSchema.pipe(Schema.omit('password'))
|
241
|
+
|
242
|
+
const columnDef = State.SQLite.getColumnDefForSchema(PublicUserSchema)
|
243
|
+
expect(columnDef.columnType).toBe('text')
|
244
|
+
})
|
245
|
+
})
|
246
|
+
|
247
|
+
describe('annotations', () => {
|
248
|
+
it('should handle schemas with custom annotations', () => {
|
249
|
+
const AnnotatedString = Schema.String.annotations({
|
250
|
+
description: 'A special string',
|
251
|
+
})
|
252
|
+
const AnnotatedNumber = Schema.Number.annotations({ min: 0, max: 100 })
|
253
|
+
|
254
|
+
expect(State.SQLite.getColumnDefForSchema(AnnotatedString).columnType).toBe('text')
|
255
|
+
expect(State.SQLite.getColumnDefForSchema(AnnotatedNumber).columnType).toBe('real')
|
256
|
+
})
|
257
|
+
})
|
258
|
+
|
259
|
+
describe('enums and literal unions', () => {
|
260
|
+
it('should handle enums and literal unions as text', () => {
|
261
|
+
const StatusEnum = Schema.Enums({
|
262
|
+
PENDING: 'pending',
|
263
|
+
ACTIVE: 'active',
|
264
|
+
INACTIVE: 'inactive',
|
265
|
+
})
|
266
|
+
|
267
|
+
const StatusUnion = Schema.Union(Schema.Literal('pending'), Schema.Literal('active'), Schema.Literal('inactive'))
|
268
|
+
|
269
|
+
expect(State.SQLite.getColumnDefForSchema(StatusEnum).columnType).toBe('text')
|
270
|
+
expect(State.SQLite.getColumnDefForSchema(StatusUnion).columnType).toBe('text')
|
271
|
+
})
|
272
|
+
})
|
273
|
+
|
274
|
+
describe('binary data', () => {
|
275
|
+
it('should handle Uint8Array as blob column', () => {
|
276
|
+
const columnDef = State.SQLite.getColumnDefForSchema(Schema.Uint8Array)
|
277
|
+
expect(columnDef.columnType).toBe('text') // Stored as JSON
|
278
|
+
})
|
279
|
+
})
|
280
|
+
|
281
|
+
describe('recursive schemas', () => {
|
282
|
+
it('should handle recursive schemas as json', () => {
|
283
|
+
interface TreeNode {
|
284
|
+
readonly value: string
|
285
|
+
readonly children: ReadonlyArray<TreeNode>
|
286
|
+
}
|
287
|
+
const TreeNode: Schema.Schema<TreeNode> = Schema.Struct({
|
288
|
+
value: Schema.String,
|
289
|
+
children: Schema.Array(Schema.suspend(() => TreeNode)),
|
290
|
+
})
|
291
|
+
|
292
|
+
const columnDef = State.SQLite.getColumnDefForSchema(TreeNode)
|
293
|
+
expect(columnDef.columnType).toBe('text') // Complex type stored as JSON
|
294
|
+
})
|
295
|
+
})
|
296
|
+
|
297
|
+
describe('annotations', () => {
|
298
|
+
describe('withColumnType', () => {
|
299
|
+
it('should respect column type annotation for text', () => {
|
300
|
+
const schema = Schema.Number.pipe(withColumnType('text'))
|
301
|
+
const columnDef = State.SQLite.getColumnDefForSchema(schema)
|
302
|
+
expect(columnDef.columnType).toBe('text')
|
303
|
+
})
|
304
|
+
|
305
|
+
it('should respect column type annotation for integer', () => {
|
306
|
+
const schema = Schema.String.pipe(withColumnType('integer'))
|
307
|
+
const columnDef = State.SQLite.getColumnDefForSchema(schema)
|
308
|
+
expect(columnDef.columnType).toBe('integer')
|
309
|
+
})
|
310
|
+
|
311
|
+
it('should respect column type annotation for real', () => {
|
312
|
+
const schema = Schema.Boolean.pipe(withColumnType('real'))
|
313
|
+
const columnDef = State.SQLite.getColumnDefForSchema(schema)
|
314
|
+
expect(columnDef.columnType).toBe('real')
|
315
|
+
})
|
316
|
+
|
317
|
+
it('should respect column type annotation for blob', () => {
|
318
|
+
const schema = Schema.String.pipe(withColumnType('blob'))
|
319
|
+
const columnDef = State.SQLite.getColumnDefForSchema(schema)
|
320
|
+
expect(columnDef.columnType).toBe('blob')
|
321
|
+
})
|
322
|
+
|
323
|
+
it('should override default type mapping', () => {
|
324
|
+
// Number normally maps to real, but we override to text
|
325
|
+
const schema = Schema.Number.pipe(withColumnType('text'))
|
326
|
+
const columnDef = State.SQLite.getColumnDefForSchema(schema)
|
327
|
+
expect(columnDef.columnType).toBe('text')
|
328
|
+
})
|
329
|
+
|
330
|
+
it('should work with dual API', () => {
|
331
|
+
// Test both forms of the dual API
|
332
|
+
const schema1 = withColumnType(Schema.String, 'integer')
|
333
|
+
const schema2 = Schema.String.pipe(withColumnType('integer'))
|
334
|
+
|
335
|
+
const columnDef1 = State.SQLite.getColumnDefForSchema(schema1)
|
336
|
+
const columnDef2 = State.SQLite.getColumnDefForSchema(schema2)
|
337
|
+
|
338
|
+
expect(columnDef1.columnType).toBe('integer')
|
339
|
+
expect(columnDef2.columnType).toBe('integer')
|
340
|
+
})
|
341
|
+
})
|
342
|
+
|
343
|
+
describe('withPrimaryKey', () => {
|
344
|
+
it('should add primary key annotation to schema', () => {
|
345
|
+
const UserSchema = Schema.Struct({
|
346
|
+
id: Schema.String.pipe(withPrimaryKey),
|
347
|
+
name: Schema.String,
|
348
|
+
email: Schema.optional(Schema.String),
|
349
|
+
nullable: Schema.NullOr(Schema.Int),
|
350
|
+
optionalComplex: Schema.optional(Schema.Struct({ color: Schema.String })),
|
351
|
+
optionalNullableText: Schema.optional(Schema.NullOr(Schema.String)),
|
352
|
+
optionalNullableComplex: Schema.optional(Schema.NullOr(Schema.Struct({ color: Schema.String }))),
|
353
|
+
})
|
354
|
+
|
355
|
+
const userTable = State.SQLite.table({
|
356
|
+
name: 'users',
|
357
|
+
schema: UserSchema,
|
358
|
+
})
|
359
|
+
|
360
|
+
expect(userTable.sqliteDef.columns.id.primaryKey).toBe(true)
|
361
|
+
expect(userTable.sqliteDef.columns.id.nullable).toBe(false)
|
362
|
+
expect(userTable.sqliteDef.columns.name.primaryKey).toBe(false)
|
363
|
+
expect(userTable.sqliteDef.columns.email.primaryKey).toBe(false)
|
364
|
+
expect(userTable.sqliteDef.columns.email.nullable).toBe(true)
|
365
|
+
expect(userTable.sqliteDef.columns.nullable.primaryKey).toBe(false)
|
366
|
+
expect(userTable.sqliteDef.columns.nullable.nullable).toBe(true)
|
367
|
+
expect(userTable.sqliteDef.columns.optionalComplex.nullable).toBe(true)
|
368
|
+
expect((userTable.rowSchema as any).fields.email.toString()).toBe('string | undefined')
|
369
|
+
expect((userTable.rowSchema as any).fields.nullable.toString()).toBe('Int | null')
|
370
|
+
expect((userTable.rowSchema as any).fields.optionalComplex.toString()).toBe(
|
371
|
+
'(parseJson <-> { readonly color: string } | undefined)',
|
372
|
+
)
|
373
|
+
})
|
374
|
+
|
375
|
+
it('should handle Schema.NullOr with complex types', () => {
|
376
|
+
const schema = Schema.Struct({
|
377
|
+
data: Schema.NullOr(Schema.Struct({ value: Schema.Number })),
|
378
|
+
}).annotations({ title: 'test' })
|
379
|
+
|
380
|
+
const table = State.SQLite.table({ schema })
|
381
|
+
|
382
|
+
expect(table.sqliteDef.columns.data.nullable).toBe(true)
|
383
|
+
expect(table.sqliteDef.columns.data.columnType).toBe('text')
|
384
|
+
expect((table.rowSchema as any).fields.data.toString()).toBe('{ readonly value: number } | null')
|
385
|
+
})
|
386
|
+
|
387
|
+
it('should handle mixed nullable and optional fields', () => {
|
388
|
+
const schema = Schema.Struct({
|
389
|
+
nullableText: Schema.NullOr(Schema.String),
|
390
|
+
optionalText: Schema.optional(Schema.String),
|
391
|
+
optionalJson: Schema.optional(Schema.Struct({ x: Schema.Number })),
|
392
|
+
}).annotations({ title: 'test' })
|
393
|
+
|
394
|
+
const table = State.SQLite.table({ schema })
|
395
|
+
|
396
|
+
// Both should be nullable at column level
|
397
|
+
expect(table.sqliteDef.columns.nullableText.nullable).toBe(true)
|
398
|
+
expect(table.sqliteDef.columns.optionalText.nullable).toBe(true)
|
399
|
+
expect(table.sqliteDef.columns.optionalJson.nullable).toBe(true)
|
400
|
+
|
401
|
+
// But different schema representations
|
402
|
+
expect((table.rowSchema as any).fields.nullableText.toString()).toBe('string | null')
|
403
|
+
expect((table.rowSchema as any).fields.optionalText.toString()).toBe('string | undefined')
|
404
|
+
expect((table.rowSchema as any).fields.optionalJson.toString()).toBe(
|
405
|
+
'(parseJson <-> { readonly x: number } | undefined)',
|
406
|
+
)
|
407
|
+
})
|
408
|
+
|
409
|
+
it('should handle lossy Schema.optional(Schema.NullOr(...)) with JSON encoding', () => {
|
410
|
+
const schema = Schema.Struct({
|
411
|
+
id: Schema.String,
|
412
|
+
lossyText: Schema.optional(Schema.NullOr(Schema.String)),
|
413
|
+
lossyComplex: Schema.optional(Schema.NullOr(Schema.Struct({ value: Schema.Number }))),
|
414
|
+
}).annotations({ title: 'lossy_test' })
|
415
|
+
|
416
|
+
const table = State.SQLite.table({ schema })
|
417
|
+
|
418
|
+
// Check column definitions for lossy fields
|
419
|
+
expect(table.sqliteDef.columns.lossyText.nullable).toBe(true)
|
420
|
+
expect(table.sqliteDef.columns.lossyText.columnType).toBe('text')
|
421
|
+
expect(table.sqliteDef.columns.lossyComplex.nullable).toBe(true)
|
422
|
+
expect(table.sqliteDef.columns.lossyComplex.columnType).toBe('text')
|
423
|
+
|
424
|
+
// Check schema representations - should use parseJson for lossless encoding
|
425
|
+
expect((table.rowSchema as any).fields.lossyText.toString()).toBe('(parseJson <-> string | null | undefined)')
|
426
|
+
expect((table.rowSchema as any).fields.lossyComplex.toString()).toBe(
|
427
|
+
'(parseJson <-> { readonly value: number } | null | undefined)',
|
428
|
+
)
|
429
|
+
|
430
|
+
// Test actual data round-tripping to ensure losslessness
|
431
|
+
// Note: Missing field case is challenging with current Effect Schema design
|
432
|
+
// as optional fields are handled at struct level, not field level
|
433
|
+
const testCases = [
|
434
|
+
// For now, test only cases where both lossy fields are present
|
435
|
+
{ name: 'both explicit null', data: { id: '2', lossyText: null, lossyComplex: null } },
|
436
|
+
{ name: 'text value, complex null', data: { id: '3', lossyText: 'hello', lossyComplex: null } },
|
437
|
+
{ name: 'text null, complex value', data: { id: '4', lossyText: null, lossyComplex: { value: 42 } } },
|
438
|
+
{ name: 'both values', data: { id: '5', lossyText: 'world', lossyComplex: { value: 42 } } },
|
439
|
+
]
|
440
|
+
|
441
|
+
testCases.forEach((testCase) => {
|
442
|
+
// Encode through insert schema
|
443
|
+
const encoded = Schema.encodeSync(table.insertSchema)(testCase.data)
|
444
|
+
// Decode through row schema
|
445
|
+
const decoded = Schema.decodeSync(table.rowSchema)(encoded)
|
446
|
+
|
447
|
+
// Check for losslessness
|
448
|
+
expect(decoded).toEqual(testCase.data)
|
449
|
+
})
|
450
|
+
})
|
451
|
+
|
452
|
+
it('should throw when primary key is used with optional schema', () => {
|
453
|
+
// Note: Schema.optional returns a property signature, not a schema, so we can't pipe it
|
454
|
+
// Instead, we use Schema.Union to create an optional schema that can be piped
|
455
|
+
const optionalString = Schema.Union(Schema.String, Schema.Undefined)
|
456
|
+
const UserSchema = Schema.Struct({
|
457
|
+
id: optionalString.pipe(withPrimaryKey),
|
458
|
+
name: Schema.String,
|
459
|
+
})
|
460
|
+
|
461
|
+
expect(() =>
|
462
|
+
State.SQLite.table({
|
463
|
+
name: 'users',
|
464
|
+
schema: UserSchema,
|
465
|
+
}),
|
466
|
+
).toThrow('Primary key columns cannot be nullable')
|
467
|
+
})
|
468
|
+
|
469
|
+
it('should throw when primary key is used with NullOr schema', () => {
|
470
|
+
const UserSchema = Schema.Struct({
|
471
|
+
id: Schema.NullOr(Schema.String).pipe(withPrimaryKey),
|
472
|
+
name: Schema.String,
|
473
|
+
})
|
474
|
+
|
475
|
+
expect(() =>
|
476
|
+
State.SQLite.table({
|
477
|
+
name: 'users',
|
478
|
+
schema: UserSchema,
|
479
|
+
}),
|
480
|
+
).toThrow('Primary key columns cannot be nullable')
|
481
|
+
})
|
482
|
+
|
483
|
+
it('should work with column type annotation', () => {
|
484
|
+
const UserSchema = Schema.Struct({
|
485
|
+
id: Schema.Number.pipe(withColumnType('integer')).pipe(withPrimaryKey),
|
486
|
+
name: Schema.String,
|
487
|
+
})
|
488
|
+
|
489
|
+
const userTable = State.SQLite.table({
|
490
|
+
name: 'users',
|
491
|
+
schema: UserSchema,
|
492
|
+
})
|
493
|
+
|
494
|
+
expect(userTable.sqliteDef.columns.id.columnType).toBe('integer')
|
495
|
+
expect(userTable.sqliteDef.columns.id.primaryKey).toBe(true)
|
496
|
+
})
|
497
|
+
|
498
|
+
it('should work with Schema.Int and primary key', () => {
|
499
|
+
const UserSchema = Schema.Struct({
|
500
|
+
id: Schema.Int.pipe(withPrimaryKey),
|
501
|
+
name: Schema.String,
|
502
|
+
})
|
503
|
+
|
504
|
+
const userTable = State.SQLite.table({
|
505
|
+
name: 'users',
|
506
|
+
schema: UserSchema,
|
507
|
+
})
|
508
|
+
|
509
|
+
expect(userTable.sqliteDef.columns.id.columnType).toBe('integer')
|
510
|
+
expect(userTable.sqliteDef.columns.id.primaryKey).toBe(true)
|
511
|
+
})
|
512
|
+
})
|
513
|
+
|
514
|
+
describe('withAutoIncrement', () => {
|
515
|
+
it('should add autoIncrement annotation to schema', () => {
|
516
|
+
const UserSchema = Schema.Struct({
|
517
|
+
id: Schema.Int.pipe(withPrimaryKey).pipe(withAutoIncrement),
|
518
|
+
name: Schema.String,
|
519
|
+
})
|
520
|
+
const userTable = State.SQLite.table({
|
521
|
+
name: 'users',
|
522
|
+
schema: UserSchema,
|
523
|
+
})
|
524
|
+
expect(userTable.sqliteDef.columns.id.autoIncrement).toBe(true)
|
525
|
+
expect(userTable.sqliteDef.columns.id.primaryKey).toBe(true)
|
526
|
+
expect(userTable.sqliteDef.columns.id.columnType).toBe('integer')
|
527
|
+
})
|
528
|
+
})
|
529
|
+
|
530
|
+
describe('withDefault', () => {
|
531
|
+
it('should add default value annotation to schema', () => {
|
532
|
+
const UserSchema = Schema.Struct({
|
533
|
+
id: Schema.String,
|
534
|
+
active: Schema.Boolean.pipe(withDefault(true)),
|
535
|
+
createdAt: Schema.String.pipe(withDefault('CURRENT_TIMESTAMP')),
|
536
|
+
})
|
537
|
+
const userTable = State.SQLite.table({
|
538
|
+
name: 'users',
|
539
|
+
schema: UserSchema,
|
540
|
+
})
|
541
|
+
expect(userTable.sqliteDef.columns.active.default._tag).toBe('Some')
|
542
|
+
expect(
|
543
|
+
userTable.sqliteDef.columns.active.default._tag === 'Some' &&
|
544
|
+
userTable.sqliteDef.columns.active.default.value,
|
545
|
+
).toBe(true)
|
546
|
+
expect(userTable.sqliteDef.columns.createdAt.default._tag).toBe('Some')
|
547
|
+
expect(
|
548
|
+
userTable.sqliteDef.columns.createdAt.default._tag === 'Some' &&
|
549
|
+
userTable.sqliteDef.columns.createdAt.default.value,
|
550
|
+
).toBe('CURRENT_TIMESTAMP')
|
551
|
+
})
|
552
|
+
|
553
|
+
it('should work with dual API', () => {
|
554
|
+
const schema1 = withDefault(Schema.Int, 0)
|
555
|
+
const schema2 = Schema.Int.pipe(withDefault(0))
|
556
|
+
const UserSchema1 = Schema.Struct({ count: schema1 })
|
557
|
+
const UserSchema2 = Schema.Struct({ count: schema2 })
|
558
|
+
const table1 = State.SQLite.table({ name: 't1', schema: UserSchema1 })
|
559
|
+
const table2 = State.SQLite.table({ name: 't2', schema: UserSchema2 })
|
560
|
+
expect(table1.sqliteDef.columns.count.default._tag).toBe('Some')
|
561
|
+
expect(
|
562
|
+
table1.sqliteDef.columns.count.default._tag === 'Some' && table1.sqliteDef.columns.count.default.value,
|
563
|
+
).toBe(0)
|
564
|
+
expect(table2.sqliteDef.columns.count.default._tag).toBe('Some')
|
565
|
+
expect(
|
566
|
+
table2.sqliteDef.columns.count.default._tag === 'Some' && table2.sqliteDef.columns.count.default.value,
|
567
|
+
).toBe(0)
|
568
|
+
})
|
569
|
+
})
|
570
|
+
|
571
|
+
describe('withUnique', () => {
|
572
|
+
it('should create unique index for column with unique annotation', () => {
|
573
|
+
const UserSchema = Schema.Struct({
|
574
|
+
id: Schema.String,
|
575
|
+
email: Schema.String.pipe(withUnique),
|
576
|
+
username: Schema.String.pipe(withUnique),
|
577
|
+
})
|
578
|
+
const userTable = State.SQLite.table({
|
579
|
+
name: 'users',
|
580
|
+
schema: UserSchema,
|
581
|
+
})
|
582
|
+
|
583
|
+
// Check that unique indexes were created
|
584
|
+
const uniqueIndexes = userTable.sqliteDef.indexes?.filter((idx) => idx.isUnique) || []
|
585
|
+
expect(uniqueIndexes).toHaveLength(2)
|
586
|
+
expect(
|
587
|
+
uniqueIndexes.some((idx) => idx.name === 'idx_users_email_unique' && idx.columns.includes('email')),
|
588
|
+
).toBe(true)
|
589
|
+
expect(
|
590
|
+
uniqueIndexes.some((idx) => idx.name === 'idx_users_username_unique' && idx.columns.includes('username')),
|
591
|
+
).toBe(true)
|
592
|
+
})
|
593
|
+
|
594
|
+
it('should combine unique indexes with user-provided indexes', () => {
|
595
|
+
const UserSchema = Schema.Struct({
|
596
|
+
id: Schema.String,
|
597
|
+
email: Schema.String.pipe(withUnique),
|
598
|
+
})
|
599
|
+
const userTable = State.SQLite.table({
|
600
|
+
name: 'users',
|
601
|
+
schema: UserSchema,
|
602
|
+
indexes: [{ name: 'idx_custom', columns: ['id', 'email'] }],
|
603
|
+
})
|
604
|
+
|
605
|
+
// Should have both custom index and unique index
|
606
|
+
expect(userTable.sqliteDef.indexes).toHaveLength(2)
|
607
|
+
expect(userTable.sqliteDef.indexes?.some((idx) => idx.name === 'idx_custom')).toBe(true)
|
608
|
+
expect(userTable.sqliteDef.indexes?.some((idx) => idx.name === 'idx_users_email_unique')).toBe(true)
|
609
|
+
})
|
610
|
+
})
|
611
|
+
|
612
|
+
describe('combined annotations', () => {
|
613
|
+
it('should work with multiple annotations', () => {
|
614
|
+
const schema = Schema.Uint8ArrayFromBase64.pipe(withColumnType('blob')).pipe(withPrimaryKey)
|
615
|
+
|
616
|
+
const UserSchema = Schema.Struct({
|
617
|
+
id: schema,
|
618
|
+
name: Schema.String,
|
619
|
+
})
|
620
|
+
|
621
|
+
const userTable = State.SQLite.table({
|
622
|
+
name: 'users',
|
623
|
+
schema: UserSchema,
|
624
|
+
})
|
625
|
+
|
626
|
+
expect(userTable.sqliteDef.columns.id.columnType).toBe('blob')
|
627
|
+
expect(userTable.sqliteDef.columns.id.primaryKey).toBe(true)
|
628
|
+
})
|
629
|
+
|
630
|
+
it('should combine all annotations', () => {
|
631
|
+
const UserSchema = Schema.Struct({
|
632
|
+
id: Schema.Int.pipe(withPrimaryKey).pipe(withAutoIncrement),
|
633
|
+
email: Schema.String.pipe(withUnique),
|
634
|
+
status: Schema.String.pipe(withDefault('active')),
|
635
|
+
metadata: Schema.Unknown.pipe(withColumnType('text')),
|
636
|
+
})
|
637
|
+
const userTable = State.SQLite.table({
|
638
|
+
name: 'users',
|
639
|
+
schema: UserSchema,
|
640
|
+
})
|
641
|
+
|
642
|
+
// Check id column
|
643
|
+
expect(userTable.sqliteDef.columns.id.primaryKey).toBe(true)
|
644
|
+
expect(userTable.sqliteDef.columns.id.autoIncrement).toBe(true)
|
645
|
+
expect(userTable.sqliteDef.columns.id.columnType).toBe('integer')
|
646
|
+
|
647
|
+
// Check email column and unique index
|
648
|
+
expect(userTable.sqliteDef.columns.email.columnType).toBe('text')
|
649
|
+
expect(userTable.sqliteDef.indexes?.some((idx) => idx.name === 'idx_users_email_unique' && idx.isUnique)).toBe(
|
650
|
+
true,
|
651
|
+
)
|
652
|
+
|
653
|
+
// Check status column
|
654
|
+
expect(userTable.sqliteDef.columns.status.default._tag).toBe('Some')
|
655
|
+
expect(
|
656
|
+
userTable.sqliteDef.columns.status.default._tag === 'Some' &&
|
657
|
+
userTable.sqliteDef.columns.status.default.value,
|
658
|
+
).toBe('active')
|
659
|
+
|
660
|
+
// Check metadata column
|
661
|
+
expect(userTable.sqliteDef.columns.metadata.columnType).toBe('text')
|
662
|
+
})
|
663
|
+
})
|
664
|
+
})
|
665
|
+
})
|