Skip to content

Commit da2d00e

Browse files
Merge pull request #39 from indexdata/CROSSLINK-210
CROSSLINK-210 Add date time field support
2 parents 71358ff + 31045d7 commit da2d00e

3 files changed

Lines changed: 123 additions & 7 deletions

File tree

pgcql/pg_field_datetime.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package pgcql
2+
3+
import (
4+
"fmt"
5+
"time"
6+
7+
"github.com/indexdata/cql-go/cql"
8+
)
9+
10+
const dateFormat = "2006-01-02"
11+
const dateTimeFormat = "2006-01-02 15:04:05"
12+
13+
type FieldDateTime struct {
14+
FieldCommon
15+
isDate bool
16+
}
17+
18+
func NewFieldDate() *FieldDateTime {
19+
return &FieldDateTime{}
20+
}
21+
22+
func (f *FieldDateTime) WithColumn(column string) *FieldDateTime {
23+
f.column = column
24+
return f
25+
}
26+
27+
func (f *FieldDateTime) WithOnlyDate() *FieldDateTime {
28+
f.isDate = true
29+
return f
30+
}
31+
32+
func (f *FieldDateTime) Generate(sc cql.SearchClause, queryArgumentIndex int) (string, []any, error) {
33+
s := f.handleEmptyTerm(sc)
34+
if s != "" {
35+
return s, []any{}, nil
36+
}
37+
relOrdered, err := f.handleOrderedRelation(sc)
38+
if err != nil {
39+
return "", nil, err
40+
}
41+
number, err := f.parseTerm(sc.Term)
42+
if err != nil {
43+
if f.isDate {
44+
return "", nil, &PgError{message: fmt.Sprintf("invalid date %s, it should be in format YYYY-MM-DD", sc.Term)}
45+
} else {
46+
return "", nil, &PgError{message: fmt.Sprintf("invalid date time %s, it should be in format YYYY-MM-DD, YYYY-MM-DD HH:MM:SS, YYYY-MM-DDTHH:MM:SSZ, YYYY-MM-DDTHH:MM:SS±HH:MM", sc.Term)}
47+
}
48+
}
49+
return f.column + " " + relOrdered + fmt.Sprintf(" $%d", queryArgumentIndex), []any{number}, nil
50+
}
51+
52+
func (f *FieldDateTime) parseTerm(term string) (time.Time, error) {
53+
if f.isDate {
54+
date, err := time.Parse(dateFormat, term)
55+
if err != nil {
56+
return time.Time{}, err
57+
}
58+
return date, nil
59+
} else {
60+
layouts := []string{
61+
dateFormat,
62+
dateTimeFormat,
63+
time.RFC3339,
64+
}
65+
var err error
66+
for _, layout := range layouts {
67+
t, e := time.Parse(layout, term)
68+
if e == nil {
69+
return t, nil
70+
}
71+
err = e
72+
}
73+
return time.Time{}, err
74+
}
75+
}

pgcql/pgcql_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"reflect"
55
"strings"
66
"testing"
7+
"time"
78

