-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdatatype_test.go
More file actions
262 lines (236 loc) · 8.8 KB
/
Copy pathdatatype_test.go
File metadata and controls
262 lines (236 loc) · 8.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
package sql_test
import (
"bytes"
"database/sql"
"testing"
"time"
)
// TestType_NullRoundTrip asserts every sql.Null* wrapper round-trips both
// the "valid" and "invalid" forms through a TEXT column. The driver must
// honor sql.Scanner / driver.Valuer for these wrappers.
func TestType_NullRoundTrip(t *testing.T) {
db := openDB(t)
mustExec(t, db, `create table t (
s text, i integer, f real, b integer, ts datetime
)`)
// Insert a fully-NULL row and a fully-populated row.
mustExec(t, db, `insert into t values (?, ?, ?, ?, ?)`,
sql.NullString{}, sql.NullInt64{}, sql.NullFloat64{}, sql.NullBool{}, sql.NullTime{})
mustExec(t, db, `insert into t values (?, ?, ?, ?, ?)`,
sql.NullString{String: "hi", Valid: true},
sql.NullInt64{Int64: 42, Valid: true},
sql.NullFloat64{Float64: 3.14, Valid: true},
sql.NullBool{Bool: true, Valid: true},
sql.NullTime{Time: time.Date(2026, 5, 1, 12, 0, 0, 0, time.UTC), Valid: true})
rows, err := db.Query(`select s, i, f, b, ts from t order by rowid`)
if err != nil {
t.Fatal(err)
}
defer rows.Close()
// Row 1: all NULL.
if !rows.Next() {
t.Fatal("missing row 1")
}
var ns sql.NullString
var ni sql.NullInt64
var nf sql.NullFloat64
var nb sql.NullBool
var nt sql.NullTime
if err := rows.Scan(&ns, &ni, &nf, &nb, &nt); err != nil {
t.Fatal(err)
}
if ns.Valid || ni.Valid || nf.Valid || nb.Valid || nt.Valid {
t.Errorf("row 1: expected all-NULL, got %+v %+v %+v %+v %+v", ns, ni, nf, nb, nt)
}
// Row 2: populated.
if !rows.Next() {
t.Fatal("missing row 2")
}
if err := rows.Scan(&ns, &ni, &nf, &nb, &nt); err != nil {
t.Fatal(err)
}
if !ns.Valid || ns.String != "hi" {
t.Errorf("NullString=%+v, want {hi true}", ns)
}
if !ni.Valid || ni.Int64 != 42 {
t.Errorf("NullInt64=%+v, want {42 true}", ni)
}
if !nf.Valid || nf.Float64 != 3.14 {
t.Errorf("NullFloat64=%+v, want {3.14 true}", nf)
}
if !nb.Valid || !nb.Bool {
t.Errorf("NullBool=%+v, want {true true}", nb)
}
if !nt.Valid || !nt.Time.Equal(time.Date(2026, 5, 1, 12, 0, 0, 0, time.UTC)) {
t.Errorf("NullTime=%+v, want 2026-05-01T12:00:00Z", nt)
}
}
// TestType_IntegerAffinity exercises column affinity rules for INTEGER:
// inserting the string "123" should coerce to int64. Documented at
// https://www.sqlite.org/datatype3.html#type_affinity.
func TestType_IntegerAffinity(t *testing.T) {
db := openDB(t)
mustExec(t, db, `create table t (i integer)`)
mustExec(t, db, `insert into t values ('123')`)
mustExec(t, db, `insert into t values (4.0)`)
mustExec(t, db, `insert into t values ('not a number')`)
rows := scanAll(t, db, `select typeof(i), i from t order by rowid`)
want := [][]any{
{"integer", int64(123)},
{"integer", int64(4)},
{"text", "not a number"}, // unparsable text keeps TEXT storage class
}
for i, w := range want {
if rows[i][0] != w[0] {
t.Errorf("row %d typeof=%v, want %v", i, rows[i][0], w[0])
}
if rows[i][1] != w[1] {
t.Errorf("row %d value=%v, want %v", i, rows[i][1], w[1])
}
}
}
// TestType_RealAffinity asserts REAL coerces strings like "1.5" to float64
// and keeps INTEGER inputs as INTEGER storage class (per spec — REAL
// affinity casts only when conversion is lossless).
func TestType_RealAffinity(t *testing.T) {
db := openDB(t)
mustExec(t, db, `create table t (f real)`)
mustExec(t, db, `insert into t values ('1.5')`)
mustExec(t, db, `insert into t values (10)`)
rows := scanAll(t, db, `select typeof(f), f from t order by rowid`)
if rows[0][0] != "real" || rows[0][1].(float64) != 1.5 {
t.Errorf("row 0=%v, want real 1.5", rows[0])
}
// SQLite REAL affinity stores integers as REAL when forced via cast,
// but raw INSERT may keep INTEGER storage. Accept either.
if rows[1][0] != "real" && rows[1][0] != "integer" {
t.Errorf("row 1 typeof=%v, want real or integer", rows[1][0])
}
}
// TestType_TextAffinity asserts TEXT does not coerce — numeric inputs
// are stored as TEXT.
func TestType_TextAffinity(t *testing.T) {
db := openDB(t)
mustExec(t, db, `create table t (s text)`)
mustExec(t, db, `insert into t values (?)`, 42)
mustExec(t, db, `insert into t values (?)`, 3.14)
rows := scanAll(t, db, `select typeof(s), s from t order by rowid`)
want := []string{"text", "text"}
for i, w := range want {
if rows[i][0] != w {
t.Errorf("row %d typeof=%v, want %s", i, rows[i][0], w)
}
}
if rows[0][1] != "42" {
t.Errorf("row 0 value=%v, want '42'", rows[0][1])
}
}
// TestType_BlobAffinity asserts BLOB preserves exact bytes including
// embedded NULs, and that BLOB column reads return []byte not string.
func TestType_BlobAffinity(t *testing.T) {
db := openDB(t)
mustExec(t, db, `create table t (b blob)`)
payload := []byte{0x00, 0x01, 0xff, 0x7f, 0x00, 0x80}
mustExec(t, db, `insert into t values (?)`, payload)
var got []byte
scanOne(t, db, &got, `select b from t`)
if !bytes.Equal(got, payload) {
t.Errorf("blob round-trip mismatch: got %x, want %x", got, payload)
}
var typ string
scanOne(t, db, &typ, `select typeof(b) from t`)
if typ != "blob" {
t.Errorf("typeof(blob)=%q, want blob", typ)
}
}
// TestType_NumericAffinity asserts NUMERIC accepts both INTEGER and REAL
// inputs and reduces to INTEGER when lossless. Per spec, "1.5" becomes
// REAL (no integer reduction), "1.0" becomes INTEGER.
func TestType_NumericAffinity(t *testing.T) {
db := openDB(t)
mustExec(t, db, `create table t (n numeric)`)
mustExec(t, db, `insert into t values ('1.5')`)
mustExec(t, db, `insert into t values ('1.0')`)
mustExec(t, db, `insert into t values ('5')`)
mustExec(t, db, `insert into t values ('abc')`)
rows := scanAll(t, db, `select typeof(n), n from t order by rowid`)
want := []string{"real", "integer", "integer", "text"}
for i, w := range want {
if rows[i][0] != w {
t.Errorf("row %d typeof=%v, want %s", i, rows[i][0], w)
}
}
}
// TestType_NoneAffinity asserts a column with no declared type (or BLOB-
// declared, which equals NONE affinity) preserves the storage class of
// the value as inserted.
func TestType_NoneAffinity(t *testing.T) {
db := openDB(t)
// Column declared without a type yields BLOB affinity = NONE.
mustExec(t, db, `create table t (v)`)
mustExec(t, db, `insert into t values (?)`, "text-input")
mustExec(t, db, `insert into t values (?)`, int64(42))
mustExec(t, db, `insert into t values (?)`, 3.14)
mustExec(t, db, `insert into t values (?)`, []byte{1, 2, 3})
rows := scanAll(t, db, `select typeof(v) from t order by rowid`)
want := []string{"text", "integer", "real", "blob"}
for i, w := range want {
if rows[i][0] != w {
t.Errorf("row %d typeof=%v, want %s", i, rows[i][0], w)
}
}
}
// TestType_NullSortOrder asserts NULLs FIRST vs NULLS LAST in ORDER BY,
// and the SQLite default (NULLs sort lowest with ASC).
func TestType_NullSortOrder(t *testing.T) {
db := openDB(t)
mustExec(t, db, `create table t (n integer)`)
mustExec(t, db, `insert into t values (2), (NULL), (1), (NULL), (3)`)
asc := scanAll(t, db, `select n from t order by n`)
// SQLite default: NULLs are sorted lowest in ASC order.
if asc[0][0] != nil || asc[1][0] != nil {
t.Errorf("default ASC: first two values should be NULL, got %+v", asc[:2])
}
nullsLast := scanAll(t, db, `select n from t order by n nulls last`)
// With NULLS LAST, the trailing two should be NULL.
n := len(nullsLast)
if nullsLast[n-1][0] != nil || nullsLast[n-2][0] != nil {
t.Errorf("NULLS LAST: last two values should be NULL, got %+v", nullsLast[n-2:])
}
nullsFirst := scanAll(t, db, `select n from t order by n desc nulls first`)
if nullsFirst[0][0] != nil || nullsFirst[1][0] != nil {
t.Errorf("DESC NULLS FIRST: first two values should be NULL, got %+v", nullsFirst[:2])
}
}
// TestType_EmptyVsNull asserts SQLite distinguishes ” from NULL and that
// the != operator returns NULL (not true) when one side is NULL.
func TestType_EmptyVsNull(t *testing.T) {
db := openDB(t)
mustExec(t, db, `create table t (s text)`)
mustExec(t, db, `insert into t values ('')`)
mustExec(t, db, `insert into t values (NULL)`)
// Count of '' should be 1 (NULL is excluded by != comparison).
var emptyCount int
scanOne(t, db, &emptyCount, `select count(*) from t where s = ''`)
if emptyCount != 1 {
t.Errorf("count(*) where s = '': %d, want 1", emptyCount)
}
// IS NULL must return exactly 1 row.
var nullCount int
scanOne(t, db, &nullCount, `select count(*) from t where s is null`)
if nullCount != 1 {
t.Errorf("count(*) where s is null: %d, want 1", nullCount)
}
// != with NULL is NULL, not true — so this returns 0 rows.
var diffCount int
scanOne(t, db, &diffCount, `select count(*) from t where s != ''`)
if diffCount != 0 {
t.Errorf("count(*) where s != '': %d, want 0 (NULL != '' is NULL)", diffCount)
}
// IS NOT operator (three-valued logic friendly) handles NULL.
var notCount int
scanOne(t, db, ¬Count, `select count(*) from t where s is not ''`)
if notCount != 1 {
t.Errorf("count(*) where s is not '': %d, want 1", notCount)
}
}