Packages
electric
0.7.3
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.6.2
1.6.1
1.6.0
1.5.1
1.5.0
1.4.16
1.4.16-beta-1
1.4.15
1.4.14
1.4.13
1.4.12
1.4.11
1.4.10
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.4
1.3.3
1.3.2
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.1.14
1.1.13
1.1.12
1.1.11
1.1.10
1.1.9
1.1.8
1.1.7
1.1.6
retired
1.1.5
retired
1.1.4
retired
1.1.3
retired
1.1.2
1.1.1
1.1.0
1.0.24
1.0.23
1.0.22
1.0.21
1.0.20
1.0.19
1.0.18
1.0.17
1.0.15
1.0.13
1.0.12
1.0.11
1.0.10
1.0.9
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
1.0.0-beta.23
1.0.0-beta.22
1.0.0-beta.20
1.0.0-beta.19
1.0.0-beta.18
1.0.0-beta.17
1.0.0-beta.16
1.0.0-beta.15
1.0.0-beta.14
1.0.0-beta.13
1.0.0-beta.12
1.0.0-beta.11
1.0.0-beta.10
1.0.0-beta.9
1.0.0-beta.8
1.0.0-beta.7
1.0.0-beta.6
1.0.0-beta.5
1.0.0-beta.4
1.0.0-beta.3
1.0.0-beta.2
1.0.0-beta.1
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.1
0.8.0
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.5.2
0.4.4
Postgres sync engine. Sync little subsets of your Postgres data into local apps and services.
Current section
Files
Jump to
Current section
Files
lib/electric/postgres/identifiers.ex
defmodule Electric.Postgres.Identifiers do
@namedatalen 63
@ascii_downcase ?a - ?A
@doc """
Parse a PostgreSQL identifier, removing quotes if present and escaping internal ones
and downcasing the identifier otherwise.
## Examples
iex> Electric.Postgres.Identifiers.parse("FooBar")
{:ok, "foobar"}
iex> Electric.Postgres.Identifiers.parse(~S|"FooBar"|)
{:ok, "FooBar"}
iex> Electric.Postgres.Identifiers.parse(~S|Foo"Bar"|)
{:error, ~S|Invalid unquoted identifier contains special characters: Foo"Bar"|}
iex> Electric.Postgres.Identifiers.parse(~S| |)
{:error, ~S|Invalid unquoted identifier contains special characters: |}
iex> Electric.Postgres.Identifiers.parse("foob@r")
{:error, ~S|Invalid unquoted identifier contains special characters: foob@r|}
iex> Electric.Postgres.Identifiers.parse(~S|"Foo"Bar"|)
{:error, ~S|Invalid identifier with unescaped quote: Foo"Bar|}
iex> Electric.Postgres.Identifiers.parse(~S|""|)
{:error, "Invalid zero-length delimited identifier"}
iex> Electric.Postgres.Identifiers.parse("")
{:error, "Invalid zero-length delimited identifier"}
iex> Electric.Postgres.Identifiers.parse(~S|" "|)
{:ok, " "}
iex> Electric.Postgres.Identifiers.parse(~S|"Foo""Bar"|)
{:ok, ~S|Foo"Bar|}
"""
@spec parse(binary(), boolean(), boolean()) :: {:ok, binary()} | {:error, term()}
def parse(ident, truncate \\ false, single_byte_encoding \\ false) when is_binary(ident) do
if String.starts_with?(ident, ~S|"|) and String.ends_with?(ident, ~S|"|) do
ident_unquoted = String.slice(ident, 1..-2//1)
parse_quoted_identifier(ident_unquoted)
else
parse_unquoted_identifier(ident, truncate, single_byte_encoding)
end
end
defp parse_quoted_identifier(""), do: {:error, "Invalid zero-length delimited identifier"}
defp parse_quoted_identifier(ident) do
if contains_unescaped_quote?(ident),
do: {:error, "Invalid identifier with unescaped quote: #{ident}"},
else: {:ok, unescape_quotes(ident)}
end
defp parse_unquoted_identifier("", _, _), do: parse_quoted_identifier("")
defp parse_unquoted_identifier(ident, truncate, single_byte_encoding) do
unless valid_unquoted_identifier?(ident),
do: {:error, "Invalid unquoted identifier contains special characters: #{ident}"},
else: {:ok, downcase(ident, truncate, single_byte_encoding)}
end
defp contains_unescaped_quote?(string) do
Regex.match?(~r/(?<!")"(?!")/, string)
end
defp unescape_quotes(string) do
string
|> String.replace(~r/""/, ~S|"|)
end
defp valid_unquoted_identifier?(identifier) do
Regex.match?(~r/^[a-zA-Z_][a-zA-Z0-9_]*$/, identifier)
end
@doc """
Downcase the identifier and truncate if necessary, using
PostgreSQL's algorithm for downcasing.
Setting `truncate` to `true` will truncate the identifier to 63 characters
Setting `single_byte_encoding` to `true` will downcase the identifier
using single byte encoding
See:
https://github.com/postgres/postgres/blob/259a0a99fe3d45dcf624788c1724d9989f3382dc/src/backend/parser/scansup.c#L46-L80
## Examples
iex> Electric.Postgres.Identifiers.downcase("FooBar")
"foobar"
iex> Electric.Postgres.Identifiers.downcase(String.duplicate("a", 100), true)
String.duplicate("a", 63)
"""
def downcase(ident, truncate \\ false, single_byte_encoding \\ false)
def downcase(ident, false, single_byte_encoding) do
downcased_ident =
ident
|> String.to_charlist()
|> Enum.map(&downcase_char(&1, single_byte_encoding))
|> List.to_string()
downcased_ident
end
def downcase(ident, true, single_byte_encoding) do
downcased_ident = downcase(ident, false, single_byte_encoding)
truncated_ident =
if String.length(ident) >= @namedatalen do
String.slice(downcased_ident, 0, @namedatalen)
else
downcased_ident
end
truncated_ident
end
# Helper function to downcase a character
defp downcase_char(ch, _) when ch in ?A..?Z, do: ch + @ascii_downcase
defp downcase_char(ch, true) when ch > 127,
do:
if(ch == Enum.at(:unicode_util.uppercase(ch), 0),
do: Enum.at(:unicode_util.lowercase(ch), 0),
else: ch
)
defp downcase_char(ch, _), do: ch
end