v4 / vlib / db / pg / orm.v
725 lines · 663 sloc · 20.0 KB · f581cbb1c964f89133439f4e303136998ddcfcfb
Raw
1module pg
2
3import orm
4import time
5import strconv
6import net.conv
7
8// ---- ORM on Conn (single pinned connection) ----
9
10// select is used internally by V's ORM for processing `SELECT ` queries.
11pub fn (c &Conn) select(config orm.SelectConfig, data orm.QueryData, where orm.QueryData) ![][]orm.Primitive {
12 c.ensure_active()!
13 where_with_tenant := orm.apply_tenant_filter(config.table, where)
14 query := orm.orm_select_gen(config, '"', true, '$', 1, where_with_tenant)
15
16 rows := pg_stmt_worker(c, query, where_with_tenant, data)!
17
18 mut ret := [][]orm.Primitive{}
19
20 for row in rows {
21 mut row_data := []orm.Primitive{}
22 for i, val in row.vals {
23 row_data << val_to_primitive(val, config.types[i])!
24 }
25 ret << row_data
26 }
27
28 return ret
29}
30
31// insert is used internally by V's ORM for processing `INSERT ` queries.
32pub fn (c &Conn) insert(table orm.Table, data orm.QueryData) ! {
33 c.ensure_active()!
34 query, converted_data :=
35 orm.orm_stmt_gen(.pg, table, '"', .insert, true, '$', 1, data, orm.QueryData{})
36 pg_stmt_worker(c, query, converted_data, orm.QueryData{})!
37}
38
39// update is used internally by V's ORM for processing `UPDATE ` queries.
40pub fn (c &Conn) update(table orm.Table, data orm.QueryData, where orm.QueryData) ! {
41 c.ensure_active()!
42 where_with_tenant := orm.apply_tenant_filter(table, where)
43 query, _ := orm.orm_stmt_gen(.default, table, '"', .update, true, '$', 1, data,
44 where_with_tenant)
45 pg_stmt_worker(c, query, data, where_with_tenant)!
46}
47
48// delete is used internally by V's ORM for processing `DELETE ` queries.
49pub fn (c &Conn) delete(table orm.Table, where orm.QueryData) ! {
50 c.ensure_active()!
51 where_with_tenant := orm.apply_tenant_filter(table, where)
52 query, _ := orm.orm_stmt_gen(.default, table, '"', .delete, true, '$', 1, orm.QueryData{},
53 where_with_tenant)
54 pg_stmt_worker(c, query, orm.QueryData{}, where_with_tenant)!
55}
56
57// last_id is used internally by V's ORM for post-processing `INSERT ` queries.
58pub fn (c &Conn) last_id() int {
59 query := 'SELECT LASTVAL();'
60
61 return c.q_int(query) or { 0 }
62}
63
64// create is used internally by V's ORM for processing table creation queries (DDL).
65pub fn (c &Conn) create(table orm.Table, fields []orm.TableField) ! {
66 query := orm.orm_table_gen(.pg, table, '"', true, 0, fields, pg_type_from_v, false) or {
67 return err
68 }
69 stmts := query.split(';')
70 for stmt in stmts {
71 if stmt != '' {
72 c.exec(stmt + ';')!
73 }
74 }
75}
76
77// drop is used internally by V's ORM for processing table destroying queries (DDL).
78pub fn (c &Conn) drop(table orm.Table) ! {
79 query := 'DROP TABLE "${table.name}";'
80 c.exec(query)!
81}
82
83// execute runs a raw SQL query and returns result rows as driver-agnostic orm.Row values,
84// with column names populated from the result metadata.
85pub fn (c &Conn) execute(query string) ![]orm.Row {
86 res := c.exec_result(query)!
87
88 mut orm_rows := []orm.Row{}
89 for r in res.rows {
90 mut vals := []string{}
91 for i in 0 .. r.vals.len {
92 vals << r.val(i)
93 }
94 orm_rows << orm.Row{
95 vals: vals
96 names: res.names
97 }
98 }
99 return orm_rows
100}
101
102// orm_begin starts a transaction on this conn.
103pub fn (c &Conn) orm_begin() ! {
104 c.begin_on_conn()!
105}
106
107// orm_commit commits the transaction on this conn.
108pub fn (c &Conn) orm_commit() ! {
109 c.commit()!
110}
111
112// orm_rollback rolls back the transaction on this conn.
113pub fn (c &Conn) orm_rollback() ! {
114 c.rollback()!
115}
116
117// orm_savepoint creates a savepoint on this conn.
118pub fn (c &Conn) orm_savepoint(name string) ! {
119 c.savepoint(name)!
120}
121
122// orm_rollback_to rolls back to a savepoint on this conn.
123pub fn (c &Conn) orm_rollback_to(name string) ! {
124 c.rollback_to(name)!
125}
126
127// orm_release_savepoint releases a savepoint on this conn.
128pub fn (c &Conn) orm_release_savepoint(name string) ! {
129 c.release_savepoint(name)!
130}
131
132// ---- ORM on DB (acquire-use-release per call) ----
133
134// select acquires a conn from the pool and runs the ORM SELECT on it.
135pub fn (mut db DB) select(config orm.SelectConfig, data orm.QueryData, where orm.QueryData) ![][]orm.Primitive {
136 mut c := db.pool.acquire()!
137 defer {
138 c.close() or {}
139 }
140 return c.select(config, data, where)
141}
142
143// insert acquires a conn from the pool, runs the ORM INSERT on it, and
144// stashes LASTVAL() captured on the same session for the calling thread.
145// V's `sql db { insert ... }` macro emits a follow-up `db.last_id()` call;
146// stashing here lets that read return the correct id even though the pool
147// may hand out a different conn for the second call.
148pub fn (mut db DB) insert(table orm.Table, data orm.QueryData) ! {
149 mut c := db.pool.acquire()!
150 defer {
151 c.close() or {}
152 }
153 c.insert(table, data)!
154 db.pool.stash_last_id(c.last_id())
155}
156
157// update acquires a conn from the pool and runs the ORM UPDATE on it.
158pub fn (mut db DB) update(table orm.Table, data orm.QueryData, where orm.QueryData) ! {
159 mut c := db.pool.acquire()!
160 defer {
161 c.close() or {}
162 }
163 c.update(table, data, where)!
164}
165
166// delete acquires a conn from the pool and runs the ORM DELETE on it.
167pub fn (mut db DB) delete(table orm.Table, where orm.QueryData) ! {
168 mut c := db.pool.acquire()!
169 defer {
170 c.close() or {}
171 }
172 c.delete(table, where)!
173}
174
175// create acquires a conn from the pool and runs CREATE TABLE on it.
176pub fn (mut db DB) create(table orm.Table, fields []orm.TableField) ! {
177 mut c := db.pool.acquire()!
178 defer {
179 c.close() or {}
180 }
181 c.create(table, fields)!
182}
183
184// drop acquires a conn from the pool and runs DROP TABLE on it.
185pub fn (mut db DB) drop(table orm.Table) ! {
186 mut c := db.pool.acquire()!
187 defer {
188 c.close() or {}
189 }
190 c.drop(table)!
191}
192
193// execute runs a raw SQL query on a pooled conn and returns result rows.
194pub fn (mut db DB) execute(query string) ![]orm.Row {
195 mut c := db.pool.acquire()!
196 defer { c.close() or {} }
197 return c.execute(query)
198}
199
200// last_id returns the id stashed by this thread's most recent `DB.insert`
201// (or 0 if there is none). LASTVAL() itself is session-scoped, so calling
202// it on a freshly-checked-out pool conn would return the wrong value or 0;
203// `DB.insert` captures it on the same conn that ran the INSERT and stashes
204// it per-thread, which is what V's ORM macro expects.
205pub fn (mut db DB) last_id() int {
206 return db.pool.take_last_id()
207}
208
209// ---- ORM on Tx (use the pinned conn) ----
210
211// select runs the ORM SELECT on the pinned transaction conn.
212pub fn (mut tx Tx) select(config orm.SelectConfig, data orm.QueryData, where orm.QueryData) ![][]orm.Primitive {
213 tx.ensure_active()!
214 return tx.conn.select(config, data, where)
215}
216
217// insert runs the ORM INSERT on the pinned transaction conn.
218pub fn (mut tx Tx) insert(table orm.Table, data orm.QueryData) ! {
219 tx.ensure_active()!
220 tx.conn.insert(table, data)!
221}
222
223// update runs the ORM UPDATE on the pinned transaction conn.
224pub fn (mut tx Tx) update(table orm.Table, data orm.QueryData, where orm.QueryData) ! {
225 tx.ensure_active()!
226 tx.conn.update(table, data, where)!
227}
228
229// delete runs the ORM DELETE on the pinned transaction conn.
230pub fn (mut tx Tx) delete(table orm.Table, where orm.QueryData) ! {
231 tx.ensure_active()!
232 tx.conn.delete(table, where)!
233}
234
235// create runs CREATE TABLE on the pinned transaction conn.
236pub fn (mut tx Tx) create(table orm.Table, fields []orm.TableField) ! {
237 tx.ensure_active()!
238 tx.conn.create(table, fields)!
239}
240
241// drop runs DROP TABLE on the pinned transaction conn.
242pub fn (mut tx Tx) drop(table orm.Table) ! {
243 tx.ensure_active()!
244 tx.conn.drop(table)!
245}
246
247// execute runs a raw SQL query on the pinned transaction conn and returns result rows.
248pub fn (mut tx Tx) execute(query string) ![]orm.Row {
249 tx.ensure_active()!
250 return tx.conn.execute(query)
251}
252
253// last_id returns the last inserted id on the pinned conn.
254pub fn (mut tx Tx) last_id() int {
255 if tx.done || isnil(tx.conn) {
256 return 0
257 }
258 return tx.conn.last_id()
259}
260
261// orm_begin is a no-op on Tx (begin already ran when the Tx was created).
262// It exists so Tx satisfies `orm.TransactionalConnection` for nested savepoints.
263pub fn (mut tx Tx) orm_begin() ! {
264}
265
266// orm_commit commits the Tx.
267pub fn (mut tx Tx) orm_commit() ! {
268 tx.commit()!
269}
270
271// orm_rollback rolls back the Tx.
272pub fn (mut tx Tx) orm_rollback() ! {
273 tx.rollback()!
274}
275
276// orm_savepoint creates a savepoint inside the Tx.
277pub fn (mut tx Tx) orm_savepoint(name string) ! {
278 tx.savepoint(name)!
279}
280
281// orm_rollback_to rolls back to a savepoint inside the Tx.
282pub fn (mut tx Tx) orm_rollback_to(name string) ! {
283 tx.rollback_to(name)!
284}
285
286// orm_release_savepoint releases a savepoint inside the Tx.
287pub fn (mut tx Tx) orm_release_savepoint(name string) ! {
288 tx.release_savepoint(name)!
289}
290
291// ---- utils ----
292
293fn pg_stmt_binder(mut types []u32, mut vals []&char, mut lens []int, mut formats []int, d orm.QueryData) {
294 for data in d.data {
295 pg_stmt_match(mut types, mut vals, mut lens, mut formats, data)
296 }
297}
298
299fn pg_stmt_match_array[T](mut types []u32, mut vals []&char, mut lens []int, mut formats []int, data []T) {
300 for element in data {
301 pg_stmt_match(mut types, mut vals, mut lens, mut formats, orm.Primitive(element))
302 }
303}
304
305fn pg_stmt_match(mut types []u32, mut vals []&char, mut lens []int, mut formats []int, data orm.Primitive) {
306 match data {
307 bool {
308 types << u32(Oid.t_bool)
309 vals << &char(&data)
310 lens << int(sizeof(bool))
311 formats << 1
312 }
313 u8 {
314 types << u32(Oid.t_char)
315 vals << &char(&data)
316 lens << int(sizeof(u8))
317 formats << 1
318 }
319 u16 {
320 types << u32(Oid.t_int2)
321 num := conv.hton16(data)
322 vals << &char(&num)
323 lens << int(sizeof(u16))
324 formats << 1
325 }
326 u32 {
327 types << u32(Oid.t_int4)
328 num := conv.hton32(data)
329 vals << &char(&num)
330 lens << int(sizeof(u32))
331 formats << 1
332 }
333 u64 {
334 types << u32(Oid.t_int8)
335 num := conv.hton64(data)
336 vals << &char(&num)
337 lens << int(sizeof(u64))
338 formats << 1
339 }
340 i8 {
341 types << u32(Oid.t_char)
342 vals << &char(&data)
343 lens << int(sizeof(i8))
344 formats << 1
345 }
346 i16 {
347 types << u32(Oid.t_int2)
348 num := conv.hton16(u16(data))
349 vals << &char(&num)
350 lens << int(sizeof(i16))
351 formats << 1
352 }
353 int {
354 types << u32(Oid.t_int4)
355 num := conv.hton32(u32(data))
356 vals << &char(&num)
357 lens << int(sizeof(int))
358 formats << 1
359 }
360 i64 {
361 types << u32(Oid.t_int8)
362 num := conv.hton64(u64(data))
363 vals << &char(&num)
364 lens << int(sizeof(i64))
365 formats << 1
366 }
367 f32 {
368 types << u32(Oid.t_float4)
369 num := conv.htonf32(f32(data))
370 vals << &char(&num)
371 lens << int(sizeof(f32))
372 formats << 1
373 }
374 f64 {
375 types << u32(Oid.t_float8)
376 num := conv.htonf64(f64(data))
377 vals << &char(&num)
378 lens << int(sizeof(f64))
379 formats << 1
380 }
381 string {
382 // If paramTypes is NULL, or any particular element in the array is zero,
383 // the server infers a data type for the parameter symbol in the same way
384 // it would do for an untyped literal string.
385 types << u32(0)
386 vals << &char(data.str)
387 lens << data.len
388 formats << 0
389 }
390 time.Time {
391 // Use a microsecond-precision representation so fractional seconds survive
392 // a write/read round-trip (relevant for TIMESTAMP/TIMESTAMPTZ columns).
393 datetime := data.format_ss_micro()
394 types << u32(0)
395 vals << &char(datetime.str)
396 lens << datetime.len
397 formats << 0
398 }
399 orm.InfixType {
400 pg_stmt_match(mut types, mut vals, mut lens, mut formats, data.right)
401 }
402 orm.Null {
403 types << u32(0) // we do not know col type, let server infer
404 vals << &char(unsafe { nil }) // NULL pointer indicates NULL
405 lens << int(0) // ignored
406 formats << 0 // ignored
407 }
408 []orm.Primitive {
409 pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
410 }
411 []bool {
412 pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
413 }
414 []f32 {
415 pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
416 }
417 []f64 {
418 pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
419 }
420 []i16 {
421 pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
422 }
423 []i64 {
424 pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
425 }
426 []i8 {
427 pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
428 }
429 []int {
430 pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
431 }
432 []string {
433 pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
434 }
435 []time.Time {
436 pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
437 }
438 []u16 {
439 pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
440 }
441 []u32 {
442 pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
443 }
444 []u64 {
445 pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
446 }
447 []u8 {
448 pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
449 }
450 []orm.InfixType {
451 pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
452 }
453 }
454}
455
456fn pg_type_from_v(typ int) !string {
457 str := match typ {
458 orm.type_idx['i8'], orm.type_idx['i16'], orm.type_idx['u8'], orm.type_idx['u16'] {
459 'SMALLINT'
460 }
461 orm.type_idx['bool'] {
462 'BOOLEAN'
463 }
464 orm.type_idx['int'], orm.type_idx['u32'] {
465 'INT'
466 }
467 orm.time_ {
468 'TIMESTAMP'
469 }
470 orm.enum_ {
471 'BIGINT'
472 }
473 orm.type_idx['i64'], orm.type_idx['u64'] {
474 'BIGINT'
475 }
476 orm.float[0] {
477 'REAL'
478 }
479 orm.float[1] {
480 'DOUBLE PRECISION'
481 }
482 orm.type_string {
483 'TEXT'
484 }
485 orm.serial {
486 'SERIAL'
487 }
488 else {
489 ''
490 }
491 }
492
493 if str == '' {
494 return error('Unknown type ${typ}')
495 }
496 return str
497}
498
499// pg_parse_timestamp parses a PostgreSQL `TIMESTAMP`/`TIMESTAMPTZ` text value into a
500// `time.Time`. It accepts the form `YYYY-MM-DD HH:mm:ss[.fraction][Z|±HH[:MM[:SS]]]`,
501// preserving up to nanosecond precision. When a timezone offset is present (as produced
502// by `TIMESTAMPTZ` columns, e.g. `+00`, `+02`, `+02:30`), the returned time is
503// normalized to UTC.
504fn pg_parse_timestamp(value string) !time.Time {
505 str := value.trim_space()
506 if str == 'infinity' || str == '-infinity' {
507 return error('pg: cannot decode special timestamp value `${str}` into time.Time')
508 }
509 // PostgreSQL appends ` BC` for dates before year 1. `time.Time` cannot represent
510 // those unambiguously, so reject them with a clear error instead of silently
511 // constructing the corresponding AD instant.
512 if str.ends_with(' BC') || str.ends_with(' bc') {
513 return error('pg: cannot decode BC timestamp value `${str}` into time.Time')
514 }
515 space_pos := str.index(' ') or {
516 // Fall back to the generic parser for values without a date/time separator.
517 return time.parse(str)
518 }
519 date_part := str[..space_pos]
520 mut time_part := str[space_pos + 1..]
521
522 // Detect and strip an optional timezone designator.
523 mut offset_seconds := 0
524 if time_part.ends_with('Z') || time_part.ends_with('z') {
525 time_part = time_part[..time_part.len - 1]
526 } else {
527 // PostgreSQL appends the offset sign (`+`/`-`) directly after the time part.
528 // Scan past the leading hour so a negative hour can never be mistaken for a sign.
529 mut sign_pos := -1
530 for i := 1; i < time_part.len; i++ {
531 c := time_part[i]
532 if c == `+` || c == `-` {
533 sign_pos = i
534 break
535 }
536 }
537 if sign_pos != -1 {
538 offset_seconds = pg_parse_offset(time_part[sign_pos..])!
539 time_part = time_part[..sign_pos]
540 }
541 }
542
543 // Split the optional fractional seconds off the `HH:mm:ss` part.
544 mut nanosecond := 0
545 mut hms := time_part
546 if dot_pos := time_part.index('.') {
547 hms = time_part[..dot_pos]
548 mut frac := time_part[dot_pos + 1..]
549 if frac.len > 9 {
550 frac = frac[..9]
551 }
552 // strconv.atoi is strict, so any non-digit (e.g. a stray suffix) errors out
553 // instead of being silently truncated by `string.int()`.
554 mut scaled := strconv.atoi(frac)!
555 for _ in 0 .. 9 - frac.len {
556 scaled *= 10
557 }
558 nanosecond = scaled
559 }
560
561 ymd := date_part.split('-')
562 if ymd.len != 3 {
563 return error('pg: invalid timestamp date `${date_part}`')
564 }
565 hms_parts := hms.split(':')
566 if hms_parts.len != 3 {
567 return error('pg: invalid timestamp time `${hms}`')
568 }
569
570 // Use strict numeric parsing so suffixes such as ` BC` or other malformed values
571 // are rejected rather than silently coerced (`string.int()` keeps the digit prefix).
572 year := strconv.atoi(ymd[0])!
573 month := strconv.atoi(ymd[1])!
574 day := strconv.atoi(ymd[2])!
575 hour := strconv.atoi(hms_parts[0])!
576 minute := strconv.atoi(hms_parts[1])!
577 second := strconv.atoi(hms_parts[2])!
578
579 // Validate the ranges up front: `time.new` *panics* on out-of-range fields, but
580 // PostgreSQL accepts values V cannot represent (e.g. years past 9999), so turn
581 // those into a clear error instead of aborting the process. Supported AD years are
582 // 1..9999; year 0 / negative years are BC (proleptic Gregorian) and unrepresentable,
583 // matching the explicit ` BC` rejection above.
584 if year > 9999 {
585 return error('pg: year out of range in timestamp `${str}`')
586 }
587 if year < 1 {
588 return error('pg: cannot decode BC/year-0 timestamp `${str}` into time.Time')
589 }
590 if month < 1 || month > 12 {
591 return error('pg: month out of range in timestamp `${str}`')
592 }
593 if day < 1 || day > 31 {
594 return error('pg: day out of range in timestamp `${str}`')
595 }
596 if hour < 0 || hour > 23 {
597 return error('pg: hour out of range in timestamp `${str}`')
598 }
599 if minute < 0 || minute > 59 {
600 return error('pg: minute out of range in timestamp `${str}`')
601 }
602 if second < 0 || second > 59 {
603 return error('pg: second out of range in timestamp `${str}`')
604 }
605
606 mut result := time.new(
607 year: year
608 month: month
609 day: day
610 hour: hour
611 minute: minute
612 second: second
613 nanosecond: nanosecond
614 is_local: false
615 )
616 if offset_seconds != 0 {
617 // Normalize to UTC by subtracting the parsed offset.
618 result = result.add_seconds(-offset_seconds)
619 // The offset can push a boundary value past the representable range in either
620 // direction, so re-check the normalized result; otherwise offset-bearing
621 // timestamps would silently bypass the range guards above. Examples:
622 // `9999-12-31 23:30:00-01` -> year 10000, `0001-01-01 00:30:00+01` -> year 0 (BC).
623 if result.year > 9999 {
624 return error('pg: year out of range in timestamp `${str}` after UTC normalization')
625 }
626 if result.year < 1 {
627 return error('pg: timestamp `${str}` normalizes to a BC/year-0 date, which is unrepresentable')
628 }
629 }
630 return result
631}
632
633// pg_parse_offset parses a PostgreSQL timezone offset such as `+02`, `-05`, `+02:30`
634// or `+02:30:00` and returns the offset in seconds (signed).
635fn pg_parse_offset(offset string) !int {
636 if offset.len < 3 {
637 return error('pg: invalid timezone offset `${offset}`')
638 }
639 sign := if offset[0] == `-` { -1 } else { 1 }
640 parts := offset[1..].split(':')
641 mut seconds := strconv.atoi(parts[0])! * 3600
642 if parts.len > 1 {
643 seconds += strconv.atoi(parts[1])! * 60
644 }
645 if parts.len > 2 {
646 seconds += strconv.atoi(parts[2])!
647 }
648 return sign * seconds
649}
650
651fn val_to_primitive(val ?string, typ int) !orm.Primitive {
652 if str := val {
653 match typ {
654 // bool
655 orm.type_idx['bool'] {
656 return orm.Primitive(str == 't')
657 }
658 // i8
659 orm.type_idx['i8'] {
660 return orm.Primitive(str.i8())
661 }
662 // i16
663 orm.type_idx['i16'] {
664 return orm.Primitive(str.i16())
665 }
666 // int
667 orm.type_idx['int'] {
668 return orm.Primitive(str.int())
669 }
670 // i64
671 orm.type_idx['i64'] {
672 return orm.Primitive(str.i64())
673 }
674 // u8
675 orm.type_idx['u8'] {
676 data := str.i8()
677 return orm.Primitive(*unsafe { &u8(&data) })
678 }
679 // u16
680 orm.type_idx['u16'] {
681 data := str.i16()
682 return orm.Primitive(*unsafe { &u16(&data) })
683 }
684 // u32
685 orm.type_idx['u32'] {
686 data := str.int()
687 return orm.Primitive(*unsafe { &u32(&data) })
688 }
689 // u64
690 orm.type_idx['u64'] {
691 data := str.i64()
692 return orm.Primitive(*unsafe { &u64(&data) })
693 }
694 // f32
695 orm.type_idx['f32'] {
696 return orm.Primitive(str.f32())
697 }
698 // f64
699 orm.type_idx['f64'] {
700 return orm.Primitive(str.f64())
701 }
702 orm.type_string {
703 return orm.Primitive(str)
704 }
705 orm.time_ {
706 // A bare (optionally signed) integer is a Unix timestamp; route every
707 // other value through the PostgreSQL-aware parser so textual timestamps
708 // and special values such as `infinity` are decoded (or rejected) there
709 // instead of silently falling through to `time.unix(0)`.
710 if timestamp := strconv.atoi64(str.trim_space()) {
711 return orm.Primitive(time.unix(timestamp))
712 }
713 return orm.Primitive(pg_parse_timestamp(str)!)
714 }
715 orm.enum_ {
716 return orm.Primitive(str.i64())
717 }
718 else {}
719 }
720
721 return error('Unknown field type ${typ}')
722 } else {
723 return orm.Null{}
724 }
725}
726