diff --git a/Cargo.lock b/Cargo.lock index 6c1f179c..a89e743a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -475,6 +475,27 @@ dependencies = [ "typenum", ] +[[package]] +name = "csv" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "csv-core" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +dependencies = [ + "memchr", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -3402,6 +3423,7 @@ dependencies = [ "camino", "clap", "convert_case", + "csv", "enum-iterator", "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 9bcc782d..65048868 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ repository = "https://github.com/sbdchd/squawk" # third party base64 = "0.12.2" console = "0.11.3" +csv = "1.4.0" glob = "0.3.1" insta = "1.39.0" jsonwebtoken = { version = "10.3.0", features = ["rust_crypto"] } diff --git a/crates/squawk_ide/src/generated/extensions/h3.sql b/crates/squawk_ide/src/generated/extensions/h3.sql new file mode 100644 index 00000000..d2fe91ba --- /dev/null +++ b/crates/squawk_ide/src/generated/extensions/h3.sql @@ -0,0 +1,389 @@ +-- squawk-ignore-file +-- pg version: 18.3 +-- update via: +-- cargo xtask sync-builtins + +-- size: 8, align: 8 +create type public.h3index; + +create function public.__h3_cell_to_children_aux(index h3index, resolution integer, current integer) returns SETOF h3index + language plpgsql; + +create function public.bigint_to_h3index(bigint) returns h3index + language c; + +-- Returns true if the given indices are neighbors. +create function public.h3_are_neighbor_cells(origin h3index, destination h3index) returns boolean + language c; + +-- Exact area for a specific cell (hexagon or pentagon). +create function public.h3_cell_area(cell h3index, unit text DEFAULT 'km^2'::text) returns double precision + language c; + +-- Finds the boundary of the index. +-- +-- Use `SET h3.extend_antimeridian TO true` to extend coordinates when crossing 180th meridian. +create function public.h3_cell_to_boundary(cell h3index) returns polygon + language c; + +-- DEPRECATED: Use `SET h3.extend_antimeridian TO true` instead. +create function public.h3_cell_to_boundary(cell h3index, extend_antimeridian boolean) returns polygon + language c; + +-- Returns the center child (finer) index contained by input index at next resolution. +create function public.h3_cell_to_center_child(cell h3index) returns h3index + language c; + +-- Returns the center child (finer) index contained by input index at given resolution. +create function public.h3_cell_to_center_child(cell h3index, resolution integer) returns h3index + language c; + +-- Returns the position of the child cell within an ordered list of all children of the cells parent at the specified resolution parentRes. The order of the ordered list is the same as that returned by cellToChildren. This is the complement of childPosToCell. +create function public.h3_cell_to_child_pos(child h3index, parentres integer) returns bigint + language c; + +-- Returns the set of children of the given index. +create function public.h3_cell_to_children(cell h3index) returns SETOF h3index + language c; + +-- Returns the set of children of the given index. +create function public.h3_cell_to_children(cell h3index, resolution integer) returns SETOF h3index + language c; + +-- Slower version of H3ToChildren but allocates less memory. +create function public.h3_cell_to_children_slow(index h3index) returns SETOF h3index + language sql; + +-- Slower version of H3ToChildren but allocates less memory. +create function public.h3_cell_to_children_slow(index h3index, resolution integer) returns SETOF h3index + language sql; + +-- DEPRECATED: Use `h3_cell_to_latlng` instead. +create function public.h3_cell_to_lat_lng(cell h3index) returns point + language c; + +-- Finds the centroid of the index. +create function public.h3_cell_to_latlng(cell h3index) returns point + language c; + +-- Produces local IJ coordinates for an H3 index anchored by an origin. +create function public.h3_cell_to_local_ij(origin h3index, index h3index) returns point + language c; + +-- Returns the parent of the given index. +create function public.h3_cell_to_parent(cell h3index) returns h3index + language c; + +-- Returns the parent of the given index. +create function public.h3_cell_to_parent(cell h3index, resolution integer) returns h3index + language c; + +-- Returns a single vertex for a given cell, as an H3 index. +create function public.h3_cell_to_vertex(cell h3index, vertexnum integer) returns h3index + language c; + +-- Returns all vertexes for a given cell, as H3 indexes. +create function public.h3_cell_to_vertexes(cell h3index) returns SETOF h3index + language c; + +-- Returns a unidirectional edge H3 index based on the provided origin and destination. +create function public.h3_cells_to_directed_edge(origin h3index, destination h3index) returns h3index + language c; + +-- Create a LinkedGeoPolygon describing the outline(s) of a set of hexagons. Polygon outlines will follow GeoJSON MultiPolygon order: Each polygon will have one outer loop, which is first in the list, followed by any holes. +create function public.h3_cells_to_multi_polygon(h3index[], OUT exterior polygon, OUT holes polygon[]) returns SETOF record + language c; + +-- Returns the child cell at a given position within an ordered list of all children of parent at the specified resolution childRes. The order of the ordered list is the same as that returned by cellToChildren. This is the complement of cellToChildPos. +create function public.h3_child_pos_to_cell(childpos bigint, parent h3index, childres integer) returns h3index + language c; + +-- Compacts the given array as best as possible. +create function public.h3_compact_cells(cells h3index[]) returns SETOF h3index + language c; + +-- Provides the coordinates defining the unidirectional edge. +create function public.h3_directed_edge_to_boundary(edge h3index) returns polygon + language c; + +-- Returns the pair of indices from the given edge. +create function public.h3_directed_edge_to_cells(edge h3index, OUT origin h3index, OUT destination h3index) returns record + language c; + +-- Exact length for a specific unidirectional edge. +create function public.h3_edge_length(edge h3index, unit text DEFAULT 'km'::text) returns double precision + language c; + +-- Returns the base cell number of the index. +create function public.h3_get_base_cell_number(h3index) returns integer + language c; + +-- Returns the destination index from the given edge. +create function public.h3_get_directed_edge_destination(edge h3index) returns h3index + language c; + +-- Returns the origin index from the given edge. +create function public.h3_get_directed_edge_origin(edge h3index) returns h3index + language c; + +-- Get the currently installed version of the extension. +create function public.h3_get_extension_version() returns text + language c; + +-- Average hexagon area in square (kilo)meters at the given resolution. +create function public.h3_get_hexagon_area_avg(resolution integer, unit text DEFAULT 'km'::text) returns double precision + language c; + +-- Average hexagon edge length in (kilo)meters at the given resolution. +create function public.h3_get_hexagon_edge_length_avg(resolution integer, unit text DEFAULT 'km'::text) returns double precision + language c; + +-- Find all icosahedron faces intersected by a given H3 index. +create function public.h3_get_icosahedron_faces(h3index) returns integer[] + language c; + +-- Number of unique H3 indexes at the given resolution. +create function public.h3_get_num_cells(resolution integer) returns bigint + language c; + +-- All the pentagon H3 indexes at the specified resolution. +create function public.h3_get_pentagons(resolution integer) returns SETOF h3index + language c; + +-- Returns all 122 resolution 0 indexes. +create function public.h3_get_res_0_cells() returns SETOF h3index + language c; + +-- Returns the resolution of the index. +create function public.h3_get_resolution(h3index) returns integer + language c; + +-- The great circle distance in radians between two spherical coordinates. +create function public.h3_great_circle_distance(a point, b point, unit text DEFAULT 'km'::text) returns double precision + language c; + +-- Produces indices within "k" distance of the origin index. +create function public.h3_grid_disk(origin h3index, k integer DEFAULT 1) returns SETOF h3index + language c; + +-- Produces indices within "k" distance of the origin index paired with their distance to the origin. +create function public.h3_grid_disk_distances(origin h3index, k integer DEFAULT 1, OUT index h3index, OUT distance integer) returns SETOF record + language c; + +-- Returns the distance in grid cells between the two indices. +create function public.h3_grid_distance(origin h3index, destination h3index) returns bigint + language c; + +-- Given two H3 indexes, return the line of indexes between them (inclusive). +-- +-- This function may fail to find the line between two indexes, for +-- example if they are very far apart. It may also fail when finding +-- distances for indexes on opposite sides of a pentagon. +create function public.h3_grid_path_cells(origin h3index, destination h3index) returns SETOF h3index + language c; + +-- Returns the hollow hexagonal ring centered at origin with distance "k". +create function public.h3_grid_ring_unsafe(origin h3index, k integer DEFAULT 1) returns SETOF h3index + language c; + +-- Returns true if this index represents a pentagonal cell. +create function public.h3_is_pentagon(h3index) returns boolean + language c; + +-- Returns true if this index has a resolution with Class III orientation. +create function public.h3_is_res_class_iii(h3index) returns boolean + language c; + +-- Returns true if the given H3Index is valid. +create function public.h3_is_valid_cell(h3index) returns boolean + language c; + +-- Returns true if the given edge is valid. +create function public.h3_is_valid_directed_edge(edge h3index) returns boolean + language c; + +-- Whether the input is a valid H3 vertex. +create function public.h3_is_valid_vertex(vertex h3index) returns boolean + language c; + +-- DEPRECATED: Use `h3_latlng_to_cell` instead. +create function public.h3_lat_lng_to_cell(latlng point, resolution integer) returns h3index + language c; + +-- Indexes the location at the specified resolution. +create function public.h3_latlng_to_cell(latlng point, resolution integer) returns h3index + language c; + +-- Produces an H3 index from local IJ coordinates anchored by an origin. +create function public.h3_local_ij_to_cell(origin h3index, coord point) returns h3index + language c; + +-- Returns all unidirectional edges with the given index as origin. +create function public.h3_origin_to_directed_edges(h3index) returns SETOF h3index + language c; + +-- Migrate h3index from pass-by-reference to pass-by-value. +create function public.h3_pg_migrate_pass_by_reference(h3index) returns h3index + language c; + +-- Takes an exterior polygon [and a set of hole polygon] and returns the set of hexagons that best fit the structure. +create function public.h3_polygon_to_cells(exterior polygon, holes polygon[], resolution integer DEFAULT 1) returns SETOF h3index + language c; + +-- Takes an exterior polygon [and a set of hole polygon] and returns the set of hexagons that best fit the structure. +create function public.h3_polygon_to_cells_experimental(exterior polygon, holes polygon[], resolution integer DEFAULT 1, containment_mode text DEFAULT 'center'::text) returns SETOF h3index + language c; + +-- Uncompacts the given array at the resolution one higher than the highest resolution in the set. +create function public.h3_uncompact_cells(cells h3index[]) returns SETOF h3index + language c; + +-- Uncompacts the given array at the given resolution. +create function public.h3_uncompact_cells(cells h3index[], resolution integer) returns SETOF h3index + language c; + +-- DEPRECATED: Use `h3_vertex_to_latlng` instead. +create function public.h3_vertex_to_lat_lng(vertex h3index) returns point + language c; + +-- Get the geocoordinates of an H3 vertex. +create function public.h3_vertex_to_latlng(vertex h3index) returns point + language c; + +create function public.h3index_cmp(h3index, h3index) returns integer + language c; + +create function public.h3index_contained_by(h3index, h3index) returns boolean + language c; + +create function public.h3index_contains(h3index, h3index) returns boolean + language c; + +create function public.h3index_distance(h3index, h3index) returns bigint + language c; + +create function public.h3index_eq(h3index, h3index) returns boolean + language c; + +create function public.h3index_ge(h3index, h3index) returns boolean + language c; + +create function public.h3index_gt(h3index, h3index) returns boolean + language c; + +create function public.h3index_hash(h3index) returns integer + language c; + +create function public.h3index_hash_extended(h3index, bigint) returns bigint + language c; + +create function public.h3index_in(cstring) returns h3index + language c; + +create function public.h3index_le(h3index, h3index) returns boolean + language c; + +create function public.h3index_lt(h3index, h3index) returns boolean + language c; + +create function public.h3index_ne(h3index, h3index) returns boolean + language c; + +create function public.h3index_out(h3index) returns cstring + language c; + +create function public.h3index_overlaps(h3index, h3index) returns boolean + language c; + +create function public.h3index_recv(internal) returns h3index + language c; + +create function public.h3index_send(h3index) returns bytea + language c; + +create function public.h3index_sortsupport(internal) returns void + language c; + +create function public.h3index_spgist_choose(internal, internal) returns void + language c; + +create function public.h3index_spgist_config(internal, internal) returns void + language c; + +create function public.h3index_spgist_inner_consistent(internal, internal) returns void + language c; + +create function public.h3index_spgist_leaf_consistent(internal, internal) returns boolean + language c; + +create function public.h3index_spgist_picksplit(internal, internal) returns void + language c; + +create function public.h3index_to_bigint(h3index) returns bigint + language c; + +-- Returns true if the two H3 indexes intersect. +create operator public.&& ( + leftarg = h3index, + rightarg = h3index, + function = public.h3index_overlaps +); + +create operator public.< ( + leftarg = h3index, + rightarg = h3index, + function = public.h3index_lt +); + +-- Returns the distance in grid cells between the two indices (at the lowest resolution of the two). +create operator public.<-> ( + leftarg = h3index, + rightarg = h3index, + function = public.h3index_distance +); + +create operator public.<= ( + leftarg = h3index, + rightarg = h3index, + function = public.h3index_le +); + +create operator public.<> ( + leftarg = h3index, + rightarg = h3index, + function = public.h3index_ne +); + +-- Returns true if A is contained by B. +create operator public.<@ ( + leftarg = h3index, + rightarg = h3index, + function = public.h3index_contained_by +); + +-- Returns true if two indexes are the same. +create operator public.= ( + leftarg = h3index, + rightarg = h3index, + function = public.h3index_eq +); + +create operator public.> ( + leftarg = h3index, + rightarg = h3index, + function = public.h3index_gt +); + +create operator public.>= ( + leftarg = h3index, + rightarg = h3index, + function = public.h3index_ge +); + +-- Returns true if A contains B. +create operator public.@> ( + leftarg = h3index, + rightarg = h3index, + function = public.h3index_contains +); + diff --git a/crates/squawk_ide/src/generated/extensions/hll.sql b/crates/squawk_ide/src/generated/extensions/hll.sql new file mode 100644 index 00000000..598f2e03 --- /dev/null +++ b/crates/squawk_ide/src/generated/extensions/hll.sql @@ -0,0 +1,262 @@ +-- squawk-ignore-file +-- pg version: 18.3 +-- update via: +-- cargo xtask sync-builtins + +-- size: -1, align: 4 +create type public.hll; + +-- size: 8, align: 8 +create type public.hll_hashval; + +create function public.hll(hll, integer, boolean) returns hll + language c; + +create function public.hll_add(hll, hll_hashval) returns hll + language c; + +create aggregate public.hll_add_agg(hll_hashval) ( + sfunc = hll_add_trans0, + stype = internal, + finalfunc = hll_pack, + combinefunc = hll_union_internal +); + +create aggregate public.hll_add_agg(hll_hashval, integer) ( + sfunc = hll_add_trans1, + stype = internal, + finalfunc = hll_pack, + combinefunc = hll_union_internal +); + +create aggregate public.hll_add_agg(hll_hashval, integer, integer) ( + sfunc = hll_add_trans2, + stype = internal, + finalfunc = hll_pack, + combinefunc = hll_union_internal +); + +create aggregate public.hll_add_agg(hll_hashval, integer, integer, bigint) ( + sfunc = hll_add_trans3, + stype = internal, + finalfunc = hll_pack, + combinefunc = hll_union_internal +); + +create aggregate public.hll_add_agg(hll_hashval, integer, integer, bigint, integer) ( + sfunc = hll_add_trans4, + stype = internal, + finalfunc = hll_pack, + combinefunc = hll_union_internal +); + +create function public.hll_add_rev(hll_hashval, hll) returns hll + language c; + +create function public.hll_add_trans0(internal, hll_hashval) returns internal + language c; + +create function public.hll_add_trans1(internal, hll_hashval, integer) returns internal + language c; + +create function public.hll_add_trans2(internal, hll_hashval, integer, integer) returns internal + language c; + +create function public.hll_add_trans3(internal, hll_hashval, integer, integer, bigint) returns internal + language c; + +create function public.hll_add_trans4(internal, hll_hashval, integer, integer, bigint, integer) returns internal + language c; + +create function public.hll_card_unpacked(internal) returns double precision + language c; + +create function public.hll_cardinality(hll) returns double precision + language c; + +create function public.hll_ceil_card_unpacked(internal) returns bigint + language c; + +create function public.hll_deserialize(bytea, internal) returns internal + language c; + +create function public.hll_empty() returns hll + language c; + +create function public.hll_empty(integer) returns hll + language c; + +create function public.hll_empty(integer, integer) returns hll + language c; + +create function public.hll_empty(integer, integer, bigint) returns hll + language c; + +create function public.hll_empty(integer, integer, bigint, integer) returns hll + language c; + +create function public.hll_eq(hll, hll) returns boolean + language c; + +create function public.hll_expthresh(hll, OUT specified bigint, OUT effective bigint) returns record + language c; + +create function public.hll_floor_card_unpacked(internal) returns bigint + language c; + +create function public.hll_hash_any(anyelement, integer DEFAULT 0) returns hll_hashval + language c; + +create function public.hll_hash_bigint(bigint, integer DEFAULT 0) returns hll_hashval + language c; + +create function public.hll_hash_boolean(boolean, integer DEFAULT 0) returns hll_hashval + language c; + +create function public.hll_hash_bytea(bytea, integer DEFAULT 0) returns hll_hashval + language c; + +create function public.hll_hash_integer(integer, integer DEFAULT 0) returns hll_hashval + language c; + +create function public.hll_hash_smallint(smallint, integer DEFAULT 0) returns hll_hashval + language c; + +create function public.hll_hash_text(text, integer DEFAULT 0) returns hll_hashval + language c; + +create function public.hll_hashval(bigint) returns hll_hashval + language c; + +create function public.hll_hashval_eq(hll_hashval, hll_hashval) returns boolean + language c; + +create function public.hll_hashval_in(cstring, oid, integer) returns hll_hashval + language c; + +create function public.hll_hashval_int4(integer) returns hll_hashval + language c; + +create function public.hll_hashval_ne(hll_hashval, hll_hashval) returns boolean + language c; + +create function public.hll_hashval_out(hll_hashval) returns cstring + language c; + +create function public.hll_in(cstring, oid, integer) returns hll + language c; + +create function public.hll_log2m(hll) returns integer + language c; + +create function public.hll_ne(hll, hll) returns boolean + language c; + +create function public.hll_out(hll) returns cstring + language c; + +create function public.hll_pack(internal) returns hll + language c; + +create function public.hll_print(hll) returns cstring + language c; + +create function public.hll_recv(internal) returns hll + language c; + +create function public.hll_regwidth(hll) returns integer + language c; + +create function public.hll_schema_version(hll) returns integer + language c; + +create function public.hll_send(hll) returns bytea + language c; + +create function public.hll_serialize(internal) returns bytea + language c; + +create function public.hll_set_defaults(i_log2m integer, i_regwidth integer, i_expthresh bigint, i_sparseon integer, OUT o_log2m integer, OUT o_regwidth integer, OUT o_expthresh bigint, OUT o_sparseon integer) returns record + language c; + +create function public.hll_set_max_sparse(integer) returns integer + language c; + +create function public.hll_set_output_version(integer) returns integer + language c; + +create function public.hll_sparseon(hll) returns integer + language c; + +create function public.hll_type(hll) returns integer + language c; + +create function public.hll_typmod_in(cstring[]) returns integer + language c; + +create function public.hll_typmod_out(integer) returns cstring + language c; + +create function public.hll_union(hll, hll) returns hll + language c; + +create aggregate public.hll_union_agg(hll) ( + sfunc = hll_union_trans, + stype = internal, + finalfunc = hll_pack, + combinefunc = hll_union_internal +); + +create function public.hll_union_internal(internal, internal) returns internal + language c; + +create function public.hll_union_trans(internal, hll) returns internal + language c; + +create operator public.# ( + rightarg = hll, + function = public.hll_cardinality +); + +create operator public.<> ( + leftarg = hll, + rightarg = hll, + function = public.hll_ne +); + +create operator public.<> ( + leftarg = hll_hashval, + rightarg = hll_hashval, + function = public.hll_hashval_ne +); + +create operator public.= ( + leftarg = hll, + rightarg = hll, + function = public.hll_eq +); + +create operator public.= ( + leftarg = hll_hashval, + rightarg = hll_hashval, + function = public.hll_hashval_eq +); + +create operator public.|| ( + leftarg = hll, + rightarg = hll, + function = public.hll_union +); + +create operator public.|| ( + leftarg = hll, + rightarg = hll_hashval, + function = public.hll_add +); + +create operator public.|| ( + leftarg = hll_hashval, + rightarg = hll, + function = public.hll_add_rev +); + diff --git a/crates/xtask/Cargo.toml b/crates/xtask/Cargo.toml index 1ecfb0b3..2a7a32e0 100644 --- a/crates/xtask/Cargo.toml +++ b/crates/xtask/Cargo.toml @@ -10,6 +10,7 @@ rust-version.workspace = true [dependencies] anyhow.workspace = true +csv.workspace = true clap.workspace = true enum-iterator.workspace = true reqwest = { workspace = true, features = ["blocking", "json"] } diff --git a/crates/xtask/src/sync_builtins.rs b/crates/xtask/src/sync_builtins.rs index 31a04148..b330c583 100644 --- a/crates/xtask/src/sync_builtins.rs +++ b/crates/xtask/src/sync_builtins.rs @@ -11,10 +11,18 @@ use crate::path::project_root; const PG_TEMP_SCHEMA_SQL: &str = "create schema pg_temp;\n\n"; -const SCHEMAS_QUERY: &str = r" +#[derive(Deserialize)] +struct SchemaQuery { + schema: String, + description: String, + extension_name: String, +} + +impl Query for SchemaQuery { + const QUERY: &'static str = r" select - n.nspname, - coalesce(d.description, ''), + n.nspname as schema, + coalesce(d.description, '') as description, coalesce(ext.extname, 'builtins') as extension_name from pg_namespace n left join pg_description d on d.objoid = n.oid and d.classoid = 'pg_namespace'::regclass @@ -24,21 +32,78 @@ where n.nspname not like 'pg_temp%' and n.nspname not like 'pg_toast%' order by n.nspname, ext.extname, n.oid; "; +} -const TYPES_QUERY: &str = r" +trait Query { + const QUERY: &'static str; + + fn run() -> Result> + where + Self: serde::de::DeserializeOwned, + { + let output = Command::new("psql") + .args(["--csv", "--command", Self::QUERY]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .context("Failed to run psql.")?; + + if !output.status.success() { + let stderr = String::from_utf8(output.stderr).context("Invalid utf8")?; + bail!("psql failed: {}", stderr); + } + + let mut reader = csv::Reader::from_reader(output.stdout.as_slice()); + let mut rows = vec![]; + for result in reader.deserialize() { + rows.push(result.context("failed to parse csv record")?); + } + Ok(rows) + } + + fn execute() -> Result<()> { + let output = Command::new("psql") + .args(["--command", Self::QUERY]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .context("Failed to run psql.")?; + + if !output.status.success() { + let stderr = String::from_utf8(output.stderr).context("Invalid utf8")?; + bail!("psql failed: {}", stderr); + } + Ok(()) + } +} + +#[derive(Deserialize)] +struct TypeQuery { + schema: String, + name: String, + size: String, + align: String, + subtype: String, + is_range: i32, + description: String, + extension_name: String, +} + +impl Query for TypeQuery { + const QUERY: &'static str = r" select - n.nspname, - t.typname, - t.typlen, + n.nspname as schema, + t.typname as name, + t.typlen as size, case t.typalign when 'c' then 1 when 's' then 2 when 'i' then 4 when 'd' then 8 - end as typalign, + end as align, coalesce(format_type(r.rngsubtype, null), '') as subtype, case when r.rngtypid is null then 0 else 1 end as is_range, - coalesce(d.description, ''), + coalesce(d.description, '') as description, coalesce(ext.extname, 'builtins') as extension_name from pg_type t join pg_namespace n on n.oid = t.typnamespace @@ -55,13 +120,25 @@ where n.nspname not like 'pg_temp%' ) order by n.nspname, t.typname, t.oid; "; +} + +#[derive(Deserialize)] +struct RelationQuery { + schema: String, + name: String, + relkind: String, + description: String, + extension_name: String, + columns: String, +} -const TABLES_QUERY: &str = r" +impl Query for RelationQuery { + const QUERY: &'static str = r" select - n.nspname, - c.relname, - c.relkind, - coalesce(d.description, ''), + n.nspname as schema, + c.relname as name, + c.relkind as relkind, + coalesce(d.description, '') as description, coalesce(ext.extname, 'builtins') as extension_name, json_agg( json_build_object( @@ -82,19 +159,38 @@ where n.nspname not like 'pg_temp%' and c.relkind in ('r', 'v') and a.attnum > 0 and not a.attisdropped -group by c.oid, n.nspname, c.relname, c.relkind, coalesce(d.description, ''), coalesce(ext.extname, 'builtins') +group by c.oid, schema, name, relkind, description, extension_name order by n.nspname, c.relname, c.oid; "; +} + +#[derive(Deserialize)] +struct FunctionQuery { + schema: String, + name: String, + args: String, + result: String, + language: String, + description: String, + prokind: String, + trans_fn: String, + trans_type: String, + final_fn: String, + combine_fn: String, + init_val: String, + extension_name: String, +} -const FUNCTIONS_QUERY: &str = r" +impl Query for FunctionQuery { + const QUERY: &'static str = r" select - n.nspname, - p.proname, + n.nspname as schema, + p.proname as name, pg_get_function_arguments(p.oid) as args, pg_get_function_result(p.oid) as result, - l.lanname, - coalesce(d.description, ''), - p.prokind, + l.lanname as language, + coalesce(d.description, '') as description, + p.prokind as prokind, a.aggtransfn::regproc::text as trans_fn, format_type(a.aggtranstype, null) as trans_type, a.aggfinalfn::regproc::text as final_fn, @@ -114,16 +210,30 @@ where n.nspname not like 'pg_temp%' and pg_get_function_arguments(p.oid) not like '%ORDER BY%' order by n.nspname, p.proname, pg_get_function_arguments(p.oid), pg_get_function_result(p.oid), p.oid; "; +} -const OPERATORS_QUERY: &str = r" +#[derive(Deserialize)] +struct OperatorQuery { + schema: String, + name: String, + left_type: String, + right_type: String, + func_schema: String, + func_name: String, + description: String, + extension_name: String, +} + +impl Query for OperatorQuery { + const QUERY: &'static str = r" select - n.nspname, - o.oprname, + n.nspname as schema, + o.oprname as name, format_type(o.oprleft, null) as left_type, format_type(o.oprright, null) as right_type, pn.nspname as func_schema, p.proname as func_name, - coalesce(d.description, ''), + coalesce(d.description, '') as description, coalesce(ext.extname, 'builtins') as extension_name from pg_operator o join pg_namespace n on n.oid = o.oprnamespace @@ -137,10 +247,21 @@ where n.nspname not like 'pg_temp%' and (n.nspname != 'public' or ext.extname is not null) order by n.nspname, o.oprname, format_type(o.oprleft, null), format_type(o.oprright, null), pn.nspname, p.proname, o.oid; "; +} -const PG_VERSION_QUERY: &str = "show server_version;"; +#[derive(Deserialize)] +struct VersionQuery { + server_version: String, +} -const CREATE_EXTENSIONS_QUERY: &str = r" +impl Query for VersionQuery { + const QUERY: &'static str = "show server_version;"; +} + +struct CreateExtensionsQuery; + +impl Query for CreateExtensionsQuery { + const QUERY: &'static str = r" do $$ declare extension_name text; @@ -149,6 +270,8 @@ begin 'bloom', 'citext', 'cube', + 'h3', + 'hll', 'hstore', 'isn', 'ltree', @@ -165,6 +288,7 @@ begin end loop; end $$; "; +} fn write_description(f: &mut W, description: &str) -> io::Result<()> { if !description.is_empty() { @@ -468,34 +592,6 @@ impl Module { } } -fn run_sql(query: &str) -> Result { - let output = Command::new("psql") - .args([ - "--tuples-only", - "--no-align", - "--field-separator", - "\t", - "--command", - query, - ]) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output() - .context("Failed to run psql.")?; - - if !output.status.success() { - let stderr = String::from_utf8(output.stderr).context("Invalid utf8")?; - bail!("psql failed: {}", stderr); - } - - String::from_utf8(output.stdout).context("Invalid utf8") -} - -fn create_extensions_if_missing() -> Result<()> { - run_sql(CREATE_EXTENSIONS_QUERY)?; - Ok(()) -} - fn header(version: &str) -> String { format!( "\ @@ -524,62 +620,42 @@ fn write_module( } fn query_schemas(modules: &mut BTreeMap) -> Result<()> { - for line in run_sql(SCHEMAS_QUERY)? - .lines() - .filter(|line| !line.is_empty()) - { - let mut parts = line.split('\t'); - let schema = parts.next().context("expected schema name")?; - let description = parts.next().context("expected schema description")?; - let extension_name = parts.next().context("expected extension name")?; - + for row in SchemaQuery::run()? { modules - .entry(extension_name.to_string()) + .entry(row.extension_name) .or_default() .schemas .push(SchemaDef { - description: description.to_string(), - schema: schema.to_string(), + description: row.description, + schema: row.schema, }); } Ok(()) } fn query_types(modules: &mut BTreeMap) -> Result<()> { - for line in run_sql(TYPES_QUERY)? - .lines() - .filter(|line| !line.is_empty()) - { - let mut parts = line.split('\t'); - let schema = parts.next().context("expected schema name")?; - let type_name = parts.next().context("expected type name")?; - let type_size = parts.next().context("expected type size")?; - let type_align = parts.next().context("expected type alignment")?; - let subtype = parts.next().context("expected subtype")?; - let is_range = parts.next().context("expected is_range")?; - let description = parts.next().context("expected type description")?; - let extension_name = parts.next().context("expected extension name")?; - if type_align.is_empty() { - bail!("unexpected type alignment for {schema}.{type_name}"); + for row in TypeQuery::run()? { + if row.align.is_empty() { + bail!("unexpected type alignment for {}.{}", row.schema, row.name); } - let module = modules.entry(extension_name.to_string()).or_default(); - if is_range == "1" { + let module = modules.entry(row.extension_name).or_default(); + if row.is_range == 1 { module.range_types.push(RangeTypeDef { - align: type_align.to_string(), - description: description.to_string(), - name: type_name.to_string(), - schema: schema.to_string(), - size: type_size.to_string(), - subtype: subtype.to_string(), + align: row.align, + description: row.description, + name: row.name, + schema: row.schema, + size: row.size, + subtype: row.subtype, }); } else { module.types.push(TypeDef { - align: type_align.to_string(), - description: description.to_string(), - name: type_name.to_string(), - schema: schema.to_string(), - size: type_size.to_string(), + align: row.align, + description: row.description, + name: row.name, + schema: row.schema, + size: row.size, }); } } @@ -588,36 +664,25 @@ fn query_types(modules: &mut BTreeMap) -> Result<()> { } fn query_relations(modules: &mut BTreeMap) -> Result<()> { - for line in run_sql(TABLES_QUERY)? - .lines() - .filter(|line| !line.is_empty()) - { - let mut parts = line.split('\t'); - let schema = parts.next().context("expected schema name")?; - let rel_name = parts.next().context("expected relation name")?; - let relkind = parts.next().context("expected relkind")?; - let description = parts.next().context("expected description")?; - let extension_name = parts.next().context("expected extension name")?; - let columns = parts.next().context("expected columns")?; - + for row in RelationQuery::run()? { let columns: Vec = - serde_json::from_str(columns).context("expected valid column json")?; + serde_json::from_str(&row.columns).context("expected valid column json")?; - let module = modules.entry(extension_name.to_string()).or_default(); - match relkind { + let module = modules.entry(row.extension_name).or_default(); + match row.relkind.as_str() { "r" => module.tables.push(TableDef { columns, - description: description.to_string(), - name: rel_name.to_string(), - schema: schema.to_string(), + description: row.description, + name: row.name, + schema: row.schema, }), "v" => module.views.push(ViewDef { columns, - description: description.to_string(), - name: rel_name.to_string(), - schema: schema.to_string(), + description: row.description, + name: row.name, + schema: row.schema, }), - _ => bail!("unexpected relation kind: {relkind}"), + _ => bail!("unexpected relation kind: {}", row.relkind), } } @@ -625,50 +690,32 @@ fn query_relations(modules: &mut BTreeMap) -> Result<()> { } fn query_functions(modules: &mut BTreeMap) -> Result<()> { - for line in run_sql(FUNCTIONS_QUERY)? - .lines() - .filter(|line| !line.is_empty()) - { - let mut parts = line.split('\t'); - let schema = parts.next().context("expected schema name")?; - let func_name = parts.next().context("expected function name")?; - let args = parts.next().context("expected function arguments")?; - let result = parts.next().context("expected function result")?; - let language = parts.next().context("expected function language")?; - let description = parts.next().context("expected function description")?; - let prokind = parts.next().context("expected function kind")?; - let trans_fn = parts.next().context("expected aggregate trans fn")?; - let trans_type = parts.next().context("expected aggregate trans type")?; - let final_fn = parts.next().context("expected aggregate final fn")?; - let combine_fn = parts.next().context("expected aggregate combine fn")?; - let init_val = parts.next().context("expected aggregate init value")?; - let extension_name = parts.next().context("expected extension name")?; - - let function = if prokind == "a" { + for row in FunctionQuery::run()? { + let function = if row.prokind == "a" { FunctionDef::Aggregate(AggregateDef { - args: args.to_string(), - combine_fn: combine_fn.to_string(), - description: description.to_string(), - final_fn: final_fn.to_string(), - init_val: init_val.to_string(), - name: func_name.to_string(), - schema: schema.to_string(), - trans_fn: trans_fn.to_string(), - trans_type: trans_type.to_string(), + args: row.args, + combine_fn: row.combine_fn, + description: row.description, + final_fn: row.final_fn, + init_val: row.init_val, + name: row.name, + schema: row.schema, + trans_fn: row.trans_fn, + trans_type: row.trans_type, }) } else { FunctionDef::Regular(RegularFunctionDef { - args: args.to_string(), - description: description.to_string(), - language: language.to_string(), - name: func_name.to_string(), - result: result.to_string(), - schema: schema.to_string(), + args: row.args, + description: row.description, + language: row.language, + name: row.name, + result: row.result, + schema: row.schema, }) }; modules - .entry(extension_name.to_string()) + .entry(row.extension_name) .or_default() .functions .push(function); @@ -678,32 +725,19 @@ fn query_functions(modules: &mut BTreeMap) -> Result<()> { } fn query_operators(modules: &mut BTreeMap) -> Result<()> { - for line in run_sql(OPERATORS_QUERY)? - .lines() - .filter(|line| !line.is_empty()) - { - let mut parts = line.split('\t'); - let schema = parts.next().context("expected schema name")?; - let op_name = parts.next().context("expected operator name")?; - let left_type = parts.next().context("expected left type")?; - let right_type = parts.next().context("expected right type")?; - let func_schema = parts.next().context("expected function schema")?; - let func_name = parts.next().context("expected function name")?; - let description = parts.next().context("expected operator description")?; - let extension_name = parts.next().context("expected extension name")?; - + for row in OperatorQuery::run()? { modules - .entry(extension_name.to_string()) + .entry(row.extension_name) .or_default() .operators .push(OperatorDef { - description: description.to_string(), - function_name: func_name.to_string(), - function_schema: func_schema.to_string(), - left_type: left_type.to_string(), - name: op_name.to_string(), - right_type: right_type.to_string(), - schema: schema.to_string(), + description: row.description, + function_name: row.func_name, + function_schema: row.func_schema, + left_type: row.left_type, + name: row.name, + right_type: row.right_type, + schema: row.schema, }); } @@ -711,10 +745,12 @@ fn query_operators(modules: &mut BTreeMap) -> Result<()> { } pub(crate) fn sync_builtins() -> Result<()> { - create_extensions_if_missing()?; + CreateExtensionsQuery::execute()?; - let version = run_sql(PG_VERSION_QUERY)?; - let version = version + let version_rows = VersionQuery::run()?; + let version_row = version_rows.first().context("version not found")?; + let version = version_row + .server_version .split_whitespace() .next() .context("version not found")?;