Current section

Files

Jump to
ash_scylla lib ash_scylla data_layer schema_utils.ex
Raw

lib/ash_scylla/data_layer/schema_utils.ex

# Copyright [2024] AshScylla Contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
defmodule AshScylla.DataLayer.SchemaUtils do
@moduledoc """
Shared schema utilities for table names, column analysis, and identifier quoting.
Centralizes functions that were previously duplicated across
`AshScylla.DataLayer.SchemaMigration`, `AshScylla.Migration`,
`AshScylla.DataLayer.MaterializedView`, and `AshScylla.ResourceGenerator`.
"""
alias AshScylla.DataLayer.Dsl
alias AshScylla.Identifier
@doc """
Returns the table name for a resource.
Checks the DSL `table/1` config first, then falls back to deriving
from the module name (using domain prefix if available).
"""
@spec get_table_name(module()) :: String.t()
def get_table_name(resource) do
case Dsl.table(resource) do
nil ->
derive_table_name(resource)
name ->
to_string(name)
end
end
@doc """
Quotes a CQL identifier (table name, column name, etc.) for safe use in CQL strings.
Uses double-quote escaping per CQL spec. Delegates validation to
`AshScylla.Identifier.validate/1`.
"""
@spec quote_name(atom() | String.t()) :: String.t() | no_return()
def quote_name(name) when is_atom(name), do: quote_name(Atom.to_string(name))
def quote_name(name) when is_binary(name) do
case Identifier.validate(name) do
{:ok, _} -> quote_name_unchecked(name)
{:error, reason} -> raise ArgumentError, reason
end
end
def quote_name(name) do
raise ArgumentError, "Invalid CQL identifier: expected string or atom, got #{inspect(name)}"
end
# Quotes without validation (internal use after validation)
@spec quote_name_unchecked(String.t()) :: String.t()
def quote_name_unchecked(name) do
if String.contains?(name, "\"") do
escaped = String.replace(name, "\"", "\"\"")
"\"#{escaped}\""
else
"\"#{name}\""
end
end
@doc """
Returns the list of columns that cannot have secondary indexes.
Currently this is only the sole partition key column (ScyllaDB forbids it).
Returns an empty list for composite partition keys or resources without
a clear single partition key.
"""
@spec unindexable_columns(module()) :: [atom()]
def unindexable_columns(resource) do
try do
pk_attrs =
resource
|> Ash.Resource.Info.attributes()
|> Enum.filter(& &1.primary_key?)
case pk_attrs do
[sole_partition_key] -> [sole_partition_key.name]
_ -> []
end
rescue
# Plain test modules (not Ash resources) have no primary key metadata
_ -> []
end
end
@doc """
Resolves a type name for a UDT, sanitizing it for safe CQL interpolation.
"""
@spec sanitize_type_name(String.t() | atom()) :: String.t()
def sanitize_type_name(type_name) when is_atom(type_name) do
type_name |> Atom.to_string() |> sanitize_type_name()
end
def sanitize_type_name(type_name) when is_binary(type_name) do
case Identifier.validate(type_name) do
{:ok, sanitized} -> sanitized
{:error, reason} -> raise ArgumentError, reason
end
end
# ---------------------------------------------------------------------------
# Private functions
# ---------------------------------------------------------------------------
defp derive_table_name(resource) do
segments = Module.split(resource)
name =
if Ash.Resource.Info.domain(resource) do
segments
|> Enum.take(-2)
|> Enum.map_join("_", &Macro.underscore/1)
else
segments
|> List.last()
|> Macro.underscore()
end
table_attr =
try do
Module.get_attribute(resource, :table)
rescue
ArgumentError -> nil
end
case table_attr do
nil -> name
"" -> name
table -> to_string(table)
end
end
end