Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions pgcql/pg_field_datetime.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package pgcql

import (
"fmt"
"time"

"github.com/indexdata/cql-go/cql"
)

const dateFormat = "2006-01-02"
const dateTimeFormat = "2006-01-02 15:04:05"

type FieldDateTime struct {
FieldCommon
isDate bool
}

func NewFieldDate() *FieldDateTime {
return &FieldDateTime{}
}

func (f *FieldDateTime) WithColumn(column string) *FieldDateTime {
f.column = column
return f
}

func (f *FieldDateTime) WithOnlyDate() *FieldDateTime {
f.isDate = true
return f
}

func (f *FieldDateTime) Generate(sc cql.SearchClause, queryArgumentIndex int) (string, []any, error) {
s := f.handleEmptyTerm(sc)
if s != "" {
return s, []any{}, nil
}
relOrdered, err := f.handleOrderedRelation(sc)
if err != nil {
return "", nil, err
}
number, err := f.parseTerm(sc.Term)
if err != nil {
if f.isDate {
return "", nil, &PgError{message: fmt.Sprintf("invalid date %s, it should be in format YYYY-MM-DD", sc.Term)}
} else {
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)}
}
}
return f.column + " " + relOrdered + fmt.Sprintf(" $%d", queryArgumentIndex), []any{number}, nil
}

func (f *FieldDateTime) parseTerm(term string) (time.Time, error) {
if f.isDate {
date, err := time.Parse(dateFormat, term)
if err != nil {
return time.Time{}, err
}
return date, nil
} else {
layouts := []string{
dateFormat,
dateTimeFormat,
time.RFC3339,
}
var err error
for _, layout := range layouts {
t, e := time.Parse(layout, term)
if e == nil {
return t, nil
}
err = e
}
return time.Time{}, err
}
}
33 changes: 33 additions & 0 deletions pgcql/pgcql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"reflect"
"strings"
"testing"
"time"

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

dateField := NewFieldDate().WithOnlyDate()
def.AddField("date", dateField)

dateTimeField := NewFieldDate()
def.AddField("datetime", dateTimeField)

dateTimeWithZone, err := time.Parse(time.RFC3339, "2026-03-05T09:34:27+01:00")
assert.NoError(t, err)

for _, testcase := range []struct {
query string
expected string
Expand Down Expand Up @@ -107,6 +117,29 @@ func TestParsing(t *testing.T) {
{"price <= beta", "error: invalid number beta", nil},
{"price all 10.95", "error: unsupported relation all", nil},
{"price = \"\"", "price IS NOT NULL", []any{}},
{"date = 2026-03-05", "date = $1", []any{time.Date(2026, 3, 5, 0, 0, 0, 0, time.UTC)}},
{"date == 2026-03-05", "date = $1", []any{time.Date(2026, 3, 5, 0, 0, 0, 0, time.UTC)}},
{"date exact 2026-03-05", "date = $1", []any{time.Date(2026, 3, 5, 0, 0, 0, 0, time.UTC)}},
{"date > 2026-03-05", "date > $1", []any{time.Date(2026, 3, 5, 0, 0, 0, 0, time.UTC)}},
{"date < 2026-03-05", "date < $1", []any{time.Date(2026, 3, 5, 0, 0, 0, 0, time.UTC)}},
{"date >= 2026-03-05", "date >= $1", []any{time.Date(2026, 3, 5, 0, 0, 0, 0, time.UTC)}},
{"date <= 2026-03-05", "date <= $1", []any{time.Date(2026, 3, 5, 0, 0, 0, 0, time.UTC)}},
{"date = April", "error: invalid date April, it should be in format YYYY-MM-DD", nil},
{"date all 2026-03-05", "error: unsupported relation all", nil},
{"date = \"\"", "date IS NOT NULL", []any{}},
{"datetime = 2026-03-05 09:34:27", "datetime = $1", []any{time.Date(2026, 3, 5, 9, 34, 27, 0, time.UTC)}},
{"datetime = 2026-03-05T09:34:27Z", "datetime = $1", []any{time.Date(2026, 3, 5, 9, 34, 27, 0, time.UTC)}},
{"datetime = 2026-03-05T09:34:27+01:00", "datetime = $1", []any{dateTimeWithZone}},
{"datetime = 2026-03-05", "datetime = $1", []any{time.Date(2026, 3, 5, 0, 0, 0, 0, time.UTC)}},
{"datetime == 2026-03-05", "datetime = $1", []any{time.Date(2026, 3, 5, 0, 0, 0, 0, time.UTC)}},
{"datetime exact 2026-03-05", "datetime = $1", []any{time.Date(2026, 3, 5, 0, 0, 0, 0, time.UTC)}},
{"datetime > 2026-03-05", "datetime > $1", []any{time.Date(2026, 3, 5, 0, 0, 0, 0, time.UTC)}},
{"datetime < 2026-03-05", "datetime < $1", []any{time.Date(2026, 3, 5, 0, 0, 0, 0, time.UTC)}},
{"datetime >= 2026-03-05", "datetime >= $1", []any{time.Date(2026, 3, 5, 0, 0, 0, 0, time.UTC)}},
{"datetime <= 2026-03-05", "datetime <= $1", []any{time.Date(2026, 3, 5, 0, 0, 0, 0, time.UTC)}},
{"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},
{"datetime all 2026-03-05", "error: unsupported relation all", nil},
{"datetime = \"\"", "datetime IS NOT NULL", []any{}},
} {
var parser cql.Parser
q, err := parser.Parse(testcase.query)
Expand Down
22 changes: 15 additions & 7 deletions pgcql/pgx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,20 @@ func TestPgx(t *testing.T) {
err := conn.Close(ctx)
assert.NoError(t, err, "failed to close db connection")
}()
_, err = conn.Exec(ctx, "CREATE TABLE mytable (id SERIAL PRIMARY KEY, title TEXT, author TEXT, tag TEXT, year INT, address JSONB)")
_, 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)")
assert.NoError(t, err, "failed to create mytable")

var rows pgx.Rows

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

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

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

var parser cql.Parser
for _, testcase := range []struct {
Expand Down Expand Up @@ -132,6 +134,12 @@ func TestPgx(t *testing.T) {
{"zip >= 0", []int{1, 2}},
{"zip = \"\"", []int{1, 2}},
{"zip2 = 19601", []int{1}},
{"start_date >= 2026-03-05", []int{1, 2}},
{"start_date > 2026-03-05", []int{2}},
{"start_date = 2026-03-05", []int{1}},
{"created_at > 2026-03-05", []int{1, 2}},
{"created_at > 2026-03-05 10:00:00", []int{2}},
{"created_at = \"\"", []int{1, 2}},
} {
runQuery(t, parser, conn, ctx, def, testcase.query, testcase.expectedIds)
}
Expand Down