Files
Bridge-and-Join-s/internal/lkgateway/fancyfb.go
T
fontvielle 50e420f4a0 DEPO на Firebird в мастере: подготовка базы одним действием, как для MSSQL
Мастер, шаг «Учёт DEPO», и «Обмен с DEPO» в настройках: СУБД «Firebird» (Fancy на Firebird 2.5).
- Подключение: драйвер nakagami/firebirdsql, путь к файлу базы или псевдоним, WIN1251. Запросы bj к DEPO
  (T-SQL) подключение переводит само (fancyfb.go): механически (dbo., TOP, ISNULL, GETDATE, LEN, N, exec)
  вне строк и комментариев, точные тексты — fsql/fb.go (каталоги, LIKE [_], COUNT(*) OVER, SELECT без FROM,
  PATINDEX); @имена — позиционные ?, GUID — в верхнем регистре. Системные запросы из кода — в fsql/depocatalog.go.
- «Подготовить» логином SYSDBA (depo_setup_fb.go): проверка базы и UDF Fancy, логин SYSDBA_BJ (Fancy даёт права
  на документы логинам SYSDBA%; для Firebird — обычный пользователь), оператор-робот Fancy, кодировщик M2M,
  привязки FTV_M2M_SETTINGS по признакам Fancy, таблицы, процедуры, права; обработку запускает bj.
  «Получить скрипт» — то же для isql в WIN1251; письмо администратору, установка процедур, плашка обновления.
- Скрипты Firebird встроены (depoprocs/firebird, тест сверяет с deploy/); количества — DOUBLE PRECISION: из
  диалекта 3 в базу диалекта 1 процедуру с NUMERIC(18,7) не создать.
- Проверено на копии базы Fancy (отдельный экземпляр bj): мастер с нуля — все проверки, BJ_PING; тестовый режим
  DEPO — К1 Бубликов, зачисление и списание до исполнения без ошибок; очередь, справочник НРД.
  Тест FBLive готовит на базе все запросы bj к DEPO: 94 из 101, оставшиеся 7 — ЛК и ручной ввод (следующий этап).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
2026-10-27 18:55:02 +03:00

471 lines
15 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package lkgateway
// Fancy DEPO на Firebird (27.10.2026). Fancy работает и на Firebird 2.5 — те же таблицы и процедуры ядра, что в
// MSSQL-версии (MSSQL-версия Fancy — перевод Firebird-версии). Весь SQL bj к DEPO написан на T-SQL; чтобы не
// держать второй текст в каждом месте вызова, подключение к Firebird переводит запросы само (fbConn):
// - точный текст Firebird — для запросов, которые механически не переводятся (fsql.FB, ключ — текст T-SQL);
// - остальное механически, вне строк и комментариев: без dbo., TOP n — FIRST n, ISNULL — COALESCE,
// GETDATE() — CURRENT_TIMESTAMP, LEN — CHAR_LENGTH, N'…' — '…', «exec имя» — EXECUTE PROCEDURE имя;
// - именованные параметры @имя (sql.Named) — позиционные ? в порядке появления в тексте;
// - GUID в параметрах — в верхнем регистре (в таблицах обмена Firebird GUID — строка в верхнем регистре,
// uniqueidentifier MSSQL сравнивается без учёта регистра); время «2006-01-02T15:04:05» — с пробелом вместо T.
// База Fancy на Firebird обычно в диалекте 1 и кодировке NONE с данными в WIN1251 — подключение в WIN1251.
// Таблицы, процедуры и права для Firebird ставит depo_setup_fb.go; DDL сюда не попадает.
import (
"context"
"database/sql/driver"
"errors"
"fmt"
"log"
"net/url"
"regexp"
"strconv"
"strings"
"sync"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/lkgateway/fsql"
"github.com/nakagami/firebirdsql"
)
// fbDSN — строка подключения драйвера firebirdsql: путь к файлу базы на сервере Firebird или псевдоним.
func fbDSN(s FancyMSSQLSettings, db string) string {
q := url.Values{}
q.Set("charset", "WIN1251")
u := &url.URL{
Scheme: "firebird",
User: url.UserPassword(s.User, s.Password),
Host: fmt.Sprintf("%s:%d", s.Server, s.DefaultPort()),
Path: "/" + db,
RawQuery: q.Encode(),
}
return u.String()
}
// validFBPath — путь к файлу базы Firebird на сервере (D:\Fancy\DEPO.GDB, /var/db/depo.fdb) или псевдоним
// из aliases.conf: без кавычек, точки с запятой и управляющих символов (путь уходит в строку подключения).
func validFBPath(s string) bool {
if strings.TrimSpace(s) == "" || len(s) > 255 {
return false
}
for _, c := range s {
if c < ' ' || strings.ContainsRune(`'";?#`, c) {
return false
}
}
return true
}
// fbIsLoginFailure — Firebird отверг логин или пароль.
func fbIsLoginFailure(err error) bool {
var fe *firebirdsql.FbError
if !errors.As(err, &fe) {
return false
}
for _, c := range fe.GDSCodes {
if c == firebirdsql.ISCLogin || c == firebirdsql.ISCLoginError {
return true
}
}
return false
}
// fbConnector — подключения к Firebird с переводом запросов.
type fbConnector struct{ base driver.Connector }
func (c fbConnector) Driver() driver.Driver { return c.base.Driver() }
func (c fbConnector) Connect(ctx context.Context) (driver.Conn, error) {
conn, err := c.base.Connect(ctx)
if err != nil {
return nil, err
}
return &fbConn{c: conn}, nil
}
// fbConn — подключение драйвера firebirdsql, которому database/sql отдаёт T-SQL bj: текст и параметры
// переводятся в fbRewrite. Остальные методы — как у драйвера.
type fbConn struct{ c driver.Conn }
func (f *fbConn) Prepare(query string) (driver.Stmt, error) {
return f.PrepareContext(context.Background(), query)
}
func (f *fbConn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) {
q, names := query, []string(nil)
if !fbNative(query) {
q, names = fbParams(fbSQL(query))
}
var st driver.Stmt
var err error
if pc, ok := f.c.(driver.ConnPrepareContext); ok {
st, err = pc.PrepareContext(ctx, q)
} else {
st, err = f.c.Prepare(q)
}
if err != nil {
return nil, fbErr(query, q, err)
}
return &fbStmt{st: st, names: names}, nil
}
func (f *fbConn) Close() error { return f.c.Close() }
func (f *fbConn) Begin() (driver.Tx, error) {
return f.BeginTx(context.Background(), driver.TxOptions{})
}
func (f *fbConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {
if bt, ok := f.c.(driver.ConnBeginTx); ok {
return bt.BeginTx(ctx, opts)
}
return f.c.Begin() //nolint:staticcheck // у драйвера без ConnBeginTx другого способа нет
}
func (f *fbConn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
ex, ok := f.c.(driver.ExecerContext)
if !ok {
return nil, driver.ErrSkip
}
q, a, err := fbRewrite(query, args)
if err != nil {
return nil, err
}
res, err := ex.ExecContext(ctx, q, a)
return res, fbErr(query, q, err)
}
func (f *fbConn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
qr, ok := f.c.(driver.QueryerContext)
if !ok {
return nil, driver.ErrSkip
}
q, a, err := fbRewrite(query, args)
if err != nil {
return nil, err
}
rows, err := qr.QueryContext(ctx, q, a)
return rows, fbErr(query, q, err)
}
func (f *fbConn) Ping(ctx context.Context) error {
if p, ok := f.c.(driver.Pinger); ok {
return p.Ping(ctx)
}
return nil
}
func (f *fbConn) CheckNamedValue(nv *driver.NamedValue) error {
if c, ok := f.c.(driver.NamedValueChecker); ok {
return c.CheckNamedValue(nv)
}
return driver.ErrSkip
}
func (f *fbConn) IsValid() bool {
if v, ok := f.c.(driver.Validator); ok {
return v.IsValid()
}
return true
}
func (f *fbConn) ResetSession(ctx context.Context) error {
if r, ok := f.c.(driver.SessionResetter); ok {
return r.ResetSession(ctx)
}
return nil
}
// fbStmt — подготовленный запрос: параметры при выполнении расставляются по именам из текста.
type fbStmt struct {
st driver.Stmt
names []string
}
func (s *fbStmt) Close() error { return s.st.Close() }
func (s *fbStmt) NumInput() int { return -1 }
func (s *fbStmt) Exec(args []driver.Value) (driver.Result, error) {
return s.ExecContext(context.Background(), fbValues(args))
}
func (s *fbStmt) Query(args []driver.Value) (driver.Rows, error) {
return s.QueryContext(context.Background(), fbValues(args))
}
func (s *fbStmt) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) {
a, err := fbArgs(s.names, args)
if err != nil {
return nil, err
}
if ex, ok := s.st.(driver.StmtExecContext); ok {
return ex.ExecContext(ctx, a)
}
return s.st.Exec(fbPlain(a)) //nolint:staticcheck // драйвер без StmtExecContext
}
func (s *fbStmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) {
a, err := fbArgs(s.names, args)
if err != nil {
return nil, err
}
if q, ok := s.st.(driver.StmtQueryContext); ok {
return q.QueryContext(ctx, a)
}
return s.st.Query(fbPlain(a)) //nolint:staticcheck // драйвер без StmtQueryContext
}
func fbValues(v []driver.Value) []driver.NamedValue {
out := make([]driver.NamedValue, len(v))
for i, x := range v {
out[i] = driver.NamedValue{Ordinal: i + 1, Value: x}
}
return out
}
func fbPlain(a []driver.NamedValue) []driver.Value {
out := make([]driver.Value, len(a))
for i, x := range a {
out[i] = x.Value
}
return out
}
// fbRewrite — текст и параметры запроса T-SQL для Firebird. DDL и блоки PSQL (установка, depo_setup_fb.go)
// написаны для Firebird — уходят как есть.
func fbRewrite(query string, args []driver.NamedValue) (string, []driver.NamedValue, error) {
if fbNative(query) {
return query, args, nil
}
q, names := fbParams(fbSQL(query))
a, err := fbArgs(names, args)
return q, a, err
}
var fbNativeRe = regexp.MustCompile(`(?i)^\s*(CREATE|ALTER|RECREATE|DROP|GRANT|REVOKE|EXECUTE\s+BLOCK|COMMENT\s+ON)\b`)
// fbNative — команда уже на Firebird (установка таблиц, процедур, прав).
func fbNative(q string) bool { return fbNativeRe.MatchString(fbStripComments(q)) }
var (
fbExecRe = regexp.MustCompile(`(?is)^\s*exec(?:ute)?\s+(?:\[?dbo\]?\.)?\[?([A-Za-z0-9_]+)\]?\s*;?\s*$`)
fbObjectIDRe = regexp.MustCompile(`(?i)^SELECT OBJECT_ID\('(?:dbo\.)?([A-Za-z0-9_]+)'\)$`)
fbDboRe = regexp.MustCompile(`(?i)\[?\bdbo\]?\.`)
fbIsNullRe = regexp.MustCompile(`(?i)\bISNULL\s*\(`)
fbGetDateRe = regexp.MustCompile(`(?i)\bGETDATE\s*\(\s*\)`)
fbLenRe = regexp.MustCompile(`(?i)\bLEN\s*\(`)
fbTopRe = regexp.MustCompile(`(?i)\bSELECT(\s+DISTINCT)?\s+TOP\s*(\(\s*@?\w+\s*\)|\d+)`)
fbGUIDRe = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`)
fbISOTimeRe = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?$`)
)
// fbSQL — текст запроса для Firebird: точный перевод из fsql.FB, иначе механический.
func fbSQL(q string) string {
if t, ok := fsql.FB[q]; ok {
return t
}
if m := fbExecRe.FindStringSubmatch(q); m != nil {
return "EXECUTE PROCEDURE " + m[1]
}
if m := fbObjectIDRe.FindStringSubmatch(q); m != nil { // depoTableExists: NULL — таблицы нет
return "SELECT (SELECT FIRST 1 1 FROM RDB$RELATIONS WHERE RDB$RELATION_NAME = '" + strings.ToUpper(m[1]) + "') FROM RDB$DATABASE"
}
return fbOutsideLiterals(q, func(code string) string {
code = fbDboRe.ReplaceAllString(code, "")
code = fbIsNullRe.ReplaceAllString(code, "COALESCE(")
code = fbGetDateRe.ReplaceAllString(code, "CURRENT_TIMESTAMP")
code = fbLenRe.ReplaceAllString(code, "CHAR_LENGTH(")
return fbTopRe.ReplaceAllStringFunc(code, func(s string) string {
m := fbTopRe.FindStringSubmatch(s)
return "SELECT FIRST " + m[2] + m[1] // в Firebird FIRST — до DISTINCT
})
})
}
// fbOutsideLiterals — замены только в коде: строки, имена в кавычках и комментарии не трогаются; N перед
// строкой (N'…' — национальная строка T-SQL) убирается.
func fbOutsideLiterals(q string, fix func(string) string) string {
var b, code strings.Builder
emit := func(lit string) {
c := code.String()
code.Reset()
if strings.HasPrefix(lit, "'") && (strings.HasSuffix(c, "N") || strings.HasSuffix(c, "n")) &&
(len(c) == 1 || !fbIdentChar(c[len(c)-2])) {
c = c[:len(c)-1]
}
b.WriteString(fix(c))
b.WriteString(lit)
}
for i := 0; i < len(q); i++ {
c := q[i]
switch {
case c == '\'' || c == '"':
j := i + 1
for j < len(q) {
if q[j] == c {
if j+1 < len(q) && q[j+1] == c {
j += 2
continue
}
break
}
j++
}
if j >= len(q) {
j = len(q) - 1
}
emit(q[i : j+1])
i = j
case c == '-' && i+1 < len(q) && q[i+1] == '-':
j := strings.IndexByte(q[i:], '\n')
if j < 0 {
j = len(q) - i
}
emit(q[i : i+j])
i += j - 1
case c == '/' && i+1 < len(q) && q[i+1] == '*':
j := strings.Index(q[i+2:], "*/")
end := len(q)
if j >= 0 {
end = i + 2 + j + 2
}
emit(q[i:end])
i = end - 1
default:
code.WriteByte(c)
}
}
b.WriteString(fix(code.String()))
return b.String()
}
// fbParams — @имя вне строк и комментариев заменяются на ?; возвращает имена по порядку (повторы — повторно).
func fbParams(q string) (string, []string) {
if !strings.Contains(q, "@") {
return q, nil
}
var b strings.Builder
var names []string
for i := 0; i < len(q); i++ {
c := q[i]
switch {
case c == '\'' || c == '"': // строка или имя в кавычках — как есть до закрывающей
j := i + 1
for j < len(q) {
if q[j] == c {
if j+1 < len(q) && q[j+1] == c {
j += 2
continue
}
break
}
j++
}
if j >= len(q) {
j = len(q) - 1
}
b.WriteString(q[i : j+1])
i = j
case c == '-' && i+1 < len(q) && q[i+1] == '-':
j := strings.IndexByte(q[i:], '\n')
if j < 0 {
j = len(q) - i
}
b.WriteString(q[i : i+j])
i += j - 1
case c == '/' && i+1 < len(q) && q[i+1] == '*':
j := strings.Index(q[i+2:], "*/")
end := len(q)
if j >= 0 {
end = i + 2 + j + 2
}
b.WriteString(q[i:end])
i = end - 1
case c == '@' && i+1 < len(q) && fbIdentStart(q[i+1]):
j := i + 1
for j < len(q) && fbIdentChar(q[j]) {
j++
}
names = append(names, q[i+1:j])
b.WriteByte('?')
i = j - 1
default:
b.WriteByte(c)
}
}
return b.String(), names
}
func fbIdentStart(c byte) bool { return c == '_' || c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' }
func fbIdentChar(c byte) bool { return fbIdentStart(c) || c >= '0' && c <= '9' }
// fbArgs — значения для ? по именам из текста: sql.Named — по имени, @p1…@pN без имени — по номеру.
// Без имён в тексте параметры передаются как есть (запрос уже с ?).
func fbArgs(names []string, args []driver.NamedValue) ([]driver.NamedValue, error) {
if len(names) == 0 {
out := make([]driver.NamedValue, len(args))
for i, a := range args {
out[i] = driver.NamedValue{Ordinal: i + 1, Value: fbValue(a.Value)}
}
return out, nil
}
out := make([]driver.NamedValue, 0, len(names))
for _, n := range names {
v, ok := fbArg(n, args)
if !ok {
return nil, fmt.Errorf("параметр @%s не передан", n)
}
out = append(out, driver.NamedValue{Ordinal: len(out) + 1, Value: fbValue(v)})
}
return out, nil
}
func fbArg(name string, args []driver.NamedValue) (driver.Value, bool) {
for _, a := range args {
if a.Name != "" && strings.EqualFold(a.Name, name) {
return a.Value, true
}
}
if len(name) > 1 && (name[0] == 'p' || name[0] == 'P') {
if n, err := strconv.Atoi(name[1:]); err == nil && n >= 1 {
for _, a := range args {
if a.Name == "" && a.Ordinal == n {
return a.Value, true
}
}
}
}
return nil, false
}
// fbValue — GUID в верхнем регистре, время ISO 8601 — с пробелом вместо T (Firebird «T» не разбирает).
func fbValue(v driver.Value) driver.Value {
s, ok := v.(string)
if !ok {
return v
}
switch {
case fbGUIDRe.MatchString(s):
return strings.ToUpper(s)
case fbISOTimeRe.MatchString(s):
return strings.Replace(s, "T", " ", 1)
}
return v
}
var fbErrLogged sync.Map
// fbErr — ошибка Firebird; для разбора в журнал (раз на запрос) — исходный текст T-SQL и что ушло в Firebird.
func fbErr(orig, sent string, err error) error {
if err == nil {
return nil
}
var fe *firebirdsql.FbError
if errors.As(err, &fe) && fe.SQLCode == -104 { // синтаксис: перевод запроса не подошёл
if _, seen := fbErrLogged.LoadOrStore(orig, true); !seen {
log.Printf("depo-fb: запрос не принят Firebird: %v\n T-SQL: %s\n Firebird: %s", err, depoStmtHead(orig), sent)
}
}
return err
}