Files
ubicloud/model/dns_zone/dns_zone.rb
Jeremy Evans 481383e572 Do not have query SQL vary based on number of array elements
SQL for queries using value lists (e.g. `column IN (value_list)`) varies
by the number of elements in the value list, because a separate placeholder
is generally used for each element in the value list.  You can get consistent
SQL by using the equivalent `column = ANY(array_expr::type[])`, which will
only use a single placeholder for the array expression.  This is useful if
you want to view/audit the queries that the application is generating.

Sequel has had a pg_auto_parameterize_in_array Database extension for
a while that handles most of this.  However, in the case where the
value list is Ruby strings, it is ambiguous what the type of the array
should be. Postgres use the unknown type, not the text type, when there
is not an explicit/implicit cast for a single quoted string. The
pg_auto_parameterize_in_array extension can assume the text[] type
in such cases, but it will break queries that need a different cast.
However, such breakage is explicit (DatabaseError raised), and not
silent, and can be fixed.  This affects queries where the column
values are plain strings in Ruby, but use a non-text database type,
such as the uuid type or an enum type.

This adds a couple singleton methods on Sequel, any_uuid and any_type,
which allow conversion of arrays (normally treated as value lists) to
an `ANY(array_expr::type[])` expression.  This converts the cases that
would fail with an explicit cast to text[] to use a uuid[] or
lb_node_state[] cast instead. This handles most of the explicit use
of `IN (value_list)`.

There is a significant amount of implicit `IN (value_list)`, because
that is what is used by default for eager loading.  This uses a new
pg_eager_any_typed_array plugin I developed to handle this case. This
will automatically use `column = ANY(array_expr::type[])`, using the
appropriate database type of the predicate key using for eager
loading.
2025-02-06 13:10:04 -08:00

96 lines
3.8 KiB
Ruby

# frozen_string_literal: true
require_relative "../../model"
class DnsZone < Sequel::Model
one_to_one :strand, key: :id
many_to_one :project
many_to_many :dns_servers
one_to_many :records, class: :DnsRecord
one_to_one :active_billing_record, class: :BillingRecord, key: :resource_id do |ds| ds.active end
include ResourceMethods
include SemaphoreMethods
semaphore :refresh_dns_servers
def insert_record(record_name:, type:, ttl:, data:)
record_name = add_dot_if_missing(record_name)
DnsRecord.create_with_id(dns_zone_id: id, name: record_name, type: type, ttl: ttl, data: data)
incr_refresh_dns_servers
end
def delete_record(record_name:, type: nil, data: nil)
fail "Type needs to be specified if data is specified!" if data && type.nil?
record_name = add_dot_if_missing(record_name)
records = records_dataset.where(name: record_name, tombstoned: false)
records = records.where(type: type) if type
records = records.where(data: data) if data
DB[:dns_record].import(
[:id, :dns_zone_id, :name, :type, :ttl, :data, :tombstoned],
records.select_map([:name, :type, :ttl, :data]).map do
[DnsRecord.generate_uuid, id, *_1, true]
end
)
incr_refresh_dns_servers
end
def purge_obsolete_records
DB.transaction do
# These are the records that are obsoleted by a another record with the
# same fields but newer date. We can delete them even if they are not
# seen yet.
obsoleted_records = records_dataset
.join(
records_dataset
.select_group(:dns_zone_id, :name, :type, :data)
.select_append { max(created_at).as(:latest_created_at) }
.as(:latest_dns_record),
[:dns_zone_id, :name, :type, :data]
)
.where { dns_record[:created_at] < latest_dns_record[:latest_created_at] }.all
# These are the tombstoned records, we can only delete them if they are
# seen by all DNS servers. We join with seen_dns_records_by_dns_servers
# and count the number of DNS servers to ensure that they are seen by all
# DNS servers.
dns_server_ids = dns_servers.map(&:id)
seen_tombstoned_records = records_dataset
.select_group(:id)
.join(:seen_dns_records_by_dns_servers, dns_record_id: :id, dns_server_id: dns_server_ids)
.where(tombstoned: true)
.having { count.function.* =~ dns_server_ids.count }.all
records_to_purge = obsoleted_records + seen_tombstoned_records
DB[:seen_dns_records_by_dns_servers].where(dns_record_id: Sequel.any_uuid(records_to_purge.map(&:id).uniq)).delete(force: true)
records_to_purge.uniq(&:id).map(&:destroy)
update(last_purged_at: Time.now)
end
end
def add_dot_if_missing(record_name)
(record_name[-1] == ".") ? record_name : record_name + "."
end
end
# Table: dns_zone
# Columns:
# id | uuid | PRIMARY KEY
# created_at | timestamp with time zone | NOT NULL DEFAULT now()
# project_id | uuid | NOT NULL
# name | text | NOT NULL
# last_purged_at | timestamp with time zone | NOT NULL DEFAULT now()
# Indexes:
# dns_zone_pkey | PRIMARY KEY btree (id)
# dns_zone_project_id_name_uidx | UNIQUE btree (project_id, name)
# Referenced By:
# cert | cert_dns_zone_id_fkey | (dns_zone_id) REFERENCES dns_zone(id)
# dns_record | dns_record_dns_zone_id_fkey | (dns_zone_id) REFERENCES dns_zone(id)
# dns_servers_dns_zones | dns_servers_dns_zones_dns_zone_id_fkey | (dns_zone_id) REFERENCES dns_zone(id)
# load_balancer | load_balancer_custom_hostname_dns_zone_id_fkey | (custom_hostname_dns_zone_id) REFERENCES dns_zone(id)