89
"github.com/indexdata/cql-go/cql"
910
"github.com/stretchr/testify/assert"
@@ -43,6 +44,15 @@ func TestParsing(t *testing.T) {
4344
price := NewFieldNumber()
4445
def.AddField("price", price)
4546

47+
dateField := NewFieldDate().WithOnlyDate()
48+
def.AddField("date", dateField)
49+
50+
dateTimeField := NewFieldDate()
51+
def.AddField("datetime", dateTimeField)
52+
53+
dateTimeWithZone, err := time.Parse(time.RFC3339, "2026-03-05T09:34:27+01:00")
54+
assert.NoError(t, err)
55+
4656
for _, testcase := range []struct {
4757
query string
4858
expected string
@@ -107,6 +117,29 @@ func TestParsing(t *testing.T) {
107117
{"price <= beta", "error: invalid number beta", nil},
108118
{"price all 10.95", "error: unsupported relation all", nil},
109119
{"price = \"\"", "price IS NOT NULL", []any{}},
120+
{"date = 2026-03-05", "date = $1", []any{time.Date(2026, 3, 5, 0, 0, 0, 0, time.UTC)}},
121+
{"date == 2026-03-05", "date = $1", []any{time.Date(2026, 3, 5, 0, 0, 0, 0, time.UTC)}},
122+
{"date exact 2026-03-05", "date = $1", []any{time.Date(2026, 3, 5, 0, 0, 0, 0, time.UTC)}},
123+
{"date > 2026-03-05", "date > $1", []any{time.Date(2026, 3, 5, 0, 0, 0, 0, time.UTC)}},
124+
{"date < 2026-03-05", "date < $1", []any{time.Date(2026, 3, 5, 0, 0, 0, 0, time.UTC)}},
125+
{"date >= 2026-03-05", "date >= $1", []any{time.Date(2026, 3, 5, 0, 0, 0, 0, time.UTC)}},
126+
{"date <= 2026-03-05", "date <= $1", []any{time.Date(2026, 3, 5, 0, 0, 0, 0, time.UTC)}},
127+
{"date = April", "error: invalid date April, it should be in format YYYY-MM-DD", nil},
128+
{"date all 2026-03-05", "error: unsupported relation all", nil},
129+
{"date = \"\"", "date IS NOT NULL", []any{}},
130+
{"datetime = 2026-03-05 09:34:27", "datetime = $1", []any{time.Date(2026, 3, 5, 9, 34, 27, 0, time.UTC)}},
131+
{"datetime = 2026-03-05T09:34:27Z", "datetime = $1", []any{time.Date(2026, 3, 5, 9, 34, 27, 0, time.UTC)}},
132+
{"datetime = 2026-03-05T09:34:27+01:00", "datetime = $1", []any{dateTimeWithZone}},
133+
{"datetime = 2026-03-05", "datetime = $1", []any{time.Date(2026, 3, 5, 0, 0, 0, 0, time.UTC)}},
134+
{"datetime == 2026-03-05", "datetime = $1", []any{time.Date(2026, 3, 5, 0, 0, 0, 0, time.UTC)}},
135+
{"datetime exact 2026-03-05", "datetime = $1", []any{time.Date(2026, 3, 5, 0, 0, 0, 0, time.UTC)}},
136+
{"datetime > 2026-03-05", "datetime > $1", []any{time.Date(2026, 3, 5, 0, 0, 0, 0, time.UTC)}},
137+
{"datetime < 2026-03-05", "datetime < $1", []any{time.Date(2026, 3, 5, 0, 0, 0, 0, time.UTC)}},
138+
{"datetime >= 2026-03-05", "datetime >= $1", []any{time.Date(2026, 3, 5, 0, 0, 0, 0, time.UTC)}},
139+
{"datetime <= 2026-03-05", "datetime <= $1", []any{time.Date(2026, 3, 5, 0, 0, 0, 0, time.UTC)}},
140+
{"datetime = April", "error: invalid date time April, it should be in format YYYY-MM-DD, YYYY-MM-DD HH:MM:SS, YYYY-MM-DDTHH:MM:SSZ, YYYY-MM-DDTHH:MM:SS±HH:MM", nil},
141+
{"datetime all 2026-03-05", "error: unsupported relation all", nil},
142+
{"datetime = \"\"", "datetime IS NOT NULL", []any{}},
110143
} {
111144
var parser cql.Parser
112145
q, err := parser.Parse(testcase.query)

pgcql/pgx_test.go

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,20 +54,20 @@ func TestPgx(t *testing.T) {
5454
err := conn.Close(ctx)
5555
assert.NoError(t, err, "failed to close db connection")
5656
}()
57-
_, err = conn.Exec(ctx, "CREATE TABLE mytable (id SERIAL PRIMARY KEY, title TEXT, author TEXT, tag TEXT, year INT, address JSONB)")
57+
_, err = conn.Exec(ctx, "CREATE TABLE mytable (id SERIAL PRIMARY KEY, title TEXT, author TEXT, tag TEXT, year INT, address JSONB, start_date date, created_at timestamp)")
5858
assert.NoError(t, err, "failed to create mytable")
5959

6060
var rows pgx.Rows
6161

62-
rows, err = conn.Query(ctx, "INSERT INTO mytable (title, author, tag, year, address) "+
63-
"VALUES ($1, $2, $3, $4, $5)", "the art of computer programming, volume 1", "donald e. knuth", "tag1", 1968,
64-
`{"city": "Reading", "country": "USA", "zip": 19601}`)
62+
rows, err = conn.Query(ctx, "INSERT INTO mytable (title, author, tag, year, address, start_date, created_at) "+
63+
"VALUES ($1, $2, $3, $4, $5, $6, $7)", "the art of computer programming, volume 1", "donald e. knuth", "tag1", 1968,
64+
`{"city": "Reading", "country": "USA", "zip": 19601}`, "2026-03-05", "2026-03-05 09:34:27")
6565
assert.NoError(t, err, "failed to insert data")
6666
rows.Close()
6767

68-
rows, err = conn.Query(ctx, "INSERT INTO mytable (title, author, tag, year, address) "+
69-
"VALUES ($1, $2, $3, $4, $5)", "the TeXbook", "d. e. knuth", "tag2", 1984,
70-
`{"city": "Stanford", "country": "USA", "zip": 67890}`)
68+
rows, err = conn.Query(ctx, "INSERT INTO mytable (title, author, tag, year, address, start_date, created_at) "+
69+
"VALUES ($1, $2, $3, $4, $5, $6, $7)", "the TeXbook", "d. e. knuth", "tag2", 1984,
70+
`{"city": "Stanford", "country": "USA", "zip": 67890}`, "2026-03-06", "2026-03-06 09:34:27")
7171
assert.NoError(t, err, "failed to insert data")
7272
rows.Close()
7373

@@ -88,6 +88,8 @@ func TestPgx(t *testing.T) {
8888
def.AddField("country", NewFieldString().WithExact().WithColumn("address->>'country'"))
8989
def.AddField("zip", NewFieldNumber().WithColumn("address->'zip'"))
9090
def.AddField("zip2", NewFieldNumber().WithColumn("(address->'zip')::numeric"))
91+
def.AddField("start_date", NewFieldDate().WithOnlyDate())
92+
def.AddField("created_at", NewFieldDate())
9193

9294
var parser cql.Parser
9395
for _, testcase := range []struct {
@@ -132,6 +134,12 @@ func TestPgx(t *testing.T) {
132134
{"zip >= 0", []int{1, 2}},
133135
{"zip = \"\"", []int{1, 2}},
134136
{"zip2 = 19601", []int{1}},
137+
{"start_date >= 2026-03-05", []int{1, 2}},
138+
{"start_date > 2026-03-05", []int{2}},
139+
{"start_date = 2026-03-05", []int{1}},
140+
{"created_at > 2026-03-05", []int{1, 2}},
141+
{"created_at > 2026-03-05 10:00:00", []int{2}},
142+
{"created_at = \"\"", []int{1, 2}},
135143
} {
136144
runQuery(t, parser, conn, ctx, def, testcase.query, testcase.expectedIds)
137145
}

0 commit comments

Comments
 (0)