Skip to content

Accept only SQL-standard type names in as_sqlr_type() #9

Description

@nbenn

Type strings, as in sqlr_column("email", "varchar(255)"), are read through a lookup table that mixes spellings from several engines (int4, bpchar, bytea, timestamptz, jsonb, double, …), and whatever the parser does not understand is dropped or passed through without an error:

library(sqlr)

as_sqlr_type("timestamp(3) with time zone")@with_timezone
#> [1] FALSE
as_sqlr_type("geometry(Point, 4326)")@name
#> [1] "geometry"
as_sqlr_type("varbinary(16)")@name
#> [1] "varbinary"
class(as_sqlr_type("varchr(255)"))[[1L]]
#> [1] "sqlr::sqlr_other_type"

The parser keeps the name before the first ( and the integers inside the parentheses, and ignores anything after the last ) (R/type_shorthand.R#L29-L48), which is how the time zone is lost. Names missing from the table fall through to sqlr_other(name) (#L90) with only the lower-cased name, although the documentation says unrecognised spellings are "carried through verbatim" (#L3-L5). A typo becomes an unmodelled type instead of an error.

The table is also the wrong thing to grow in core. Every dialect brings spellings to add, and some cannot be added without picking an engine: bare float is double precision in Postgres (docs) but a 4-byte float in MySQL (docs) and DuckDB (docs). Today it falls through, so a column written as "float" cannot compare equal to its own Postgres reflection, which sqlr.postgres reads as sqlr_double() (R/parse_type.R#L16):

library(sqlr)

sqlr_diff(
  sqlr_table("t", sqlr_column("x", "float")),
  sqlr_table("t", sqlr_column("x", sqlr_double()))
)
#> [1] "column x: type sqlr::sqlr_other_type(name=float) vs sqlr::sqlr_float_type(bytes=8)"

Proposal

Accept SQL-standard type names, plus text and uuid, as listed below, and reject everything else:

Class Spellings
sqlr_integer_type smallint, integer, int, bigint
sqlr_float_type real, double precision, float(p) (4 bytes for p up to 24, else 8)
sqlr_decimal_type numeric(p, s), decimal(p, s)
sqlr_string_type char(n) and character(n) (n = 1 when omitted), varchar(n), character varying(n), text
sqlr_binary_type binary(n) (fixed-length), varbinary(n), blob
sqlr_boolean_type boolean
sqlr_time_type date; time and timestamp, with optional (p) and an optional with time zone or without time zone
sqlr_json_type json
sqlr_uuid_type uuid

Engine aliases such as int4, timestamptz and bytea are rejected, so the accepted set changes only when the IR does, not when a dialect is added. Unsigned and 1-byte integers and binary JSON have no standard spelling and stay constructor-only (sqlr_int(unsigned = TRUE), sqlr_integer_type(bytes = 1L), sqlr_json(binary = TRUE)). The error for an unknown spelling points to sqlr_other(), which becomes the only way to carry a type verbatim.

The same table, run in reverse, gives types a format() method. Then sqlr_diff() can report varchar(255) vs text rather than sqlr::sqlr_string_type(size=255, fixed=FALSE) vs sqlr::sqlr_string_type(size=NA, fixed=FALSE) (R/compare.R#L108-L119). Types without a standard spelling format as their constructor call.

The exported sqlr_parse_type() generic (R/generics.R#L24-L42) has no method in either dialect, because each reads types inside its sqlr_reflect_schema() method, so it can be dropped.

This is not the generic ANSI dialect the README rules out (README.md#L155-L159). Nothing renders this grammar; it only builds IR objects, and it is testable without a database.

SQLite reflection currently reads declared types through as_sqlr_type(), including non-standard spellings its renderer emits (DOUBLE, JSONB), so nbenn/sqlr.sqlite#1 has to land first.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions